MTPPy2.0 0.1.5__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.
- examples/__init__.py +0 -0
- examples/minimal+pid.py +82 -0
- examples/minimal.py +32 -0
- examples/minimal_recipe.py +117 -0
- examples/pea_alarm_base.py +47 -0
- examples/pea_minimal.py +258 -0
- mtppy/__init__.py +1 -0
- mtppy/part1/__init__.py +4 -0
- mtppy/part1/empty_folder_test.py +7 -0
- mtppy/part3/__init__.py +4 -0
- mtppy/part3/data_assembly_set/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/active_element.py +22 -0
- mtppy/part3/data_assembly_set/active_elements/drives/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/drives/ana_drv.py +446 -0
- mtppy/part3/data_assembly_set/active_elements/drives/ana_drv_test.py +567 -0
- mtppy/part3/data_assembly_set/active_elements/drives/bin_drv.py +365 -0
- mtppy/part3/data_assembly_set/active_elements/drives/bin_drv_test.py +503 -0
- mtppy/part3/data_assembly_set/active_elements/drives/mon_ana_drv.py +469 -0
- mtppy/part3/data_assembly_set/active_elements/drives/mon_ana_drv_test.py +418 -0
- mtppy/part3/data_assembly_set/active_elements/drives/mon_bin_drv.py +295 -0
- mtppy/part3/data_assembly_set/active_elements/drives/mon_bin_drv_test.py +265 -0
- mtppy/part3/data_assembly_set/active_elements/pid/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/pid/pid_ctrl.py +333 -0
- mtppy/part3/data_assembly_set/active_elements/pid/pid_ctrl_test.py +156 -0
- mtppy/part3/data_assembly_set/active_elements/valves/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/valves/ana_vlv.py +402 -0
- mtppy/part3/data_assembly_set/active_elements/valves/ana_vlv_test.py +732 -0
- mtppy/part3/data_assembly_set/active_elements/valves/bin_vlv.py +279 -0
- mtppy/part3/data_assembly_set/active_elements/valves/bin_vlv_test.py +585 -0
- mtppy/part3/data_assembly_set/active_elements/valves/mon_ana_vlv.py +402 -0
- mtppy/part3/data_assembly_set/active_elements/valves/mon_ana_vlv_test.py +270 -0
- mtppy/part3/data_assembly_set/active_elements/valves/mon_bin_vlv.py +286 -0
- mtppy/part3/data_assembly_set/active_elements/valves/mon_bin_vlv_test.py +224 -0
- mtppy/part3/data_assembly_set/base_functions/__init__.py +4 -0
- mtppy/part3/data_assembly_set/base_functions/operation_mode.py +252 -0
- mtppy/part3/data_assembly_set/base_functions/operation_mode_test.py +260 -0
- mtppy/part3/data_assembly_set/base_functions/oslevel.py +35 -0
- mtppy/part3/data_assembly_set/base_functions/source_mode.py +189 -0
- mtppy/part3/data_assembly_set/base_functions/source_mode_test.py +112 -0
- mtppy/part3/data_assembly_set/base_functions/wqc.py +75 -0
- mtppy/part3/data_assembly_set/base_functions/wqc_test.py +86 -0
- mtppy/part3/data_assembly_set/data_assembly.py +65 -0
- mtppy/part3/data_assembly_set/diagnostic_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/diagnostic_elements/diagnostic_element.py +19 -0
- mtppy/part3/data_assembly_set/indicator_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/indicator_elements/indicator_element.py +22 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/__init__.py +4 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/ana_view.py +68 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/bin_view.py +73 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/dint_view.py +76 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/string_view.py +55 -0
- mtppy/part3/data_assembly_set/operation_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/operation_elements/operation_element.py +19 -0
- mtppy/part3/data_assembly_set/pea_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/pea_elements/pea_element.py +28 -0
- mtppy/part3/data_assembly_set/pea_elements/pea_information_label/__init__.py +4 -0
- mtppy/part3/data_assembly_set/pea_elements/pea_information_label/pea_information_label.py +231 -0
- mtppy/part3/data_assembly_set/pea_elements/web_server_url_info/__init__.py +4 -0
- mtppy/part3/data_assembly_set/pea_elements/web_server_url_info/web_server_url_info.py +42 -0
- mtppy/part3/definitions/__init__.py +4 -0
- mtppy/part3/definitions/mtp_access.py +15 -0
- mtppy/part4/__init__.py +4 -0
- mtppy/part4/automation_services/__init__.py +4 -0
- mtppy/part4/automation_services/base_functions/__init__.py +4 -0
- mtppy/part4/automation_services/base_functions/service_operation_mode.py +368 -0
- mtppy/part4/automation_services/base_functions/service_operation_mode_test.py +242 -0
- mtppy/part4/automation_services/base_functions/service_operator_interaction.py +62 -0
- mtppy/part4/automation_services/base_functions/service_source_mode.py +138 -0
- mtppy/part4/automation_services/base_functions/service_source_mode_test.py +179 -0
- mtppy/part4/automation_services/helpers/__init__.py +4 -0
- mtppy/part4/automation_services/helpers/command_en_control.py +330 -0
- mtppy/part4/automation_services/helpers/command_en_control_test.py +302 -0
- mtppy/part4/automation_services/helpers/state_machine.py +167 -0
- mtppy/part4/automation_services/helpers/state_machine_test.py +294 -0
- mtppy/part4/automation_services/helpers/thread_control.py +39 -0
- mtppy/part4/automation_services/helpers/thread_control_test.py +80 -0
- mtppy/part4/automation_services/procedure.py +111 -0
- mtppy/part4/automation_services/procedure_test.py +185 -0
- mtppy/part4/automation_services/service.py +463 -0
- mtppy/part4/automation_services/service_elements/__init__.py +4 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/__init__.py +4 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/ana_serv_param.py +108 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/ana_serv_param_test.py +59 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/bin_serv_param.py +70 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/bin_serv_param_test.py +39 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/common_serv_param.py +111 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/dint_serv_param.py +77 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/dint_serv_param_test.py +58 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/parameter_element.py +175 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/parameter_element_test.py +246 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/string_serv_param.py +48 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/string_serv_param_test.py +37 -0
- mtppy/part4/automation_services/service_elements/procedure_health_view.py +36 -0
- mtppy/part4/automation_services/service_elements/service_control.py +473 -0
- mtppy/part4/automation_services/service_elements/service_control_test.py +150 -0
- mtppy/part4/automation_services/service_elements/service_element.py +20 -0
- mtppy/part4/automation_services/service_test.py +407 -0
- mtppy/part4/definitions/__init__.py +4 -0
- mtppy/part4/definitions/command_codes.py +58 -0
- mtppy/part4/definitions/command_codes_test.py +44 -0
- mtppy/part4/definitions/state_codes.py +65 -0
- mtppy/part4/definitions/state_codes_test.py +44 -0
- mtppy/part4/process_values/__init__.py +4 -0
- mtppy/part4/process_values/base_functions/__init__.py +4 -0
- mtppy/part4/process_values/base_functions/vqc.py +48 -0
- mtppy/part4/process_values/input_elements/__init__.py +4 -0
- mtppy/part4/process_values/input_elements/ana_process_value_in.py +160 -0
- mtppy/part4/process_values/input_elements/bin_process_value_in.py +83 -0
- mtppy/part4/process_values/input_elements/dint_process_value_in.py +159 -0
- mtppy/part4/process_values/input_elements/input_element.py +40 -0
- mtppy/part4/process_values/input_elements/string_process_value_in.py +45 -0
- mtppy/part5/__init__.py +4 -0
- mtppy/part5/communication_object.py +99 -0
- mtppy/part5/communication_object_test.py +59 -0
- mtppy/part5/definitions/__init__.py +4 -0
- mtppy/part5/definitions/iec61131_data_types.py +31 -0
- mtppy/part5/definitions/iec61131_data_types_test.py +82 -0
- mtppy/part5/definitions/opcua_data_types.py +36 -0
- mtppy/part5/definitions/opcua_data_types_test.py +90 -0
- mtppy/part5/definitions/w3c_xml_data_types.py +177 -0
- mtppy/part5/definitions/w3c_xml_data_types_test.py +143 -0
- mtppy/part5/server_assembly_set/__init__.py +5 -0
- mtppy/part6/__init__.py +4 -0
- mtppy/part6/alarm_set/__init__.py +4 -0
- mtppy/part6/alarm_set/alarm.py +132 -0
- mtppy/part6/alarm_set/alarm_test.py +265 -0
- mtppy/pea/__init__.py +19 -0
- mtppy/pea/data_assembly_factory.py +124 -0
- mtppy/pea/data_assembly_factory_test.py +229 -0
- mtppy/pea/mtp_generator/__init__.py +4 -0
- mtppy/pea/mtp_generator/mtp_generator.py +1183 -0
- mtppy/pea/mtp_generator/mtp_generator_test.py +518 -0
- mtppy/pea/pea.py +139 -0
- mtppy/pea/pea_opcua_server/__init__.py +4 -0
- mtppy/pea/pea_opcua_server/marshalling.py +60 -0
- mtppy/pea/pea_opcua_server/opcua_options_provider/__init__.py +4 -0
- mtppy/pea/pea_opcua_server/opcua_options_provider/instances/basic_opcua_options_provider.py +50 -0
- mtppy/pea/pea_opcua_server/opcua_options_provider/opcua_options_provider.py +103 -0
- mtppy/pea/pea_opcua_server/pea_opcua_server.py +492 -0
- mtppy/pea/pea_opcua_server/subscription_list.py +55 -0
- mtppy/pea/pea_test.py +59 -0
- mtppy/pea/rmtp_pea.py +155 -0
- mtppy/pea/rmtp_pea_test.py +27 -0
- mtppy/pea/service_factory.py +178 -0
- mtppy/pea/service_factory_test.py +156 -0
- mtppy/pea/user_logic/__init__.py +4 -0
- mtppy/pea/user_logic/user_logic_pea.py +72 -0
- mtppy/pea/user_logic/user_logic_registry.py +106 -0
- mtppy/pea/user_logic/user_logic_registry_test.py +167 -0
- mtppy/pea/user_logic/user_logic_service.py +228 -0
- mtppy/pea/user_logic/user_logic_service_test.py +289 -0
- mtppy/utils/__init__.py +4 -0
- mtppy/utils/data_assembly_dict.py +61 -0
- mtppy/utils/data_assembly_dict_test.py +107 -0
- mtppy/utils/data_item.py +446 -0
- mtppy/utils/data_item_com_test.py +82 -0
- mtppy/utils/data_item_test.py +325 -0
- mtppy/utils/logging.py +23 -0
- mtppy/utils/logging_test.py +26 -0
- mtppy/utils/mtp_file_handling/__init__.py +4 -0
- mtppy/utils/mtp_file_handling/extract_manifest_from_mtp_file.py +22 -0
- mtppy/utils/mtp_file_handling/extract_mtp_file.py +22 -0
- mtppy/utils/mtp_file_handling/is_valid_mtp.py +22 -0
- mtppy/utils/mtp_file_handling/is_valid_path_for_manifest.py +15 -0
- mtppy/utils/mtp_file_handling/is_valid_path_to_manifest.py +17 -0
- mtppy/utils/mtp_file_handling/is_valid_path_to_mtp.py +17 -0
- mtppy/utils/mtp_file_handling/load_manifest_file.py +24 -0
- mtppy/utils/mtp_file_handling/mtp_reader/__init__.py +4 -0
- mtppy/utils/mtp_file_handling/mtp_reader/mtp_reader.py +103 -0
- mtppy/utils/mtp_file_handling/mtp_reader/mtp_reader_test.py +96 -0
- mtppy/utils/mtp_file_handling/mtp_reader/mtp_xml.py +226 -0
- mtppy/utils/mtp_file_handling/mtp_reader/mtp_xml_test.py +108 -0
- mtppy/utils/mtp_file_handling/mtp_writer/__init__.py +4 -0
- mtppy/utils/mtp_file_handling/mtp_writer/mtp_writer.py +77 -0
- mtppy/utils/mtp_file_handling/mtp_writer/ressources/get_content_types.py +10 -0
- mtppy/utils/mtp_file_handling/mtp_writer/ressources/get_rels.py +11 -0
- mtppy/utils/mtp_file_handling/mtp_writer/test_mtp_writer.py +25 -0
- mtppy/utils/mtp_file_handling/write_manifest_file.py +29 -0
- mtppy/utils/mtp_file_handling/write_manifest_file_test.py +22 -0
- mtppy/utils/mtp_file_handling/write_mtp_file.py +43 -0
- mtppy/utils/mtp_file_handling/write_mtp_file_test.py +22 -0
- mtppy/utils/testing.py +31 -0
- mtppy2_0-0.1.5.dist-info/METADATA +75 -0
- mtppy2_0-0.1.5.dist-info/RECORD +214 -0
- mtppy2_0-0.1.5.dist-info/WHEEL +5 -0
- mtppy2_0-0.1.5.dist-info/entry_points.txt +3 -0
- mtppy2_0-0.1.5.dist-info/licenses/LICENSE +21 -0
- mtppy2_0-0.1.5.dist-info/top_level.txt +4 -0
- tools/__init__.py +0 -0
- tools/create_bugreport.py +150 -0
- tools/inspect_mtp.py +110 -0
- utils/__init__.py +1 -0
- utils/file_handling/__init__.py +4 -0
- utils/file_handling/cleanup_folder.py +9 -0
- utils/file_handling/get_utc_foldername_with_ms.py +15 -0
- utils/file_handling/get_work_dir.py +9 -0
- utils/file_handling/is_valid_path_for_json.py +11 -0
- utils/file_handling/is_valid_path_to_json.py +17 -0
- utils/file_handling/load_json_file.py +18 -0
- utils/file_handling/read_file_from_file_archive.py +43 -0
- utils/file_handling/read_file_string.py +10 -0
- utils/file_handling/validate_json_data.py +28 -0
- utils/file_handling/write_json_file.py +23 -0
- utils/get_uuid.py +18 -0
- utils/get_uuid_test.py +17 -0
- utils/logging.py +100 -0
- utils/logging_test.py +67 -0
- utils/opcua/check_node.py +79 -0
- utils/resolve_unit/resolve_unit.py +11 -0
- utils/resolve_unit/unit_lib.py +2101 -0
- utils/resolve_unit/unit_lib_test.py +36 -0
- utils/type_conversion.py +236 -0
- utils/type_conversion_test.py +114 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Test Suite for Data Item."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
|
|
8
|
+
from itertools import product
|
|
9
|
+
from typing import ClassVar
|
|
10
|
+
from unittest.mock import Mock
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from mtppy.part3.definitions.mtp_access import MTPAccess
|
|
15
|
+
from mtppy.part5.definitions.w3c_xml_data_types import W3cXmlDataTypes
|
|
16
|
+
from mtppy.utils.data_item import DataItem, DataItemCollection
|
|
17
|
+
|
|
18
|
+
types = [e for e in W3cXmlDataTypes if e.python_type]
|
|
19
|
+
values = [-10, -5.6, 0, 1, 3.5, True, False]
|
|
20
|
+
combinations = list(product(types, values))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.parametrize("data_type, value", combinations)
|
|
24
|
+
def test_compatible_types(data_type, value):
|
|
25
|
+
attr = DataItem("", data_type, MTPAccess.ReadAccess, value)
|
|
26
|
+
assert isinstance(
|
|
27
|
+
attr._value,
|
|
28
|
+
data_type.python_type,
|
|
29
|
+
), f"Value {attr._value} is not of type {data_type.python_type}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
incompatible_types = [W3cXmlDataTypes.SHORT, W3cXmlDataTypes.FLOAT]
|
|
33
|
+
incompatible_values = ["string"]
|
|
34
|
+
incompatible_combinations = list(product(incompatible_types, incompatible_values))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.mark.parametrize("data_type, value", incompatible_combinations)
|
|
38
|
+
def test_incompatible_types(data_type, value):
|
|
39
|
+
attr = DataItem("", data_type, MTPAccess.ReadAccess, value)
|
|
40
|
+
assert not isinstance(
|
|
41
|
+
attr._value,
|
|
42
|
+
data_type.python_type,
|
|
43
|
+
), f"Value {attr._value} is of type {data_type.python_type}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.parametrize(
|
|
47
|
+
"unsupported",
|
|
48
|
+
[W3cXmlDataTypes.TOKEN, W3cXmlDataTypes.ANY_URI, W3cXmlDataTypes.DATE_TIME],
|
|
49
|
+
)
|
|
50
|
+
def test_unsupported_w3c_xml_data_type(unsupported):
|
|
51
|
+
"""Test that a TypeError is raised when an invalid w3c_xml_data_type is passed."""
|
|
52
|
+
with pytest.raises(TypeError):
|
|
53
|
+
DataItem(
|
|
54
|
+
name="TestItem",
|
|
55
|
+
w3c_xml_data_type=unsupported,
|
|
56
|
+
access=MTPAccess.ReadAccess,
|
|
57
|
+
init_value=None,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_invalid_w3c_xml_data_type():
|
|
62
|
+
"""Test that a TypeError is raised when an invalid w3c_xml_data_type is passed."""
|
|
63
|
+
with pytest.raises(TypeError):
|
|
64
|
+
DataItem(
|
|
65
|
+
name="TestItem",
|
|
66
|
+
w3c_xml_data_type="invalid_type",
|
|
67
|
+
access=MTPAccess.ReadAccess,
|
|
68
|
+
init_value=None,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_invalid_access():
|
|
73
|
+
"""Test that a TypeError is raised when an invalid access type is passed."""
|
|
74
|
+
with pytest.raises(TypeError):
|
|
75
|
+
DataItem(
|
|
76
|
+
name="TestItem",
|
|
77
|
+
w3c_xml_data_type=W3cXmlDataTypes.FLOAT,
|
|
78
|
+
access="invalid_access",
|
|
79
|
+
init_value=1.0,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_external_write_callback():
|
|
84
|
+
class OPCUANode:
|
|
85
|
+
def __init__(self, cb):
|
|
86
|
+
self.cb = cb
|
|
87
|
+
self.value = 0
|
|
88
|
+
|
|
89
|
+
def set_value(self, value):
|
|
90
|
+
self.value = value
|
|
91
|
+
self.cb(value)
|
|
92
|
+
|
|
93
|
+
test_value = {"value": 0}
|
|
94
|
+
|
|
95
|
+
def callback_function(new_value):
|
|
96
|
+
test_value["value"] = new_value
|
|
97
|
+
|
|
98
|
+
attr = DataItem(
|
|
99
|
+
"",
|
|
100
|
+
W3cXmlDataTypes.SHORT,
|
|
101
|
+
MTPAccess.ReadWriteAccess,
|
|
102
|
+
0,
|
|
103
|
+
external_write_cb=callback_function,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
node = OPCUANode(attr.ext_wrt_cb)
|
|
107
|
+
|
|
108
|
+
node.set_value(10)
|
|
109
|
+
assert test_value["value"] == 10
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_communication_object_attachment(mocker):
|
|
113
|
+
class CommObj:
|
|
114
|
+
def __init__(self):
|
|
115
|
+
self.value = 0
|
|
116
|
+
self._node_id = "dummy"
|
|
117
|
+
|
|
118
|
+
def write_value_callback(self, value):
|
|
119
|
+
self.value = value
|
|
120
|
+
|
|
121
|
+
attr = DataItem(
|
|
122
|
+
"",
|
|
123
|
+
W3cXmlDataTypes.SHORT,
|
|
124
|
+
MTPAccess.ReadWriteAccess,
|
|
125
|
+
10,
|
|
126
|
+
external_write_cb=Mock(),
|
|
127
|
+
)
|
|
128
|
+
comm_object = CommObj()
|
|
129
|
+
attr.attach_communication_object(comm_object)
|
|
130
|
+
|
|
131
|
+
assert attr.comm_obj == comm_object
|
|
132
|
+
assert comm_object.value == 0
|
|
133
|
+
attr.set_value(13)
|
|
134
|
+
assert comm_object.value == 13
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_data_item_properties_and_default_initial_value():
|
|
138
|
+
attr = DataItem(
|
|
139
|
+
"Counter",
|
|
140
|
+
W3cXmlDataTypes.INT,
|
|
141
|
+
MTPAccess.ReadAccess,
|
|
142
|
+
description="A counter",
|
|
143
|
+
init_value=None,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
assert attr.get_value() == 0
|
|
147
|
+
assert attr.init_value == 0
|
|
148
|
+
assert attr.description == "A counter"
|
|
149
|
+
assert attr.get_value_str() == "Counter=0"
|
|
150
|
+
assert attr.comm_obj is None
|
|
151
|
+
assert attr.ext_wrt_cb is None
|
|
152
|
+
assert attr.int_wrt_cb is None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_set_value_calls_internal_callback_and_updates_from_callback():
|
|
156
|
+
callback_values = []
|
|
157
|
+
|
|
158
|
+
def internal_callback(value):
|
|
159
|
+
callback_values.append(value)
|
|
160
|
+
|
|
161
|
+
attr = DataItem(
|
|
162
|
+
"Counter",
|
|
163
|
+
W3cXmlDataTypes.INT,
|
|
164
|
+
MTPAccess.ReadWriteAccess,
|
|
165
|
+
0,
|
|
166
|
+
internal_write_cb=internal_callback,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
attr.set_value(4)
|
|
170
|
+
|
|
171
|
+
assert callback_values == [4]
|
|
172
|
+
assert attr.get_value() == 0
|
|
173
|
+
assert attr._is_notifying is False
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_internal_callback_can_update_data_item_recursively():
|
|
177
|
+
attr = None
|
|
178
|
+
|
|
179
|
+
def internal_callback(value):
|
|
180
|
+
if value < 2:
|
|
181
|
+
attr.set_value(value + 1)
|
|
182
|
+
|
|
183
|
+
attr = DataItem(
|
|
184
|
+
"Counter",
|
|
185
|
+
W3cXmlDataTypes.INT,
|
|
186
|
+
MTPAccess.ReadWriteAccess,
|
|
187
|
+
0,
|
|
188
|
+
internal_write_cb=internal_callback,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
attr.set_value(1)
|
|
192
|
+
|
|
193
|
+
assert attr.get_value() == 2
|
|
194
|
+
assert attr._is_notifying is False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_set_value_notifies_external_object_when_value_is_unchanged():
|
|
198
|
+
communication_object = Mock()
|
|
199
|
+
attr = DataItem("Counter", W3cXmlDataTypes.INT, MTPAccess.ReadAccess, 4)
|
|
200
|
+
attr.attach_communication_object(communication_object)
|
|
201
|
+
|
|
202
|
+
attr.set_value(4)
|
|
203
|
+
|
|
204
|
+
communication_object.write_value_callback.assert_called_once_with(4)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_set_value_uses_int_class_when_conversion_fails():
|
|
208
|
+
attr = DataItem("Counter", W3cXmlDataTypes.INT, MTPAccess.ReadAccess, 0)
|
|
209
|
+
|
|
210
|
+
attr.set_value("invalid")
|
|
211
|
+
|
|
212
|
+
assert attr.get_value() is int
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_data_item_collection_supports_mapping_operations_and_values():
|
|
216
|
+
collection = SampleCollection()
|
|
217
|
+
|
|
218
|
+
assert len(collection) == 2
|
|
219
|
+
assert list(collection) == ["First", "Second"]
|
|
220
|
+
assert collection.keys() == {"First", "Second"}
|
|
221
|
+
assert collection.values() == [1, "initial"]
|
|
222
|
+
assert collection.items() == [("First", 1), ("Second", "initial")]
|
|
223
|
+
assert collection["First"] == 1
|
|
224
|
+
assert collection.get_item("First").name == "First"
|
|
225
|
+
|
|
226
|
+
collection["First"] = 7
|
|
227
|
+
assert collection["First"] == 7
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_data_item_collection_supports_attribute_access_and_assignment():
|
|
231
|
+
collection = SampleCollection()
|
|
232
|
+
|
|
233
|
+
assert collection.First == 1
|
|
234
|
+
|
|
235
|
+
with pytest.raises(AttributeError, match="no attribute"):
|
|
236
|
+
collection.first
|
|
237
|
+
|
|
238
|
+
collection.First = 3
|
|
239
|
+
assert collection.First == 3
|
|
240
|
+
|
|
241
|
+
assert collection.extra == "I'm special"
|
|
242
|
+
|
|
243
|
+
collection.extra = "attribute!"
|
|
244
|
+
assert collection.extra == "attribute!"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_data_item_assignment_to_unknown_key():
|
|
248
|
+
collection = SampleCollection()
|
|
249
|
+
|
|
250
|
+
with pytest.raises(KeyError):
|
|
251
|
+
collection.missing = 12
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_data_item_collection_rejects_unknown_keys_and_deletion():
|
|
255
|
+
collection = SampleCollection()
|
|
256
|
+
|
|
257
|
+
with pytest.raises(KeyError, match="key-immutable"):
|
|
258
|
+
collection["Unknown"] = 1
|
|
259
|
+
|
|
260
|
+
with pytest.raises(TypeError, match="key-immutable"):
|
|
261
|
+
del collection["First"]
|
|
262
|
+
|
|
263
|
+
with pytest.raises(KeyError):
|
|
264
|
+
collection["Unknown"]
|
|
265
|
+
|
|
266
|
+
with pytest.raises(KeyError):
|
|
267
|
+
collection.get_item("Unknown")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_data_item_collection_rejects_unknown_attributes():
|
|
271
|
+
collection = SampleCollection()
|
|
272
|
+
|
|
273
|
+
assert hasattr(collection, "missing") is False
|
|
274
|
+
|
|
275
|
+
with pytest.raises(AttributeError, match="has no attribute 'missing'"):
|
|
276
|
+
collection.missing
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_data_item_collection_repr_contains_class_and_values():
|
|
280
|
+
collection = SampleCollection()
|
|
281
|
+
|
|
282
|
+
assert repr(collection) == (
|
|
283
|
+
"class=SampleCollection,data_items={'First': 1, 'Second': 'initial'})"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_data_item_collection_deep_copies_class_data_items():
|
|
288
|
+
first_collection = SampleCollection()
|
|
289
|
+
second_collection = SampleCollection()
|
|
290
|
+
|
|
291
|
+
TEST_VALUE = 9
|
|
292
|
+
first_collection["First"] = TEST_VALUE
|
|
293
|
+
|
|
294
|
+
assert first_collection["First"] == TEST_VALUE
|
|
295
|
+
assert second_collection["First"] == 1
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class BaseCollection(DataItemCollection):
|
|
299
|
+
"""A base collection of data items."""
|
|
300
|
+
|
|
301
|
+
FIRST = "First"
|
|
302
|
+
CLASS_DATA_ITEMS: ClassVar[dict[str, DataItem]] = {
|
|
303
|
+
FIRST: DataItem(FIRST, W3cXmlDataTypes.INT, MTPAccess.ReadAccess, 1),
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
class SampleCollection(BaseCollection):
|
|
308
|
+
"""A subclass of BaseCollection."""
|
|
309
|
+
|
|
310
|
+
SECOND = "Second"
|
|
311
|
+
CLASS_DATA_ITEMS: ClassVar[dict[str, DataItem]] = {
|
|
312
|
+
SECOND: DataItem(
|
|
313
|
+
SECOND,
|
|
314
|
+
W3cXmlDataTypes.STRING,
|
|
315
|
+
MTPAccess.ReadAccess,
|
|
316
|
+
"initial",
|
|
317
|
+
),
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
extra: str
|
|
321
|
+
|
|
322
|
+
def __init__(self) -> None:
|
|
323
|
+
"""Initialize the SampleCollection instance."""
|
|
324
|
+
super().__init__()
|
|
325
|
+
self.extra = "I'm special"
|
mtppy/utils/logging.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""predefined logging levels for the MTPPy2.0 framework."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Final, cast
|
|
9
|
+
|
|
10
|
+
from utils.logging import CUSTOM_LEVELS, CustomLogger
|
|
11
|
+
|
|
12
|
+
logging.setLoggerClass(CustomLogger)
|
|
13
|
+
logger: CustomLogger = cast("CustomLogger", logging.getLogger("MTPPy2.0Logger"))
|
|
14
|
+
|
|
15
|
+
INFO_LEVELS: Final = CUSTOM_LEVELS
|
|
16
|
+
CHOICES: Final = [level_name.upper() for level_name in INFO_LEVELS]
|
|
17
|
+
|
|
18
|
+
logger.add_custom_levels(INFO_LEVELS)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def setLogLevel(log_level: int | str) -> None:
|
|
22
|
+
"""Set the root logger level from a numeric value or level name."""
|
|
23
|
+
logging.getLogger().setLevel(log_level)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Tests for the framework logging facade."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from mtppy.utils.logging import CHOICES, INFO_LEVELS, CustomLogger, logger, setLogLevel
|
|
6
|
+
from utils.logging import APP_LEVEL
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_framework_logger_is_pylance_visible_custom_logger() -> None:
|
|
10
|
+
assert isinstance(logger, CustomLogger)
|
|
11
|
+
logger.app("framework application message")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_framework_logging_levels_match_choices() -> None:
|
|
15
|
+
assert [name.upper() for name in INFO_LEVELS] == CHOICES
|
|
16
|
+
assert INFO_LEVELS == {"opcua": 1, "mtp": 2, "app": 4, "pol": 6}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_set_log_level_accepts_custom_level_name() -> None:
|
|
20
|
+
root_logger = logging.getLogger()
|
|
21
|
+
original_level = root_logger.level
|
|
22
|
+
try:
|
|
23
|
+
setLogLevel("APP")
|
|
24
|
+
assert root_logger.level == APP_LEVEL
|
|
25
|
+
finally:
|
|
26
|
+
root_logger.setLevel(original_level)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""extract manifest from mtp file."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
import os
|
|
8
|
+
import zipfile
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def extract_manifest_from_mtp_file(mtp_path, extract_dir):
|
|
12
|
+
try:
|
|
13
|
+
with zipfile.ZipFile(mtp_path, "r") as zip_ref:
|
|
14
|
+
aml_files = [
|
|
15
|
+
f for f in zip_ref.namelist() if f.lower().endswith("manifest.aml")
|
|
16
|
+
]
|
|
17
|
+
if not aml_files:
|
|
18
|
+
raise FileNotFoundError("No Manifest.aml file found in MTP package.")
|
|
19
|
+
zip_ref.extractall(extract_dir)
|
|
20
|
+
return os.path.join(extract_dir, aml_files[0])
|
|
21
|
+
except zipfile.BadZipFile:
|
|
22
|
+
raise ValueError("Failed to unzip the MTP package. The file may be corrupted.")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""extract mtp file."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
import zipfile
|
|
8
|
+
|
|
9
|
+
from utils.file_handling.get_work_dir import get_work_dir
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def extract_mtp_file(mtp_path):
|
|
13
|
+
"""Extract files from an MTP file."""
|
|
14
|
+
extract_folder = get_work_dir()
|
|
15
|
+
|
|
16
|
+
with zipfile.ZipFile(mtp_path, "r") as zip_ref:
|
|
17
|
+
print(f"Contents of the zip file {mtp_path}:")
|
|
18
|
+
print(zip_ref.namelist()) # List all the files inside the zip archive
|
|
19
|
+
zip_ref.extractall(extract_folder) # Extract to the folder we defined
|
|
20
|
+
|
|
21
|
+
print(f"Files extracted to: {extract_folder}")
|
|
22
|
+
return extract_folder
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""determine if mtp is valid."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
import zipfile
|
|
8
|
+
|
|
9
|
+
from mtppy.utils.mtp_file_handling.is_valid_path_to_mtp import is_valid_path_to_mtp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def is_valid_mtp(path):
|
|
13
|
+
if not is_valid_path_to_mtp(path):
|
|
14
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
15
|
+
if not zipfile.is_zipfile(path):
|
|
16
|
+
raise ValueError(f"File is not a valid ZIP archive: {path}")
|
|
17
|
+
with zipfile.ZipFile(path, "r") as zip_ref:
|
|
18
|
+
if not any(
|
|
19
|
+
file.lower().endswith("manifest.aml") for file in zip_ref.namelist()
|
|
20
|
+
):
|
|
21
|
+
raise FileNotFoundError("No .aml file found in the MTP package.")
|
|
22
|
+
return True
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""determine if path for manifest is valid."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_valid_path_for_manifest(path):
|
|
12
|
+
path_object = Path(path)
|
|
13
|
+
return (path_object.suffix == ".aml") and (
|
|
14
|
+
path_object.name.lower() == "manifest.aml"
|
|
15
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""determine if path to manifest is valid."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_valid_path_to_manifest(path: str | Path) -> bool:
|
|
11
|
+
path_object = Path(path)
|
|
12
|
+
return (
|
|
13
|
+
path_object.exists()
|
|
14
|
+
and path_object.is_file()
|
|
15
|
+
and (path_object.suffix == ".aml")
|
|
16
|
+
and (path_object.name.lower() == "manifest.aml")
|
|
17
|
+
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""determine if path to mtp is valid."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_valid_path_to_mtp(path):
|
|
12
|
+
path_object = Path(path)
|
|
13
|
+
return (
|
|
14
|
+
path_object.exists()
|
|
15
|
+
and path_object.is_file()
|
|
16
|
+
and (path_object.suffix == ".mtp")
|
|
17
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""load manifest method."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
|
|
8
|
+
import xml.etree.ElementTree as ET
|
|
9
|
+
|
|
10
|
+
from mtppy.utils.mtp_file_handling.is_valid_path_to_manifest import (
|
|
11
|
+
is_valid_path_to_manifest,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_manifest_file(file_path) -> ET.ElementTree:
|
|
16
|
+
"""Load and return manifest content from file as XML Element Tree."""
|
|
17
|
+
if not is_valid_path_to_manifest(file_path):
|
|
18
|
+
raise FileNotFoundError(f"Invalid path: {file_path}")
|
|
19
|
+
return ET.parse(file_path)
|
|
20
|
+
|
|
21
|
+
# with open(file_path, 'r') as f:
|
|
22
|
+
# return json.load(f)
|
|
23
|
+
#
|
|
24
|
+
# root = ET.fromstring(xml_string).
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Class MtpReader, the mockup for reading an MTP-File."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from xml.etree.ElementTree import ElementTree
|
|
10
|
+
|
|
11
|
+
from mtppy.pea.data_assembly_factory import DataAssemblyConfig
|
|
12
|
+
from mtppy.utils.logging import logger
|
|
13
|
+
from mtppy.utils.mtp_file_handling.extract_manifest_from_mtp_file import (
|
|
14
|
+
extract_manifest_from_mtp_file,
|
|
15
|
+
)
|
|
16
|
+
from mtppy.utils.mtp_file_handling.is_valid_mtp import is_valid_mtp
|
|
17
|
+
from mtppy.utils.mtp_file_handling.load_manifest_file import load_manifest_file
|
|
18
|
+
from mtppy.utils.mtp_file_handling.mtp_reader.mtp_xml import (
|
|
19
|
+
get_data_assemblies,
|
|
20
|
+
get_opcua_node_ids,
|
|
21
|
+
get_services,
|
|
22
|
+
)
|
|
23
|
+
from utils.file_handling.cleanup_folder import cleanup_folder
|
|
24
|
+
from utils.file_handling.get_work_dir import get_work_dir
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MtpReader:
|
|
28
|
+
"""A class to read and parse MTP files.
|
|
29
|
+
|
|
30
|
+
This class provides functionality to read MTP files, extract their manifest,
|
|
31
|
+
and create mappings between aliases and OPC UA node IDs.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, mtp_path: str | Path | None = None) -> None:
|
|
35
|
+
"""Initialize the MtpReader with an optional MTP file path.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
mtp_path: Path to the MTP file to read. If None, no file is read initially but all the public data structured are initialized
|
|
39
|
+
"""
|
|
40
|
+
self._manifest: ElementTree = ElementTree()
|
|
41
|
+
self.alias_opcua_map = {}
|
|
42
|
+
self.data_assemblies: dict[str, DataAssemblyConfig] = {}
|
|
43
|
+
self.services = {}
|
|
44
|
+
|
|
45
|
+
if mtp_path:
|
|
46
|
+
self._manifest = self.read_mtp(mtp_path)
|
|
47
|
+
root = self._manifest.getroot()
|
|
48
|
+
node_ids = get_opcua_node_ids(root)
|
|
49
|
+
self.data_assemblies = get_data_assemblies(root)
|
|
50
|
+
self.alias_opcua_map = self.create_alias_opcua_node_map(
|
|
51
|
+
self.data_assemblies,
|
|
52
|
+
node_ids,
|
|
53
|
+
)
|
|
54
|
+
self.services = get_services(root)
|
|
55
|
+
|
|
56
|
+
def read_mtp(self, mtp_path: str | Path) -> ElementTree:
|
|
57
|
+
"""Read and parse an MTP file.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
mtp_path: Path to the MTP file to read.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Parsed XML tree of the manifest
|
|
64
|
+
|
|
65
|
+
Raises:
|
|
66
|
+
FileNotFoundError: If file does not exist
|
|
67
|
+
ValueError: If file is not readable
|
|
68
|
+
"""
|
|
69
|
+
logger.info(f"🔍 Checking MTP package: {mtp_path}")
|
|
70
|
+
is_valid_mtp(mtp_path)
|
|
71
|
+
|
|
72
|
+
logger.info("📦 Extracting AML file...")
|
|
73
|
+
workdir = get_work_dir(base="../workdir")
|
|
74
|
+
aml_file_path = extract_manifest_from_mtp_file(mtp_path, workdir)
|
|
75
|
+
|
|
76
|
+
result = load_manifest_file(aml_file_path)
|
|
77
|
+
|
|
78
|
+
logger.info(" ♲ Cleaning up...")
|
|
79
|
+
cleanup_folder(workdir)
|
|
80
|
+
|
|
81
|
+
logger.info("✅ MTP parsing complete.")
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
def create_alias_opcua_node_map(
|
|
85
|
+
self,
|
|
86
|
+
data_assemblies: dict[str, DataAssemblyConfig],
|
|
87
|
+
node_ids: dict[str, str],
|
|
88
|
+
) -> dict[str, str]:
|
|
89
|
+
"""Create a mapping between MTP aliases and OPC UA node IDs.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Dictionary mapping aliases to OPC UA node IDs.
|
|
93
|
+
"""
|
|
94
|
+
alias_opcua_node_map: dict[str, str] = {}
|
|
95
|
+
for da_name, value in data_assemblies.items():
|
|
96
|
+
for alias, ref in value.items():
|
|
97
|
+
if (
|
|
98
|
+
isinstance(ref, dict)
|
|
99
|
+
and "value" in ref
|
|
100
|
+
and ref["value"] in node_ids
|
|
101
|
+
):
|
|
102
|
+
alias_opcua_node_map[f"{da_name}.{alias}"] = node_ids[ref["value"]]
|
|
103
|
+
return alias_opcua_node_map
|