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.
Files changed (53) hide show
  1. fameio/cli/convert_results.py +4 -6
  2. fameio/cli/make_config.py +3 -5
  3. fameio/cli/options.py +6 -4
  4. fameio/cli/parser.py +53 -29
  5. fameio/cli/reformat.py +58 -0
  6. fameio/input/__init__.py +4 -4
  7. fameio/input/loader/__init__.py +4 -6
  8. fameio/input/loader/controller.py +11 -16
  9. fameio/input/loader/loader.py +11 -9
  10. fameio/input/metadata.py +26 -29
  11. fameio/input/resolver.py +4 -6
  12. fameio/input/scenario/agent.py +18 -16
  13. fameio/input/scenario/attribute.py +85 -31
  14. fameio/input/scenario/contract.py +23 -28
  15. fameio/input/scenario/exception.py +3 -6
  16. fameio/input/scenario/fameiofactory.py +7 -12
  17. fameio/input/scenario/generalproperties.py +7 -8
  18. fameio/input/scenario/scenario.py +14 -18
  19. fameio/input/scenario/stringset.py +5 -6
  20. fameio/input/schema/agenttype.py +8 -10
  21. fameio/input/schema/attribute.py +30 -36
  22. fameio/input/schema/java_packages.py +6 -7
  23. fameio/input/schema/schema.py +9 -11
  24. fameio/input/validator.py +178 -41
  25. fameio/input/writer.py +20 -29
  26. fameio/logs.py +28 -19
  27. fameio/output/agent_type.py +14 -16
  28. fameio/output/conversion.py +9 -12
  29. fameio/output/csv_writer.py +33 -23
  30. fameio/output/data_transformer.py +11 -11
  31. fameio/output/execution_dao.py +170 -0
  32. fameio/output/input_dao.py +16 -19
  33. fameio/output/output_dao.py +7 -7
  34. fameio/output/reader.py +8 -10
  35. fameio/output/yaml_writer.py +2 -3
  36. fameio/scripts/__init__.py +15 -4
  37. fameio/scripts/convert_results.py +18 -17
  38. fameio/scripts/exception.py +1 -1
  39. fameio/scripts/make_config.py +3 -4
  40. fameio/scripts/reformat.py +71 -0
  41. fameio/scripts/reformat.py.license +3 -0
  42. fameio/series.py +78 -47
  43. fameio/time.py +15 -18
  44. fameio/tools.py +42 -4
  45. {fameio-3.2.0.dist-info → fameio-3.3.0.dist-info}/METADATA +33 -23
  46. fameio-3.3.0.dist-info/RECORD +60 -0
  47. {fameio-3.2.0.dist-info → fameio-3.3.0.dist-info}/entry_points.txt +1 -0
  48. fameio-3.2.0.dist-info/RECORD +0 -56
  49. {fameio-3.2.0.dist-info → fameio-3.3.0.dist-info}/LICENSE.txt +0 -0
  50. {fameio-3.2.0.dist-info → fameio-3.3.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
  51. {fameio-3.2.0.dist-info → fameio-3.3.0.dist-info}/LICENSES/CC-BY-4.0.txt +0 -0
  52. {fameio-3.2.0.dist-info → fameio-3.3.0.dist-info}/LICENSES/CC0-1.0.txt +0 -0
  53. {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
- Initialises the metadata by searching the given definitions' top level for metadata.
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
- A component that can contain metadata and may be associated with Objects that have metadata but do not extend
79
- Metadata itself, like, e.g., Strings in a list.
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 said `file_name` relative to specified `directory` if file was found there, None otherwise"""
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
@@ -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
- Logs a warning if any unexpected keys are presented at top level of `data`
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
- Initialise agent `attributes` from dict; Must only be called when creating a new Agent
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
- _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."
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
- """Parses an Attribute's definition"""
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
- data_type = Attribute._get_data_type(name, definitions)
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(name: str, definitions: Any) -> Attribute.__DefinitionType:
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 log_scenario_error(Attribute._ERR_LIST_EMPTY.format(name))
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 log_scenario_error(Attribute._ERR_DICT_EMPTY.format(name))
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 log_scenario_error(Attribute._ERR_MIXED_DATA.format(repr(definitions)))
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 log_scenario_error(Attribute._ERR_MIXED_DATA.format(repr(values)))
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(name: str, definitions: dict[str, Any]) -> dict[str, Attribute]:
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 = name + Attribute.NAME_STRING_SEPARATOR + nested_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: