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,106 @@
|
|
|
1
|
+
"""UserLogicRegistry is a decorator for registering user logic."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable, Iterator
|
|
9
|
+
|
|
10
|
+
from mtppy.part4.automation_services.service import Service
|
|
11
|
+
from mtppy.utils.logging import logger
|
|
12
|
+
|
|
13
|
+
UserFunc = Callable[..., bool]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UserLogicRegistry:
|
|
17
|
+
"""Stores and dispatches user-defined logic functions by name.
|
|
18
|
+
|
|
19
|
+
A registry maps string names to callables that accept a
|
|
20
|
+
[Service][mtppy.part4.service.service.Service] instance and return a
|
|
21
|
+
boolean. If a name is dispatched that has not been registered, the
|
|
22
|
+
configured default answer is returned instead.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
_registry (dict[str, UserFunc]): Dictionary to store registered functions.
|
|
26
|
+
_default_answer (bool): Default return value when no function is registered.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
"""Initialize an empty registry and set default answer to True."""
|
|
31
|
+
# Dictionary to store registered functions.
|
|
32
|
+
self._registry: dict[str, UserFunc] = {}
|
|
33
|
+
self._default_answer = True
|
|
34
|
+
|
|
35
|
+
def register(self, *names: str) -> Callable[[UserFunc], UserFunc]:
|
|
36
|
+
"""Register a function under several ``names``.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
*names: The keys used to store and later dispatch the function.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
A decorator that records the decorated function and returns it
|
|
43
|
+
unchanged.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
KeyError if one of the names was already used or is not of str type
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def decorator(func: UserFunc) -> UserFunc:
|
|
50
|
+
for name in names:
|
|
51
|
+
if type(name) is not str or not name:
|
|
52
|
+
message = "name must be a non-empty string"
|
|
53
|
+
raise KeyError(message)
|
|
54
|
+
if name in self._registry:
|
|
55
|
+
registered_name = self._registry[name].__name__
|
|
56
|
+
message = f"name {name} already used for {registered_name}"
|
|
57
|
+
raise KeyError(message)
|
|
58
|
+
self._registry[name] = func
|
|
59
|
+
return func
|
|
60
|
+
|
|
61
|
+
return decorator
|
|
62
|
+
|
|
63
|
+
def dispatch(
|
|
64
|
+
self,
|
|
65
|
+
them: Service,
|
|
66
|
+
name: str,
|
|
67
|
+
*args: object,
|
|
68
|
+
**kwargs: object,
|
|
69
|
+
) -> bool:
|
|
70
|
+
"""Call the function registered under ``name`` if it exists.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
them: The service instance passed as the first argument to the
|
|
74
|
+
registered function.
|
|
75
|
+
name: The registered name of the function to call.
|
|
76
|
+
*args: Positional arguments forwarded to the registered function.
|
|
77
|
+
**kwargs: Keyword arguments forwarded to the registered function.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The result of the registered function, or ``_default_answer`` if no
|
|
81
|
+
function is registered under ``name``.
|
|
82
|
+
"""
|
|
83
|
+
logger.debug(f"name={name}, args={args}, kwargs={kwargs}")
|
|
84
|
+
if name in self._registry:
|
|
85
|
+
try:
|
|
86
|
+
return self._registry[name](them, *args, **kwargs)
|
|
87
|
+
except Exception as ex: # noqa: BLE001
|
|
88
|
+
msg = f"Exception in user function {name}:{ex}"
|
|
89
|
+
logger.critical(msg)
|
|
90
|
+
else:
|
|
91
|
+
msg = f"no user function registered, returning {self._default_answer}"
|
|
92
|
+
logger.info(msg)
|
|
93
|
+
|
|
94
|
+
return self._default_answer
|
|
95
|
+
|
|
96
|
+
def registered_names(self) -> tuple[str, ...]:
|
|
97
|
+
"""Return registered callback names in registration order."""
|
|
98
|
+
return tuple(self._registry)
|
|
99
|
+
|
|
100
|
+
def items(self) -> Iterator[tuple[str, UserFunc]]:
|
|
101
|
+
"""Iterate over registered callback names and functions."""
|
|
102
|
+
return iter(self._registry.items())
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
global_registry = UserLogicRegistry()
|
|
106
|
+
register = global_registry.register
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Unit tests for the UserLogicRegistry class."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
|
|
8
|
+
import unittest
|
|
9
|
+
from unittest.mock import Mock
|
|
10
|
+
from mtppy.pea.user_logic.user_logic_registry import UserLogicRegistry, global_registry
|
|
11
|
+
from mtppy.pea.user_logic.user_logic_registry import register
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestUserLogicRegistry(unittest.TestCase):
|
|
15
|
+
"""Test cases for the UserLogicRegistry class."""
|
|
16
|
+
|
|
17
|
+
def setUp(self):
|
|
18
|
+
"""Set up test fixtures before each test method."""
|
|
19
|
+
self.registry = UserLogicRegistry()
|
|
20
|
+
|
|
21
|
+
def test_init_empty_registry(self):
|
|
22
|
+
"""Test that the registry initializes with an empty registry and default answer."""
|
|
23
|
+
self.assertEqual(self.registry._registry, {})
|
|
24
|
+
self.assertEqual(self.registry._default_answer, True)
|
|
25
|
+
|
|
26
|
+
def test_register_function(self):
|
|
27
|
+
"""Test that functions can be registered with names."""
|
|
28
|
+
|
|
29
|
+
def mock_func(service):
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
# Register the function
|
|
33
|
+
decorated_func = self.registry.register("test_func")(mock_func)
|
|
34
|
+
|
|
35
|
+
# Check that function is registered
|
|
36
|
+
self.assertIn("test_func", self.registry._registry)
|
|
37
|
+
self.assertEqual(self.registry._registry["test_func"], mock_func)
|
|
38
|
+
|
|
39
|
+
# Check that decorator returns the original function
|
|
40
|
+
self.assertEqual(decorated_func, mock_func)
|
|
41
|
+
|
|
42
|
+
def test_register_multiple_names(self):
|
|
43
|
+
"""Test that functions can be registered under multiple names."""
|
|
44
|
+
|
|
45
|
+
def mock_func(service):
|
|
46
|
+
return True
|
|
47
|
+
|
|
48
|
+
# Register the function under multiple names
|
|
49
|
+
self.registry.register("func1", "func2", "func3")(mock_func)
|
|
50
|
+
|
|
51
|
+
# Check that all names are registered
|
|
52
|
+
self.assertIn("func1", self.registry._registry)
|
|
53
|
+
self.assertIn("func2", self.registry._registry)
|
|
54
|
+
self.assertIn("func3", self.registry._registry)
|
|
55
|
+
self.assertEqual(self.registry._registry["func1"], mock_func)
|
|
56
|
+
self.assertEqual(self.registry._registry["func2"], mock_func)
|
|
57
|
+
self.assertEqual(self.registry._registry["func3"], mock_func)
|
|
58
|
+
|
|
59
|
+
def test_dispatch_existing_function(self):
|
|
60
|
+
"""Test that dispatch calls registered functions."""
|
|
61
|
+
service_mock = Mock()
|
|
62
|
+
|
|
63
|
+
def mock_func(service):
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
# Register the function
|
|
67
|
+
self.registry.register("test_func")(mock_func)
|
|
68
|
+
|
|
69
|
+
# Dispatch the function
|
|
70
|
+
result = self.registry.dispatch(service_mock, "test_func")
|
|
71
|
+
|
|
72
|
+
# Check that function was called and returned expected value
|
|
73
|
+
self.assertFalse(result)
|
|
74
|
+
|
|
75
|
+
def test_dispatch_non_existing_function(self):
|
|
76
|
+
"""Test that dispatch returns default answer for non-existing functions."""
|
|
77
|
+
service_mock = Mock()
|
|
78
|
+
|
|
79
|
+
# Dispatch a non-existing function
|
|
80
|
+
result = self.registry.dispatch(service_mock, "non_existing_func")
|
|
81
|
+
|
|
82
|
+
# Should return the default answer (True)
|
|
83
|
+
self.assertTrue(result)
|
|
84
|
+
|
|
85
|
+
def test_dispatch_with_args_and_kwargs(self):
|
|
86
|
+
"""Test that dispatch forwards arguments to registered functions."""
|
|
87
|
+
service_mock = Mock()
|
|
88
|
+
|
|
89
|
+
def mock_func(service, arg1, arg2, kwarg1=None):
|
|
90
|
+
self.assertEqual(arg1, "test_arg1")
|
|
91
|
+
self.assertEqual(arg2, "test_arg2")
|
|
92
|
+
self.assertEqual(kwarg1, "test_kwarg1")
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
# Register the function
|
|
96
|
+
self.registry.register("test_func")(mock_func)
|
|
97
|
+
|
|
98
|
+
# Dispatch with arguments
|
|
99
|
+
result = self.registry.dispatch(
|
|
100
|
+
service_mock, "test_func", "test_arg1", "test_arg2", kwarg1="test_kwarg1"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Check that function returned expected value
|
|
104
|
+
self.assertTrue(result)
|
|
105
|
+
|
|
106
|
+
def test_global_registry(self):
|
|
107
|
+
"""Test that the global registry works as expected."""
|
|
108
|
+
# Test that global registry exists
|
|
109
|
+
self.assertIsInstance(global_registry, UserLogicRegistry)
|
|
110
|
+
|
|
111
|
+
# Test registering via global registry
|
|
112
|
+
def global_func(service):
|
|
113
|
+
return "global_result"
|
|
114
|
+
|
|
115
|
+
global_registry.register("global_test")(global_func)
|
|
116
|
+
self.assertIn("global_test", global_registry._registry)
|
|
117
|
+
|
|
118
|
+
def test_register_decorator(self):
|
|
119
|
+
"""Test that the register decorator works."""
|
|
120
|
+
|
|
121
|
+
def decorated_func(service):
|
|
122
|
+
return "decorator_result"
|
|
123
|
+
|
|
124
|
+
# Apply the decorator
|
|
125
|
+
registered_func = register("decorated_test")(decorated_func)
|
|
126
|
+
|
|
127
|
+
# Should be registered in global registry
|
|
128
|
+
self.assertIn("decorated_test", global_registry._registry)
|
|
129
|
+
self.assertEqual(global_registry._registry["decorated_test"], decorated_func)
|
|
130
|
+
|
|
131
|
+
def test_register_same_name_raises_exception(self):
|
|
132
|
+
"""Test that registering two different functions under the same name raises an exception."""
|
|
133
|
+
|
|
134
|
+
def func1(service):
|
|
135
|
+
return "result1"
|
|
136
|
+
|
|
137
|
+
def func2(service):
|
|
138
|
+
return "result2"
|
|
139
|
+
|
|
140
|
+
# Register first function
|
|
141
|
+
self.registry.register("same_name")(func1)
|
|
142
|
+
|
|
143
|
+
# Register second function with same name - should raise exception
|
|
144
|
+
with self.assertRaises(KeyError):
|
|
145
|
+
self.registry.register("same_name")(func2)
|
|
146
|
+
|
|
147
|
+
def test_register_invalid_key_name_raises_exception(self):
|
|
148
|
+
"""Test that registering a function with an invalid key name raises an exception."""
|
|
149
|
+
|
|
150
|
+
def mock_func(service):
|
|
151
|
+
return True
|
|
152
|
+
|
|
153
|
+
# Test with None as key name (invalid for dict)
|
|
154
|
+
with self.assertRaises(KeyError):
|
|
155
|
+
self.registry.register(None)(mock_func)
|
|
156
|
+
|
|
157
|
+
# Test with list as key name (invalid for dict)
|
|
158
|
+
with self.assertRaises(KeyError):
|
|
159
|
+
self.registry.register([])(mock_func)
|
|
160
|
+
|
|
161
|
+
# Test with dict as key name (invalid for dict)
|
|
162
|
+
with self.assertRaises(KeyError):
|
|
163
|
+
self.registry.register({})(mock_func)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
if __name__ == "__main__":
|
|
167
|
+
unittest.main()
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""UserLogicService implements Service and executes registered user logic."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
|
|
11
|
+
from mtppy.part3.data_assembly_set.data_assembly import DataAssembly
|
|
12
|
+
from mtppy.part4.definitions.state_codes import StateCodes
|
|
13
|
+
from mtppy.part4.automation_services.service import Service
|
|
14
|
+
from mtppy.part4.automation_services.procedure import Procedure
|
|
15
|
+
from mtppy.utils.logging import logger
|
|
16
|
+
from mtppy.pea.user_logic.user_logic_registry import UserLogicRegistry
|
|
17
|
+
|
|
18
|
+
UserFunc = Callable[[Service], bool]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UserLogicService(Service):
|
|
22
|
+
"""Implementation of Service that calls user logic, registered by the
|
|
23
|
+
UserLogicRegistry.
|
|
24
|
+
|
|
25
|
+
This service acts as a bridge between the service execution framework and
|
|
26
|
+
user-defined logic functions that are registered in a UserLogicRegistry.
|
|
27
|
+
|
|
28
|
+
The state model follows the UML-State-Chart notation with entry, do, and exit
|
|
29
|
+
actions. Each state in the service lifecycle has three phases:
|
|
30
|
+
- entry: Executed once when entering the state
|
|
31
|
+
- do: Executed repeatedly while in the state (for cyclic states) until user
|
|
32
|
+
function returns `False`
|
|
33
|
+
- exit: Executed once when leaving the state
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
_registry: The UserLogicRegistry containing user-defined functions.
|
|
37
|
+
_cycle_time: Time interval (in seconds) for cyclic state execution.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
ENTRY = "entry"
|
|
41
|
+
DO = "do"
|
|
42
|
+
EXIT = "exit"
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
tag_name: str,
|
|
47
|
+
tag_description: str,
|
|
48
|
+
registry: UserLogicRegistry | None = None,
|
|
49
|
+
procedures: list[Procedure] | None = None,
|
|
50
|
+
cycle_time: float = 0.5,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Initialize the UserLogicService.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
tag_name: The name of the service instance.
|
|
56
|
+
tag_description: A description of the service.
|
|
57
|
+
registry: Optional UserLogicRegistry containing user functions.
|
|
58
|
+
procedures: Optional list of procedures for the service.
|
|
59
|
+
cycle_time: Time interval (in seconds) for cyclic state execution.
|
|
60
|
+
"""
|
|
61
|
+
super().__init__(tag_name, tag_description)
|
|
62
|
+
|
|
63
|
+
self.add_procedure(procedures)
|
|
64
|
+
|
|
65
|
+
self._registry = registry
|
|
66
|
+
self._cycle_time = cycle_time
|
|
67
|
+
|
|
68
|
+
def set_cycle_time(self, cycle_time: float) -> None:
|
|
69
|
+
"""Set the cycle time for this service.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
cycle_time: Time interval (in seconds) for cyclic state execution.
|
|
73
|
+
"""
|
|
74
|
+
self._cycle_time = cycle_time
|
|
75
|
+
|
|
76
|
+
def set_registry(self, registry: UserLogicRegistry) -> None:
|
|
77
|
+
"""Set the user logic registry for this service.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
registry: The UserLogicRegistry containing user functions to be used.
|
|
81
|
+
"""
|
|
82
|
+
self._registry = registry
|
|
83
|
+
|
|
84
|
+
def dispatch(self, name: str, *args, **kwargs) -> bool:
|
|
85
|
+
"""Dispatch call to the registered function.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
name: Name of the function to be called.
|
|
89
|
+
*args: Positional arguments to forward to the registered function.
|
|
90
|
+
**kwargs: Keyword arguments to forward to the registered function.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
The result of the registered function, or the default answer if no
|
|
94
|
+
function is registered under the given name.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
RuntimeError: If no registry is attached to the service.
|
|
98
|
+
"""
|
|
99
|
+
if self._registry is not None:
|
|
100
|
+
return self._registry.dispatch(self, name, *args, **kwargs)
|
|
101
|
+
else:
|
|
102
|
+
raise RuntimeError("no registry registered")
|
|
103
|
+
|
|
104
|
+
def _user_code_callback_name(
|
|
105
|
+
self,
|
|
106
|
+
proc_name: str,
|
|
107
|
+
state_name: str,
|
|
108
|
+
action: str,
|
|
109
|
+
) -> str:
|
|
110
|
+
"""Build the lookup name for the dispatch table.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
proc_name: Name of the procedure.
|
|
114
|
+
state_name: Name of the state.
|
|
115
|
+
action: Action type (entry, do, exit).
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
The formatted callback name for dispatching.
|
|
119
|
+
"""
|
|
120
|
+
return f"{self.name}.{proc_name}.{state_name}.{action}"
|
|
121
|
+
|
|
122
|
+
def _transient_state(self, state_name: str) -> None:
|
|
123
|
+
"""Call the user function bound to the specific transient state.
|
|
124
|
+
|
|
125
|
+
This method handles transient states that have entry, do, and exit phases.
|
|
126
|
+
It calls the user functions for each phase and manages state transitions.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
state_name: Name of the transient state to execute.
|
|
130
|
+
"""
|
|
131
|
+
logger.app(f"self={self.name}, state_name={state_name}")
|
|
132
|
+
proc_name = self._lookup_procedure_cur_name()
|
|
133
|
+
if proc_name != "undefined":
|
|
134
|
+
self.dispatch(
|
|
135
|
+
self._user_code_callback_name(proc_name, state_name, self.ENTRY)
|
|
136
|
+
)
|
|
137
|
+
self.dispatch(self._user_code_callback_name(proc_name, state_name, self.DO))
|
|
138
|
+
self.dispatch(
|
|
139
|
+
self._user_code_callback_name(proc_name, state_name, self.EXIT)
|
|
140
|
+
)
|
|
141
|
+
self.state_change()
|
|
142
|
+
else:
|
|
143
|
+
# this should not happen!
|
|
144
|
+
logger.warning("proc undefined called!")
|
|
145
|
+
time.sleep(self._cycle_time)
|
|
146
|
+
|
|
147
|
+
def _cyclic_state(self, state_name: str) -> None:
|
|
148
|
+
"""Call the user function bound to the specific cyclic state.
|
|
149
|
+
|
|
150
|
+
This method handles cyclic states that execute repeatedly until the
|
|
151
|
+
state changes or the service is stopped.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
state_name: Name of the cyclic state to execute.
|
|
155
|
+
"""
|
|
156
|
+
logger.app(f"self={self.name}, state_name={state_name}")
|
|
157
|
+
proc_name = self._lookup_procedure_cur_name()
|
|
158
|
+
cycle = 0
|
|
159
|
+
self.dispatch(self._user_code_callback_name(proc_name, state_name, self.ENTRY))
|
|
160
|
+
while self.is_state(state_name) and not (self.thread_ctrl.is_stopped()):
|
|
161
|
+
self.dispatch(self._user_code_callback_name(proc_name, state_name, self.DO))
|
|
162
|
+
time.sleep(self._cycle_time)
|
|
163
|
+
cycle += 1
|
|
164
|
+
self.dispatch(self._user_code_callback_name(proc_name, state_name, self.EXIT))
|
|
165
|
+
|
|
166
|
+
def idle(self) -> None:
|
|
167
|
+
"""Execute the entry, do, and exit functions bound to idle."""
|
|
168
|
+
self._cyclic_state(StateCodes.IDLE.string)
|
|
169
|
+
|
|
170
|
+
def starting(self) -> None:
|
|
171
|
+
"""Execute the entry, do, and exit functions bound to starting."""
|
|
172
|
+
self._transient_state(StateCodes.STARTING.string)
|
|
173
|
+
|
|
174
|
+
def execute(self) -> None:
|
|
175
|
+
"""Execute the entry, do, and exit functions bound to execute."""
|
|
176
|
+
self._cyclic_state(StateCodes.EXECUTE.string)
|
|
177
|
+
|
|
178
|
+
def completing(self) -> None:
|
|
179
|
+
"""Execute the entry, do, and exit functions bound to completing."""
|
|
180
|
+
self._transient_state(StateCodes.COMPLETING.string)
|
|
181
|
+
|
|
182
|
+
def completed(self) -> None:
|
|
183
|
+
"""Execute the entry, do, and exit functions bound to completed."""
|
|
184
|
+
self._cyclic_state(StateCodes.COMPLETING.string)
|
|
185
|
+
|
|
186
|
+
def pausing(self) -> None:
|
|
187
|
+
"""Execute the entry, do, and exit functions bound to pausing."""
|
|
188
|
+
self._transient_state(StateCodes.PAUSING.string)
|
|
189
|
+
|
|
190
|
+
def paused(self) -> None:
|
|
191
|
+
"""Execute the entry, do, and exit functions bound to paused."""
|
|
192
|
+
self._cyclic_state(StateCodes.PAUSED.string)
|
|
193
|
+
|
|
194
|
+
def resuming(self) -> None:
|
|
195
|
+
"""Execute the entry, do, and exit functions bound to resuming."""
|
|
196
|
+
self._transient_state(StateCodes.RESUMING.string)
|
|
197
|
+
|
|
198
|
+
def holding(self) -> None:
|
|
199
|
+
"""Execute the entry, do, and exit functions bound to holding."""
|
|
200
|
+
self._transient_state(StateCodes.HOLDING.string)
|
|
201
|
+
|
|
202
|
+
def held(self) -> None:
|
|
203
|
+
"""Execute the entry, do, and exit functions bound to held."""
|
|
204
|
+
self._cyclic_state(StateCodes.HELD.string)
|
|
205
|
+
|
|
206
|
+
def unholding(self) -> None:
|
|
207
|
+
"""Execute the entry, do, and exit functions bound to unholding."""
|
|
208
|
+
self._transient_state(StateCodes.UNHOLDING.string)
|
|
209
|
+
|
|
210
|
+
def stopping(self) -> None:
|
|
211
|
+
"""Execute the entry, do, and exit functions bound to stopping."""
|
|
212
|
+
self._transient_state(StateCodes.STOPPING.string)
|
|
213
|
+
|
|
214
|
+
def stopped(self) -> None:
|
|
215
|
+
"""Execute the entry, do, and exit functions bound to stopped."""
|
|
216
|
+
self._cyclic_state(StateCodes.STOPPED.string)
|
|
217
|
+
|
|
218
|
+
def aborting(self) -> None:
|
|
219
|
+
"""Execute the entry, do, and exit functions bound to aborting."""
|
|
220
|
+
self._transient_state(StateCodes.ABORTING.string)
|
|
221
|
+
|
|
222
|
+
def aborted(self) -> None:
|
|
223
|
+
"""Execute the entry, do, and exit functions bound to aborted."""
|
|
224
|
+
self._cyclic_state(StateCodes.ABORTED.string)
|
|
225
|
+
|
|
226
|
+
def resetting(self) -> None:
|
|
227
|
+
"""Execute the entry, do, and exit functions bound to resetting."""
|
|
228
|
+
self._transient_state(StateCodes.RESETTING.string)
|