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.
Files changed (214) hide show
  1. examples/__init__.py +0 -0
  2. examples/minimal+pid.py +82 -0
  3. examples/minimal.py +32 -0
  4. examples/minimal_recipe.py +117 -0
  5. examples/pea_alarm_base.py +47 -0
  6. examples/pea_minimal.py +258 -0
  7. mtppy/__init__.py +1 -0
  8. mtppy/part1/__init__.py +4 -0
  9. mtppy/part1/empty_folder_test.py +7 -0
  10. mtppy/part3/__init__.py +4 -0
  11. mtppy/part3/data_assembly_set/__init__.py +4 -0
  12. mtppy/part3/data_assembly_set/active_elements/__init__.py +4 -0
  13. mtppy/part3/data_assembly_set/active_elements/active_element.py +22 -0
  14. mtppy/part3/data_assembly_set/active_elements/drives/__init__.py +4 -0
  15. mtppy/part3/data_assembly_set/active_elements/drives/ana_drv.py +446 -0
  16. mtppy/part3/data_assembly_set/active_elements/drives/ana_drv_test.py +567 -0
  17. mtppy/part3/data_assembly_set/active_elements/drives/bin_drv.py +365 -0
  18. mtppy/part3/data_assembly_set/active_elements/drives/bin_drv_test.py +503 -0
  19. mtppy/part3/data_assembly_set/active_elements/drives/mon_ana_drv.py +469 -0
  20. mtppy/part3/data_assembly_set/active_elements/drives/mon_ana_drv_test.py +418 -0
  21. mtppy/part3/data_assembly_set/active_elements/drives/mon_bin_drv.py +295 -0
  22. mtppy/part3/data_assembly_set/active_elements/drives/mon_bin_drv_test.py +265 -0
  23. mtppy/part3/data_assembly_set/active_elements/pid/__init__.py +4 -0
  24. mtppy/part3/data_assembly_set/active_elements/pid/pid_ctrl.py +333 -0
  25. mtppy/part3/data_assembly_set/active_elements/pid/pid_ctrl_test.py +156 -0
  26. mtppy/part3/data_assembly_set/active_elements/valves/__init__.py +4 -0
  27. mtppy/part3/data_assembly_set/active_elements/valves/ana_vlv.py +402 -0
  28. mtppy/part3/data_assembly_set/active_elements/valves/ana_vlv_test.py +732 -0
  29. mtppy/part3/data_assembly_set/active_elements/valves/bin_vlv.py +279 -0
  30. mtppy/part3/data_assembly_set/active_elements/valves/bin_vlv_test.py +585 -0
  31. mtppy/part3/data_assembly_set/active_elements/valves/mon_ana_vlv.py +402 -0
  32. mtppy/part3/data_assembly_set/active_elements/valves/mon_ana_vlv_test.py +270 -0
  33. mtppy/part3/data_assembly_set/active_elements/valves/mon_bin_vlv.py +286 -0
  34. mtppy/part3/data_assembly_set/active_elements/valves/mon_bin_vlv_test.py +224 -0
  35. mtppy/part3/data_assembly_set/base_functions/__init__.py +4 -0
  36. mtppy/part3/data_assembly_set/base_functions/operation_mode.py +252 -0
  37. mtppy/part3/data_assembly_set/base_functions/operation_mode_test.py +260 -0
  38. mtppy/part3/data_assembly_set/base_functions/oslevel.py +35 -0
  39. mtppy/part3/data_assembly_set/base_functions/source_mode.py +189 -0
  40. mtppy/part3/data_assembly_set/base_functions/source_mode_test.py +112 -0
  41. mtppy/part3/data_assembly_set/base_functions/wqc.py +75 -0
  42. mtppy/part3/data_assembly_set/base_functions/wqc_test.py +86 -0
  43. mtppy/part3/data_assembly_set/data_assembly.py +65 -0
  44. mtppy/part3/data_assembly_set/diagnostic_elements/__init__.py +4 -0
  45. mtppy/part3/data_assembly_set/diagnostic_elements/diagnostic_element.py +19 -0
  46. mtppy/part3/data_assembly_set/indicator_elements/__init__.py +4 -0
  47. mtppy/part3/data_assembly_set/indicator_elements/indicator_element.py +22 -0
  48. mtppy/part3/data_assembly_set/indicator_elements/views/__init__.py +4 -0
  49. mtppy/part3/data_assembly_set/indicator_elements/views/ana_view.py +68 -0
  50. mtppy/part3/data_assembly_set/indicator_elements/views/bin_view.py +73 -0
  51. mtppy/part3/data_assembly_set/indicator_elements/views/dint_view.py +76 -0
  52. mtppy/part3/data_assembly_set/indicator_elements/views/string_view.py +55 -0
  53. mtppy/part3/data_assembly_set/operation_elements/__init__.py +4 -0
  54. mtppy/part3/data_assembly_set/operation_elements/operation_element.py +19 -0
  55. mtppy/part3/data_assembly_set/pea_elements/__init__.py +4 -0
  56. mtppy/part3/data_assembly_set/pea_elements/pea_element.py +28 -0
  57. mtppy/part3/data_assembly_set/pea_elements/pea_information_label/__init__.py +4 -0
  58. mtppy/part3/data_assembly_set/pea_elements/pea_information_label/pea_information_label.py +231 -0
  59. mtppy/part3/data_assembly_set/pea_elements/web_server_url_info/__init__.py +4 -0
  60. mtppy/part3/data_assembly_set/pea_elements/web_server_url_info/web_server_url_info.py +42 -0
  61. mtppy/part3/definitions/__init__.py +4 -0
  62. mtppy/part3/definitions/mtp_access.py +15 -0
  63. mtppy/part4/__init__.py +4 -0
  64. mtppy/part4/automation_services/__init__.py +4 -0
  65. mtppy/part4/automation_services/base_functions/__init__.py +4 -0
  66. mtppy/part4/automation_services/base_functions/service_operation_mode.py +368 -0
  67. mtppy/part4/automation_services/base_functions/service_operation_mode_test.py +242 -0
  68. mtppy/part4/automation_services/base_functions/service_operator_interaction.py +62 -0
  69. mtppy/part4/automation_services/base_functions/service_source_mode.py +138 -0
  70. mtppy/part4/automation_services/base_functions/service_source_mode_test.py +179 -0
  71. mtppy/part4/automation_services/helpers/__init__.py +4 -0
  72. mtppy/part4/automation_services/helpers/command_en_control.py +330 -0
  73. mtppy/part4/automation_services/helpers/command_en_control_test.py +302 -0
  74. mtppy/part4/automation_services/helpers/state_machine.py +167 -0
  75. mtppy/part4/automation_services/helpers/state_machine_test.py +294 -0
  76. mtppy/part4/automation_services/helpers/thread_control.py +39 -0
  77. mtppy/part4/automation_services/helpers/thread_control_test.py +80 -0
  78. mtppy/part4/automation_services/procedure.py +111 -0
  79. mtppy/part4/automation_services/procedure_test.py +185 -0
  80. mtppy/part4/automation_services/service.py +463 -0
  81. mtppy/part4/automation_services/service_elements/__init__.py +4 -0
  82. mtppy/part4/automation_services/service_elements/parameter_elements/__init__.py +4 -0
  83. mtppy/part4/automation_services/service_elements/parameter_elements/ana_serv_param.py +108 -0
  84. mtppy/part4/automation_services/service_elements/parameter_elements/ana_serv_param_test.py +59 -0
  85. mtppy/part4/automation_services/service_elements/parameter_elements/bin_serv_param.py +70 -0
  86. mtppy/part4/automation_services/service_elements/parameter_elements/bin_serv_param_test.py +39 -0
  87. mtppy/part4/automation_services/service_elements/parameter_elements/common_serv_param.py +111 -0
  88. mtppy/part4/automation_services/service_elements/parameter_elements/dint_serv_param.py +77 -0
  89. mtppy/part4/automation_services/service_elements/parameter_elements/dint_serv_param_test.py +58 -0
  90. mtppy/part4/automation_services/service_elements/parameter_elements/parameter_element.py +175 -0
  91. mtppy/part4/automation_services/service_elements/parameter_elements/parameter_element_test.py +246 -0
  92. mtppy/part4/automation_services/service_elements/parameter_elements/string_serv_param.py +48 -0
  93. mtppy/part4/automation_services/service_elements/parameter_elements/string_serv_param_test.py +37 -0
  94. mtppy/part4/automation_services/service_elements/procedure_health_view.py +36 -0
  95. mtppy/part4/automation_services/service_elements/service_control.py +473 -0
  96. mtppy/part4/automation_services/service_elements/service_control_test.py +150 -0
  97. mtppy/part4/automation_services/service_elements/service_element.py +20 -0
  98. mtppy/part4/automation_services/service_test.py +407 -0
  99. mtppy/part4/definitions/__init__.py +4 -0
  100. mtppy/part4/definitions/command_codes.py +58 -0
  101. mtppy/part4/definitions/command_codes_test.py +44 -0
  102. mtppy/part4/definitions/state_codes.py +65 -0
  103. mtppy/part4/definitions/state_codes_test.py +44 -0
  104. mtppy/part4/process_values/__init__.py +4 -0
  105. mtppy/part4/process_values/base_functions/__init__.py +4 -0
  106. mtppy/part4/process_values/base_functions/vqc.py +48 -0
  107. mtppy/part4/process_values/input_elements/__init__.py +4 -0
  108. mtppy/part4/process_values/input_elements/ana_process_value_in.py +160 -0
  109. mtppy/part4/process_values/input_elements/bin_process_value_in.py +83 -0
  110. mtppy/part4/process_values/input_elements/dint_process_value_in.py +159 -0
  111. mtppy/part4/process_values/input_elements/input_element.py +40 -0
  112. mtppy/part4/process_values/input_elements/string_process_value_in.py +45 -0
  113. mtppy/part5/__init__.py +4 -0
  114. mtppy/part5/communication_object.py +99 -0
  115. mtppy/part5/communication_object_test.py +59 -0
  116. mtppy/part5/definitions/__init__.py +4 -0
  117. mtppy/part5/definitions/iec61131_data_types.py +31 -0
  118. mtppy/part5/definitions/iec61131_data_types_test.py +82 -0
  119. mtppy/part5/definitions/opcua_data_types.py +36 -0
  120. mtppy/part5/definitions/opcua_data_types_test.py +90 -0
  121. mtppy/part5/definitions/w3c_xml_data_types.py +177 -0
  122. mtppy/part5/definitions/w3c_xml_data_types_test.py +143 -0
  123. mtppy/part5/server_assembly_set/__init__.py +5 -0
  124. mtppy/part6/__init__.py +4 -0
  125. mtppy/part6/alarm_set/__init__.py +4 -0
  126. mtppy/part6/alarm_set/alarm.py +132 -0
  127. mtppy/part6/alarm_set/alarm_test.py +265 -0
  128. mtppy/pea/__init__.py +19 -0
  129. mtppy/pea/data_assembly_factory.py +124 -0
  130. mtppy/pea/data_assembly_factory_test.py +229 -0
  131. mtppy/pea/mtp_generator/__init__.py +4 -0
  132. mtppy/pea/mtp_generator/mtp_generator.py +1183 -0
  133. mtppy/pea/mtp_generator/mtp_generator_test.py +518 -0
  134. mtppy/pea/pea.py +139 -0
  135. mtppy/pea/pea_opcua_server/__init__.py +4 -0
  136. mtppy/pea/pea_opcua_server/marshalling.py +60 -0
  137. mtppy/pea/pea_opcua_server/opcua_options_provider/__init__.py +4 -0
  138. mtppy/pea/pea_opcua_server/opcua_options_provider/instances/basic_opcua_options_provider.py +50 -0
  139. mtppy/pea/pea_opcua_server/opcua_options_provider/opcua_options_provider.py +103 -0
  140. mtppy/pea/pea_opcua_server/pea_opcua_server.py +492 -0
  141. mtppy/pea/pea_opcua_server/subscription_list.py +55 -0
  142. mtppy/pea/pea_test.py +59 -0
  143. mtppy/pea/rmtp_pea.py +155 -0
  144. mtppy/pea/rmtp_pea_test.py +27 -0
  145. mtppy/pea/service_factory.py +178 -0
  146. mtppy/pea/service_factory_test.py +156 -0
  147. mtppy/pea/user_logic/__init__.py +4 -0
  148. mtppy/pea/user_logic/user_logic_pea.py +72 -0
  149. mtppy/pea/user_logic/user_logic_registry.py +106 -0
  150. mtppy/pea/user_logic/user_logic_registry_test.py +167 -0
  151. mtppy/pea/user_logic/user_logic_service.py +228 -0
  152. mtppy/pea/user_logic/user_logic_service_test.py +289 -0
  153. mtppy/utils/__init__.py +4 -0
  154. mtppy/utils/data_assembly_dict.py +61 -0
  155. mtppy/utils/data_assembly_dict_test.py +107 -0
  156. mtppy/utils/data_item.py +446 -0
  157. mtppy/utils/data_item_com_test.py +82 -0
  158. mtppy/utils/data_item_test.py +325 -0
  159. mtppy/utils/logging.py +23 -0
  160. mtppy/utils/logging_test.py +26 -0
  161. mtppy/utils/mtp_file_handling/__init__.py +4 -0
  162. mtppy/utils/mtp_file_handling/extract_manifest_from_mtp_file.py +22 -0
  163. mtppy/utils/mtp_file_handling/extract_mtp_file.py +22 -0
  164. mtppy/utils/mtp_file_handling/is_valid_mtp.py +22 -0
  165. mtppy/utils/mtp_file_handling/is_valid_path_for_manifest.py +15 -0
  166. mtppy/utils/mtp_file_handling/is_valid_path_to_manifest.py +17 -0
  167. mtppy/utils/mtp_file_handling/is_valid_path_to_mtp.py +17 -0
  168. mtppy/utils/mtp_file_handling/load_manifest_file.py +24 -0
  169. mtppy/utils/mtp_file_handling/mtp_reader/__init__.py +4 -0
  170. mtppy/utils/mtp_file_handling/mtp_reader/mtp_reader.py +103 -0
  171. mtppy/utils/mtp_file_handling/mtp_reader/mtp_reader_test.py +96 -0
  172. mtppy/utils/mtp_file_handling/mtp_reader/mtp_xml.py +226 -0
  173. mtppy/utils/mtp_file_handling/mtp_reader/mtp_xml_test.py +108 -0
  174. mtppy/utils/mtp_file_handling/mtp_writer/__init__.py +4 -0
  175. mtppy/utils/mtp_file_handling/mtp_writer/mtp_writer.py +77 -0
  176. mtppy/utils/mtp_file_handling/mtp_writer/ressources/get_content_types.py +10 -0
  177. mtppy/utils/mtp_file_handling/mtp_writer/ressources/get_rels.py +11 -0
  178. mtppy/utils/mtp_file_handling/mtp_writer/test_mtp_writer.py +25 -0
  179. mtppy/utils/mtp_file_handling/write_manifest_file.py +29 -0
  180. mtppy/utils/mtp_file_handling/write_manifest_file_test.py +22 -0
  181. mtppy/utils/mtp_file_handling/write_mtp_file.py +43 -0
  182. mtppy/utils/mtp_file_handling/write_mtp_file_test.py +22 -0
  183. mtppy/utils/testing.py +31 -0
  184. mtppy2_0-0.1.5.dist-info/METADATA +75 -0
  185. mtppy2_0-0.1.5.dist-info/RECORD +214 -0
  186. mtppy2_0-0.1.5.dist-info/WHEEL +5 -0
  187. mtppy2_0-0.1.5.dist-info/entry_points.txt +3 -0
  188. mtppy2_0-0.1.5.dist-info/licenses/LICENSE +21 -0
  189. mtppy2_0-0.1.5.dist-info/top_level.txt +4 -0
  190. tools/__init__.py +0 -0
  191. tools/create_bugreport.py +150 -0
  192. tools/inspect_mtp.py +110 -0
  193. utils/__init__.py +1 -0
  194. utils/file_handling/__init__.py +4 -0
  195. utils/file_handling/cleanup_folder.py +9 -0
  196. utils/file_handling/get_utc_foldername_with_ms.py +15 -0
  197. utils/file_handling/get_work_dir.py +9 -0
  198. utils/file_handling/is_valid_path_for_json.py +11 -0
  199. utils/file_handling/is_valid_path_to_json.py +17 -0
  200. utils/file_handling/load_json_file.py +18 -0
  201. utils/file_handling/read_file_from_file_archive.py +43 -0
  202. utils/file_handling/read_file_string.py +10 -0
  203. utils/file_handling/validate_json_data.py +28 -0
  204. utils/file_handling/write_json_file.py +23 -0
  205. utils/get_uuid.py +18 -0
  206. utils/get_uuid_test.py +17 -0
  207. utils/logging.py +100 -0
  208. utils/logging_test.py +67 -0
  209. utils/opcua/check_node.py +79 -0
  210. utils/resolve_unit/resolve_unit.py +11 -0
  211. utils/resolve_unit/unit_lib.py +2101 -0
  212. utils/resolve_unit/unit_lib_test.py +36 -0
  213. utils/type_conversion.py +236 -0
  214. 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,4 @@
1
+ __copyright__ = (
2
+ "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
3
+ )
4
+ __license__ = "MIT"
@@ -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,4 @@
1
+ __copyright__ = (
2
+ "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
3
+ )
4
+ __license__ = "MIT"
@@ -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