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,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,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:
|