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.
- fameio/cli/convert_results.py +6 -4
- fameio/cli/make_config.py +6 -4
- fameio/cli/parser.py +41 -29
- fameio/input/__init__.py +1 -1
- fameio/input/loader/__init__.py +9 -7
- fameio/input/loader/controller.py +59 -8
- fameio/input/loader/loader.py +14 -7
- fameio/input/metadata.py +35 -13
- fameio/input/resolver.py +5 -4
- fameio/input/scenario/agent.py +50 -16
- fameio/input/scenario/attribute.py +14 -15
- fameio/input/scenario/contract.py +152 -43
- fameio/input/scenario/exception.py +44 -18
- fameio/input/scenario/fameiofactory.py +63 -7
- fameio/input/scenario/generalproperties.py +17 -6
- fameio/input/scenario/scenario.py +111 -28
- fameio/input/scenario/stringset.py +27 -8
- fameio/input/schema/agenttype.py +21 -2
- fameio/input/schema/attribute.py +91 -22
- fameio/input/schema/java_packages.py +8 -5
- fameio/input/schema/schema.py +35 -9
- fameio/input/validator.py +22 -15
- fameio/input/writer.py +136 -36
- fameio/logs.py +3 -31
- fameio/output/__init__.py +5 -1
- fameio/output/agent_type.py +86 -23
- fameio/output/conversion.py +47 -29
- fameio/output/csv_writer.py +88 -18
- fameio/output/data_transformer.py +7 -14
- fameio/output/input_dao.py +62 -21
- fameio/output/output_dao.py +26 -4
- fameio/output/reader.py +58 -13
- fameio/output/yaml_writer.py +15 -6
- fameio/scripts/__init__.py +9 -2
- fameio/scripts/convert_results.py +123 -50
- fameio/scripts/convert_results.py.license +1 -1
- fameio/scripts/exception.py +7 -0
- fameio/scripts/make_config.py +34 -12
- fameio/scripts/make_config.py.license +1 -1
- fameio/series.py +117 -33
- fameio/time.py +74 -17
- fameio/tools.py +7 -5
- {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/METADATA +19 -13
- fameio-3.2.0.dist-info/RECORD +56 -0
- {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/WHEEL +1 -1
- CHANGELOG.md +0 -288
- fameio-3.1.1.dist-info/RECORD +0 -56
- {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/LICENSE.txt +0 -0
- {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/LICENSES/CC-BY-4.0.txt +0 -0
- {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/LICENSES/CC0-1.0.txt +0 -0
- {fameio-3.1.1.dist-info → fameio-3.2.0.dist-info}/entry_points.txt +0 -0
fameio/input/scenario/agent.py
CHANGED
@@ -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,
|
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
|
23
|
-
|
24
|
-
|
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.
|
32
|
-
assert_or_raise(bool(type_name and type_name.strip()), self.
|
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
|
-
"""
|
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.
|
42
|
-
agent_id = get_or_raise(definitions, Agent.KEY_ID, Agent.
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
agent.init_attributes_from_dict(
|
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
|
-
"""
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
172
|
-
return self._nested if self.
|
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
|
182
|
-
return [entry.value for entry in self._nested_list] if self.
|
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,
|
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.
|
10
|
-
from fameio.
|
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-
|
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:
|
46
|
-
metadata:
|
50
|
+
expiration_time: int | None = None,
|
51
|
+
metadata: dict | None = None,
|
47
52
|
) -> None:
|
48
|
-
"""
|
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
|
-
|
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
|
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) ->
|
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
|
-
"""
|
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
|
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
|
-
"""
|
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 =
|
123
|
-
receiver_id =
|
124
|
-
product_name =
|
125
|
-
|
126
|
-
|
127
|
-
)
|
128
|
-
|
129
|
-
|
130
|
-
|
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
|
-
|
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
|
-
"""
|
147
|
-
|
148
|
-
|
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
|
-
"""
|
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(
|
187
|
-
|
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
|
-
|
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
|
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
|
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
|
13
|
-
"""
|
14
|
-
|
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
|
-
|
18
|
-
|
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
|
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,
|
25
|
-
"""
|
26
|
-
if
|
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
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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:
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
"""
|
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
|
-
"""
|
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)
|