fameio 3.1.0__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/__init__.py +2 -3
- fameio/cli/convert_results.py +6 -4
- fameio/cli/make_config.py +6 -4
- fameio/cli/options.py +3 -3
- fameio/cli/parser.py +43 -31
- fameio/input/__init__.py +1 -9
- fameio/input/loader/__init__.py +9 -7
- fameio/input/loader/controller.py +64 -14
- fameio/input/loader/loader.py +14 -7
- fameio/input/metadata.py +37 -18
- fameio/input/resolver.py +5 -4
- fameio/input/scenario/__init__.py +7 -8
- fameio/input/scenario/agent.py +52 -19
- fameio/input/scenario/attribute.py +28 -29
- fameio/input/scenario/contract.py +161 -52
- fameio/input/scenario/exception.py +45 -22
- 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/__init__.py +5 -5
- fameio/input/schema/agenttype.py +29 -11
- fameio/input/schema/attribute.py +174 -84
- fameio/input/schema/java_packages.py +8 -5
- fameio/input/schema/schema.py +35 -9
- fameio/input/validator.py +58 -42
- fameio/input/writer.py +139 -41
- fameio/logs.py +23 -17
- fameio/output/__init__.py +5 -1
- fameio/output/agent_type.py +93 -27
- fameio/output/conversion.py +48 -30
- fameio/output/csv_writer.py +88 -18
- fameio/output/data_transformer.py +12 -21
- fameio/output/input_dao.py +68 -32
- fameio/output/output_dao.py +26 -4
- fameio/output/reader.py +61 -18
- fameio/output/yaml_writer.py +18 -9
- fameio/scripts/__init__.py +9 -2
- fameio/scripts/convert_results.py +144 -52
- 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 +132 -47
- fameio/time.py +88 -37
- fameio/tools.py +9 -8
- {fameio-3.1.0.dist-info → fameio-3.2.0.dist-info}/METADATA +19 -13
- fameio-3.2.0.dist-info/RECORD +56 -0
- {fameio-3.1.0.dist-info → fameio-3.2.0.dist-info}/WHEEL +1 -1
- CHANGELOG.md +0 -279
- fameio-3.1.0.dist-info/RECORD +0 -56
- {fameio-3.1.0.dist-info → fameio-3.2.0.dist-info}/LICENSE.txt +0 -0
- {fameio-3.1.0.dist-info → fameio-3.2.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {fameio-3.1.0.dist-info → fameio-3.2.0.dist-info}/LICENSES/CC-BY-4.0.txt +0 -0
- {fameio-3.1.0.dist-info → fameio-3.2.0.dist-info}/LICENSES/CC0-1.0.txt +0 -0
- {fameio-3.1.0.dist-info → fameio-3.2.0.dist-info}/entry_points.txt +0 -0
fameio/input/resolver.py
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
-
# SPDX-FileCopyrightText:
|
1
|
+
# SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
4
|
+
from __future__ import annotations
|
5
|
+
|
4
6
|
import glob
|
5
7
|
from os import path
|
6
|
-
from typing import Optional
|
7
8
|
|
8
9
|
|
9
10
|
class PathResolver:
|
@@ -23,7 +24,7 @@ class PathResolver:
|
|
23
24
|
return glob.glob(absolute_path)
|
24
25
|
|
25
26
|
# noinspection PyMethodMayBeStatic
|
26
|
-
def resolve_series_file_path(self, file_name: str) ->
|
27
|
+
def resolve_series_file_path(self, file_name: str) -> str | None:
|
27
28
|
"""
|
28
29
|
Searches for the file in the current working directory and returns its absolute file path
|
29
30
|
|
@@ -38,7 +39,7 @@ class PathResolver:
|
|
38
39
|
return file_name if path.isabs(file_name) else PathResolver._search_file_in_directory(file_name, path.curdir)
|
39
40
|
|
40
41
|
@staticmethod
|
41
|
-
def _search_file_in_directory(file_name: str, directory: str) ->
|
42
|
+
def _search_file_in_directory(file_name: str, directory: str) -> str | None:
|
42
43
|
"""Returns path to said `file_name` relative to specified `directory` if file was found there, None otherwise"""
|
43
44
|
file_path = path.join(directory, file_name)
|
44
45
|
return file_path if path.exists(file_path) else None
|
@@ -1,10 +1,9 @@
|
|
1
|
-
# SPDX-FileCopyrightText:
|
1
|
+
# SPDX-FileCopyrightText: 2025 German Aerospace Center <fame@dlr.de>
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
4
|
-
|
5
|
-
from .
|
6
|
-
from .
|
7
|
-
from .
|
8
|
-
from .
|
9
|
-
from .
|
10
|
-
from .stringset import StringSet
|
4
|
+
from .agent import Agent # noqa: F401
|
5
|
+
from .attribute import Attribute # noqa: F401
|
6
|
+
from .contract import Contract # noqa: F401
|
7
|
+
from .generalproperties import GeneralProperties # noqa: F401
|
8
|
+
from .scenario import Scenario # noqa: F401
|
9
|
+
from .stringset import StringSet # noqa: F401
|
fameio/input/scenario/agent.py
CHANGED
@@ -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 __future__ import annotations
|
@@ -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(
|
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():
|
@@ -79,7 +113,6 @@ class Agent(Metadata):
|
|
79
113
|
|
80
114
|
def _notify_data_changed(self):
|
81
115
|
"""Placeholder method used to signal data changes to derived types"""
|
82
|
-
pass
|
83
116
|
|
84
117
|
@property
|
85
118
|
def id(self) -> int:
|
@@ -89,7 +122,7 @@ class Agent(Metadata):
|
|
89
122
|
@property
|
90
123
|
def display_id(self) -> str:
|
91
124
|
"""Returns the ID of the Agent as a string for display purposes"""
|
92
|
-
return "#{
|
125
|
+
return f"#{self._id}"
|
93
126
|
|
94
127
|
@property
|
95
128
|
def type_name(self) -> str:
|
@@ -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 __future__ import annotations
|
@@ -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,28 +78,27 @@ 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
|
-
|
89
|
-
return Attribute.__DefinitionType.VALUE
|
87
|
+
return Attribute.__DefinitionType.VALUE
|
90
88
|
|
91
89
|
@staticmethod
|
92
90
|
def _get_data_type_list(definitions: list[Any]) -> Attribute.__DefinitionType:
|
93
91
|
"""Returns type of data from a given non-empty list `definitions`"""
|
94
|
-
if all(
|
92
|
+
if all(Attribute._is_value_definition(entry) for entry in definitions):
|
95
93
|
return Attribute.__DefinitionType.VALUE_LIST
|
96
|
-
|
94
|
+
if Attribute._is_list_of_dict(definitions):
|
97
95
|
return Attribute.__DefinitionType.NESTED_LIST
|
98
|
-
|
96
|
+
raise log_scenario_error(Attribute._ERR_MIXED_DATA.format(repr(definitions)))
|
99
97
|
|
100
98
|
@staticmethod
|
101
99
|
def _is_list_of_dict(definitions: list) -> bool:
|
102
100
|
"""Returns True if given `definitions` is a list of (only) dict"""
|
103
|
-
return all(
|
101
|
+
return all(isinstance(entry, dict) for entry in definitions)
|
104
102
|
|
105
103
|
@staticmethod
|
106
104
|
def _get_data_type_dict(definitions: dict[str, Any]) -> Attribute.__DefinitionType:
|
@@ -108,13 +106,13 @@ class Attribute(Metadata):
|
|
108
106
|
low_keys = keys_to_lower(definitions)
|
109
107
|
if Attribute.KEY_VALUE in low_keys.keys():
|
110
108
|
return Attribute.__DefinitionType.VALUE
|
111
|
-
|
109
|
+
if Attribute.KEY_VALUES in low_keys.keys():
|
112
110
|
values = low_keys[Attribute.KEY_VALUES]
|
113
|
-
if all(
|
111
|
+
if all(Attribute._is_value_definition(entry) for entry in values):
|
114
112
|
return Attribute.__DefinitionType.VALUE_LIST
|
115
|
-
|
113
|
+
if Attribute._is_list_of_dict(values):
|
116
114
|
return Attribute.__DefinitionType.NESTED_LIST
|
117
|
-
|
115
|
+
raise log_scenario_error(Attribute._ERR_MIXED_DATA.format(repr(values)))
|
118
116
|
return Attribute.__DefinitionType.NESTED
|
119
117
|
|
120
118
|
@staticmethod
|
@@ -158,7 +156,7 @@ class Attribute(Metadata):
|
|
158
156
|
"""Returns value or list of values if available on this Attribute (ignoring any Metadata), else None"""
|
159
157
|
if self._value is not None:
|
160
158
|
return self._value
|
161
|
-
|
159
|
+
if self._value_list is not None:
|
162
160
|
return [item.value for item in self._value_list]
|
163
161
|
return None
|
164
162
|
|
@@ -169,8 +167,8 @@ class Attribute(Metadata):
|
|
169
167
|
|
170
168
|
@property
|
171
169
|
def nested(self) -> dict[str, Attribute]:
|
172
|
-
"""Returns dictionary of all nested Attributes if nested Attributes are present, else
|
173
|
-
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 {}
|
174
172
|
|
175
173
|
@property
|
176
174
|
def has_nested_list(self) -> bool:
|
@@ -179,8 +177,8 @@ class Attribute(Metadata):
|
|
179
177
|
|
180
178
|
@property
|
181
179
|
def nested_list(self) -> list[dict[str, Attribute]]:
|
182
|
-
"""Return list of all nested Attribute dictionaries if such are present, else
|
183
|
-
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 []
|
184
182
|
|
185
183
|
def __repr__(self) -> str:
|
186
184
|
return self._full_name
|
@@ -188,16 +186,17 @@ class Attribute(Metadata):
|
|
188
186
|
def _to_dict(self) -> dict[str, Any]:
|
189
187
|
if self._value is not None:
|
190
188
|
return {self.KEY_VALUE: self._value}
|
191
|
-
|
189
|
+
if self._value_list is not None:
|
192
190
|
return {
|
193
191
|
self.KEY_VALUES: [{self.KEY_VALUE: entry.value, **entry.meta.to_dict()} for entry in self._value_list]
|
194
192
|
}
|
195
|
-
|
193
|
+
if self._nested is not None:
|
196
194
|
return {name: attribute.to_dict() for name, attribute in self.nested.items()}
|
197
|
-
|
195
|
+
if self._nested_list is not None:
|
198
196
|
return {
|
199
197
|
self.KEY_VALUES: [
|
200
198
|
{**{name: attribute.to_dict() for name, attribute in entry.value.items()}, **entry.meta.to_dict()}
|
201
199
|
for entry in self._nested_list
|
202
200
|
]
|
203
201
|
}
|
202
|
+
return {}
|
@@ -1,21 +1,24 @@
|
|
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 __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,14 +29,17 @@ 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
|
|
42
|
+
# pylint: disable=too-many-arguments, too-many-positional-arguments
|
37
43
|
def __init__(
|
38
44
|
self,
|
39
45
|
sender_id: int,
|
@@ -41,27 +47,44 @@ class Contract(Metadata):
|
|
41
47
|
product_name: str,
|
42
48
|
delivery_interval: int,
|
43
49
|
first_delivery_time: int,
|
44
|
-
expiration_time:
|
45
|
-
metadata:
|
50
|
+
expiration_time: int | None = None,
|
51
|
+
metadata: dict | None = None,
|
46
52
|
) -> None:
|
47
|
-
"""
|
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
|
+
"""
|
48
71
|
super().__init__({self.KEY_METADATA: metadata} if metadata else None)
|
49
|
-
|
72
|
+
if product_name.strip() == "":
|
73
|
+
raise log_error(self.ContractError(self._ERR_PRODUCT_EMPTY))
|
50
74
|
if sender_id == receiver_id:
|
51
75
|
log().warning(self._ERR_SENDER_IS_RECEIVER.format(sender_id))
|
52
76
|
if delivery_interval <= 0:
|
53
|
-
raise
|
77
|
+
raise log_error(self.ContractError(self._ERR_INTERVAL_NOT_POSITIVE.format(delivery_interval)))
|
54
78
|
self._sender_id = sender_id
|
55
79
|
self._receiver_id = receiver_id
|
56
80
|
self._product_name = product_name
|
57
81
|
self._delivery_interval = delivery_interval
|
58
82
|
self._first_delivery_time = first_delivery_time
|
59
83
|
self._expiration_time = expiration_time
|
60
|
-
self._attributes = {}
|
84
|
+
self._attributes: dict = {}
|
61
85
|
|
62
86
|
def _notify_data_changed(self):
|
63
87
|
"""Placeholder method used to signal data changes to derived types"""
|
64
|
-
pass
|
65
88
|
|
66
89
|
@property
|
67
90
|
def product_name(self) -> str:
|
@@ -76,7 +99,7 @@ class Contract(Metadata):
|
|
76
99
|
@property
|
77
100
|
def display_sender_id(self) -> str:
|
78
101
|
"""Returns the sender ID of the contract as a string for display purposes"""
|
79
|
-
return "#{
|
102
|
+
return f"#{self._sender_id}"
|
80
103
|
|
81
104
|
@property
|
82
105
|
def receiver_id(self) -> int:
|
@@ -86,7 +109,7 @@ class Contract(Metadata):
|
|
86
109
|
@property
|
87
110
|
def display_receiver_id(self) -> str:
|
88
111
|
"""Returns the receiver ID of the contract as a string for display purposes"""
|
89
|
-
return "#{
|
112
|
+
return f"#{self._receiver_id}"
|
90
113
|
|
91
114
|
@property
|
92
115
|
def delivery_interval(self) -> int:
|
@@ -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,19 +288,23 @@ 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
|
-
for index in
|
190
|
-
contracts.append(Contract._copy_contract(
|
298
|
+
for index, sender in enumerate(senders):
|
299
|
+
contracts.append(Contract._copy_contract(sender, receivers[0], base_data))
|
191
300
|
elif len(senders) == 1 and len(receivers) > 1:
|
192
|
-
for index in
|
193
|
-
contracts.append(Contract._copy_contract(senders[0],
|
301
|
+
for index, receiver in enumerate(receivers):
|
302
|
+
contracts.append(Contract._copy_contract(senders[0], receiver, base_data))
|
194
303
|
elif len(senders) == len(receivers):
|
195
|
-
for index in range(len(senders)):
|
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
|