bec-ipython-client 3.1.3__tar.gz → 3.86.2__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.
Files changed (45) hide show
  1. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/.gitignore +4 -0
  2. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/PKG-INFO +6 -6
  3. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/bec_magics.py +27 -2
  4. bec_ipython_client-3.86.2/bec_ipython_client/bec_startup.py +60 -0
  5. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/callbacks/device_progress.py +12 -9
  6. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/callbacks/ipython_live_updates.py +53 -22
  7. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/callbacks/live_table.py +88 -39
  8. bec_ipython_client-3.86.2/bec_ipython_client/callbacks/move_device.py +218 -0
  9. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/callbacks/utils.py +30 -52
  10. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/main.py +141 -18
  11. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/signals.py +9 -3
  12. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/demo.py +2 -1
  13. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/pyproject.toml +5 -5
  14. bec_ipython_client-3.86.2/tests/client_tests/conftest.py +19 -0
  15. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/tests/client_tests/test_bec_client.py +85 -19
  16. bec_ipython_client-3.86.2/tests/client_tests/test_ipython_live_updates.py +350 -0
  17. bec_ipython_client-3.86.2/tests/client_tests/test_live_table.py +423 -0
  18. bec_ipython_client-3.86.2/tests/client_tests/test_move_callback.py +223 -0
  19. bec_ipython_client-3.86.2/tests/end-2-end/_ensure_requirements_container.py +21 -0
  20. bec_ipython_client-3.86.2/tests/end-2-end/test_procedures_e2e.py +134 -0
  21. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/tests/end-2-end/test_scans_e2e.py +73 -16
  22. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/tests/end-2-end/test_scans_lib_e2e.py +150 -32
  23. bec_ipython_client-3.1.3/bec_ipython_client/bec_startup.py +0 -98
  24. bec_ipython_client-3.1.3/bec_ipython_client/callbacks/move_device.py +0 -157
  25. bec_ipython_client-3.1.3/tests/client_tests/test_ipython_live_updates.py +0 -159
  26. bec_ipython_client-3.1.3/tests/client_tests/test_live_table.py +0 -280
  27. bec_ipython_client-3.1.3/tests/client_tests/test_move_callback.py +0 -170
  28. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/__init__.py +0 -0
  29. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/beamline_mixin.py +0 -0
  30. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/callbacks/__init__.py +0 -0
  31. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/high_level_interfaces/__init__.py +0 -0
  32. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/high_level_interfaces/bec_hli.py +0 -0
  33. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/high_level_interfaces/spec_hli.py +0 -0
  34. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/plugins/SLS/__init__.py +0 -0
  35. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/plugins/SLS/sls_info.py +0 -0
  36. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/plugins/XTreme/__init__.py +0 -0
  37. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/plugins/XTreme/x-treme.py +0 -0
  38. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/plugins/__init__.py +0 -0
  39. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/plugins/flomni/flomni_config.yaml +0 -0
  40. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/prettytable.py +0 -0
  41. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/bec_ipython_client/progressbar.py +0 -0
  42. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/tests/client_tests/test_beamline_mixins.py +0 -0
  43. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/tests/client_tests/test_device_progress.py +0 -0
  44. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/tests/client_tests/test_pretty_table.py +0 -0
  45. {bec_ipython_client-3.1.3 → bec_ipython_client-3.86.2}/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,13 +1,13 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: bec_ipython_client
3
- Version: 3.1.3
3
+ Version: 3.86.2
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
10
+ Requires-Python: >=3.11
11
11
  Requires-Dist: bec-lib~=3.0
12
12
  Requires-Dist: ipython~=8.22
13
13
  Requires-Dist: numpy<3.0,>=1.24
