bec-ipython-client 3.35.6__py3-none-any.whl → 3.84.0__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.

Potentially problematic release.


This version of bec-ipython-client might be problematic. Click here for more details.

@@ -1,20 +1,23 @@
1
1
  import collections
2
+ import time
2
3
  from unittest import mock
3
4
 
4
5
  import pytest
5
6
 
6
7
  from bec_ipython_client.callbacks.move_device import (
7
8
  LiveUpdatesReadbackProgressbar,
8
- ReadbackDataMixin,
9
+ ReadbackDataHandler,
9
10
  )
10
11
  from bec_lib import messages
11
12
  from bec_lib.endpoints import MessageEndpoints
12
13
 
13
14
 
14
15
  @pytest.fixture
15
- def readback_data_mixin(bec_client_mock):
16
- with mock.patch.object(bec_client_mock.device_manager, "connector"):
17
- yield ReadbackDataMixin(bec_client_mock.device_manager, ["samx", "samy"])
16
+ def readback_data_handler(bec_client_mock, connected_connector):
17
+ with mock.patch.object(bec_client_mock.device_manager, "connector", connected_connector):
18
+ yield ReadbackDataHandler(
19
+ bec_client_mock.device_manager, ["samx", "samy"], request_id="something"
20
+ )
18
21
 
19
22
 
20
23
  def test_move_callback(bec_client_mock):
@@ -33,30 +36,25 @@ def test_move_callback(bec_client_mock):
33
36
  return readback[0]
34
37
 
35
38
  req_done = collections.deque()
36
- msg_acc = messages.DeviceReqStatusMessage(
37
- device="samx", success=True, metadata={"RID": "something"}
38
- )
39
- req_done.extend([[None], [None], [None], [msg_acc]])
39
+ req_done.extend([{"samx": (False, False)}, {"samx": (False, False)}, {"samx": (True, True)}])
40
40
 
41
41
  def mock_req_msg(*args):
42
42
  if len(req_done) > 1:
43
43
  return req_done.popleft()
44
44
  return req_done[0]
45
45
 
46
- with mock.patch("bec_ipython_client.callbacks.move_device.check_alarms") as check_alarms_mock:
47
- with mock.patch.object(ReadbackDataMixin, "wait_for_RID"):
48
- with mock.patch.object(LiveUpdatesReadbackProgressbar, "wait_for_request_acceptance"):
46
+ with mock.patch("bec_ipython_client.callbacks.move_device.check_alarms"):
47
+ with mock.patch.object(LiveUpdatesReadbackProgressbar, "wait_for_request_acceptance"):
48
+ with mock.patch.object(
49
+ LiveUpdatesReadbackProgressbar, "_print_client_msgs_asap"
50
+ ) as mock_client_msgs:
49
51
  with mock.patch.object(
50
- LiveUpdatesReadbackProgressbar, "_print_client_msgs_asap"
51
- ) as mock_client_msgs:
52
- with mock.patch.object(
53
- LiveUpdatesReadbackProgressbar, "_print_client_msgs_all"
54
- ) as mock_client_msgs_all:
55
- with mock.patch.object(
56
- ReadbackDataMixin, "get_device_values", mock_readback
57
- ):
52
+ LiveUpdatesReadbackProgressbar, "_print_client_msgs_all"
53
+ ) as mock_client_msgs_all:
54
+ with mock.patch.object(ReadbackDataHandler, "get_device_values", mock_readback):
55
+ with mock.patch.object(ReadbackDataHandler, "device_states", mock_req_msg):
58
56
  with mock.patch.object(
59
- ReadbackDataMixin, "get_request_done_msgs", mock_req_msg
57
+ ReadbackDataHandler, "done", side_effect=[False, False, True]
60
58
  ):
61
59
  LiveUpdatesReadbackProgressbar(bec=client, request=request).run()
62
60
  assert mock_client_msgs.called is True
@@ -82,28 +80,21 @@ def test_move_callback_with_report_instruction(bec_client_mock):
82
80
  return readback[0]
83
81
 
84
82
  req_done = collections.deque()
85
- msg_acc = messages.DeviceReqStatusMessage(
86
- device="samx", success=True, metadata={"RID": "something"}
87
- )
88
- req_done.extend([[None], [None], [None], [msg_acc]])
83
+ req_done.extend([{"samx": (False, False)}, {"samx": (False, False)}, {"samx": (True, True)}])
89
84
 
