fameio 3.1.1__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 +10 -10
- fameio/cli/make_config.py +9 -9
- fameio/cli/options.py +6 -4
- fameio/cli/parser.py +87 -51
- fameio/cli/reformat.py +58 -0
- fameio/input/__init__.py +4 -4
- fameio/input/loader/__init__.py +13 -13
- fameio/input/loader/controller.py +64 -18
- fameio/input/loader/loader.py +25 -16
- fameio/input/metadata.py +57 -38
- fameio/input/resolver.py +9 -10
- fameio/input/scenario/agent.py +62 -26
- fameio/input/scenario/attribute.py +93 -40
- fameio/input/scenario/contract.py +160 -56
- fameio/input/scenario/exception.py +41 -18
- fameio/input/scenario/fameiofactory.py +57 -6
- fameio/input/scenario/generalproperties.py +22 -12
- fameio/input/scenario/scenario.py +117 -38
- fameio/input/scenario/stringset.py +29 -11
- fameio/input/schema/agenttype.py +27 -10
- fameio/input/schema/attribute.py +108 -45
- fameio/input/schema/java_packages.py +14 -12
- fameio/input/schema/schema.py +39 -15
- fameio/input/validator.py +198 -54
- fameio/input/writer.py +137 -46
- fameio/logs.py +28 -47
- fameio/output/__init__.py +5 -1
- fameio/output/agent_type.py +89 -28
- fameio/output/conversion.py +52 -37
- fameio/output/csv_writer.py +107 -27
- fameio/output/data_transformer.py +17 -24
- fameio/output/execution_dao.py +170 -0
- fameio/output/input_dao.py +71 -33
- fameio/output/output_dao.py +33 -11
- fameio/output/reader.py +64 -21
- fameio/output/yaml_writer.py +16 -8
- fameio/scripts/__init__.py +22 -4
- fameio/scripts/convert_results.py +126 -52
- fameio/scripts/convert_results.py.license +1 -1
- fameio/scripts/exception.py +7 -0
- fameio/scripts/make_config.py +34 -13
- fameio/scripts/make_config.py.license +1 -1
- fameio/scripts/reformat.py +71 -0
- fameio/scripts/reformat.py.license +3 -0
- fameio/series.py +174 -59
- fameio/time.py +79 -25
- fameio/tools.py +48 -8
- {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/METADATA +50 -34
- fameio-3.3.0.dist-info/RECORD +60 -0
- {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/WHEEL +1 -1
- {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/entry_points.txt +1 -0
- CHANGELOG.md +0 -288
- fameio-3.1.1.dist-info/RECORD +0 -56
- {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/LICENSE.txt +0 -0
- {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/LICENSES/CC-BY-4.0.txt +0 -0
- {fameio-3.1.1.dist-info → fameio-3.3.0.dist-info}/LICENSES/CC0-1.0.txt +0 -0
@@ -9,11 +9,12 @@ 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
|
+
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
|
@@ -59,51 +72,80 @@ class Attribute(Metadata):
|
|
59
72
|
self._nested_list: list[Attribute.__NestedMeta] | None = None
|
60
73
|
|
61
74
|
if data_type is Attribute.__DefinitionType.VALUE:
|
62
|
-
|
63
|
-
self._value = value
|
75
|
+
self._value = self._extract_value(definitions).value # type: ignore[arg-type]
|
64
76
|
elif data_type is Attribute.__DefinitionType.VALUE_LIST:
|
65
|
-
self._value_list = self._extract_values(definitions)
|
77
|
+
self._value_list = self._extract_values(definitions) # type: ignore[arg-type]
|
66
78
|
elif data_type is Attribute.__DefinitionType.NESTED:
|
67
|
-
self._nested = Attribute._build_attribute_dict(name, definitions)
|
79
|
+
self._nested = Attribute._build_attribute_dict(name, definitions) # type: ignore[arg-type]
|
68
80
|
elif data_type is Attribute.__DefinitionType.NESTED_LIST:
|
69
81
|
self._nested_list = []
|
70
82
|
values = keys_to_lower(definitions)[Attribute.KEY_VALUES] if isinstance(definitions, dict) else definitions
|
71
|
-
for list_index, definition in enumerate(values):
|
83
|
+
for list_index, definition in enumerate(values): # type: ignore[arg-type]
|
72
84
|
list_meta = MetadataComponent(definition)
|
73
85
|
list_extended_name = name + Attribute.NAME_STRING_SEPARATOR + str(list_index)
|
74
86
|
nested_items = Attribute._build_attribute_dict(list_extended_name, definition)
|
75
87
|
self._nested_list.append(Attribute.__NestedMeta(value=nested_items, meta=list_meta))
|
76
88
|
|
77
89
|
@staticmethod
|
78
|
-
def _get_data_type(
|
79
|
-
"""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
|
+
"""
|
80
102
|
if isinstance(definitions, list):
|
81
103
|
if len(definitions) == 0:
|
82
|
-
|
104
|
+
raise log_error(ValueError(Attribute._ERR_LIST_EMPTY))
|
83
105
|
return Attribute._get_data_type_list(definitions)
|
84
106
|
if isinstance(definitions, dict):
|
85
107
|
if len(definitions) == 0:
|
86
|
-
|
108
|
+
raise log_error(ValueError(Attribute._ERR_DICT_EMPTY))
|
87
109
|
return Attribute._get_data_type_dict(definitions)
|
88
110
|
return Attribute.__DefinitionType.VALUE
|
89
111
|
|
90
112
|
@staticmethod
|
91
113
|
def _get_data_type_list(definitions: list[Any]) -> Attribute.__DefinitionType:
|
92
|
-
"""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
|
+
"""
|
93
125
|
if all(Attribute._is_value_definition(entry) for entry in definitions):
|
94
126
|
return Attribute.__DefinitionType.VALUE_LIST
|
95
127
|
if Attribute._is_list_of_dict(definitions):
|
96
128
|
return Attribute.__DefinitionType.NESTED_LIST
|
97
|
-
|
129
|
+
raise log_error(ValueError(Attribute._ERR_MIXED_DATA))
|
98
130
|
|
99
131
|
@staticmethod
|
100
132
|
def _is_list_of_dict(definitions: list) -> bool:
|
101
|
-
"""Returns True if given `definitions` is a list of (only) dict"""
|
133
|
+
"""Returns True if given `definitions` is a list of (only) dict."""
|
102
134
|
return all(isinstance(entry, dict) for entry in definitions)
|
103
135
|
|
104
136
|
@staticmethod
|
105
137
|
def _get_data_type_dict(definitions: dict[str, Any]) -> Attribute.__DefinitionType:
|
106
|
-
"""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
|
+
"""
|
107
149
|
low_keys = keys_to_lower(definitions)
|
108
150
|
if Attribute.KEY_VALUE in low_keys.keys():
|
109
151
|
return Attribute.__DefinitionType.VALUE
|
@@ -113,19 +155,19 @@ class Attribute(Metadata):
|
|
113
155
|
return Attribute.__DefinitionType.VALUE_LIST
|
114
156
|
if Attribute._is_list_of_dict(values):
|
115
157
|
return Attribute.__DefinitionType.NESTED_LIST
|
116
|
-
|
158
|
+
raise log_error(ValueError(Attribute._ERR_MIXED_DATA))
|
117
159
|
return Attribute.__DefinitionType.NESTED
|
118
160
|
|
119
161
|
@staticmethod
|
120
162
|
def _is_value_definition(definition: Any) -> bool:
|
121
|
-
"""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."""
|
122
164
|
if isinstance(definition, dict):
|
123
165
|
return Attribute.KEY_VALUE in keys_to_lower(definition).keys()
|
124
166
|
return isinstance(definition, (str, Number))
|
125
167
|
|
126
168
|
@staticmethod
|
127
169
|
def _extract_value(definition: str | Number | dict[str, Any]) -> Attribute.__ValueMeta:
|
128
|
-
"""Creates a ValueMeta Tuple associating a Value with its optional metadata"""
|
170
|
+
"""Creates a ValueMeta Tuple associating a Value with its optional metadata."""
|
129
171
|
if isinstance(definition, dict):
|
130
172
|
return Attribute.__ValueMeta(
|
131
173
|
value=keys_to_lower(definition)[Attribute.KEY_VALUE], meta=MetadataComponent(definition)
|
@@ -134,27 +176,38 @@ class Attribute(Metadata):
|
|
134
176
|
|
135
177
|
@staticmethod
|
136
178
|
def _extract_values(definition: list | dict) -> list[Attribute.__ValueMeta]:
|
137
|
-
"""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."""
|
138
180
|
values = keys_to_lower(definition)[Attribute.KEY_VALUES] if isinstance(definition, dict) else definition
|
139
181
|
return [Attribute._extract_value(entry) for entry in values]
|
140
182
|
|
141
183
|
@staticmethod
|
142
|
-
def _build_attribute_dict(
|
143
|
-
"""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
|
+
"""
|
144
197
|
inner_elements = {}
|
145
198
|
for nested_name, value in definitions.items():
|
146
|
-
full_name =
|
199
|
+
full_name = parent_name + Attribute.NAME_STRING_SEPARATOR + nested_name
|
147
200
|
inner_elements[nested_name] = Attribute(full_name, value)
|
148
201
|
return inner_elements
|
149
202
|
|
150
203
|
@property
|
151
204
|
def has_value(self) -> bool:
|
152
|
-
"""Returns True if Attribute has any value assigned"""
|
205
|
+
"""Returns True if Attribute has any value assigned."""
|
153
206
|
return self._value is not None or self._value_list is not None
|
154
207
|
|
155
208
|
@property
|
156
209
|
def value(self) -> str | Number | list[str | Number] | None:
|
157
|
-
"""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."""
|
158
211
|
if self._value is not None:
|
159
212
|
return self._value
|
160
213
|
if self._value_list is not None:
|
@@ -163,23 +216,23 @@ class Attribute(Metadata):
|
|
163
216
|
|
164
217
|
@property
|
165
218
|
def has_nested(self) -> bool:
|
166
|
-
"""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."""
|
167
220
|
return self._nested is not None
|
168
221
|
|
169
222
|
@property
|
170
223
|
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.
|
224
|
+
"""Returns dictionary of all nested Attributes if nested Attributes are present, else empty dict."""
|
225
|
+
return self._nested if self._nested is not None else {}
|
173
226
|
|
174
227
|
@property
|
175
228
|
def has_nested_list(self) -> bool:
|
176
|
-
"""Returns True if list of nested items is present"""
|
229
|
+
"""Returns True if list of nested items is present."""
|
177
230
|
return self._nested_list is not None
|
178
231
|
|
179
232
|
@property
|
180
233
|
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.
|
234
|
+
"""Return list of all nested Attribute dictionaries if such are present, else an empty list."""
|
235
|
+
return [entry.value for entry in self._nested_list] if self._nested_list is not None else []
|
183
236
|
|
184
237
|
def __repr__(self) -> str:
|
185
238
|
return self._full_name
|
@@ -3,18 +3,21 @@
|
|
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
|
-
"""Contract between two Agents of a scenario"""
|
17
|
+
"""Contract between two Agents of a scenario."""
|
18
|
+
|
19
|
+
class ContractError(InputError):
|
20
|
+
"""An error that occurred while parsing Contract definitions."""
|
18
21
|
|
19
22
|
KEY_SENDER: Final[str] = "SenderId".lower()
|
20
23
|
KEY_RECEIVER: Final[str] = "ReceiverId".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,116 +47,198 @@ 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
|
-
"""Constructs a new Contract
|
53
|
+
"""Constructs a new Contract.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
sender_id: unique id of sender
|
57
|
+
receiver_id: unique id of receiver
|
58
|
+
product_name: name of contracted product
|
59
|
+
delivery_interval: time interval in steps between contract deliveries
|
60
|
+
first_delivery_time: absolute time of first contract execution
|
61
|
+
expiration_time: absolute time at which contract execution stops
|
62
|
+
metadata: any metadata associated with the contract
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
new Contract
|
66
|
+
|
67
|
+
Raises:
|
68
|
+
ContractError: if delivery interval is invalid, logged on level "ERROR"
|
69
|
+
"""
|
49
70
|
super().__init__({self.KEY_METADATA: metadata} if metadata else None)
|
50
|
-
|
71
|
+
if product_name.strip() == "":
|
72
|
+
raise log_error(self.ContractError(self._ERR_PRODUCT_EMPTY))
|
51
73
|
if sender_id == receiver_id:
|
52
74
|
log().warning(self._ERR_SENDER_IS_RECEIVER.format(sender_id))
|
53
75
|
if delivery_interval <= 0:
|
54
|
-
raise
|
76
|
+
raise log_error(self.ContractError(self._ERR_INTERVAL_NOT_POSITIVE.format(delivery_interval)))
|
55
77
|
self._sender_id = sender_id
|
56
78
|
self._receiver_id = receiver_id
|
57
79
|
self._product_name = product_name
|
58
80
|
self._delivery_interval = delivery_interval
|
59
81
|
self._first_delivery_time = first_delivery_time
|
60
82
|
self._expiration_time = expiration_time
|
61
|
-
self._attributes = {}
|
83
|
+
self._attributes: dict = {}
|
62
84
|
|
63
85
|
def _notify_data_changed(self):
|
64
|
-
"""Placeholder method used to signal data changes to derived types"""
|
86
|
+
"""Placeholder method used to signal data changes to derived types."""
|
65
87
|
|
66
88
|
@property
|
67
89
|
def product_name(self) -> str:
|
68
|
-
"""Returns the product name of the contract"""
|
90
|
+
"""Returns the product name of the contract."""
|
69
91
|
return self._product_name
|
70
92
|
|
71
93
|
@property
|
72
94
|
def sender_id(self) -> int:
|
73
|
-
"""Returns the sender ID of the contract"""
|
95
|
+
"""Returns the sender ID of the contract."""
|
74
96
|
return self._sender_id
|
75
97
|
|
76
98
|
@property
|
77
99
|
def display_sender_id(self) -> str:
|
78
|
-
"""Returns the sender ID of the contract as a string for display purposes"""
|
100
|
+
"""Returns the sender ID of the contract as a string for display purposes."""
|
79
101
|
return f"#{self._sender_id}"
|
80
102
|
|
81
103
|
@property
|
82
104
|
def receiver_id(self) -> int:
|
83
|
-
"""Returns the receiver ID of the contract"""
|
105
|
+
"""Returns the receiver ID of the contract."""
|
84
106
|
return self._receiver_id
|
85
107
|
|
86
108
|
@property
|
87
109
|
def display_receiver_id(self) -> str:
|
88
|
-
"""Returns the receiver ID of the contract as a string for display purposes"""
|
110
|
+
"""Returns the receiver ID of the contract as a string for display purposes."""
|
89
111
|
return f"#{self._receiver_id}"
|
90
112
|
|
91
113
|
@property
|
92
114
|
def delivery_interval(self) -> int:
|
93
|
-
"""Returns the delivery interval of the contract (in steps)"""
|
115
|
+
"""Returns the delivery interval of the contract (in steps)."""
|
94
116
|
return self._delivery_interval
|
95
117
|
|
96
118
|
@property
|
97
119
|
def first_delivery_time(self) -> int:
|
98
|
-
"""Returns the first delivery time of the contract"""
|
120
|
+
"""Returns the first delivery time of the contract."""
|
99
121
|
return self._first_delivery_time
|
100
122
|
|
101
123
|
@property
|
102
|
-
def expiration_time(self) ->
|
103
|
-
"""Returns the expiration time of the contract if available, None otherwise"""
|
124
|
+
def expiration_time(self) -> int | None:
|
125
|
+
"""Returns the expiration time of the contract if available, None otherwise."""
|
104
126
|
return self._expiration_time
|
105
127
|
|
106
128
|
@property
|
107
129
|
def attributes(self) -> dict[str, Attribute]:
|
108
|
-
"""Returns dictionary of all Attributes of the contract"""
|
130
|
+
"""Returns dictionary of all Attributes of the contract."""
|
109
131
|
return self._attributes
|
110
132
|
|
111
133
|
def add_attribute(self, name: str, value: Attribute) -> None:
|
112
|
-
"""Adds a new attribute to the Contract
|
134
|
+
"""Adds a new attribute to the Contract.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
name: of the attribute
|
138
|
+
value: of the attribute
|
139
|
+
|
140
|
+
Raises:
|
141
|
+
ContractError: if attribute already exists, logged on level "ERROR"
|
142
|
+
"""
|
113
143
|
if name in self._attributes:
|
114
|
-
raise
|
144
|
+
raise log_error(self.ContractError(self._ERR_DOUBLE_ATTRIBUTE.format(name)))
|
115
145
|
self._attributes[name] = value
|
116
146
|
self._notify_data_changed()
|
117
147
|
|
118
148
|
@classmethod
|
119
149
|
def from_dict(cls, definitions: dict) -> Contract:
|
120
|
-
"""Parses
|
150
|
+
"""Parses contract from given `definitions`.
|
151
|
+
|
152
|
+
Args:
|
153
|
+
definitions: dictionary representation of a contract
|
154
|
+
|
155
|
+
Returns:
|
156
|
+
new contract
|
157
|
+
|
158
|
+
Raises:
|
159
|
+
ContractError: if definitions are incomplete or erroneous, logged on level "ERROR"
|
160
|
+
"""
|
121
161
|
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
|
-
)
|
162
|
+
sender_id = Contract._get_or_raise(definitions, Contract.KEY_SENDER, Contract._ERR_MISSING_KEY)
|
163
|
+
receiver_id = Contract._get_or_raise(definitions, Contract.KEY_RECEIVER, Contract._ERR_MISSING_KEY)
|
164
|
+
product_name = Contract._get_or_raise(definitions, Contract.KEY_PRODUCT, Contract._ERR_MISSING_KEY)
|
165
|
+
|
166
|
+
first_delivery_time = Contract._get_time(definitions, Contract.KEY_FIRST_DELIVERY)
|
167
|
+
delivery_interval = Contract._get_or_raise(definitions, Contract.KEY_INTERVAL, Contract._ERR_MISSING_KEY)
|
168
|
+
expiration_time = Contract._get_time(definitions, Contract.KEY_EXPIRE, mandatory=False)
|
169
|
+
|
170
|
+
contract = cls(sender_id, receiver_id, product_name, delivery_interval, first_delivery_time, expiration_time)
|
140
171
|
contract._extract_metadata(definitions)
|
141
|
-
|
142
|
-
contract._init_attributes_from_dict(attribute_definitions)
|
172
|
+
contract._init_attributes_from_dict(definitions.get(Contract.KEY_ATTRIBUTES, {}))
|
143
173
|
return contract
|
144
174
|
|
175
|
+
@staticmethod
|
176
|
+
def _get_or_raise(dictionary: dict, key: str, error_message: str) -> Any:
|
177
|
+
"""Returns value associated with `key` in given `dictionary`, or raises exception if key or value is missing.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
dictionary: to search the key in
|
181
|
+
key: to be searched
|
182
|
+
error_message: to be logged and included in the raised exception if key is missing
|
183
|
+
|
184
|
+
Returns:
|
185
|
+
value associated with given key in given dictionary
|
186
|
+
|
187
|
+
Raises:
|
188
|
+
ContractError: if given key is not in given dictionary or value is None, logged on level "ERROR"
|
189
|
+
"""
|
190
|
+
if key not in dictionary or dictionary[key] is None:
|
191
|
+
raise log_error(Contract.ContractError(error_message.format(key)))
|
192
|
+
return dictionary[key]
|
193
|
+
|
194
|
+
@staticmethod
|
195
|
+
@overload
|
196
|
+
def _get_time(definitions: dict, key: str) -> int: ... # noqa: E704
|
197
|
+
|
198
|
+
@staticmethod
|
199
|
+
@overload
|
200
|
+
def _get_time(definitions: dict, key: str, mandatory: bool) -> int | None: ... # noqa: E704
|
201
|
+
|
202
|
+
@staticmethod
|
203
|
+
def _get_time(definitions: dict, key: str, mandatory: bool = True) -> int | None:
|
204
|
+
"""Extract time representation value at given key, and, if present, convert to integer, else return None.
|
205
|
+
|
206
|
+
Args:
|
207
|
+
definitions: to search given key in
|
208
|
+
key: to check for an associated value
|
209
|
+
mandatory: if true, also raises an error if key is missing
|
210
|
+
|
211
|
+
Returns:
|
212
|
+
None if key is not mandatory/present, else the integer representation of the time value associated with key
|
213
|
+
|
214
|
+
Raises:
|
215
|
+
ContractError: if found value could not be converted or mandatory value is missing, logged on level "ERROR"
|
216
|
+
"""
|
217
|
+
if key in definitions:
|
218
|
+
value = definitions[key]
|
219
|
+
try:
|
220
|
+
return FameTime.convert_string_if_is_datetime(value)
|
221
|
+
except ConversionError as e:
|
222
|
+
raise log_error(Contract.ContractError(Contract._ERR_TIME_CONVERSION.format(key, value))) from e
|
223
|
+
if mandatory:
|
224
|
+
raise log_error(Contract.ContractError(Contract._ERR_MISSING_KEY.format(key)))
|
225
|
+
return None
|
226
|
+
|
145
227
|
def _init_attributes_from_dict(self, attributes: dict[str, Any]) -> None:
|
146
|
-
"""Resets Contract `attributes` from dict
|
147
|
-
|
148
|
-
|
228
|
+
"""Resets Contract `attributes` from dict.
|
229
|
+
|
230
|
+
Args:
|
231
|
+
attributes: key-value pairs of attributes to be set
|
232
|
+
|
233
|
+
Raises:
|
234
|
+
ContractError: if some of provided attributes were already present
|
235
|
+
"""
|
149
236
|
for name, value in attributes.items():
|
150
237
|
full_name = f"{type}.{id}{name}"
|
151
238
|
self.add_attribute(name, Attribute(full_name, value))
|
152
239
|
|
153
240
|
def _to_dict(self) -> dict:
|
154
|
-
"""Serializes the Contract content to a dict"""
|
241
|
+
"""Serializes the Contract content to a dict."""
|
155
242
|
result = {
|
156
243
|
self.KEY_SENDER: self.sender_id,
|
157
244
|
self.KEY_RECEIVER: self.receiver_id,
|
@@ -169,7 +256,20 @@ class Contract(Metadata):
|
|
169
256
|
|
170
257
|
@staticmethod
|
171
258
|
def split_contract_definitions(multi_definition: dict) -> list[dict]:
|
172
|
-
"""
|
259
|
+
"""Split given M:N `multi_definition` of contracts to multiple 1:1 contracts.
|
260
|
+
|
261
|
+
Splits given dictionary of contracts with potentially more than ore sender and/or receiver into a list
|
262
|
+
of individual contract definitions with one sender and one receiver.
|
263
|
+
|
264
|
+
Args:
|
265
|
+
multi_definition: contract definitions with potentially more than ore sender and/or receiver
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
list of contract definitions with exactly one sender and receiver
|
269
|
+
|
270
|
+
Raises:
|
271
|
+
ContractError: if multi_definition is incomplete or erroneous, logged on level "ERROR"
|
272
|
+
"""
|
173
273
|
contracts = []
|
174
274
|
base_data = {}
|
175
275
|
multi_definition = keys_to_lower(multi_definition)
|
@@ -183,8 +283,12 @@ class Contract(Metadata):
|
|
183
283
|
]:
|
184
284
|
if key in multi_definition:
|
185
285
|
base_data[key] = multi_definition[key]
|
186
|
-
senders = ensure_is_list(
|
187
|
-
|
286
|
+
senders = ensure_is_list(
|
287
|
+
Contract._get_or_raise(multi_definition, Contract.KEY_SENDER, Contract._ERR_MISSING_KEY)
|
288
|
+
)
|
289
|
+
receivers = ensure_is_list(
|
290
|
+
Contract._get_or_raise(multi_definition, Contract.KEY_RECEIVER, Contract._ERR_MISSING_KEY)
|
291
|
+
)
|
188
292
|
if len(senders) > 1 and len(receivers) == 1:
|
189
293
|
for index, sender in enumerate(senders):
|
190
294
|
contracts.append(Contract._copy_contract(sender, receivers[0], base_data))
|
@@ -195,12 +299,12 @@ class Contract(Metadata):
|
|
195
299
|
for index in range(len(senders)): # pylint: disable=consider-using-enumerate
|
196
300
|
contracts.append(Contract._copy_contract(senders[index], receivers[index], base_data))
|
197
301
|
else:
|
198
|
-
|
302
|
+
raise log_error(Contract.ContractError(Contract._ERR_MULTI_CONTRACT_CORRUPT.format(senders, receivers)))
|
199
303
|
return contracts
|
200
304
|
|
201
305
|
@staticmethod
|
202
306
|
def _copy_contract(sender: int, receiver: int, base_data: dict) -> dict:
|
203
|
-
"""Returns a new contract definition dictionary, with given `sender` and `receiver` and copied `base_data
|
307
|
+
"""Returns a new contract definition dictionary, with given `sender` and `receiver` and copied `base_data`."""
|
204
308
|
contract = {
|
205
309
|
Contract.KEY_SENDER: sender,
|
206
310
|
Contract.KEY_RECEIVER: receiver,
|