@@ -15,7 +15,7 @@ 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'
@@ -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()
@@ -0,0 +1,60 @@
1
+ import os
2
+ import sys
3
+ import threading
4
+
5
+ import numpy as np # not needed but always nice to have
6
+
7
+ from bec_ipython_client.main import BECIPythonClient as _BECIPythonClient
8
+ from bec_ipython_client.main import main_dict as _main_dict
9
+ from bec_lib import plugin_helper
10
+ from bec_lib.acl_login import BECAuthenticationError
11
+ from bec_lib.logger import bec_logger as _bec_logger
12
+ from bec_lib.redis_connector import RedisConnector as _RedisConnector
13
+
14
+ try:
15
+ from bec_widgets.cli.client_utils import BECGuiClient
16
+ except ImportError:
17
+ BECGuiClient = None
18
+
19
+ logger = _bec_logger.logger
20
+
21
+ bec = _BECIPythonClient(
22
+ _main_dict["config"], _RedisConnector, wait_for_server=_main_dict["wait_for_server"]
23
+ )
24
+ _main_dict["bec"] = bec
25
+
26
+
27
+ try:
28
+ bec.start()
29
+ except (BECAuthenticationError, KeyboardInterrupt) as exc:
30
+ logger.error(f"{exc} Exiting.")
31
+ os._exit(0)
32
+ except Exception:
33
+ sys.excepthook(*sys.exc_info())
34
+ else:
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()
41
+
42
+ _available_plugins = plugin_helper.get_ipython_client_startup_plugins(state="post")
43
+ if _available_plugins:
44
+ for name, plugin in _available_plugins.items():
45
+ logger.success(f"Loading plugin: {plugin['source']}")
46
+ base = os.path.dirname(plugin["module"].__file__)
47
+ with open(os.path.join(base, "post_startup.py"), "r", encoding="utf-8") as file:
48
+ # pylint: disable=exec-used
49
+ exec(file.read())
50
+
51
+ else:
52
+ bec._ip.prompts.status = 1
53
+
54
+ if not bec._hli_funcs:
55
+ bec.load_high_level_interface("bec_hli")
56
+
57
+ if _main_dict["startup_file"]:
58
+ with open(_main_dict["startup_file"], "r", encoding="utf-8") as file:
59
+ # pylint: disable=exec-used
60
+ exec(file.read())
@@ -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
@@ -71,6 +76,9 @@ class IPythonLiveUpdates:
71
76
  scan_report_type = list(instr.keys())[0]
72
77
  scan_def_id = self.client.scans._scan_def_id
73
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
74
82
  if scan_def_id is None or interactive_scan:
75
83
  if scan_report_type == "readback":
76
84
  LiveUpdatesReadbackProgressbar(
@@ -123,21 +131,26 @@ class IPythonLiveUpdates:
123
131
  )
124
132
  self._active_callback.run()
125
133
 
126
- 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]:
127
137
  """Get the available request blocks.
128
138
 
129
139
  Args:
130
140
  queue (QueueItem): The queue item.
131
141
  request (messages.ScanQueueMessage): The request message.
142
+
143
+ Returns:
144
+ list[messages.RequestBlock]: The list of available request blocks.
132
145
  """
133
146
  available_blocks = [
134
147
  req_block
135
148
  for req_block in queue.request_blocks
136
- if req_block["RID"] == request.metadata["RID"]
149
+ if req_block.RID == request.metadata["RID"]
137
150
  ]
138
151
  return available_blocks
139
152
 
140
- def process_request(self, request: messages.ScanQueueMessage, callbacks: any) -> None:
153
+ def process_request(self, request: messages.ScanQueueMessage, callbacks: Any) -> None:
141
154
  """Process the request and report instructions."""
142
155
  # pylint: disable=protected-access
143
156
  try:
@@ -148,11 +161,14 @@ class IPythonLiveUpdates:
148
161
  scan_request = ScanRequestMixin(self.client, request.metadata["RID"])
149
162
  scan_request.wait()
150
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
+
151
167
  # get the corresponding queue item
152
- while not scan_request.request_storage.storage[-1].queue:
168
+ while not scan_request.scan_queue_request.queue:
153
169
  time.sleep(0.01)
154
170
 
155
- self._current_queue = queue = scan_request.request_storage.storage[-1].queue
171
+ self._current_queue = queue = scan_request.scan_queue_request.queue
156
172
  self._request_block_id = req_id = self._active_request.metadata.get("RID")
157
173
 
158
174
  while queue.status not in ["COMPLETED", "ABORTED", "HALTED"]:
@@ -161,7 +177,7 @@ class IPythonLiveUpdates:
161
177
 
162
178
  available_blocks = self._available_req_blocks(queue, request)
163
179
  req_block = available_blocks[self._request_block_index[req_id]]
164
- report_instructions = req_block.get("report_instructions", [])
180
+ report_instructions = req_block.report_instructions or []
165
181
  self._process_report_instructions(report_instructions)