90
85
  def mock_req_msg(*args):
91
86
  if len(req_done) > 1:
92
87
  return req_done.popleft()
93
88
  return req_done[0]
94
89
 
95
- with mock.patch("bec_ipython_client.callbacks.move_device.check_alarms") as check_alarms_mock:
96
- with mock.patch.object(ReadbackDataMixin, "wait_for_RID"):
97
- with mock.patch.object(LiveUpdatesReadbackProgressbar, "wait_for_request_acceptance"):
98
- with mock.patch.object(LiveUpdatesReadbackProgressbar, "_print_client_msgs_asap"):
99
- with mock.patch.object(
100
- LiveUpdatesReadbackProgressbar, "_print_client_msgs_all"
101
- ):
102
- with mock.patch.object(
103
- ReadbackDataMixin, "get_device_values", mock_readback
104
- ):
90
+ with mock.patch("bec_ipython_client.callbacks.move_device.check_alarms"):
91
+ with mock.patch.object(LiveUpdatesReadbackProgressbar, "wait_for_request_acceptance"):
92
+ with mock.patch.object(LiveUpdatesReadbackProgressbar, "_print_client_msgs_asap"):
93
+ with mock.patch.object(LiveUpdatesReadbackProgressbar, "_print_client_msgs_all"):
94
+ with mock.patch.object(ReadbackDataHandler, "get_device_values", mock_readback):
95
+ with mock.patch.object(ReadbackDataHandler, "device_states", mock_req_msg):
105
96
  with mock.patch.object(
106
- ReadbackDataMixin, "get_request_done_msgs", mock_req_msg
97
+ ReadbackDataHandler, "done", side_effect=[False, False, False, True]
107
98
  ):
108
99
  LiveUpdatesReadbackProgressbar(
109
100
  bec=client,
@@ -112,70 +103,121 @@ def test_move_callback_with_report_instruction(bec_client_mock):
112
103
  ).run()
113
104
 
114
105
 
