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
tools/inspect_mtp.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Read MTP-file and inspect aspects like services, data_assemblies and opcua-nodes.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to read MTP files (-f <filename>) and inspect
|
|
4
|
+
various aspects including services (-s), data assemblies (-d), and OPC UA node (-n).
|
|
5
|
+
|
|
6
|
+
The two most import CLI attributes are -k <regexp> that allows to filter services and
|
|
7
|
+
data assemblies by their system names and -a <regexp> that allows to filter on
|
|
8
|
+
the attribute level of data assemblies.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
```python -m tools.inspect_mtp -f minimal.mtp -d -n -k upper -a RefB```
|
|
12
|
+
would give the RefBaseSystemUnitPath of DataAssembly upper_bound and the
|
|
13
|
+
list of node-ids
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__copyright__ = (
|
|
17
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
18
|
+
)
|
|
19
|
+
__license__ = "MIT"
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
|
|
23
|
+
from mtppy.pea.data_assembly_factory import DataAssemblyFactory
|
|
24
|
+
from mtppy.utils.mtp_file_handling.mtp_reader.mtp_reader import MtpReader
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parseargs() -> argparse.Namespace:
|
|
28
|
+
"""Parse CLI arguments.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
argparse.Namespace: Parsed arguments namespace.
|
|
32
|
+
"""
|
|
33
|
+
p = argparse.ArgumentParser("test MtpReader")
|
|
34
|
+
p.add_argument(
|
|
35
|
+
"-k",
|
|
36
|
+
"--key",
|
|
37
|
+
default=".+",
|
|
38
|
+
help="regular expression for filtering by key",
|
|
39
|
+
)
|
|
40
|
+
p.add_argument(
|
|
41
|
+
"-a",
|
|
42
|
+
"--attribute",
|
|
43
|
+
default=".+",
|
|
44
|
+
help="regular expression for filtering by attribute",
|
|
45
|
+
)
|
|
46
|
+
p.add_argument(
|
|
47
|
+
"-c",
|
|
48
|
+
"--create",
|
|
49
|
+
action="store_true",
|
|
50
|
+
help="create data assemblies (only in conjunction with -d)",
|
|
51
|
+
)
|
|
52
|
+
p.add_argument("-f", "--filename", help="filename")
|
|
53
|
+
p.add_argument("-n", "--nodeid", action="store_true", help="node id s")
|
|
54
|
+
p.add_argument("-s", "--service", action="store_true", help="print services")
|
|
55
|
+
p.add_argument(
|
|
56
|
+
"-d",
|
|
57
|
+
"--dataassembly",
|
|
58
|
+
action="store_true",
|
|
59
|
+
help="print data assemblies",
|
|
60
|
+
)
|
|
61
|
+
return p.parse_args()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
"""Main function to execute the MTP inspection tool."""
|
|
66
|
+
import re
|
|
67
|
+
from pprint import pprint
|
|
68
|
+
|
|
69
|
+
args = parseargs()
|
|
70
|
+
|
|
71
|
+
reader = MtpReader(args.filename)
|
|
72
|
+
|
|
73
|
+
if args.dataassembly:
|
|
74
|
+
print(f'\n{"- " * 20} data assemblies')
|
|
75
|
+
pprint(
|
|
76
|
+
{
|
|
77
|
+
k: {
|
|
78
|
+
inner_k: inner_v
|
|
79
|
+
for inner_k, inner_v in v.items()
|
|
80
|
+
if re.match(args.attribute, inner_k)
|
|
81
|
+
}
|
|
82
|
+
for k, v in reader.data_assemblies.items()
|
|
83
|
+
if re.match(args.key, k)
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
if args.create:
|
|
87
|
+
DataAssemblyFactory.create_data_assemblies(reader.data_assemblies)
|
|
88
|
+
|
|
89
|
+
if args.nodeid:
|
|
90
|
+
print(f'\n{"- " * 20} nodeids')
|
|
91
|
+
pprint(
|
|
92
|
+
{
|
|
93
|
+
k: reader.alias_opcua_map[k]
|
|
94
|
+
for k in reader.alias_opcua_map
|
|
95
|
+
if re.match(args.key, k)
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
if args.service:
|
|
99
|
+
print(f'\n{"- " * 20} services')
|
|
100
|
+
pprint(
|
|
101
|
+
reader.services,
|
|
102
|
+
compact=True,
|
|
103
|
+
indent=2,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
print("- " * 20)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
main()
|
utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""utility function to get a UTC folder name with milliseconds."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
|
|
8
|
+
import datetime
|
|
9
|
+
|
|
10
|
+
from datetime import UTC
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_utc_foldername_with_ms():
|
|
14
|
+
dt = datetime.datetime.now(tz=UTC)
|
|
15
|
+
return dt.strftime("%Y-%m-%d_%H-%M-%S") + f"-{dt.microsecond // 1000:03d}Z"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from utils.file_handling.get_utc_foldername_with_ms import get_utc_foldername_with_ms
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_work_dir(base: str = "out"):
|
|
7
|
+
path = Path(base) / get_utc_foldername_with_ms()
|
|
8
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
9
|
+
return path.resolve()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""utility function to check if a path points to a valid JSON file.
|
|
2
|
+
"""
|
|
3
|
+
__copyright__ = "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
4
|
+
__license__ = "MIT"
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_valid_path_for_json(path):
|
|
10
|
+
path_object = Path(path)
|
|
11
|
+
return path_object.suffix == ".json"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""utility function to check if a path points to a valid JSON 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
|
+
|
|
10
|
+
from utils.file_handling.is_valid_path_for_json import is_valid_path_for_json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_valid_path_to_json(path):
|
|
14
|
+
path_object = Path(path)
|
|
15
|
+
return (
|
|
16
|
+
path_object.exists() and path_object.is_file() and is_valid_path_for_json(path)
|
|
17
|
+
)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""utility function to load JSON data from a file."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from utils.file_handling.is_valid_path_to_json import is_valid_path_to_json
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def load_json_file(file_path):
|
|
14
|
+
"""Load and return JSON content from file."""
|
|
15
|
+
if not is_valid_path_to_json(file_path):
|
|
16
|
+
raise FileNotFoundError(f"Invalid path: {file_path}")
|
|
17
|
+
with open(file_path) as f:
|
|
18
|
+
return json.load(f)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""utility function to read a specific file from a zip archive."""
|
|
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 zipfile
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def read_file_from_file_archive(archive_path, filename):
|
|
15
|
+
"""Read a specific file from a zip archive and returns its contents as a string.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
archive_path: Path to the file archive.
|
|
19
|
+
filename: Name of the file to read inside the archive (case-sensitive).
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Contents of the file as a string, or None if not found.
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
with zipfile.ZipFile(archive_path, "r") as zip_ref:
|
|
26
|
+
# List all files in the zip
|
|
27
|
+
file_list = zip_ref.namelist()
|
|
28
|
+
|
|
29
|
+
if filename in file_list:
|
|
30
|
+
with zip_ref.open(filename) as file:
|
|
31
|
+
return file.read().decode(
|
|
32
|
+
"utf-8"
|
|
33
|
+
) # You can change encoding if needed
|
|
34
|
+
else:
|
|
35
|
+
logger.info(f"'{filename}' not found in the archive.")
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
except zipfile.BadZipFile:
|
|
39
|
+
logger.error("The file is not a valid zip archive.")
|
|
40
|
+
return None
|
|
41
|
+
except (ValueError, NotImplementedError, RuntimeError) as e:
|
|
42
|
+
logger.error(f"error while reading '{filename}': {e}")
|
|
43
|
+
return None
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""utility function to validate JSON data against a schema."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from jsonschema import ValidationError, validate
|
|
11
|
+
|
|
12
|
+
from utils.file_handling.is_valid_path_to_json import is_valid_path_to_json
|
|
13
|
+
from utils.file_handling.load_json_file import load_json_file
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def validate_json_data(data, schema_path):
|
|
19
|
+
try:
|
|
20
|
+
if not is_valid_path_to_json(schema_path):
|
|
21
|
+
raise FileNotFoundError(f"Invalid schema path: {schema_path}")
|
|
22
|
+
schema = load_json_file(schema_path)
|
|
23
|
+
validate(instance=data, schema=schema)
|
|
24
|
+
logger.info(f"Json data validation successful for {schema_path}.")
|
|
25
|
+
return True
|
|
26
|
+
except ValidationError as e:
|
|
27
|
+
logger.error(f"error validating {schema_path}: {e.message}")
|
|
28
|
+
return False
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""utility function to write data to a JSON file."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
from utils.file_handling.is_valid_path_for_json import is_valid_path_for_json
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def write_json_file(data, output_path):
|
|
15
|
+
|
|
16
|
+
if is_valid_path_for_json(output_path):
|
|
17
|
+
try:
|
|
18
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
19
|
+
json.dump(data, f, indent=4)
|
|
20
|
+
except OSError as e:
|
|
21
|
+
raise OSError(f"Failed to write JSON output: {e}")
|
|
22
|
+
else:
|
|
23
|
+
raise TypeError("Invalid path to write JSON output!")
|
utils/get_uuid.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""**get_uuid** wraps uuid.uuid4."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_uuid() -> str:
|
|
12
|
+
"""Generate random uuid v4 for different elements.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
return: uuid v4 as string
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
return str(uuid.uuid4())
|
utils/get_uuid_test.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from utils.get_uuid import get_uuid
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_get_uuid():
|
|
7
|
+
random_id = get_uuid()
|
|
8
|
+
parts = random_id.split("-")
|
|
9
|
+
assert len(parts) == 5
|
|
10
|
+
|
|
11
|
+
expected_lengths = [8, 4, 4, 4, 12]
|
|
12
|
+
for i, expected_len in zip(parts, expected_lengths):
|
|
13
|
+
assert len(i) == expected_len
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
pytest.main(["test_get_uuid.py", "-s"])
|
utils/logging.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""provides a custom logger with support for custom info logging levels."""
|
|
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 collections.abc import Mapping
|
|
10
|
+
from typing import Any, Final
|
|
11
|
+
|
|
12
|
+
OPCUA_LEVEL: Final = logging.INFO + 1
|
|
13
|
+
MTP_LEVEL: Final = logging.INFO + 2
|
|
14
|
+
APP_LEVEL: Final = logging.INFO + 4
|
|
15
|
+
POL_LEVEL: Final = logging.INFO + 6
|
|
16
|
+
|
|
17
|
+
CUSTOM_LEVELS: Final[dict[str, int]] = {
|
|
18
|
+
"opcua": OPCUA_LEVEL - logging.INFO,
|
|
19
|
+
"mtp": MTP_LEVEL - logging.INFO,
|
|
20
|
+
"app": APP_LEVEL - logging.INFO,
|
|
21
|
+
"pol": POL_LEVEL - logging.INFO,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_LOGGER_FORMAT: Final = (
|
|
25
|
+
"%(asctime)s.%(msecs)03d [%(levelname)s] "
|
|
26
|
+
"%(thread)s::%(module)s.%(funcName)s: %(message)s"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
logging.basicConfig(
|
|
30
|
+
format=_LOGGER_FORMAT,
|
|
31
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
32
|
+
level=logging.DEBUG,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
for _name, _offset in CUSTOM_LEVELS.items():
|
|
36
|
+
logging.addLevelName(logging.INFO + _offset, _name.upper())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CustomLogger(logging.Logger):
|
|
40
|
+
"""Custom logger class that allows adding custom logging levels.
|
|
41
|
+
|
|
42
|
+
The levels are between the standard logging levels INFO and WARNING.
|
|
43
|
+
The logger is configured to display the timestamp, log level, thread, module,
|
|
44
|
+
function name, and message in the log output.
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
```python
|
|
48
|
+
from utils.logging import CustomLogger
|
|
49
|
+
logging.setLoggerClass(CustomLogger)
|
|
50
|
+
logger = logging.getLogger("MTPPy2.0Logger")
|
|
51
|
+
logger.add_custom_levels({"opcua": 1, "mtp": 2, "app": 4, "pol": 6})
|
|
52
|
+
```
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, name: str, level: int = logging.NOTSET) -> None:
|
|
56
|
+
"""Initialize a logger with the standard logging constructor."""
|
|
57
|
+
super().__init__(name, level)
|
|
58
|
+
|
|
59
|
+
def add_custom_levels(self, custom_levels: Mapping[str, int]) -> None:
|
|
60
|
+
"""Register level names using offsets from ``logging.INFO``.
|
|
61
|
+
|
|
62
|
+
The logger methods are explicit and are not generated dynamically. This
|
|
63
|
+
keeps the framework API visible to static type checkers.
|
|
64
|
+
"""
|
|
65
|
+
for level_name, offset in custom_levels.items():
|
|
66
|
+
if not level_name or not isinstance(offset, int):
|
|
67
|
+
raise ValueError("custom level names must have integer offsets")
|
|
68
|
+
logging.addLevelName(logging.INFO + offset, level_name.upper())
|
|
69
|
+
|
|
70
|
+
def opcua(self, msg: str, *args: object, **kwargs: object) -> None:
|
|
71
|
+
"""Log a message at the OPC UA level."""
|
|
72
|
+
self._log_custom_level(msg, OPCUA_LEVEL, args, kwargs)
|
|
73
|
+
|
|
74
|
+
def mtp(self, msg: str, *args: object, **kwargs: object) -> None:
|
|
75
|
+
"""Log a message at the MTP level."""
|
|
76
|
+
self._log_custom_level(msg, MTP_LEVEL, args, kwargs)
|
|
77
|
+
|
|
78
|
+
def app(self, msg: str, *args: object, **kwargs: object) -> None:
|
|
79
|
+
"""Log a message at the application level."""
|
|
80
|
+
self._log_custom_level(msg, APP_LEVEL, args, kwargs)
|
|
81
|
+
|
|
82
|
+
def pol(self, msg: str, *args: object, **kwargs: object) -> None:
|
|
83
|
+
"""Log a message at the POL level."""
|
|
84
|
+
self._log_custom_level(msg, POL_LEVEL, args, kwargs)
|
|
85
|
+
|
|
86
|
+
def _log_custom_level(
|
|
87
|
+
self,
|
|
88
|
+
msg: str,
|
|
89
|
+
level: int,
|
|
90
|
+
args: tuple[object, ...],
|
|
91
|
+
kwargs: dict[str, Any],
|
|
92
|
+
) -> None:
|
|
93
|
+
if self.isEnabledFor(level):
|
|
94
|
+
self._log(level, msg, args, stacklevel=2, **kwargs)
|
|
95
|
+
|
|
96
|
+
def add_custom_level(self, level_name: str, level_value: int) -> None:
|
|
97
|
+
"""Register one custom level name without modifying the logger API."""
|
|
98
|
+
if not level_name or not isinstance(level_value, int):
|
|
99
|
+
raise ValueError("custom level names must have integer values")
|
|
100
|
+
logging.addLevelName(level_value, level_name.upper())
|
utils/logging_test.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Tests for the reusable custom logging implementation."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from utils.logging import (
|
|
8
|
+
APP_LEVEL,
|
|
9
|
+
CUSTOM_LEVELS,
|
|
10
|
+
MTP_LEVEL,
|
|
11
|
+
OPCUA_LEVEL,
|
|
12
|
+
POL_LEVEL,
|
|
13
|
+
CustomLogger,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.mark.parametrize(
|
|
18
|
+
("method_name", "expected_level"),
|
|
19
|
+
[
|
|
20
|
+
("opcua", OPCUA_LEVEL),
|
|
21
|
+
("mtp", MTP_LEVEL),
|
|
22
|
+
("app", APP_LEVEL),
|
|
23
|
+
("pol", POL_LEVEL),
|
|
24
|
+
],
|
|
25
|
+
)
|
|
26
|
+
def test_custom_logger_methods_emit_expected_levels(
|
|
27
|
+
caplog, method_name: str, expected_level: int
|
|
28
|
+
) -> None:
|
|
29
|
+
logger = CustomLogger(f"test.custom.{method_name}")
|
|
30
|
+
logger.setLevel(logging.NOTSET)
|
|
31
|
+
logger.addHandler(caplog.handler)
|
|
32
|
+
logger.propagate = False
|
|
33
|
+
|
|
34
|
+
with caplog.at_level(logging.NOTSET):
|
|
35
|
+
getattr(logger, method_name)("message %s", method_name)
|
|
36
|
+
|
|
37
|
+
assert len(caplog.records) == 1
|
|
38
|
+
assert caplog.records[0].levelno == expected_level
|
|
39
|
+
assert caplog.records[0].levelname == method_name.upper()
|
|
40
|
+
assert caplog.records[0].message == f"message {method_name}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_custom_logger_registers_custom_level_names() -> None:
|
|
44
|
+
for name, offset in CUSTOM_LEVELS.items():
|
|
45
|
+
assert logging.getLevelName(logging.INFO + offset) == name.upper()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_custom_logger_rejects_invalid_level_registration() -> None:
|
|
49
|
+
logger = CustomLogger("test.invalid-level")
|
|
50
|
+
|
|
51
|
+
with pytest.raises(ValueError):
|
|
52
|
+
logger.add_custom_level("", logging.INFO)
|
|
53
|
+
with pytest.raises(ValueError):
|
|
54
|
+
logger.add_custom_level("invalid", 1.5) # type: ignore[arg-type]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_custom_logger_does_not_emit_disabled_levels(caplog) -> None:
|
|
58
|
+
logger = CustomLogger("test.disabled")
|
|
59
|
+
logger.setLevel(APP_LEVEL)
|
|
60
|
+
logger.addHandler(caplog.handler)
|
|
61
|
+
logger.propagate = False
|
|
62
|
+
|
|
63
|
+
with caplog.at_level(logging.NOTSET):
|
|
64
|
+
logger.mtp("hidden")
|
|
65
|
+
logger.app("visible")
|
|
66
|
+
|
|
67
|
+
assert [record.message for record in caplog.records] == ["visible"]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""utility function to check if a node exists on an OPC UA server.
|
|
2
|
+
"""
|
|
3
|
+
__copyright__ = "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
4
|
+
__license__ = "MIT"
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from opcua import Client
|
|
10
|
+
from opcua.ua.uaerrors import UaStatusCodeError
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
def check_node(server_info, node_info: list(dict[str, Any])):
|
|
15
|
+
"""Checks, if a Node exists on the OPC UA Server.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
server_info (dict): Information of the OPC UA Server according to JSON schema
|
|
19
|
+
node_info (dict): NodeInformation according to JSON schema
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
bool: True if Node exists, else False
|
|
23
|
+
|
|
24
|
+
Example of server_info and node_info:
|
|
25
|
+
{
|
|
26
|
+
"endpoint": "opc.tcp://localhost:4840",
|
|
27
|
+
"user": "",
|
|
28
|
+
"password": ""
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
{
|
|
32
|
+
"identifier": "1",
|
|
33
|
+
"contextualizedName": "TestTag",
|
|
34
|
+
"namespaceInfo": {
|
|
35
|
+
"type": "uri",
|
|
36
|
+
"namespaceUri": "https://semodia.com/ua/"
|
|
37
|
+
},
|
|
38
|
+
"nodeIdInfo": {
|
|
39
|
+
"type": "s",
|
|
40
|
+
"value": "Ti_V"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
identifier = node_info["index"]
|
|
46
|
+
contextualized_name = node_info["contextualizedName"]
|
|
47
|
+
endpoint_url = server_info["endpoint"]
|
|
48
|
+
username = server_info.get("user", "")
|
|
49
|
+
password = server_info.get("password", "")
|
|
50
|
+
|
|
51
|
+
client = Client(endpoint_url)
|
|
52
|
+
try:
|
|
53
|
+
logger.info(f"Processing: {identifier}-{contextualized_name}")
|
|
54
|
+
|
|
55
|
+
if len(username) > 0:
|
|
56
|
+
client.set_user(username)
|
|
57
|
+
client.set_password(password)
|
|
58
|
+
client.connect()
|
|
59
|
+
|
|
60
|
+
namespace_info = node_info["namespaceInfo"]
|
|
61
|
+
namespace_type = namespace_info["type"]
|
|
62
|
+
namespace_index = None
|
|
63
|
+
if namespace_type == "uri":
|
|
64
|
+
namespace_index = client.get_namespace_index(namespace_info["namespaceUri"])
|
|
65
|
+
else:
|
|
66
|
+
namespace_index = namespace_info["namespaceIndex"]
|
|
67
|
+
|
|
68
|
+
node_id_info = node_info["nodeIdInfo"]
|
|
69
|
+
node = client.get_node("ns=" + str(namespace_index) + ";" + node_id_info["type"] + "=" + str(node_id_info["value"]))
|
|
70
|
+
|
|
71
|
+
# Access of the value to check if it's okay
|
|
72
|
+
value = node.get_value()
|
|
73
|
+
return (value is not None)
|
|
74
|
+
# TODO why this check doesn't work?
|
|
75
|
+
except UaStatusCodeError as uae:
|
|
76
|
+
logger.error(f"Node '{identifier}-{contextualized_name}' threw an error: {uae}")
|
|
77
|
+
return False
|
|
78
|
+
finally:
|
|
79
|
+
client.disconnect()
|