fameio 3.1.1__py3-none-any.whl → 3.2.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 (52) hide show
  1. fameio/cli/convert_results.py +6 -4
  2. fameio/cli/make_config.py +6 -4
  3. fameio/cli/parser.py +41 -29
  4. fameio/input/__init__.py +1 -1
  5. fameio/input/loader/__init__.py +9 -7
  6. fameio/input/loader/controller.py +59 -8
  7. fameio/input/loader/loader.py +14 -7
  8. fameio/input/metadata.py +35 -13
  9. fameio/input/resolver.py +5 -4
  10. fameio/input/scenario/agent.py +50 -16
  11. fameio/input/scenario/attribute.py +14 -15
  12. fameio/input/scenario/contract.py +152 -43
  13. fameio/input/scenario/exception.py +44 -18
  14. fameio/input/scenario/fameiofactory.py +63 -7
  15. fameio/input/scenario/generalproperties.py +17 -6
  16. fameio/input/scenario/scenario.py +111 -28
  17. fameio/input/scenario/stringset.py +27 -8
  18. fameio/input/schema/agenttype.py +21 -2
  19. fameio/input/schema/attribute.py +91 -22
  20. fameio/input/schema/java_packages.py +8 -5
  21. fameio/input/schema/schema.py +35 -9
  22. fameio/input/validator.py +22 -15
  23. fameio/input/writer.py +136 -36
  24. fameio/logs.py +3 -31
  25. fameio/output/__init__.py +5 -1
  26. fameio/output/agent_type.py +86 -23
  27. fameio/output/conversion.py +47 -29
  28. fameio/output/csv_writer.py +88 -18
  29. fameio/output/data_transformer.py +7 -14
  30. fameio/output/input_dao.py +62 -21
  31. fameio/output/output_dao.py +26 -4
  32. fameio/output/reader.py +58 -13
  33. fameio/output/yaml_writer.py +15 -6
  34. fameio/scripts/__init__.py +9 -2
  35. fameio/scripts/convert_results.py +123 -50
  36. fameio/scripts/convert_results.py.license +1 -1
  37. fameio/scripts/exception.py +7 -0
  38. fameio/scripts/make_config.py +34 -12
  39. fameio/scripts/make_config.py.license +1 -1
  40. fameio/series.py +117 -33
  41. fameio/time.py +74 -17
  42. fameio/tools.py +7 -5
  43. {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/METADATA +19 -13
  44. fameio-3.2.0.dist-info/RECORD +56 -0
  45. {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/WHEEL +1 -1
  46. CHANGELOG.md +0 -288
  47. fameio-3.1.1.dist-info/RECORD +0 -56
  48. {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/LICENSE.txt +0 -0
  49. {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
  50. {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/LICENSES/CC-BY-4.0.txt +0 -0
  51. {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/LICENSES/CC0-1.0.txt +0 -0
  52. {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/entry_points.txt +0 -0
@@ -7,9 +7,10 @@ import ast
7
7
  from typing import Any, Final
8
8
 
9
9
  from fameio.input.metadata import Metadata
10
+ from fameio.logs import log
10
11
  from fameio.tools import keys_to_lower
11
12
  from .attribute import Attribute
12
- from .exception import assert_or_raise, get_or_default, get_or_raise
13
+ from .exception import assert_or_raise, get_or_raise
13
14
 
14
15
 
15
16
  class Agent(Metadata):
@@ -18,37 +19,70 @@ class Agent(Metadata):
18
19
  KEY_TYPE: Final[str] = "Type".lower()
19
20
  KEY_ID: Final[str] = "Id".lower()
20
21
  KEY_ATTRIBUTES: Final[str] = "Attributes".lower()
22
+ KEY_EXT: Final[str] = "Ext".lower()
23
+ RESERVED_KEYS: set[str] = {KEY_TYPE, KEY_ID, KEY_ATTRIBUTES, KEY_EXT, Metadata.KEY_METADATA}
21
24
 
22
- _ERR_MISSING_KEY = "Agent requires `key` '{}' but is missing it."
23
- _ERR_MISSING_TYPE = "Agent requires `type` but is missing it."
24
- _ERR_MISSING_ID = "Agent requires a positive integer `id` but was '{}'."
25
+ _ERR_MISSING_KEY = "Agent definition requires key '{}' but is missing it."
26
+ _ERR_TYPE_EMPTY = "Agent `type` must not be empty."
27
+ _ERR_ILLEGAL_ID = "Agent requires a positive integer `id` but was '{}'."
25
28
  _ERR_DOUBLE_ATTRIBUTE = "Cannot add attribute '{}' to agent {} because it already exists."
26
29
  _ERR_ATTRIBUTE_OVERWRITE = "Agent's attributes are already set and would be overwritten."
30
+ _WARN_UNEXPECTED_KEY = "Ignoring unexpected key(s) {} in top level of agent with id: {}"
27
31
 
28
- def __init__(self, agent_id: int, type_name: str, metadata: dict = None) -> None:
32
+ def __init__(self, agent_id: int, type_name: str, metadata: dict | None = None) -> None:
29
33
  """Constructs a new Agent"""
30
34
  super().__init__({Agent.KEY_METADATA: metadata} if metadata else None)
31
- assert_or_raise(isinstance(agent_id, int) and agent_id >= 0, self._ERR_MISSING_ID.format(agent_id))
32
- assert_or_raise(bool(type_name and type_name.strip()), self._ERR_MISSING_TYPE)
35
+ assert_or_raise(isinstance(agent_id, int) and agent_id >= 0, self._ERR_ILLEGAL_ID.format(agent_id))
36
+ assert_or_raise(bool(type_name and type_name.strip()), self._ERR_TYPE_EMPTY)
33
37
  self._id: int = agent_id
34
38
  self._type_name: str = type_name.strip()
35
39
  self._attributes: dict[str, Attribute] = {}
36
40
 
37
41
  @classmethod
38
42
  def from_dict(cls, definitions: dict) -> Agent:
39
- """Parses an agent from provided `definitions`"""
43
+ """
44
+ Args:
45
+ definitions: dictionary representation of an agent
46
+
47
+ Returns:
48
+ new agent
49
+
50
+ Raises:
51
+ ScenarioError: if definitions are incomplete or erroneous, logged on level "ERROR"
52
+ """
40
53
  definitions = keys_to_lower(definitions)
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)
43
- agent = cls(agent_id, agent_type)
44
- agent._extract_metadata(definitions)
45
- attribute_definitions = get_or_default(definitions, Agent.KEY_ATTRIBUTES, {})
46
- agent.init_attributes_from_dict(attribute_definitions)
47
- agent._meta_data = get_or_default(definitions, Agent.KEY_METADATA, {})
54
+ agent_type = get_or_raise(definitions, Agent.KEY_TYPE, Agent._ERR_MISSING_KEY)
55
+ agent_id = get_or_raise(definitions, Agent.KEY_ID, Agent._ERR_MISSING_KEY)
56
+ Agent.validate_keys(definitions, agent_id)
57
+ metadata = definitions.get(Agent.KEY_METADATA, None)
58
+ agent = cls(agent_id, agent_type, metadata)
59
+ agent.init_attributes_from_dict(definitions.get(Agent.KEY_ATTRIBUTES, {}))
48
60
  return agent
49
61
 
62
+ @staticmethod
63
+ 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`
66
+ Expected keys are defined in `Agent.RESERVED_KEYS`
67
+
68
+ Args:
69
+ data: agent definition to be checked
70
+ agent_id: id of agent to be checked
71
+ """
72
+ unexpected_keys = set(data.keys()) - Agent.RESERVED_KEYS
73
+ if unexpected_keys:
74
+ log().warning(Agent._WARN_UNEXPECTED_KEY.format(unexpected_keys, agent_id))
75
+
50
76
  def init_attributes_from_dict(self, attributes: dict[str, Any]) -> None:
51
- """Initialize Agent `attributes` from dict; Must only be called when creating a new Agent"""
77
+ """
78
+ Initialise agent `attributes` from dict; Must only be called when creating a new Agent
79
+
80
+ Args:
81
+ attributes: to be set
82
+
83
+ Raises:
84
+ ScenarioError: if attributes were already initialised
85
+ """
52
86
  assert_or_raise(not self._attributes, self._ERR_ATTRIBUTE_OVERWRITE)
53
87
  self._attributes = {}
54
88
  for name, value in attributes.items():
@@ -9,7 +9,7 @@ from typing import Any, NamedTuple, Final
9
9
 
10
10
  from fameio.input.metadata import Metadata, MetadataComponent
11
11
  from fameio.tools import keys_to_lower
12
- from .exception import log_and_raise
12
+ from .exception import log_scenario_error
13
13
 
14
14
 
15
15
  class Attribute(Metadata):
@@ -49,7 +49,7 @@ class Attribute(Metadata):
49
49
  """Parses an Attribute's definition"""
50
50
  self._full_name = name
51
51
  if definitions is None:
52
- log_and_raise(Attribute._ERR_VALUE_MISSING.format(name))
52
+ raise log_scenario_error(Attribute._ERR_VALUE_MISSING.format(name))
53
53
  super().__init__(definitions)
54
54
  data_type = Attribute._get_data_type(name, definitions)
55
55
 
@@ -59,16 +59,15 @@ class Attribute(Metadata):
59
59
  self._nested_list: list[Attribute.__NestedMeta] | None = None
60
60
 
61
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
62
+ self._value = self._extract_value(definitions).value # type: ignore[arg-type]
64
63
  elif data_type is Attribute.__DefinitionType.VALUE_LIST:
65
- self._value_list = self._extract_values(definitions)
64
+ self._value_list = self._extract_values(definitions) # type: ignore[arg-type]
66
65
  elif data_type is Attribute.__DefinitionType.NESTED:
67
- self._nested = Attribute._build_attribute_dict(name, definitions)
66
+ self._nested = Attribute._build_attribute_dict(name, definitions) # type: ignore[arg-type]
68
67
  elif data_type is Attribute.__DefinitionType.NESTED_LIST:
69
68
  self._nested_list = []
70
69
  values = keys_to_lower(definitions)[Attribute.KEY_VALUES] if isinstance(definitions, dict) else definitions
71
- for list_index, definition in enumerate(values):
70
+ for list_index, definition in enumerate(values): # type: ignore[arg-type]
72
71
  list_meta = MetadataComponent(definition)
73
72
  list_extended_name = name + Attribute.NAME_STRING_SEPARATOR + str(list_index)
74
73
  nested_items = Attribute._build_attribute_dict(list_extended_name, definition)
@@ -79,11 +78,11 @@ class Attribute(Metadata):
79
78
  """Returns type of data derived from given `definitions`"""
80
79
  if isinstance(definitions, list):
81
80
  if len(definitions) == 0:
82
- log_and_raise(Attribute._ERR_LIST_EMPTY.format(name))
81
+ raise log_scenario_error(Attribute._ERR_LIST_EMPTY.format(name))
83
82
  return Attribute._get_data_type_list(definitions)
84
83
  if isinstance(definitions, dict):
85
84
  if len(definitions) == 0:
86
- log_and_raise(Attribute._ERR_DICT_EMPTY.format(name))
85
+ raise log_scenario_error(Attribute._ERR_DICT_EMPTY.format(name))
87
86
  return Attribute._get_data_type_dict(definitions)
88
87
  return Attribute.__DefinitionType.VALUE
89
88
 
@@ -94,7 +93,7 @@ class Attribute(Metadata):
94
93
  return Attribute.__DefinitionType.VALUE_LIST
95
94
  if Attribute._is_list_of_dict(definitions):
96
95
  return Attribute.__DefinitionType.NESTED_LIST
97
- log_and_raise(Attribute._ERR_MIXED_DATA.format(repr(definitions)))
96
+ raise log_scenario_error(Attribute._ERR_MIXED_DATA.format(repr(definitions)))
98
97
 
99
98
  @staticmethod
100
99
  def _is_list_of_dict(definitions: list) -> bool:
@@ -113,7 +112,7 @@ class Attribute(Metadata):
113
112
  return Attribute.__DefinitionType.VALUE_LIST
114
113
  if Attribute._is_list_of_dict(values):
115
114
  return Attribute.__DefinitionType.NESTED_LIST
116
- log_and_raise(Attribute._ERR_MIXED_DATA.format(repr(values)))
115
+ raise log_scenario_error(Attribute._ERR_MIXED_DATA.format(repr(values)))
117
116
  return Attribute.__DefinitionType.NESTED
118
117
 
119
118
  @staticmethod
@@ -168,8 +167,8 @@ class Attribute(Metadata):
168
167
 
169
168
  @property
170
169
  def nested(self) -> dict[str, Attribute]:
171
- """Returns dictionary of all nested Attributes if nested Attributes are present, else None"""
172
- return self._nested if self.has_nested else None
170
+ """Returns dictionary of all nested Attributes if nested Attributes are present, else empty dict"""
171
+ return self._nested if self._nested is not None else {}
173
172
 
174
173
  @property
175
174
  def has_nested_list(self) -> bool:
@@ -178,8 +177,8 @@ class Attribute(Metadata):
178
177
 
179
178
  @property
180
179
  def nested_list(self) -> list[dict[str, Attribute]]:
181
- """Return list of all nested Attribute dictionaries if such are present, else None"""
182
- return [entry.value for entry in self._nested_list] if self.has_nested_list else None
180
+ """Return list of all nested Attribute dictionaries if such are present, else an empty list"""
181
+ return [entry.value for entry in self._nested_list] if self._nested_list is not None else []
183
182
 
184
183
  def __repr__(self) -> str:
185
184
  return self._full_name
@@ -3,19 +3,22 @@
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
  from __future__ import annotations
5
5
 
6
- from typing import Any, Optional, Final
6
+ from typing import Any, Final, overload
7
7
 
8
+ from fameio.input import InputError
8
9
  from fameio.input.metadata import Metadata
9
- from fameio.logs import log
10
- from fameio.time import FameTime
10
+ from fameio.input.scenario.attribute import Attribute
11
+ from fameio.logs import log, log_error
12
+ from fameio.time import FameTime, ConversionError
11
13
  from fameio.tools import ensure_is_list, keys_to_lower
12
- from .attribute import Attribute
13
- from .exception import get_or_default, get_or_raise, log_and_raise
14
14
 
15
15
 
16
16
  class Contract(Metadata):
17
17
  """Contract between two Agents of a scenario"""
18
18
 
19
+ class ContractError(InputError):
20
+ """An error that occurred while parsing Contract definitions"""
21
+
19
22
  KEY_SENDER: Final[str] = "SenderId".lower()
20
23
  KEY_RECEIVER: Final[str] = "ReceiverId".lower()
21
24
  KEY_PRODUCT: Final[str] = "ProductName".lower()
@@ -26,13 +29,15 @@ class Contract(Metadata):
26
29
 
27
30
  _ERR_MISSING_KEY = "Contract requires key '{}' but is missing it."
28
31
  _ERR_MULTI_CONTRACT_CORRUPT = (
29
- "Definition of Contracts is valid only for One-to-One, One-to-many, Many-to-one, "
32
+ "Definition of Contracts is valid only for One-to-One, One-to-Many, Many-to-One, "
30
33
  "or N-to-N sender-to-receiver numbers. Found M-to-N pairing in Contract with "
31
34
  "Senders: {} and Receivers: {}."
32
35
  )
33
36
  _ERR_INTERVAL_NOT_POSITIVE = "Contract delivery interval must be a positive integer but was: {}"
34
37
  _ERR_SENDER_IS_RECEIVER = "Contract sender and receiver have the same id: {}"
35
38
  _ERR_DOUBLE_ATTRIBUTE = "Cannot add attribute '{}' to contract because it already exists."
39
+ _ERR_TIME_CONVERSION = "Contract item '{}' is an ill-formatted time: '{}'"
40
+ _ERR_PRODUCT_EMPTY = f"A Contract's `{KEY_PRODUCT}` must be a non-emtpy string."
36
41
 
37
42
  # pylint: disable=too-many-arguments, too-many-positional-arguments
38
43
  def __init__(
@@ -42,23 +47,41 @@ class Contract(Metadata):
42
47
  product_name: str,
43
48
  delivery_interval: int,
44
49
  first_delivery_time: int,
45
- expiration_time: Optional[int] = None,
46
- metadata: Optional[dict] = None,
50
+ expiration_time: int | None = None,
51
+ metadata: dict | None = None,
47
52
  ) -> None:
48
- """Constructs a new Contract"""
53
+ """
54
+ Constructs a new Contract
55
+
56
+ Args:
57
+ sender_id: unique id of sender
58
+ receiver_id: unique id of receiver
59
+ product_name: name of contracted product
60
+ delivery_interval: time interval in steps between contract deliveries
61
+ first_delivery_time: absolute time of first contract execution
62
+ expiration_time: absolute time at which contract execution stops
63
+ metadata: any metadata associated with the contract
64
+
65
+ Returns:
66
+ new Contract
67
+
68
+ Raises:
69
+ ContractError: if delivery interval is invalid, logged on level "ERROR"
70
+ """
49
71
  super().__init__({self.KEY_METADATA: metadata} if metadata else None)
50
- assert product_name != ""
72
+ if product_name.strip() == "":
73
+ raise log_error(self.ContractError(self._ERR_PRODUCT_EMPTY))
51
74
  if sender_id == receiver_id:
52
75
  log().warning(self._ERR_SENDER_IS_RECEIVER.format(sender_id))
53
76
  if delivery_interval <= 0:
54
- raise ValueError(self._ERR_INTERVAL_NOT_POSITIVE.format(delivery_interval))
77
+ raise log_error(self.ContractError(self._ERR_INTERVAL_NOT_POSITIVE.format(delivery_interval)))
55
78
  self._sender_id = sender_id
56
79
  self._receiver_id = receiver_id
57
80
  self._product_name = product_name
58
81
  self._delivery_interval = delivery_interval
59
82
  self._first_delivery_time = first_delivery_time
60
83
  self._expiration_time = expiration_time
61
- self._attributes = {}
84
+ self._attributes: dict = {}
62
85
 
63
86
  def _notify_data_changed(self):
64
87
  """Placeholder method used to signal data changes to derived types"""
@@ -99,7 +122,7 @@ class Contract(Metadata):
99
122
  return self._first_delivery_time
100
123
 
101
124
  @property
102
- def expiration_time(self) -> Optional[int]:
125
+ def expiration_time(self) -> int | None:
103
126
  """Returns the expiration time of the contract if available, None otherwise"""
104
127
  return self._expiration_time
105
128
 
@@ -109,43 +132,113 @@ class Contract(Metadata):
109
132
  return self._attributes
110
133
 
111
134
  def add_attribute(self, name: str, value: Attribute) -> None:
112
- """Adds a new attribute to the Contract (raise an error if it already exists)"""
135
+ """
136
+ Adds a new attribute to the Contract
137
+
138
+ Args:
139
+ name: of the attribute
140
+ value: of the attribute
141
+
142
+ Raises:
143
+ ContractError: if attribute already exists, logged on level "ERROR"
144
+ """
113
145
  if name in self._attributes:
114
- raise ValueError(self._ERR_DOUBLE_ATTRIBUTE.format(name))
146
+ raise log_error(self.ContractError(self._ERR_DOUBLE_ATTRIBUTE.format(name)))
115
147
  self._attributes[name] = value
116
148
  self._notify_data_changed()
117
149
 
118
150
  @classmethod
119
151
  def from_dict(cls, definitions: dict) -> Contract:
120
- """Parses Contract from given `definitions`"""
152
+ """
153
+ Parses contract from given `definitions`
154
+
155
+ Args:
156
+ definitions: dictionary representation of a contract
157
+
158
+ Returns:
159
+ new contract
160
+
161
+ Raises:
162
+ ContractError: if definitions are incomplete or erroneous, logged on level "ERROR"
163
+ """
121
164
  definitions = keys_to_lower(definitions)
122
- sender_id = get_or_raise(definitions, Contract.KEY_SENDER, Contract._ERR_MISSING_KEY)
123
- receiver_id = get_or_raise(definitions, Contract.KEY_RECEIVER, Contract._ERR_MISSING_KEY)
124
- product_name = get_or_raise(definitions, Contract.KEY_PRODUCT, Contract._ERR_MISSING_KEY)
125
- first_delivery_time = FameTime.convert_string_if_is_datetime(
126
- get_or_raise(definitions, Contract.KEY_FIRST_DELIVERY, Contract._ERR_MISSING_KEY)
127
- )
128
- delivery_interval = get_or_raise(definitions, Contract.KEY_INTERVAL, Contract._ERR_MISSING_KEY)
129
- expiration_time = get_or_default(definitions, Contract.KEY_EXPIRE, None)
130
- expiration_time = FameTime.convert_string_if_is_datetime(expiration_time) if expiration_time else None
131
-
132
- contract = cls(
133
- sender_id,
134
- receiver_id,
135
- product_name,
136
- delivery_interval,
137
- first_delivery_time,
138
- expiration_time,
139
- )
165
+ sender_id = Contract._get_or_raise(definitions, Contract.KEY_SENDER, Contract._ERR_MISSING_KEY)
166
+ receiver_id = Contract._get_or_raise(definitions, Contract.KEY_RECEIVER, Contract._ERR_MISSING_KEY)
167
+ product_name = Contract._get_or_raise(definitions, Contract.KEY_PRODUCT, Contract._ERR_MISSING_KEY)
168
+
169
+ first_delivery_time = Contract._get_time(definitions, Contract.KEY_FIRST_DELIVERY)
170
+ delivery_interval = Contract._get_or_raise(definitions, Contract.KEY_INTERVAL, Contract._ERR_MISSING_KEY)
171
+ expiration_time = Contract._get_time(definitions, Contract.KEY_EXPIRE, mandatory=False)
172
+
173
+ contract = cls(sender_id, receiver_id, product_name, delivery_interval, first_delivery_time, expiration_time)
140
174
  contract._extract_metadata(definitions)
141
- attribute_definitions = get_or_default(definitions, Contract.KEY_ATTRIBUTES, {})
142
- contract._init_attributes_from_dict(attribute_definitions)
175
+ contract._init_attributes_from_dict(definitions.get(Contract.KEY_ATTRIBUTES, {}))
143
176
  return contract
144
177
 
178
+ @staticmethod
179
+ def _get_or_raise(dictionary: dict, key: str, error_message: str) -> Any:
180
+ """
181
+ Returns value associated with `key` in given `dictionary`, or raises exception if key or value is missing
182
+
183
+ Args:
184
+ dictionary: to search the key in
185
+ key: to be searched
186
+ error_message: to be logged and included in the raised exception if key is missing
187
+
188
+ Returns:
189
+ value associated with given key in given dictionary
190
+
191
+ Raises:
192
+ ContractError: if given key is not in given dictionary or value is None, logged on level "ERROR"
193
+ """
194
+ if key not in dictionary or dictionary[key] is None:
195
+ raise log_error(Contract.ContractError(error_message.format(key)))
196
+ return dictionary[key]
197
+
198
+ @staticmethod
199
+ @overload
200
+ def _get_time(definitions: dict, key: str) -> int: ... # noqa: E704
201
+
202
+ @staticmethod
203
+ @overload
204
+ def _get_time(definitions: dict, key: str, mandatory: bool) -> int | None: ... # noqa: E704
205
+
206
+ @staticmethod
207
+ def _get_time(definitions: dict, key: str, mandatory: bool = True) -> int | None:
208
+ """
209
+ Extract time representation value at given key, and, if present, convert to integer, else return None
210
+
211
+ Args:
212
+ definitions: to search given key in
213
+ key: to check for an associated value
214
+ mandatory: if true, also raises an error if key is missing
215
+
216
+ Returns:
217
+ None if key is not mandatory/present, else the integer representation of the time value associated with key
218
+
219
+ Raises:
220
+ ContractError: if found value could not be converted or mandatory value is missing, logged on level "ERROR"
221
+ """
222
+ if key in definitions:
223
+ value = definitions[key]
224
+ try:
225
+ return FameTime.convert_string_if_is_datetime(value)
226
+ except ConversionError as e:
227
+ raise log_error(Contract.ContractError(Contract._ERR_TIME_CONVERSION.format(key, value))) from e
228
+ if mandatory:
229
+ raise log_error(Contract.ContractError(Contract._ERR_MISSING_KEY.format(key)))
230
+ return None
231
+
145
232
  def _init_attributes_from_dict(self, attributes: dict[str, Any]) -> None:
146
- """Resets Contract `attributes` from dict; Must only be called when creating a new Contract"""
147
- assert len(self._attributes) == 0
148
- self._attributes = {}
233
+ """
234
+ Resets Contract `attributes` from dict
235
+
236
+ Args:
237
+ attributes: key-value pairs of attributes to be set
238
+
239
+ Raises:
240
+ ContractError: if some of provided attributes were already present
241
+ """
149
242
  for name, value in attributes.items():
150
243
  full_name = f"{type}.{id}{name}"
151
244
  self.add_attribute(name, Attribute(full_name, value))
@@ -169,7 +262,19 @@ class Contract(Metadata):
169
262
 
170
263
  @staticmethod
171
264
  def split_contract_definitions(multi_definition: dict) -> list[dict]:
172
- """Splits given `multi_definition` dictionary into list of individual Contract definitions"""
265
+ """
266
+ Splits given dictionary of contracts with potentially more than ore sender and/or receiver into a list
267
+ of individual contract definitions with one sender and one receiver
268
+
269
+ Args:
270
+ multi_definition: contract definitions with potentially more than ore sender and/or receiver
271
+
272
+ Returns:
273
+ list of contract definitions with exactly one sender and receiver
274
+
275
+ Raises:
276
+ ContractError: if multi_definition is incomplete or erroneous, logged on level "ERROR"
277
+ """
173
278
  contracts = []
174
279
  base_data = {}
175
280
  multi_definition = keys_to_lower(multi_definition)
@@ -183,8 +288,12 @@ class Contract(Metadata):
183
288
  ]:
184
289
  if key in multi_definition:
185
290
  base_data[key] = multi_definition[key]
186
- senders = ensure_is_list(get_or_raise(multi_definition, Contract.KEY_SENDER, Contract._ERR_MISSING_KEY))
187
- receivers = ensure_is_list(get_or_raise(multi_definition, Contract.KEY_RECEIVER, Contract._ERR_MISSING_KEY))
291
+ senders = ensure_is_list(
292
+ Contract._get_or_raise(multi_definition, Contract.KEY_SENDER, Contract._ERR_MISSING_KEY)
293
+ )
294
+ receivers = ensure_is_list(
295
+ Contract._get_or_raise(multi_definition, Contract.KEY_RECEIVER, Contract._ERR_MISSING_KEY)
296
+ )
188
297
  if len(senders) > 1 and len(receivers) == 1:
189
298
  for index, sender in enumerate(senders):
190
299
  contracts.append(Contract._copy_contract(sender, receivers[0], base_data))
@@ -195,7 +304,7 @@ class Contract(Metadata):
195
304
  for index in range(len(senders)): # pylint: disable=consider-using-enumerate
196
305
  contracts.append(Contract._copy_contract(senders[index], receivers[index], base_data))
197
306
  else:
198
- log_and_raise(Contract._ERR_MULTI_CONTRACT_CORRUPT.format(senders, receivers))
307
+ raise log_error(Contract.ContractError(Contract._ERR_MULTI_CONTRACT_CORRUPT.format(senders, receivers)))
199
308
  return contracts
200
309
 
201
310
  @staticmethod
@@ -1,35 +1,61 @@
1
1
  # SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
- from typing import NoReturn, Any, Union
4
+ from __future__ import annotations
5
+
6
+ from typing import Any
5
7
 
6
8
  from fameio.input import ScenarioError
7
- from fameio.logs import log_error, log
9
+ from fameio.logs import log_error
8
10
 
9
11
  _DEFAULT_USED = "Using default value '{}' for missing key '{}'"
10
12
 
11
13
 
12
- def log_and_raise(message: str) -> NoReturn:
13
- """Raises ScenarioError with given `message`"""
14
- raise log_error(ScenarioError(message))
14
+ def log_scenario_error(message: str) -> ScenarioError:
15
+ """
16
+ Creates exception with given `message`, logs it on level "Error" and returns it
17
+
18
+ Args:
19
+ message: to be logged and included in the exception if key is missing
20
+
21
+ Returns:
22
+ created ScenarioError, logged on level "ERROR"
23
+ """
24
+ error = ScenarioError(message)
25
+ log_error(error)
26
+ return error
27
+
15
28
 
29
+ def get_or_raise(dictionary: dict, key: str, error_message: str) -> Any:
30
+ """
31
+ Returns value associated with `key` in given `dictionary`, or raises exception if key or value is missing
16
32
 
17
- def get_or_raise(dictionary: dict, key: str, message: str) -> Union[Any, NoReturn]:
18
- """Returns value associated with `key` in given `dictionary`, or raises ScenarioException if key is missing"""
33
+ Args:
34
+ dictionary: to search the key in
35
+ key: to be searched
36
+ error_message: to be logged and included in the raised exception if key is missing
37
+
38
+ Returns:
39
+ value associated with given key in given dictionary
40
+
41
+ Raises:
42
+ ScenarioError: if given key is not in given dictionary or value is None, logged on level "ERROR"
43
+ """
19
44
  if key not in dictionary or dictionary[key] is None:
20
- raise log_error(ScenarioError(message.format(key)))
45
+ raise log_scenario_error(error_message.format(key))
21
46
  return dictionary[key]
22
47
 
23
48
 
24
- def assert_or_raise(assertion: bool, msg: str) -> None:
25
- """Raises new ScenarioError with given `msg` if `assertion` is False"""
26
- if not assertion:
27
- raise log_error(ScenarioError(msg))
49
+ def assert_or_raise(assertion: bool, error_message: str) -> None:
50
+ """
51
+ Raises exception with given `msg` if `assertion` is False
28
52
 
53
+ Args:
54
+ assertion: expression that must be True, else an exception is raised
55
+ error_message: to be logged and included in the raised exception if key is missing
29
56
 
30
- def get_or_default(dictionary: dict, key: str, default_value) -> Any:
31
- """Returns value associated with `key` in given `dictionary`, or the given `default_value` if key is missing"""
32
- if key in dictionary and dictionary[key] is not None:
33
- return dictionary[key]
34
- log().debug(_DEFAULT_USED.format(default_value, key))
35
- return default_value
57
+ Raises:
58
+ ScenarioError: if assertion is False, logged on level "ERROR"
59
+ """
60
+ if not assertion:
61
+ raise log_scenario_error(error_message)
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2024 German Aerospace Center <fame@dlr.de>
1
+ # SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
  from fameio.input.schema import Schema
@@ -9,31 +9,87 @@ from .stringset import StringSet
9
9
 
10
10
 
11
11
  class FameIOFactory:
12
- """Factory used to instantiate the types defined in a scenario file.
12
+ """
13
+ Factory used to instantiate the types defined in a scenario file.
13
14
  This allows a client to subclass some types in order to extend what a scenario can contain.
14
15
  """
15
16
 
16
17
  @staticmethod
17
18
  def new_schema_from_dict(definitions: dict) -> Schema:
18
- """Constructs a new Schema from provided `definitions`"""
19
+ """
20
+ Load given dictionary `definitions` into a new schema
21
+
22
+ Args:
23
+ definitions: dictionary representation of schema
24
+
25
+ Returns:
26
+ new Schema
27
+
28
+ Raises:
29
+ SchemaError: if definitions are incomplete or erroneous, logged on level "ERROR"
30
+ """
19
31
  return Schema.from_dict(definitions)
20
32
 
21
33
  @staticmethod
22
34
  def new_general_properties_from_dict(definitions: dict) -> GeneralProperties:
23
- """Constructs a new GeneralProperties instance from provided `definitions`"""
35
+ """
36
+ Parse general properties from provided `definitions`
37
+
38
+ Args:
39
+ definitions: dictionary representation of general properties
40
+
41
+ Returns:
42
+ new GeneralProperties
43
+
44
+ Raises:
45
+ ScenarioError: if definitions are incomplete or erroneous, logged on level "ERROR"
46
+ """
24
47
  return GeneralProperties.from_dict(definitions)
25
48
 
26
49
  @staticmethod
27
50
  def new_agent_from_dict(definitions: dict) -> Agent:
28
- """Constructs a new Agent from provided `definitions`"""
51
+ """
52
+ Parses an agent from provided `definitions`
53
+
54
+ Args:
55
+ definitions: dictionary representation of an agent
56
+
57
+ Returns:
58
+ new agent
59
+
60
+ Raises:
61
+ ScenarioError: if definitions are incomplete or erroneous, logged on level "ERROR"
62
+ """
29
63
  return Agent.from_dict(definitions)
30
64
 
31
65
  @staticmethod
32
66
  def new_contract_from_dict(definitions: dict) -> Contract:
33
- """Constructs a new Contract from provided `definitions`"""
67
+ """
68
+ Parses contract from given `definitions`
69
+
70
+ Args:
71
+ definitions: dictionary representation of a contract
72
+
73
+ Returns:
74
+ new contract
75
+
76
+ Raises:
77
+ ContractError: if definitions are incomplete or erroneous, logged on level "ERROR"
78
+ """
34
79
  return Contract.from_dict(definitions)
35
80
 
36
81
  @staticmethod
37
82
  def new_string_set_from_dict(definition: StringSet.StringSetType) -> StringSet:
38
- """Constructs a new StringSet from provided `definitions`"""
83
+ """
84
+ Returns string set initialised from `definition`
85
+
86
+ Args:
87
+ definition: dictionary representation of string set
88
+
89
+ Returns:
90
+ new string set
91
+
92
+ Raises:
93
+ StringSetError: if definitions are incomplete or erroneous, logged on level "ERROR"
94
+ """
39
95
  return StringSet.from_dict(definition)