115
- def test_readback_data_mixin(readback_data_mixin):
116
- readback_data_mixin.device_manager.connector.get.side_effect = [
117
- messages.DeviceMessage(
106
+ def test_readback_data_handler(readback_data_handler):
107
+ readback_data_handler.data = {
108
+ "samx": messages.DeviceMessage(
118
109
  signals={"samx": {"value": 10}, "samx_setpoint": {"value": 20}},
119
110
  metadata={"device": "samx"},
120
111
  ),
121
- messages.DeviceMessage(
112
+ "samy": messages.DeviceMessage(
122
113
  signals={"samy": {"value": 10}, "samy_setpoint": {"value": 20}},
123
114
  metadata={"device": "samy"},
124
115
  ),
125
- ]
126
- res = readback_data_mixin.get_device_values()
116
+ }
117
+
118
+ res = readback_data_handler.get_device_values()
127
119
  assert res == [10, 10]
128
120
 
129
121
 
130
- def test_readback_data_mixin_multiple_hints(readback_data_mixin):
131
- readback_data_mixin.device_manager.devices.samx._info["hints"]["fields"] = [
122
+ def test_readback_data_handler_multiple_hints(readback_data_handler):
123
+ readback_data_handler.device_manager.devices.samx._info["hints"]["fields"] = [
132
124
  "samx_setpoint",
133
125
  "samx",
134
126
  ]
135
- readback_data_mixin.device_manager.connector.get.side_effect = [
136
- messages.DeviceMessage(
127
+ readback_data_handler.data = {
128
+ "samx": messages.DeviceMessage(
137
129
  signals={"samx": {"value": 10}, "samx_setpoint": {"value": 20}},
138
130
  metadata={"device": "samx"},
139
131
  ),
140
- messages.DeviceMessage(
132
+ "samy": messages.DeviceMessage(
141
133
  signals={"samy": {"value": 10}, "samy_setpoint": {"value": 20}},
142
134
  metadata={"device": "samy"},
143
135
  ),
144
- ]
145
- res = readback_data_mixin.get_device_values()
136
+ }
137
+ res = readback_data_handler.get_device_values()
146
138
  assert res == [20, 10]
147
139
 
148
140
 
149
- def test_readback_data_mixin_multiple_no_hints(readback_data_mixin):
150
- readback_data_mixin.device_manager.devices.samx._info["hints"]["fields"] = []
151
- readback_data_mixin.device_manager.connector.get.side_effect = [
152
- messages.DeviceMessage(
141
+ def test_readback_data_handler_multiple_no_hints(readback_data_handler):
142
+ readback_data_handler.device_manager.devices.samx._info["hints"]["fields"] = []
143
+ readback_data_handler.data = {
144
+ "samx": messages.DeviceMessage(
153
145
  signals={"samx": {"value": 10}, "samx_setpoint": {"value": 20}},
154
146
  metadata={"device": "samx"},
155
147
  ),
156
- messages.DeviceMessage(
148
+ "samy": messages.DeviceMessage(
157
149
  signals={"samy": {"value": 10}, "samy_setpoint": {"value": 20}},
158
150
  metadata={"device": "samy"},
159
151
  ),
160
- ]
161
- res = readback_data_mixin.get_device_values()
152
+ }
153
+ res = readback_data_handler.get_device_values()
162
154
  assert res == [10, 10]
163
155
 
164
156
 
165
- def test_get_request_done_msgs(readback_data_mixin):
166
- res = readback_data_mixin.get_request_done_msgs()
167
- readback_data_mixin.device_manager.connector.pipeline.assert_called_once()
168
- assert (
169
- mock.call(
170
- MessageEndpoints.device_req_status("samx"),
171
- readback_data_mixin.device_manager.connector.pipeline.return_value,
172
- )
173
- in readback_data_mixin.device_manager.connector.get.call_args_list
157
+ def test_readback_data_handler_init(readback_data_handler):
158
+ """
159
+ Test that the ReadbackDataHandler is initialized correctly.
160
+ """
161
+
162
+ # Initial state
163
+ assert readback_data_handler._devices_done_state == {
164
+ "samx": (False, False),
165
+ "samy": (False, False),
166
+ }
167
+ assert readback_data_handler._devices_received == {"samx": False, "samy": False}
168
+ assert readback_data_handler.data == {}
169
+
170
+
171
+ def test_readback_data_handler_readback_callbacks(readback_data_handler):
172
+ """
173
+ Test that the readback callback properly updates the readback data.
174
+ """
175
+
176
+ # Submit readback for samx
177
+ msg = messages.DeviceMessage(
178
+ signals={"samx": {"value": 15}}, metadata={"device": "samx", "RID": "something"}
174
179
  )
175
- assert (
176
- mock.call(
177
- MessageEndpoints.device_req_status("samy"),
178
- readback_data_mixin.device_manager.connector.pipeline.return_value,
179
- )
180
- in readback_data_mixin.device_manager.connector.get.call_args_list
180
+ readback_data_handler.connector.set_and_publish(MessageEndpoints.device_readback("samx"), msg)
181
+
182
+ msg_old = messages.DeviceMessage(
183
+ signals={"samy": {"value": 10}}, metadata={"device": "samx", "RID": "something_else"}
184
+ )
185
+ readback_data_handler.connector.set_and_publish(
186
+ MessageEndpoints.device_readback("samy"), msg_old
187
+ )
188
+ while (
189
+ not readback_data_handler._devices_received["samx"]
190
+ or "samx" not in readback_data_handler.data
191
+ ):
192
+ time.sleep(0.01)
193
+ assert readback_data_handler.data["samx"].signals["samx"]["value"] == 15
194
+ dev_data = readback_data_handler.get_device_values()
195
+ assert dev_data[0] == 15
196
+ assert dev_data[1] == 10 # samy remains unchanged
197
+
198
+
199
+ def test_readback_data_handler_request_done_callbacks(readback_data_handler):
200
+ """
201
+ Test that the request done callback properly updates the device done state.
202
+ """
203
+
204
+ # Submit request done for samx
205
+ msg = messages.DeviceReqStatusMessage(device="samx", success=True, request_id="something")
206
+ readback_data_handler.connector.xadd(
207
+ MessageEndpoints.device_req_status("something"), {"data": msg}
208
+ )
209
+ while not readback_data_handler._devices_done_state["samx"][0]:
210
+ time.sleep(0.01)
211
+ assert readback_data_handler._devices_done_state["samx"] == (True, True)
212
+
213
+ assert readback_data_handler.done() is False
214
+
215
+ # Submit request done for samy
216
+ msg = messages.DeviceReqStatusMessage(device="samy", success=False, request_id="something")
217
+ readback_data_handler.connector.xadd(
218
+ MessageEndpoints.device_req_status("something"), {"data": msg}
181
219
  )
220
+ while not readback_data_handler._devices_done_state["samy"][0]:
221
+ time.sleep(0.01)
222
+ assert readback_data_handler._devices_done_state["samy"] == (True, False)
223
+ assert readback_data_handler.done() is True
@@ -0,0 +1,21 @@
1
+ from time import sleep
2
+
3
+ from bec_server.scan_server.procedures.constants import PROCEDURE, ProcedureWorkerError
4
+ from bec_server.scan_server.procedures.container_utils import PodmanCliUtils
5
+
6
+ image_name = (
7
+ f"ghcr.io/bec-project/{PROCEDURE.CONTAINER.REQUIREMENTS_IMAGE_NAME}:v{PROCEDURE.BEC_VERSION}"
8
+ )
9
+ podman = PodmanCliUtils()
10
+
11
+ for i in range(1, 6):
12
+ try:
13
+ output = podman._run_and_capture_error("podman", "pull", image_name)
14
+ print("successfully pulled requirements image for current version")
15
+ exit(0)
16
+ except ProcedureWorkerError as e:
17
+ print(e)
18
+ print("retrying in 5 minutes...")
19
+ sleep(5 * 60)
20
+ print(f"No more retries. Check if {image_name} actually exists!")
21
+ exit(1)
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from dataclasses import dataclass
5
+ from importlib.metadata import version
6
+ from typing import TYPE_CHECKING, Callable, Generator
7
+ from unittest.mock import MagicMock, patch
8
+
9
+ import pytest
10
+
11
+ from bec_ipython_client.main import BECIPythonClient
12
+ from bec_lib import messages
13
+ from bec_lib.endpoints import MessageEndpoints
14
+ from bec_lib.logger import bec_logger
15
+ from bec_server.scan_server.procedures.constants import _CONTAINER, _WORKER
16
+ from bec_server.scan_server.procedures.container_utils import get_backend
17
+ from bec_server.scan_server.procedures.container_worker import ContainerProcedureWorker
18
+ from bec_server.scan_server.procedures.manager import ProcedureManager
19
+
20
+ if TYPE_CHECKING:
21
+ from pytest_bec_e2e.plugin import LogTestTool
22
+
23
+ logger = bec_logger.logger
24
+
25
+ # pylint: disable=protected-access
26
+
27
+ # Random order disabled for this module so that the test for building the worker container runs first
28
+ # and we can use lower timeouts for the remaining tests
29
+ pytestmark = pytest.mark.random_order(disabled=True)
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class PATCHED_CONSTANTS:
34
+ WORKER = _WORKER()
35
+ CONTAINER = _CONTAINER()
36
+ MANAGER_SHUTDOWN_TIMEOUT_S = 2
37
+ BEC_VERSION = version("bec_lib")
38
+ REDIS_HOST = "localhost"
39
+
40
+
41
+ @pytest.fixture
42
+ def client_logtool_and_manager(
43
+ bec_ipython_client_fixture_with_logtool: tuple[BECIPythonClient, "LogTestTool"],
44
+ ) -> Generator[tuple[BECIPythonClient, "LogTestTool", ProcedureManager], None, None]:
45
+ client, logtool = bec_ipython_client_fixture_with_logtool
46
+ server = MagicMock()
47
+ server.bootstrap_server = f"{client.connector.host}:{client.connector.port}"
48
+ manager = ProcedureManager(server, ContainerProcedureWorker)
49
+ yield client, logtool, manager
50
+ manager.shutdown()
51
+
52
+
53
+ def _wait_while(cond: Callable[[], bool], timeout_s):
54
+ start = time.monotonic()
55
+ while cond():
56
+ if (time.monotonic() - start) > timeout_s:
57
+ raise TimeoutError()
58
+ time.sleep(0.01)
59
+
60
+
61
+ @pytest.mark.timeout(100)
62
+ def test_building_worker_image():
63
+ podman_utils = get_backend()
64
+ build = podman_utils.build_worker_image()
65
+ assert len(build._command_output.splitlines()[-1]) == 64 # type: ignore
66
+ assert podman_utils.image_exists(f"bec_procedure_worker:v{version('bec_lib')}")
67
+
68
+
69
+ @pytest.mark.timeout(100)
70
+ @patch("bec_server.scan_server.procedures.manager.procedure_registry.is_registered", lambda _: True)
71
+ def test_procedure_runner_spawns_worker(
72
+ client_logtool_and_manager: tuple[BECIPythonClient, "LogTestTool", ProcedureManager],
73
+ ):
74
+ client, _, manager = client_logtool_and_manager
75
+ assert manager._active_workers == {}
76
+ endpoint = MessageEndpoints.procedure_request()
77
+ msg = messages.ProcedureRequestMessage(
78
+ identifier="sleep", args_kwargs=((), {"time_s": 2}), queue="test"
79
+ )
80
+
81
+ logs = []
82
+
83
+ def cb(worker: ContainerProcedureWorker):
84
+ nonlocal logs
85
+ logs = worker._backend.logs(worker._container_id)
86
+
87
+ manager.add_callback("test", cb)
88
+ client.connector.xadd(topic=endpoint, msg_dict=msg.model_dump())
89
+
90
+ _wait_while(lambda: manager._active_workers == {}, 5)
91
+ _wait_while(lambda: manager._active_workers != {}, 20)
92
+
93
+ assert logs != []
94
+
95
+
96
+ @pytest.mark.timeout(100)
97
+ @patch("bec_server.scan_server.procedures.manager.procedure_registry.is_registered", lambda _: True)
98
+ @patch("bec_server.scan_server.procedures.container_worker.PROCEDURE", PATCHED_CONSTANTS())
99
+ def test_happy_path_container_procedure_runner(
100
+ client_logtool_and_manager: tuple[BECIPythonClient, "LogTestTool", ProcedureManager],
101
+ ):
102
+ test_args = (1, 2, 3)
103
+ test_kwargs = {"a": "b", "c": "d"}
104
+ client, logtool, manager = client_logtool_and_manager
105
+ assert manager._active_workers == {}
106
+ conn = client.connector
107
+ endpoint = MessageEndpoints.procedure_request()
108
+ msg = messages.ProcedureRequestMessage(
109
+ identifier="log execution message args", args_kwargs=(test_args, test_kwargs)
110
+ )
111
+ conn.xadd(topic=endpoint, msg_dict=msg.model_dump())
112
+
113
+ _wait_while(lambda: manager._active_workers == {}, 5)
114
+ _wait_while(lambda: manager._active_workers != {}, 20)
115
+
116
+ logtool.fetch()
117
+ assert logtool.is_present_in_any_message("procedure accepted: True, message:")
118
+ assert logtool.is_present_in_any_message(
119
+ "ContainerWorker started container for queue primary"
120
+ ), f"Log content relating to procedures: {manager._logs}"
121
+
122
+ res, msg = logtool.are_present_in_order(
123
+ [
124
+ "Container worker 'primary' status update: IDLE",
125
+ "Container worker 'primary' status update: RUNNING",
126
+ "Container worker 'primary' status update: IDLE",
127
+ "Container worker 'primary' status update: FINISHED",
128
+ ]
129
+ )
130
+ assert res, f"failed on {msg}"
131
+
132
+ assert logtool.is_present_in_any_message(
133
+ f"Builtin procedure log_message_args_kwargs called with args: {test_args} and kwargs: {test_kwargs}"
134
+ )
@@ -737,12 +737,12 @@ def test_update_config(bec_ipython_client_fixture):
737
737
  bec = bec_ipython_client_fixture
738
738
  bec.metadata.update({"unit_test": "test_update_config"})
739
739
  demo_config_path = os.path.join(os.path.dirname(configs.__file__), "demo_config.yaml")
740
- config = bec.config._load_config_from_file(demo_config_path)
740
+ config = bec.device_manager.config_helper._load_config_from_file(demo_config_path)
741
741
  config.pop("samx")
742
- bec.config.send_config_request(action="set", config=config)
742
+ bec.device_manager.config_helper.send_config_request(action="set", config=config)
743
743
  assert "samx" not in bec.device_manager.devices
744
- config = bec.config._load_config_from_file(demo_config_path)
745
- bec.config.send_config_request(action="set", config=config)
744
+ config = bec.device_manager.config_helper._load_config_from_file(demo_config_path)
745
+ bec.device_manager.config_helper.send_config_request(action="set", config=config)
746
746
 
747
747
 
748
748
  @pytest.mark.timeout(100)