166
182
 
167
183
  self._reset()
@@ -185,12 +201,19 @@ class IPythonLiveUpdates:
185
201
  self.client.queue.request_scan_halt()
186
202
 
187
203
  def _element_in_queue(self) -> bool:
188
- queue = self.client.queue.queue_storage.current_scan_queue.get("primary", {}).get(
189
- "info", []
190
- )
191
- if not queue:
204
+ if self.client.queue is None:
192
205
  return False
193
- return self._current_queue.queue_id in queue[0].get("queue_id")
206
+ if (csq := self.client.queue.queue_storage.current_scan_queue) is None:
207
+ return False
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
194
217
 
195
218
  def _process_queue(
196
219
  self, queue: QueueItem, request: messages.ScanQueueMessage, req_id: str
@@ -210,9 +233,11 @@ class IPythonLiveUpdates:
210
233
  if not queue.request_blocks or not queue.status or queue.queue_position is None:
211
234
  return False
212
235
  if queue.status == "PENDING" and queue.queue_position > 0:
213
- status = self.client.queue.queue_storage.current_scan_queue.get("primary", {}).get(
214
- "status"
215
- )
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
216
241
  print(
217
242
  "Scan is enqueued and is waiting for execution. Current position in queue:"
218
243
  f" {queue.queue_position + 1}. Queue status: {status}.",
@@ -220,14 +245,16 @@ class IPythonLiveUpdates:
220
245
  flush=True,
221
246
  )
222
247
  available_blocks = self._available_req_blocks(queue, request)
248
+ if not available_blocks:
249
+ return False
223
250
  req_block = available_blocks[self._request_block_index[req_id]]
224
- if req_block["content"]["scan_type"] in [
251
+ if req_block.msg.scan_type in [
225
252
  "open_scan_def",
226
253
  "mv",
227
254
  ]: # TODO: make this more general for all scan types that don't have report instructions
228
255
  return True
229
256
 
230
- report_instructions = req_block["report_instructions"]
257
+ report_instructions = req_block.report_instructions or []
231
258
  if not report_instructions:
232
259
  return False
233
260
  self._process_report_instructions(report_instructions)
@@ -255,7 +282,11 @@ class IPythonLiveUpdates:
255
282
  self._current_queue = None
256
283
  self._user_callback = None
257
284
  self._processed_instructions = 0
258
- 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
+ )
259
290
  self._active_request = None
260
291
 
261
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
@@ -54,7 +56,6 @@ class LiveUpdatesTable(LiveUpdatesBase):
54
56
  super().__init__(
55
57
  bec, report_instruction=report_instruction, request=request, callbacks=callbacks
56
58
  )
57
- self.scan_queue_request = None
58
59
  self.scan_item = None
59
60
  self.dev_values = None
60
61
  self.point_data = None
@@ -66,16 +67,19 @@ class LiveUpdatesTable(LiveUpdatesBase):
66
67
  if print_table_data is not None
67
68
  else self.REPORT_TYPE == "scan_progress"
68
69
  )
70
+ self._devices_with_bad_precision = set()
69
71
 
70
72
  def wait_for_scan_to_start(self):
71
73
  """wait until the scan starts"""
72
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.")
73
77
  queue_pos = self.scan_item.queue.queue_position
74
78
  self.check_alarms()
75
79
  if self.scan_item.status == "closed":
76
80
  break
77
81
  if queue_pos is None:
78
- 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}")
79
83
  continue
80
84
  if queue_pos == 0:
81
85
  break
@@ -157,7 +161,20 @@ class LiveUpdatesTable(LiveUpdatesBase):
157
161
  return header
158
162
 
159
163
  def update_scan_item(self, timeout: float = 15):
160
- """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
+
161
178
  start = time.time()
162
179
  while self.scan_queue_request.scan is None:
163
180
  self.check_alarms()
@@ -168,25 +185,38 @@ class LiveUpdatesTable(LiveUpdatesBase):
168
185
 
169
186
  def core(self):
170
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
171
200
  req_ID = self.scan_queue_request.requestID
172
201
  while True:
173
202
  request_block = [
174
- 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
175
204
  ][0]
176
- if not request_block["is_scan"]:
205
+ if not request_block.is_scan:
177
206
  break
178
- if request_block["report_instructions"]:
207
+ if request_block.report_instructions:
179
208
  break
180
209
  self.check_alarms()
181
210
 
182
- self._run_update(self.report_instruction[self.REPORT_TYPE])
183
-
184
211
  def _run_update(self, target_num_points: int):
185
212
  """run the update loop with the progress bar
