MTPPy2.0 0.1.5__py3-none-any.whl

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