bec-ipython-client 2.28.0__tar.gz → 3.88.0__tar.gz

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.

Files changed (44) hide show
  1. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/.gitignore +4 -0
  2. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/PKG-INFO +9 -9
  3. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/bec_magics.py +27 -2
  4. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/bec_startup.py +14 -13
  5. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/callbacks/device_progress.py +12 -9
  6. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/callbacks/ipython_live_updates.py +58 -23
  7. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/callbacks/live_table.py +94 -44
  8. bec_ipython_client-3.88.0/bec_ipython_client/callbacks/move_device.py +218 -0
  9. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/callbacks/utils.py +30 -52
  10. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/main.py +145 -20
  11. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/signals.py +9 -3
  12. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/demo.py +10 -1
  13. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/pyproject.toml +8 -8
  14. bec_ipython_client-3.88.0/tests/client_tests/conftest.py +19 -0
  15. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/tests/client_tests/test_bec_client.py +85 -19
  16. bec_ipython_client-3.88.0/tests/client_tests/test_ipython_live_updates.py +350 -0
  17. bec_ipython_client-3.88.0/tests/client_tests/test_live_table.py +423 -0
  18. bec_ipython_client-3.88.0/tests/client_tests/test_move_callback.py +223 -0
  19. bec_ipython_client-3.88.0/tests/end-2-end/_ensure_requirements_container.py +21 -0
  20. bec_ipython_client-3.88.0/tests/end-2-end/test_procedures_e2e.py +134 -0
  21. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/tests/end-2-end/test_scans_e2e.py +181 -40
  22. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/tests/end-2-end/test_scans_lib_e2e.py +206 -31
  23. bec_ipython_client-2.28.0/bec_ipython_client/callbacks/move_device.py +0 -157
  24. bec_ipython_client-2.28.0/tests/client_tests/test_ipython_live_updates.py +0 -159
  25. bec_ipython_client-2.28.0/tests/client_tests/test_live_table.py +0 -280
  26. bec_ipython_client-2.28.0/tests/client_tests/test_move_callback.py +0 -170
  27. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/__init__.py +0 -0
  28. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/beamline_mixin.py +0 -0
  29. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/callbacks/__init__.py +0 -0
  30. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/high_level_interfaces/__init__.py +0 -0
  31. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/high_level_interfaces/bec_hli.py +0 -0
  32. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/high_level_interfaces/spec_hli.py +0 -0
  33. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/SLS/__init__.py +0 -0
  34. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/SLS/sls_info.py +0 -0
  35. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/XTreme/__init__.py +0 -0
  36. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/XTreme/x-treme.py +0 -0
  37. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/__init__.py +0 -0
  38. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/plugins/flomni/flomni_config.yaml +0 -0
  39. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/prettytable.py +0 -0
  40. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/bec_ipython_client/progressbar.py +0 -0
  41. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/tests/client_tests/test_beamline_mixins.py +0 -0
  42. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/tests/client_tests/test_device_progress.py +0 -0
  43. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/tests/client_tests/test_pretty_table.py +0 -0
  44. {bec_ipython_client-2.28.0 → bec_ipython_client-3.88.0}/tests/conftest.py +0 -0
@@ -7,6 +7,10 @@
7
7
  **/.vscode
8
8
  **/.pytest_cache
9
9
  **/*.egg*
10
+ **/*.env
11
+
12
+ # bec_widgets saved profiles
13
+ widgets_settings/profiles/*
10
14
 
11
15
  # recovery_config files
12
16
  recovery_config_*
@@ -1,25 +1,25 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: bec_ipython_client
3
- Version: 2.28.0
3
+ Version: 3.88.0
4
4
  Summary: BEC IPython client
5
- Project-URL: Bug Tracker, https://gitlab.psi.ch/bec/bec/issues
6
- Project-URL: Homepage, https://gitlab.psi.ch/bec/bec
5
+ Project-URL: Bug Tracker, https://github.com/bec-project/bec/issues
6
+ Project-URL: Homepage, https://github.com/bec-project/bec
7
7
  Classifier: Development Status :: 3 - Alpha
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Topic :: Scientific/Engineering
10
- Requires-Python: >=3.10
11
- Requires-Dist: bec-lib
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: bec-lib~=3.0
12
12
  Requires-Dist: ipython~=8.22
13
- Requires-Dist: numpy~=1.24
13
+ Requires-Dist: numpy<3.0,>=1.24
14
14
  Requires-Dist: pyepics~=3.5
15
15
  Requires-Dist: requests~=2.31
16
16
  Requires-Dist: rich~=13.7
17
17
  Provides-Extra: dev
18
- Requires-Dist: black~=24.0; extra == 'dev'
18
+ Requires-Dist: black~=25.0; extra == 'dev'
19
19
  Requires-Dist: coverage~=7.0; extra == 'dev'
20
20
  Requires-Dist: isort>=5.13.2,~=5.13; extra == 'dev'
21
21
  Requires-Dist: pylint~=3.0; extra == 'dev'
22
- Requires-Dist: pytest-bec-e2e; extra == 'dev'
22
+ Requires-Dist: pytest-bec-e2e~=3.0; extra == 'dev'
23
23
  Requires-Dist: pytest-random-order~=1.1; extra == 'dev'
24
24
  Requires-Dist: pytest-redis~=3.1; extra == 'dev'
25
25
  Requires-Dist: pytest-retry~=1.1; extra == 'dev'
@@ -2,8 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
+ import rich
5
6
  from IPython.core.magic import Magics, line_magic, magics_class
6
7
 
8
+ from bec_lib.metadata_schema import get_metadata_schema_for_scan
9
+
7
10
  if TYPE_CHECKING:
8
11
  from bec_ipython_client import BECIPythonClient
9
12
 
@@ -28,7 +31,7 @@ class BECMagics(Magics):
28
31
  def resume(self, line):
29
32
  "Resume the scan"
30
33
  self.client.queue.request_scan_continuation()
31
- return self.client.live_updates.continue_request()
34
+ return self.client._live_updates.continue_request()
32
35
 
33
36
  @line_magic
34
37
  def pause(self, line):
@@ -52,9 +55,31 @@ class BECMagics(Magics):
52
55
  scan_report = self.client.scans._available_scans["fermat_scan"]._get_scan_report_type(
53
56
  hide_report
54
57
  )
55
- return self.client.live_updates.process_request(request, scan_report, [])
58
+ return self.client._live_updates.process_request(request, scan_report, [])
56
59
 
57
60
  @line_magic
58
61
  def halt(self, line):
59
62
  "Request a scan halt, i.e. abort without cleanup."
60
63
  return self.client.queue.request_scan_halt()
64
+
65
+ @line_magic
66
+ def server_restart(self, line):
67
+ "Request a server restart"
68
+ return self.client._request_server_restart()
69
+
70
+ @line_magic
71
+ def schema(self, line):
72
+ "print the metadata schema for a given scan"
73
+ scans = self.shell.user_ns["scans"]
74
+ if not hasattr(scans, line):
75
+ print(f"Scan {line} does not exist.")
76
+ else:
77
+ rich.print_json(data=get_metadata_schema_for_scan(line).model_json_schema())
78
+
79
+ @line_magic
80
+ def su(self, line):
81
+ "Switch user"
82
+ # pylint: disable=protected-access
83
+ self.client._client.acl.login(line)
84
+ self.client._client._update_username()
85
+ self.client._refresh_ipython_username()
@@ -1,19 +1,20 @@
1
1
  import os
2
2
  import sys
3
+ import threading
3
4
 
4
5
  import numpy as np # not needed but always nice to have
5
6
 
6
7
  from bec_ipython_client.main import BECIPythonClient as _BECIPythonClient
7
8
  from bec_ipython_client.main import main_dict as _main_dict
8
9
  from bec_lib import plugin_helper
10
+ from bec_lib.acl_login import BECAuthenticationError
9
11
  from bec_lib.logger import bec_logger as _bec_logger
10
12
  from bec_lib.redis_connector import RedisConnector as _RedisConnector
11
13
 
12
14
  try:
13
- from bec_widgets.cli.auto_updates import AutoUpdates
14
- from bec_widgets.cli.client import BECDockArea as _BECDockArea
15
+ from bec_widgets.cli.client_utils import BECGuiClient
15
16
  except ImportError:
16
- _BECDockArea = None
17
+ BECGuiClient = None
17
18
 
18
19
  logger = _bec_logger.logger
19
20
 
@@ -22,21 +23,21 @@ bec = _BECIPythonClient(
22
23
  )
23
24
  _main_dict["bec"] = bec
24
25
 
26
+
25
27
  try:
26
28
  bec.start()
29
+ except (BECAuthenticationError, KeyboardInterrupt) as exc:
30
+ logger.error(f"{exc} Exiting.")
31
+ os._exit(0)
27
32
  except Exception:
28
33
  sys.excepthook(*sys.exc_info())
29
34
  else:
30
- if bec.started and not _main_dict["args"].nogui and _BECDockArea is not None:
31
- gui = bec.gui = _BECDockArea()
32
- gui.show()
33
- if gui.auto_updates is None:
34
- AutoUpdates.create_default_dock = True
35
- AutoUpdates.enabled = True
36
- gui.auto_updates = AutoUpdates(gui=gui)
37
- if gui.auto_updates.create_default_dock:
38
- gui.auto_updates.start_default_dock()
39
- fig = gui.auto_updates.get_default_figure()
35
+ if bec.started and BECGuiClient is not None:
36
+ gui = bec.gui = BECGuiClient()
37
+ if _main_dict["args"].gui_id:
38
+ gui.connect_to_gui_server(_main_dict["args"].gui_id)
39
+ if not _main_dict["args"].nogui:
40
+ gui.show()
40
41
 
41
42
  _available_plugins = plugin_helper.get_ipython_client_startup_plugins(state="post")
42
43
  if _available_plugins:
@@ -14,11 +14,16 @@ class LiveUpdatesDeviceProgress(LiveUpdatesTable):
14
14
 
15
15
  REPORT_TYPE = "device_progress"
16
16
 
17
- def _run_update(self, device_names: str):
17
+ def core(self):
18
+ """core function to run the live updates for the table"""
19
+ self._wait_for_report_instructions()
20
+ self._run_update(self.report_instruction[self.REPORT_TYPE])
21
+
22
+ def _run_update(self, device_names: list[str]):
18
23
  """Run the update loop for the progress bar.
19
24
 
20
25
  Args:
21
- device_names (str): The name of the device to monitor.
26
+ device_names (list[str]): The name of the device to monitor.
22
27
  """
23
28
  with ScanProgressBar(
24
29
  scan_number=self.scan_item.scan_number, clear_on_exit=False
@@ -27,11 +32,9 @@ class LiveUpdatesDeviceProgress(LiveUpdatesTable):
27
32
  if self._update_progressbar(progressbar, device_names):
28
33
  break
29
34
  self._print_client_msgs_asap()
35
+ self._print_client_msgs_all()
30
36
 
31
- # TODO #286
32
- # self._print_client_msgs_all()
33
-
34
- def _update_progressbar(self, progressbar: ScanProgressBar, device_names: str) -> bool:
37
+ def _update_progressbar(self, progressbar: ScanProgressBar, device_names: list[str]) -> bool:
35
38
  """Update the progressbar based on the device status message
36
39
 
37
40
  Args:
@@ -43,17 +46,17 @@ class LiveUpdatesDeviceProgress(LiveUpdatesTable):
43
46
  self.check_alarms()
44
47
  status = self.bec.connector.get(MessageEndpoints.device_progress(device_names[0]))
45
48
  if not status:
46
- logger.debug("waiting for new data point")
49
+ logger.trace("waiting for new data point")
47
50
  time.sleep(0.1)
48
51
  return False
49
52
  if status.metadata.get("scan_id") != self.scan_item.scan_id:
50
- logger.debug("waiting for new data point")
53
+ logger.trace("waiting for new data point")
51
54
  time.sleep(0.1)
52
55
  return False
53
56
 
54
57
  point_id = status.content.get("value")
55
58
  if point_id is None:
56
- logger.debug("waiting for new data point")
59
+ logger.trace("waiting for new data point")
57
60
  time.sleep(0.1)
58
61
  return False
59
62
 
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import collections
4
4
  import time
5
- from typing import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, Any
6
6
 
7
7
  from bec_ipython_client.callbacks.device_progress import LiveUpdatesDeviceProgress
8
8
  from bec_lib.bec_errors import ScanInterruption
@@ -31,22 +31,27 @@ class IPythonLiveUpdates:
31
31
  self._interrupted_request = None
32
32
  self._active_callback = None
33
33
  self._processed_instructions = 0
34
- self._active_request = None
34
+ self._active_request: messages.ScanQueueMessage | None = None
35
35
  self._user_callback = None
36
36
  self._request_block_index = collections.defaultdict(lambda: 0)
37
37
  self._request_block_id = None
38
- self.print_table_data = True
39
38
  self._current_queue = None
40
39
 
40
+ @property
41
+ def print_table_data(self):
42
+ return self.client.live_updates_config.print_live_table
43
+
41
44
  def _process_report_instructions(self, report_instructions: list) -> None:
42
45
  """Process instructions for the live updates.
43
46
 
44
47
  Args:
45
48
  report_instructions (list): The list of report instructions.
46
49
  """
47
- scan_type = self._active_request.content["scan_type"]
50
+ if not self._active_request:
51
+ return
52
+ scan_type = self._active_request.scan_type
48
53
  if scan_type in ["open_scan_def", "close_scan_def"]:
49
- self._process_instruction({"scan_progress": 0})
54
+ self._process_instruction({"scan_progress": {"points": 0, "show_table": True}})
50
55
  return
51
56
  if scan_type == "close_scan_group":
52
57
  return
@@ -70,7 +75,11 @@ class IPythonLiveUpdates:
70
75
  """
71
76
  scan_report_type = list(instr.keys())[0]
72
77
  scan_def_id = self.client.scans._scan_def_id
73
- if scan_def_id is None:
78
+ interactive_scan = self.client.scans._interactive_scan
79
+ if self._active_request is None:
80
+ # Already checked in caller method. It is just for type checking purposes.
81
+ return
82
+ if scan_def_id is None or interactive_scan:
74
83
  if scan_report_type == "readback":
75
84
  LiveUpdatesReadbackProgressbar(
76
85
  self.client,
@@ -122,21 +131,26 @@ class IPythonLiveUpdates:
122
131
  )
123
132
  self._active_callback.run()
124
133
 
125
- def _available_req_blocks(self, queue: QueueItem, request: messages.ScanQueueMessage):
134
+ def _available_req_blocks(
135
+ self, queue: QueueItem, request: messages.ScanQueueMessage
136
+ ) -> list[messages.RequestBlock]:
126
137
  """Get the available request blocks.
127
138
 
128
139
  Args:
129
140
  queue (QueueItem): The queue item.
130
141
  request (messages.ScanQueueMessage): The request message.
142
+
143
+ Returns:
144
+ list[messages.RequestBlock]: The list of available request blocks.
131
145
  """
132
146
  available_blocks = [
133
147
  req_block
134
148
  for req_block in queue.request_blocks
135
- if req_block["RID"] == request.metadata["RID"]
149
+ if req_block.RID == request.metadata["RID"]
136
150
  ]
137
151
  return available_blocks
138
152
 
139
- def process_request(self, request: messages.ScanQueueMessage, callbacks: any) -> None:
153
+ def process_request(self, request: messages.ScanQueueMessage, callbacks: Any) -> None:
140
154
  """Process the request and report instructions."""
141
155
  # pylint: disable=protected-access
142
156
  try:
@@ -147,11 +161,14 @@ class IPythonLiveUpdates:
147
161
  scan_request = ScanRequestMixin(self.client, request.metadata["RID"])
148
162
  scan_request.wait()
149
163
 
164
+ # After .wait, we can be sure that the queue item is available, so we can
165
+ assert scan_request.scan_queue_request is not None
166
+
150
167
  # get the corresponding queue item
151
- while not scan_request.request_storage.storage[-1].queue:
168
+ while not scan_request.scan_queue_request.queue:
152
169
  time.sleep(0.01)
153
170
 
154
- self._current_queue = queue = scan_request.request_storage.storage[-1].queue
171
+ self._current_queue = queue = scan_request.scan_queue_request.queue
155
172
  self._request_block_id = req_id = self._active_request.metadata.get("RID")
156
173
 
157
174
  while queue.status not in ["COMPLETED", "ABORTED", "HALTED"]:
@@ -160,7 +177,7 @@ class IPythonLiveUpdates:
160
177
 
161
178
  available_blocks = self._available_req_blocks(queue, request)
162
179
  req_block = available_blocks[self._request_block_index[req_id]]
163
- report_instructions = req_block.get("report_instructions", [])
180
+ report_instructions = req_block.report_instructions or []
164
181
  self._process_report_instructions(report_instructions)
165
182
 
166
183
  self._reset()
@@ -184,12 +201,19 @@ class IPythonLiveUpdates:
184
201
  self.client.queue.request_scan_halt()
185
202
 
186
203
  def _element_in_queue(self) -> bool:
187
- queue = self.client.queue.queue_storage.current_scan_queue.get("primary", {}).get(
188
- "info", []
189
- )
190
- if not queue:
204
+ if self.client.queue is None:
205
+ return False
206
+ if (csq := self.client.queue.queue_storage.current_scan_queue) is None:
191
207
  return False
192
- return self._current_queue.queue_id in queue[0].get("queue_id")
208
+ scan_queue_status = csq.get("primary")
209
+ if scan_queue_status is None:
210
+ return False
211
+ queue_info = scan_queue_status.info
212
+ if not queue_info:
213
+ return False
214
+ if self._current_queue is None:
215
+ return False
216
+ return self._current_queue.queue_id == queue_info[0].queue_id
193
217
 
194
218
  def _process_queue(
195
219
  self, queue: QueueItem, request: messages.ScanQueueMessage, req_id: str
@@ -209,9 +233,11 @@ class IPythonLiveUpdates:
209
233
  if not queue.request_blocks or not queue.status or queue.queue_position is None:
210
234
  return False
211
235
  if queue.status == "PENDING" and queue.queue_position > 0:
212
- status = self.client.queue.queue_storage.current_scan_queue.get("primary", {}).get(
213
- "status"
214
- )
236
+ primary_queue = self.client.queue.queue_storage.current_scan_queue.get("primary")
237
+
238
+ if primary_queue is None:
239
+ return False
240
+ status = primary_queue.status
215
241
  print(
216
242
  "Scan is enqueued and is waiting for execution. Current position in queue:"
217
243
  f" {queue.queue_position + 1}. Queue status: {status}.",
@@ -219,14 +245,16 @@ class IPythonLiveUpdates:
219
245
  flush=True,
220
246
  )
221
247
  available_blocks = self._available_req_blocks(queue, request)
248
+ if not available_blocks:
249
+ return False
222
250
  req_block = available_blocks[self._request_block_index[req_id]]
223
- if req_block["content"]["scan_type"] in [
251
+ if req_block.msg.scan_type in [
224
252
  "open_scan_def",
225
253
  "mv",
226
254
  ]: # TODO: make this more general for all scan types that don't have report instructions
227
255
  return True
228
256
 
229
- report_instructions = req_block["report_instructions"]
257
+ report_instructions = req_block.report_instructions or []
230
258
  if not report_instructions:
231
259
  return False
232
260
  self._process_report_instructions(report_instructions)
@@ -235,6 +263,9 @@ class IPythonLiveUpdates:
235
263
  if self._active_callback and complete_rbl:
236
264
  return True
237
265
 
266
+ if complete_rbl and self.client.scans._interactive_scan:
267
+ return True
268
+
238
269
  if not queue.active_request_block:
239
270
  return True
240
271
 
@@ -251,7 +282,11 @@ class IPythonLiveUpdates:
251
282
  self._current_queue = None
252
283
  self._user_callback = None
253
284
  self._processed_instructions = 0
254
- scan_closed = forced or (self._active_request.content["scan_type"] == "close_scan_def")
285
+ scan_closed = (
286
+ forced
287
+ or self._active_request is None
288
+ or (self._active_request.scan_type == "close_scan_def")
289
+ )
255
290
  self._active_request = None
256
291
 
257
292
  if self.client.scans._scan_def_id and not scan_closed:
@@ -2,7 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import time
4
4
  from collections.abc import Callable
5
- from typing import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, Any, SupportsFloat
6
+
7
+ import numpy as np
6
8
 
7
9
  from bec_ipython_client.prettytable import PrettyTable
8
10
  from bec_ipython_client.progressbar import ScanProgressBar
@@ -20,7 +22,9 @@ logger = bec_logger.logger
20
22
  def sort_devices(devices, scan_devices) -> list:
21
23
  """sort the devices to ensure that the table starts with scan motors"""
22
24
  for scan_dev in list(scan_devices)[::-1]:
23
- devices.remove(scan_dev)
25
+ root_dev = scan_dev.split(".")[0]
26
+ if root_dev in devices:
27
+ devices.remove(root_dev)
24
28
  devices.insert(0, scan_dev)
25
29
  return devices
26
30
 
@@ -52,7 +56,6 @@ class LiveUpdatesTable(LiveUpdatesBase):
52
56
  super().__init__(
53
57
  bec, report_instruction=report_instruction, request=request, callbacks=callbacks
54
58
  )
55
- self.scan_queue_request = None
56
59
  self.scan_item = None
57
60
  self.dev_values = None
58
61
  self.point_data = None
@@ -64,16 +67,19 @@ class LiveUpdatesTable(LiveUpdatesBase):
64
67
  if print_table_data is not None
65
68
  else self.REPORT_TYPE == "scan_progress"
66
69
  )
70
+ self._devices_with_bad_precision = set()
67
71
 
68
72
  def wait_for_scan_to_start(self):
69
73
  """wait until the scan starts"""
70
74
  while True:
75
+ if not self.scan_item or not self.scan_item.queue:
76
+ raise RuntimeError("No scan item or scan queue available.")
71
77
  queue_pos = self.scan_item.queue.queue_position
72
78
  self.check_alarms()
73
79
  if self.scan_item.status == "closed":
74
80
  break
75
81
  if queue_pos is None:
76
- logger.debug(f"Could not find queue entry for scan_id {self.scan_item.scan_id}")
82
+ logger.trace(f"Could not find queue entry for scan_id {self.scan_item.scan_id}")
77
83
  continue
78
84
  if queue_pos == 0:
79
85
  break
@@ -112,7 +118,7 @@ class LiveUpdatesTable(LiveUpdatesBase):
112
118
  def devices(self):
113
119
  """get the devices for the callback"""
114
120
  if self.point_data.metadata["scan_type"] == "step":
115
- return self.get_devices_from_scan_data(self.scan_item.data[0])
121
+ return self.get_devices_from_scan_data(self.scan_item.live_data[0])
116
122
  if self.point_data.metadata["scan_type"] == "fly":
117
123
  devices = list(self.point_data.content["data"].keys())
118
124
  if len(devices) > self.MAX_DEVICES:
@@ -127,7 +133,6 @@ class LiveUpdatesTable(LiveUpdatesBase):
127
133
  monitored_devices = device_manager.devices.monitored_devices(
128
134
  [device_manager.devices[dev] for dev in scan_devices]
129
135
  )
130
- # devices = [hint for dev in monitored_devices for hint in dev._hints]
131
136
  devices = [dev.name for dev in monitored_devices]
132
137
  devices = sort_devices(devices, scan_devices)
133
138
  if len(devices) > self.MAX_DEVICES:
@@ -156,7 +161,20 @@ class LiveUpdatesTable(LiveUpdatesBase):
156
161
  return header
157
162
 
158
163
  def update_scan_item(self, timeout: float = 15):
159
- """get the current scan item"""
164
+ """
165
+ Get the current scan item and update self.scan_item
166
+
167
+ Args:
168
+ timeout (float): timeout in seconds
169
+
170
+ Raises:
171
+ RuntimeError: if no scan queue request is available
172
+ TimeoutError: if no scan item is found before reaching the timeout
173
+
174
+ """
175
+ if not self.scan_queue_request:
176
+ raise RuntimeError("No scan queue request available.")
177
+
160
178
  start = time.time()
161
179
  while self.scan_queue_request.scan is None:
162
180
  self.check_alarms()
@@ -167,31 +185,44 @@ class LiveUpdatesTable(LiveUpdatesBase):
167
185
 
168
186
  def core(self):
169
187
  """core function to run the live updates for the table"""
188
+ self._wait_for_report_instructions()
189
+ show_table = self.report_instruction[self.REPORT_TYPE].get("show_table", True)
190
+ self._print_table_data = show_table
191
+ self._run_update(self.report_instruction[self.REPORT_TYPE]["points"])
192
+
193
+ def _wait_for_report_instructions(self):
194
+ """wait until the report instructions are available"""
195
+ if not self.scan_queue_request or not self.scan_item or not self.scan_item.queue:
196
+ logger.warning(
197
+ f"Cannot wait for report instructions. scan_queue_request: {self.scan_queue_request}, scan_item: {self.scan_item}, scan_item.queue: {getattr(self.scan_item, 'queue', None)}"
198
+ )
199
+ return
170
200
  req_ID = self.scan_queue_request.requestID
171
201
  while True:
172
202
  request_block = [
173
- req for req in self.scan_item.queue.request_blocks if req["RID"] == req_ID
203
+ req for req in self.scan_item.queue.request_blocks if req.RID == req_ID
174
204
  ][0]
175
- if not request_block["is_scan"]:
205
+ if not request_block.is_scan:
176
206
  break
177
- if request_block["report_instructions"]:
207
+ if request_block.report_instructions:
178
208
  break
179
209
  self.check_alarms()
180
210
 
181
- self._run_update(self.report_instruction[self.REPORT_TYPE])
182
-
183
211
  def _run_update(self, target_num_points: int):
184
212
  """run the update loop with the progress bar
185
213
 
186
214
  Args:
187
215
  target_num_points (int): number of points to be collected
188
216
  """
217
+ if not self.scan_item:
218
+ logger.warning("No scan item available for live updates.")
219
+ return
189
220
  with ScanProgressBar(
190
221
  scan_number=self.scan_item.scan_number, clear_on_exit=self._print_table_data
191
222
  ) as progressbar:
192
223
  while True:
193
224
  self.check_alarms()
194
- self.point_data = self.scan_item.data.get(self.point_id)
225
+ self.point_data = self.scan_item.live_data.get(self.point_id)
195
226
  if self.scan_item.num_points:
196
227
  progressbar.max_points = self.scan_item.num_points
197
228
  if target_num_points == 0:
@@ -207,7 +238,7 @@ class LiveUpdatesTable(LiveUpdatesBase):
207
238
  self.bec.callbacks.poll()
208
239
  self.scan_item.poll_callbacks()
209
240
  else:
210
- logger.debug("waiting for new data point")
241
+ logger.trace("waiting for new data point")
211
242
  time.sleep(0.1)
212
243
 
213
244
  if not self.scan_item.num_points:
@@ -218,6 +249,24 @@ class LiveUpdatesTable(LiveUpdatesBase):
218
249
  if self.point_id > self.scan_item.num_points:
219
250
  raise RuntimeError("Received more points than expected.")
220
251
 
252
+ if len(self.scan_item.live_data) == 0 and self.scan_item.status == "closed":
253
+ msg = self.scan_item.status_message
254
+ if not msg:
255
+ continue
256
+ if msg.readout_priority.get("monitored", []):
257
+ continue
258
+
259
+ logger.warning(
260
+ f"\n Scan {self.scan_item.scan_number} finished. No monitored devices enabled, please check your config."
261
+ )
262
+ break
263
+
264
+ def _warn_bad_precisions(self):
265
+ if self._devices_with_bad_precision != set():
266
+ for dev, prec in self._devices_with_bad_precision:
267
+ logger.warning(f"Device {dev} reported malformed precision of {prec}!")
268
+ self._devices_with_bad_precision = set()
269
+
221
270
  @property
222
271
  def _print_table_data(self) -> bool:
223
272
  """Checks if the table should be printed or not.
@@ -248,6 +297,8 @@ class LiveUpdatesTable(LiveUpdatesBase):
248
297
 
249
298
  def print_table_data(self):
250
299
  """print the table data for the current point_id"""
300
+ # pylint: disable=protected-access
301
+ self._print_client_msgs_asap()
251
302
  if not self._print_table_data:
252
303
  return
253
304
 
@@ -258,41 +309,40 @@ class LiveUpdatesTable(LiveUpdatesBase):
258
309
 
259
310
  if self.point_id % 100 == 0:
260
311
  print(self.table.get_header_lines())
261
- ind = 0
312
+
313
+ signals_precisions = []
262
314
  for dev in self.devices:
263
315
  if dev in self.bec.device_manager.devices:
264
316
  obj = self.bec.device_manager.devices[dev]
265
317
  for hint in obj._hints:
266
- signal = self.point_data.content["data"].get(dev, {}).get(hint)
267
- if signal is None or signal.get("value") is None:
268
- print_value = "N/A"
318
+ signal = self.point_data.content["data"].get(obj.root.name, {}).get(hint)
319
+ if signal is None:
320
+ signals_precisions.append((None, None))
269
321
  else:
270
- try:
271
- precision = getattr(obj, "precision")
272
- except AttributeError:
273
- precision = 2
274
- value = signal.get("value")
275
- if isinstance(value, (int, float)):
276
- print_value = f"{value:.{precision}f}"
277
- else:
278
- print_value = str(value)
279
- self.dev_values[ind] = print_value
280
- ind += 1
322
+ prec = getattr(obj, "precision", 2)
323
+ if not isinstance(prec, int):
324
+ self._devices_with_bad_precision.add((dev, prec))
325
+ prec = 2
326
+ signals_precisions.append((signal, prec))
281
327
  else:
282
- signal = self.point_data.content["data"].get(dev, {})
283
- value = signal.get("value")
284
- if value is not None:
285
- if isinstance(value, (int, float)):
286
- print_value = f"{value:.2f}"
287
- else:
288
- print_value = str(value)
289
- else:
290
- print_value = "N/A"
291
- self.dev_values[ind] = print_value
292
- ind += 1
328
+ signals_precisions.append((self.point_data.content["data"].get(dev, {}), 2))
329
+
330
+ for i, (signal, precision) in enumerate(signals_precisions):
331
+ self.dev_values[i] = self._format_value(signal, precision)
332
+
293
333
  print(self.table.get_row(str(self.point_id), *self.dev_values))
294
- # pylint: disable=protected-access
295
- self._print_client_msgs_asap()
334
+
335
+ def _format_value(self, signal: dict | None, precision: int = 2):
336
+ if signal is None:
337
+ return "N/A"
338
+ val = signal.get("value")
339
+ if isinstance(val, SupportsFloat) and not isinstance(val, np.ndarray):
340
+ if precision < 0:
341
+ # This is to cover the special case when EPICS returns a negative precision.
342
+ # More info: https://epics.anl.gov/tech-talk/2004/msg00434.php
343
+ return f"{float(val):.{-precision}g}"
344
+ return f"{float(val):.{precision}f}"
345
+ return str(val)
296
346
 
297
347
  def close_table(self):
298
348
  """close the table and print the footer"""
@@ -304,8 +354,7 @@ class LiveUpdatesTable(LiveUpdatesBase):
304
354
  f"Scan {self.scan_item.scan_number} finished. Scan ID {self.scan_item.scan_id}. Elapsed time: {elapsed_time:.2f} s"
305
355
  )
306
356
  )
307
- # TODO #286
308
- # self._print_client_msgs_all()
357
+ self._warn_bad_precisions()
309
358
 
310
359
  def process_request(self):
311
360
  """process the request and start the core loop for live updates"""
@@ -335,3 +384,4 @@ class LiveUpdatesTable(LiveUpdatesBase):
335
384
  self.wait_for_scan_item_to_finish()
336
385
  if self._print_table_data:
337
386
  self.close_table()
387
+ self._print_client_msgs_all()