186
213
 
187
214
  Args:
188
215
  target_num_points (int): number of points to be collected
189
216
  """
217
+ if not self.scan_item:
218
+ logger.warning("No scan item available for live updates.")
219
+ return
190
220
  with ScanProgressBar(
191
221
  scan_number=self.scan_item.scan_number, clear_on_exit=self._print_table_data
192
222
  ) as progressbar:
@@ -208,7 +238,7 @@ class LiveUpdatesTable(LiveUpdatesBase):
208
238
  self.bec.callbacks.poll()
209
239
  self.scan_item.poll_callbacks()
210
240
  else:
211
- logger.debug("waiting for new data point")
241
+ logger.trace("waiting for new data point")
212
242
  time.sleep(0.1)
213
243
 
214
244
  if not self.scan_item.num_points:
@@ -219,6 +249,24 @@ class LiveUpdatesTable(LiveUpdatesBase):
219
249
  if self.point_id > self.scan_item.num_points:
220
250
  raise RuntimeError("Received more points than expected.")
221
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
+
222
270
  @property
223
271
  def _print_table_data(self) -> bool:
224
272
  """Checks if the table should be printed or not.
@@ -249,6 +297,8 @@ class LiveUpdatesTable(LiveUpdatesBase):
249
297
 
250
298
  def print_table_data(self):
251
299
  """print the table data for the current point_id"""
300
+ # pylint: disable=protected-access
301
+ self._print_client_msgs_asap()
252
302
  if not self._print_table_data:
253
303
  return
254
304
 
@@ -259,41 +309,40 @@ class LiveUpdatesTable(LiveUpdatesBase):
259
309
 
260
310
  if self.point_id % 100 == 0:
261
311
  print(self.table.get_header_lines())
262
- ind = 0
312
+
313
+ signals_precisions = []
263
314
  for dev in self.devices:
264
315
  if dev in self.bec.device_manager.devices:
265
316
  obj = self.bec.device_manager.devices[dev]
266
317
  for hint in obj._hints:
267
318
  signal = self.point_data.content["data"].get(obj.root.name, {}).get(hint)
268
- if signal is None or signal.get("value") is None:
269
- print_value = "N/A"
319
+ if signal is None:
320
+ signals_precisions.append((None, None))
270
321
  else:
271
- try:
272
- precision = getattr(obj, "precision")
273
- except AttributeError:
274
- precision = 2
275
- value = signal.get("value")
276
- if isinstance(value, (int, float)):
277
- print_value = f"{value:.{precision}f}"
278
- else:
279
- print_value = str(value)
280
- self.dev_values[ind] = print_value
281
- 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))
282
327
  else:
283
- signal = self.point_data.content["data"].get(dev, {})
284
- value = signal.get("value")
285
- if value is not None:
286
- if isinstance(value, (int, float)):
287
- print_value = f"{value:.2f}"
288
- else:
289
- print_value = str(value)
290
- else:
291
- print_value = "N/A"
292
- self.dev_values[ind] = print_value
293
- 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
+
294
333
  print(self.table.get_row(str(self.point_id), *self.dev_values))
295
- # pylint: disable=protected-access
296
- 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)
297
346
 
298
347
  def close_table(self):
299
348
  """close the table and print the footer"""
@@ -305,8 +354,7 @@ class LiveUpdatesTable(LiveUpdatesBase):
305
354
  f"Scan {self.scan_item.scan_number} finished. Scan ID {self.scan_item.scan_id}. Elapsed time: {elapsed_time:.2f} s"
306
355
  )
307
356
  )
308
- # TODO #286
309
- # self._print_client_msgs_all()
357
+ self._warn_bad_precisions()
310
358
 
311
359
  def process_request(self):
312
360
  """process the request and start the core loop for live updates"""
@@ -336,3 +384,4 @@ class LiveUpdatesTable(LiveUpdatesBase):
336
384
  self.wait_for_scan_item_to_finish()
337
385
  if self._print_table_data:
338
386
  self.close_table()
387
+ self._print_client_msgs_all()