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,96 @@
1
+ """Tests for MtpReader orchestration and alias mapping."""
2
+
3
+ from pathlib import Path
4
+ from xml.etree.ElementTree import ElementTree, fromstring
5
+
6
+ from mtppy.utils.mtp_file_handling.mtp_reader.mtp_reader import MtpReader
7
+
8
+
9
+ def test_empty_reader_has_empty_collections() -> None:
10
+ reader = MtpReader()
11
+
12
+ assert reader.data_assemblies == {}
13
+ assert reader.services == {}
14
+ assert reader.alias_opcua_map == {}
15
+
16
+
17
+ def test_create_alias_opcua_node_map_only_maps_referenced_attributes() -> None:
18
+ reader = MtpReader()
19
+ data_assemblies = {
20
+ "assembly": {
21
+ "Mapped": {"value": "reference"},
22
+ "Missing": {"value": "unknown"},
23
+ "Plain": "value",
24
+ }
25
+ }
26
+
27
+ assert reader.create_alias_opcua_node_map(
28
+ data_assemblies, {"reference": "ns=2;s=assembly.Mapped"}
29
+ ) == {"assembly.Mapped": "ns=2;s=assembly.Mapped"}
30
+
31
+
32
+ def test_read_mtp_runs_validation_extraction_loading_and_cleanup(monkeypatch) -> None:
33
+ reader = MtpReader()
34
+ manifest = ElementTree(fromstring("<Manifest />"))
35
+ calls: list[tuple[str, object]] = []
36
+
37
+ monkeypatch.setattr(
38
+ "mtppy.utils.mtp_file_handling.mtp_reader.mtp_reader.is_valid_mtp",
39
+ lambda path: calls.append(("validate", path)),
40
+ )
41
+ monkeypatch.setattr(
42
+ "mtppy.utils.mtp_file_handling.mtp_reader.mtp_reader.get_work_dir",
43
+ lambda base: calls.append(("workdir", base)) or Path("workdir"),
44
+ )
45
+ monkeypatch.setattr(
46
+ "mtppy.utils.mtp_file_handling.mtp_reader.mtp_reader.extract_manifest_from_mtp_file",
47
+ lambda path, workdir: calls.append(("extract", (path, workdir)))
48
+ or Path("manifest.xml"),
49
+ )
50
+ monkeypatch.setattr(
51
+ "mtppy.utils.mtp_file_handling.mtp_reader.mtp_reader.load_manifest_file",
52
+ lambda path: calls.append(("load", path)) or manifest,
53
+ )
54
+ monkeypatch.setattr(
55
+ "mtppy.utils.mtp_file_handling.mtp_reader.mtp_reader.cleanup_folder",
56
+ lambda workdir: calls.append(("cleanup", workdir)),
57
+ )
58
+
59
+ result = reader.read_mtp("example.mtp")
60
+
61
+ assert result is manifest
62
+ assert calls == [
63
+ ("validate", "example.mtp"),
64
+ ("workdir", "../workdir"),
65
+ ("extract", ("example.mtp", Path("workdir"))),
66
+ ("load", Path("manifest.xml")),
67
+ ("cleanup", Path("workdir")),
68
+ ]
69
+
70
+
71
+ def test_reader_initializes_parsed_collections(monkeypatch) -> None:
72
+ reader = MtpReader()
73
+ manifest = ElementTree(fromstring("<Manifest />"))
74
+ monkeypatch.setattr(
75
+ MtpReader,
76
+ "read_mtp",
77
+ lambda self, path: manifest,
78
+ )
79
+ monkeypatch.setattr(
80
+ "mtppy.utils.mtp_file_handling.mtp_reader.mtp_reader.get_opcua_node_ids",
81
+ lambda root: {"ref": "ns=2;s=assembly.Value"},
82
+ )
83
+ monkeypatch.setattr(
84
+ "mtppy.utils.mtp_file_handling.mtp_reader.mtp_reader.get_data_assemblies",
85
+ lambda root: {"assembly": {"Value": {"value": "ref"}}},
86
+ )
87
+ monkeypatch.setattr(
88
+ "mtppy.utils.mtp_file_handling.mtp_reader.mtp_reader.get_services",
89
+ lambda root: {"service": {}},
90
+ )
91
+
92
+ result = MtpReader("example.mtp")
93
+
94
+ assert result.data_assemblies == {"assembly": {"Value": {"value": "ref"}}}
95
+ assert result.services == {"service": {}}
96
+ assert result.alias_opcua_map == {"assembly.Value": "ns=2;s=assembly.Value"}
@@ -0,0 +1,226 @@
1
+ """Utilities for XML file handling."""
2
+
3
+ __copyright__ = (
4
+ "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
5
+ )
6
+ __license__ = "MIT"
7
+
8
+ import uuid
9
+ from typing import Any
10
+ from xml.etree.ElementTree import Element
11
+
12
+ from mtppy.pea.data_assembly_factory import DataAssemblyConfig
13
+
14
+
15
+ def is_valid_guid(guid_string: str) -> bool:
16
+ """Check if the given string is a valid GUID/UUID.
17
+
18
+ Args:
19
+ guid_string: The string to validate.
20
+
21
+ Returns:
22
+ bool: True if the string is a valid GUID, False otherwise.
23
+ """
24
+ try:
25
+ uuid.UUID(guid_string)
26
+ return True
27
+ except (ValueError, TypeError):
28
+ return False
29
+
30
+
31
+ NMSPC = {"ns0": "http://www.dke.de/CAEX"}
32
+ _OPCUA_ITEM = "MTPServerAssemblyICLib/DataItem/OPCUAItem"
33
+ _EXT_IF = "ns0:ExternalInterface"
34
+ _INT_EL = "ns0:InternalElement"
35
+ _ATTR = "ns0:Attribute"
36
+ _RBCP = "RefBaseClassPath"
37
+ _VAL = "ns0:Value"
38
+ _DEF_VAL = "ns0:DefaultValue"
39
+ _INST_HRCHY = "ns0:InstanceHierarchy"
40
+ _DA_SET = "DataAssemblies"
41
+ _SRV_SET = "Services"
42
+
43
+
44
+ def get_child_value_text(elem: Element) -> str:
45
+ """Get the first <Value>content</Value> child of an element.
46
+
47
+ Args:
48
+ elem: The XML element to search.
49
+
50
+ Returns:
51
+ str: The value text if found, empty string otherwise.
52
+ """
53
+ element = elem.find(f"./{_VAL}", namespaces=NMSPC)
54
+ if element is not None and element.text is not None:
55
+ return element.text
56
+ return ""
57
+
58
+
59
+ def get_child_default_value_text(elem: Element) -> str:
60
+ """Get the first <DefaultValue>content</DefaultValue> of an element.
61
+
62
+ Args:
63
+ elem: The XML element to search.
64
+
65
+ Returns:
66
+ str: The default value text if found, empty string otherwise.
67
+ """
68
+ element = elem.find(f"./{_DEF_VAL}", namespaces=NMSPC)
69
+ if element is not None and element.text is not None:
70
+ return element.text
71
+ return ""
72
+
73
+
74
+ def get_all_child_attribute_text(
75
+ elem: Element,
76
+ node_ids: dict[str, str] | None = None,
77
+ ) -> dict[str, dict[str, str]]:
78
+ """Get all child attribute texts from an element.
79
+
80
+ Args:
81
+ elem: The XML element to process.
82
+ node_ids: Optional mapping of node IDs to their replacement values.
83
+
84
+ Returns:
85
+ dict: Dictionary containing attribute names and values.
86
+ """
87
+ result: dict[str, dict[str, str]] = {}
88
+ attributes = elem.findall(f"./{_ATTR}[@Name]", namespaces=NMSPC)
89
+ for attr in attributes:
90
+ value = get_child_value_text(attr)
91
+ default = get_child_default_value_text(attr)
92
+ if node_ids and value in node_ids:
93
+ result[attr.attrib["Name"]] = {"value": node_ids[value], "default": default}
94
+ else:
95
+ result[attr.attrib["Name"]] = {"value": value, "default": default}
96
+ return result
97
+
98
+
99
+ def get_opcua_node_id(root: Element, identifier: str) -> str:
100
+ """Get OPC UA node ID from an ExternalInterface element.
101
+
102
+ Args:
103
+ root: The root ElementTree to search.
104
+ identifier: The ID of the ExternalInterface element.
105
+
106
+ Returns:
107
+ str: The OPC UA node ID in the format "ns=<namespace>;s=<identifier>",
108
+ or an empty string if not found.
109
+ """
110
+ result: dict[str, str] = {}
111
+ xpath = f".//{_EXT_IF}[@ID='{identifier}'][@{_RBCP}='{_OPCUA_ITEM}']/{_ATTR}"
112
+ elements = root.findall(xpath, namespaces=NMSPC)
113
+ if len(elements):
114
+ for e in elements:
115
+ if "Name" in e.attrib and e.attrib["Name"] and e.text:
116
+ result[e.attrib["Name"]] = e.text
117
+ return f"ns={result['namespace']};s={result['Identifier']}"
118
+ return ""
119
+
120
+
121
+ def get_data_assemblies(root: Element | None) -> dict[str, DataAssemblyConfig]:
122
+ """Extract data assemblies from the XML root element.
123
+
124
+ Args:
125
+ root: The root ElementTree to search.
126
+
127
+ Returns:
128
+ dict: A dictionary mapping data assembly names to their configurations.
129
+ """
130
+ result: dict[str, DataAssemblyConfig] = {}
131
+ if root is not None:
132
+ xpath = f".//{_INST_HRCHY}[@Name='{_DA_SET}']"
133
+ dass = root.findall(xpath, namespaces=NMSPC)
134
+ if len(dass):
135
+ xpath = f"./{_INT_EL}"
136
+ for das in dass:
137
+ for data_assembly in das.findall(xpath, namespaces=NMSPC):
138
+ name = data_assembly.attrib["Name"]
139
+ value = get_all_child_attribute_text(data_assembly)
140
+ # add attributes to values (ID, Name, RefBaseSystemUnitPath, RefID)
141
+ for attr, attr_value in data_assembly.attrib.items():
142
+ value[attr] = attr_value
143
+ if name in result:
144
+ for k, v in value.items():
145
+ if k in result[name]:
146
+ result[name][k] = [result[name][k], v]
147
+ else:
148
+ result[name][k] = v
149
+ else:
150
+ result[name] = value
151
+ return result
152
+
153
+
154
+ def get_named_childs_recursive(parent: Element, xpath: str) -> dict[str, Any]:
155
+ """Recursively get named children from an XML element.
156
+
157
+ Args:
158
+ parent: The parent XML element.
159
+ xpath: The XPath expression to find child elements.
160
+
161
+ Returns:
162
+ dict: A dictionary of named children and their attributes.
163
+ """
164
+ result = {}
165
+ for child in parent.findall(xpath, namespaces=NMSPC):
166
+ name = child.attrib["Name"]
167
+ value = get_all_child_attribute_text(child)
168
+ for attr, attr_value in child.attrib.items():
169
+ value[attr] = attr_value
170
+ for grandchild, gc_value in get_named_childs_recursive(child, xpath).items():
171
+ value[grandchild] = gc_value
172
+ if name in result:
173
+ for k, v in value.items():
174
+ if k in result[name]:
175
+ if result[name][k] != v:
176
+ result[name][k] = [result[name][k], v]
177
+ else:
178
+ result[name][k] = v
179
+ else:
180
+ result[name] = value
181
+ return result
182
+
183
+
184
+ def get_services(root: Element | None) -> dict[str, dict[str, str | dict[str, str]]]:
185
+ """Extract services from the XML root element.
186
+
187
+ Args:
188
+ root: The root ElementTree to search.
189
+
190
+ Returns:
191
+ dict: A dictionary mapping service names to their configurations.
192
+ """
193
+ result: dict[str, dict[str, str]] = {}
194
+ if root is not None:
195
+ xpath = f".//{_INST_HRCHY}[@Name='{_SRV_SET}']"
196
+ sss = root.findall(xpath, namespaces=NMSPC)
197
+ if len(sss):
198
+ xpath = f"./{_INT_EL}"
199
+ for services in sss:
200
+ result = dict(get_named_childs_recursive(services, xpath).items())
201
+ return result
202
+
203
+
204
+ def get_opcua_node_ids(root: Element | None) -> dict[str, str]:
205
+ """Get all OPC UA node IDs from ExternalInterface elements.
206
+
207
+ Args:
208
+ root: The root ElementTree to search.
209
+
210
+ Returns:
211
+ dict: A dictionary mapping ExternalInterface IDs to their node information.
212
+ """
213
+ result: dict[str, str] = {}
214
+ if root is not None:
215
+ elements = root.findall(
216
+ f".//{_EXT_IF}[@{_RBCP}='{_OPCUA_ITEM}']",
217
+ namespaces=NMSPC,
218
+ )
219
+ if len(elements):
220
+ for elem in elements:
221
+ value = get_all_child_attribute_text(elem)
222
+ ref = elem.attrib["ID"]
223
+ ns = value["Namespace"]["value"]
224
+ identifier = value["Identifier"]["value"]
225
+ result[ref] = f"ns={ns};s={identifier}"
226
+ return result
@@ -0,0 +1,108 @@
1
+ """Tests for MTP manifest XML extraction helpers."""
2
+
3
+ from xml.etree.ElementTree import Element, fromstring
4
+
5
+ from mtppy.utils.mtp_file_handling.mtp_reader import mtp_xml
6
+
7
+ NS = "http://www.dke.de/CAEX"
8
+
9
+
10
+ def element(xml: str) -> Element:
11
+ return fromstring(f'<root xmlns="{NS}">{xml}</root>')
12
+
13
+
14
+ def test_is_valid_guid() -> None:
15
+ assert mtp_xml.is_valid_guid("12345678-1234-5678-1234-567812345678")
16
+ assert not mtp_xml.is_valid_guid("invalid")
17
+
18
+
19
+ def test_child_value_and_default_text() -> None:
20
+ attribute = element("<Value>actual</Value><DefaultValue>default</DefaultValue>")
21
+
22
+ assert mtp_xml.get_child_value_text(attribute) == "actual"
23
+ assert mtp_xml.get_child_default_value_text(attribute) == "default"
24
+ assert mtp_xml.get_child_value_text(element("<Other />")) == ""
25
+
26
+
27
+ def test_get_all_child_attribute_text_replaces_node_reference() -> None:
28
+ assembly = element(
29
+ '<Attribute Name="TagName"><Value>assembly</Value></Attribute>'
30
+ '<Attribute Name="Node"><Value>node-ref</Value><DefaultValue>0</DefaultValue></Attribute>'
31
+ )
32
+
33
+ assert mtp_xml.get_all_child_attribute_text(assembly) == {
34
+ "TagName": {"value": "assembly", "default": ""},
35
+ "Node": {"value": "node-ref", "default": "0"},
36
+ }
37
+ assert (
38
+ mtp_xml.get_all_child_attribute_text(
39
+ assembly, {"node-ref": "ns=2;s=assembly.Node"}
40
+ )["Node"]["value"]
41
+ == "ns=2;s=assembly.Node"
42
+ )
43
+
44
+
45
+ def test_get_opcua_node_id() -> None:
46
+ interface = element(
47
+ '<ExternalInterface ID="ref" RefBaseClassPath="MTPServerAssemblyICLib/DataItem/OPCUAItem">'
48
+ '<Attribute Name="namespace">2</Attribute>'
49
+ '<Attribute Name="Identifier">assembly.Value</Attribute>'
50
+ "</ExternalInterface>"
51
+ )
52
+
53
+ assert mtp_xml.get_opcua_node_id(interface, "ref") == "ns=2;s=assembly.Value"
54
+ assert mtp_xml.get_opcua_node_id(interface, "missing") == ""
55
+
56
+
57
+ def test_get_data_assemblies_extracts_attributes_and_merges_duplicates() -> None:
58
+ root = fromstring(
59
+ f'<root xmlns="{NS}">'
60
+ '<InstanceHierarchy Name="DataAssemblies">'
61
+ '<InternalElement Name="upper_bound" ID="id-1" RefBaseSystemUnitPath="type-1">'
62
+ '<Attribute Name="TagName"><Value>upper_bound</Value></Attribute>'
63
+ "</InternalElement>"
64
+ '<InternalElement Name="upper_bound" ID="id-2" RefBaseSystemUnitPath="type-2">'
65
+ '<Attribute Name="TagName"><Value>upper_bound</Value></Attribute>'
66
+ "</InternalElement>"
67
+ "</InstanceHierarchy></root>"
68
+ )
69
+
70
+ assemblies = mtp_xml.get_data_assemblies(root)
71
+
72
+ assert assemblies["upper_bound"]["ID"] == ["id-1", "id-2"]
73
+ assert assemblies["upper_bound"]["TagName"] == [
74
+ {"value": "upper_bound", "default": ""},
75
+ {"value": "upper_bound", "default": ""},
76
+ ]
77
+ assert mtp_xml.get_data_assemblies(None) == {}
78
+
79
+
80
+ def test_get_services_recurses_through_nested_elements() -> None:
81
+ root = fromstring(
82
+ f'<root xmlns="{NS}">'
83
+ '<InstanceHierarchy Name="Services">'
84
+ '<InternalElement Name="rand_num_gen" RefBaseSystemUnitPath="service">'
85
+ '<Attribute Name="Name"><Value>rand_num_gen</Value></Attribute>'
86
+ '<InternalElement Name="cont" RefBaseSystemUnitPath="procedure">'
87
+ '<Attribute Name="Name"><Value>cont</Value></Attribute>'
88
+ "</InternalElement></InternalElement>"
89
+ "</InstanceHierarchy></root>"
90
+ )
91
+
92
+ services = mtp_xml.get_services(root)
93
+
94
+ assert services["rand_num_gen"]["cont"]["RefBaseSystemUnitPath"] == "procedure"
95
+ assert mtp_xml.get_services(None) == {}
96
+
97
+
98
+ def test_get_opcua_node_ids_extracts_external_interface_mapping() -> None:
99
+ root = fromstring(
100
+ f'<root xmlns="{NS}">'
101
+ '<ExternalInterface ID="ref" RefBaseClassPath="MTPServerAssemblyICLib/DataItem/OPCUAItem">'
102
+ '<Attribute Name="Namespace"><Value>2</Value></Attribute>'
103
+ '<Attribute Name="Identifier"><Value>upper_bound.VOp</Value></Attribute>'
104
+ "</ExternalInterface></root>"
105
+ )
106
+
107
+ assert mtp_xml.get_opcua_node_ids(root) == {"ref": "ns=2;s=upper_bound.VOp"}
108
+ assert mtp_xml.get_opcua_node_ids(None) == {}
@@ -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,77 @@
1
+ """Class MtpWriter writes an MTP file."""
2
+
3
+ __copyright__ = (
4
+ "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
5
+ )
6
+ __license__ = "MIT"
7
+
8
+ import logging
9
+ from pathlib import Path
10
+
11
+ from mtppy.utils.mtp_file_handling.is_valid_path_to_mtp import is_valid_path_to_mtp
12
+ from mtppy.utils.mtp_file_handling.mtp_writer.ressources.get_content_types import (
13
+ get_content_types,
14
+ )
15
+ from mtppy.utils.mtp_file_handling.mtp_writer.ressources.get_rels import get_rels
16
+ from mtppy.utils.mtp_file_handling.write_manifest_file import write_manifest_file
17
+ from mtppy.utils.mtp_file_handling.write_mtp_file import write_mtp_file
18
+ from utils.file_handling.cleanup_folder import cleanup_folder
19
+ from utils.file_handling.get_utc_foldername_with_ms import get_utc_foldername_with_ms
20
+ from utils.file_handling.get_work_dir import get_work_dir
21
+
22
+
23
+ class MtpWriter:
24
+ def __init__(self):
25
+ """Writer for MTP files.
26
+ """
27
+
28
+ def write_mtp(
29
+ self,
30
+ manifest_content: str,
31
+ out_path: str | None = None,
32
+ mtp_name: str | None = None,
33
+ ):
34
+ try:
35
+ logging.info("🔍 Checking Manifest content....")
36
+ # conditional check?
37
+ logging.info("✅ Checking Manifest content --> PASSED")
38
+
39
+ workdir = get_work_dir(base="../workdir")
40
+ logging.info("📦 Write Manifest AML file...")
41
+ manifest_file_path = Path(workdir).joinpath("manifest.aml")
42
+ write_manifest_file(manifest_content, manifest_file_path)
43
+
44
+ logging.info("✅ Write Manifest AML file complete.")
45
+
46
+ if out_path is None:
47
+ out_dir_path = get_work_dir(base="../output")
48
+ else:
49
+ out_dir_path = out_path
50
+
51
+ if mtp_name is None:
52
+ mtp_file_name = f"MTPPy2.0-MTP-{get_utc_foldername_with_ms()}.mtp"
53
+ else:
54
+ mtp_file_name = f"{mtp_name}.mtp"
55
+ mtp_path = Path(out_dir_path).joinpath(mtp_file_name)
56
+ is_valid_path_to_mtp(mtp_path)
57
+
58
+ logging.info(f"💾 Saving data to MTP: {out_dir_path}")
59
+ base_dir = Path(__file__).parent
60
+ resource_path = base_dir.joinpath("ressources/CAEX_ClassModel_V.3.0.xsd")
61
+ write_mtp_file(
62
+ mtp_path,
63
+ {
64
+ "manifest.aml": manifest_file_path,
65
+ "_rels/.rels": get_rels(),
66
+ "[Content_Types].xml": get_content_types(),
67
+ "CAEX_ClassModel_V.3.0.xsd": resource_path,
68
+ },
69
+ [],
70
+ )
71
+
72
+ logging.info(" ♲ Cleaning up...")
73
+ cleanup_folder(workdir)
74
+
75
+ logging.info("✅ MTP creation complete.")
76
+ except Exception as e:
77
+ print(f"❌ Error: {e}")
@@ -0,0 +1,10 @@
1
+ """get content types as XML helper."""
2
+
3
+ __copyright__ = (
4
+ "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
5
+ )
6
+ __license__ = "MIT"
7
+
8
+
9
+ def get_content_types() -> str:
10
+ return '<?xml version="1.0" encoding="utf-8"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="aml" ContentType="model/vnd.automationml+xml" /><Default Extension="xsd" ContentType="text/xml" /><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" /></Types>'
@@ -0,0 +1,11 @@
1
+ """get relations as XML method."""
2
+
3
+ __copyright__ = (
4
+ "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
5
+ )
6
+ __license__ = "MIT"
7
+
8
+
9
+ def get_rels() -> str:
10
+
11
+ return '<?xml version="1.0" encoding="utf-8"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Type="http://schemas.automationml.org/container/relationship/RootDocument" Target="/manifest.aml" Id="R37fdc16552e64602" /><Relationship Type="http://schemas.automationml.org/container/relationship/CAEXSchema" Target="/CAEX_ClassModel_V.3.0.xsd" Id="Rbd8467381b1c4394" /></Relationships>'
@@ -0,0 +1,25 @@
1
+ """test suite for the mtp writer."""
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.mtp_writer.mtp_writer import MtpWriter
11
+ from utils.file_handling.get_work_dir import get_work_dir
12
+
13
+
14
+ def test_mtp_writer():
15
+ mtp_writer = MtpWriter()
16
+
17
+ extract_folder = get_work_dir("../output")
18
+ root = ET.Element("root")
19
+ child = ET.SubElement(root, "item", attrib={"id": "1"})
20
+ child.text = "Hello World"
21
+ xml_str = ET.tostring(root, encoding="utf-8", xml_declaration=True).decode("utf-8")
22
+
23
+ mtp_writer.write_mtp(xml_str, extract_folder, "My-First-MTPPy-MTP")
24
+
25
+ # cleanup_folder(extract_folder)
@@ -0,0 +1,29 @@
1
+ """Write the manifest part of an MTP file."""
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_for_manifest import (
11
+ is_valid_path_for_manifest,
12
+ )
13
+
14
+
15
+ def write_manifest_file(xml_content: str, output_file_path):
16
+ """Write and return manifest content from file as XML Element Tree.
17
+
18
+ Args:
19
+ xml_content: XML content to write
20
+ output_file_path: Path where the manifest file should be written
21
+
22
+ Raises:
23
+ NotADirectoryError: If the output file path is invalid
24
+ """
25
+ if not is_valid_path_for_manifest(output_file_path):
26
+ raise NotADirectoryError(f"Invalid path: {output_file_path}")
27
+ tree = ET.ElementTree(ET.fromstring(xml_content))
28
+ ET.indent(tree, space=" ")
29
+ tree.write(output_file_path, encoding="utf-8", xml_declaration=True)
@@ -0,0 +1,22 @@
1
+ """test suite for the writing the manifest part of an MTP file."""
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.write_manifest_file import write_manifest_file
11
+ from utils.file_handling.cleanup_folder import cleanup_folder
12
+ from utils.file_handling.get_work_dir import get_work_dir
13
+
14
+
15
+ def test_write_manifest_file():
16
+ extract_folder = get_work_dir()
17
+ root = ET.Element("root")
18
+ child = ET.SubElement(root, "item", attrib={"id": "1"})
19
+ child.text = "value"
20
+ xml_str = ET.tostring(root, encoding="utf-8", xml_declaration=True).decode("utf-8")
21
+ write_manifest_file(xml_str, f"{extract_folder}/manifest.aml")
22
+ cleanup_folder(extract_folder)
@@ -0,0 +1,43 @@
1
+ """write an MTP File."""
2
+
3
+ __copyright__ = (
4
+ "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
5
+ )
6
+ __license__ = "MIT"
7
+
8
+ import logging
9
+ import os
10
+ import zipfile
11
+ from pathlib import WindowsPath
12
+
13
+
14
+ def write_mtp_file(output_path, files, folders):
15
+ """Combine files and folders to an MTP file.
16
+ files: dict mapping {file_name: source_path} or {file_name: bytes/str content} to zip
17
+ With content "data.txt": "some text content",
18
+ With file reference "config.json": "/path/to/config.json",
19
+ folders: list of folder paths to zip recursively, preserving relative structure (optional)
20
+ """
21
+ with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf:
22
+ if files:
23
+ for file_name, source in files.items():
24
+ if isinstance(source, WindowsPath) or not isinstance(source, (bytes, str)):
25
+ zf.write(source, arcname=file_name)
26
+ else:
27
+ zf.writestr(file_name, source)
28
+
29
+ if folders:
30
+ for folder in folders:
31
+ folder = os.path.normpath(folder)
32
+ base_name = os.path.basename(folder)
33
+ for root, _, filenames in os.walk(folder):
34
+ for filename in filenames:
35
+ filepath = os.path.join(root, filename)
36
+ rel_path = os.path.relpath(filepath, folder)
37
+ arcname = os.path.join(base_name, rel_path)
38
+ zf.write(filepath, arcname=arcname)
39
+
40
+ logging.debug(f"MTP File written to: {output_path}")
41
+ return output_path
42
+
43
+ # with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zf: