bec-ipython-client 2.21.2__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.
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/.gitignore +4 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/PKG-INFO +9 -9
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/bec_magics.py +29 -4
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/bec_startup.py +14 -13
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/callbacks/device_progress.py +12 -9
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/callbacks/ipython_live_updates.py +58 -23
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/callbacks/live_table.py +94 -44
- bec_ipython_client-3.86.2/bec_ipython_client/callbacks/move_device.py +218 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/callbacks/utils.py +30 -52
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/main.py +147 -21
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/signals.py +9 -3
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/demo.py +10 -1
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/pyproject.toml +8 -8
- bec_ipython_client-3.86.2/tests/client_tests/conftest.py +19 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/tests/client_tests/test_bec_client.py +85 -19
- bec_ipython_client-3.86.2/tests/client_tests/test_ipython_live_updates.py +350 -0
- bec_ipython_client-3.86.2/tests/client_tests/test_live_table.py +423 -0
- bec_ipython_client-3.86.2/tests/client_tests/test_move_callback.py +223 -0
- bec_ipython_client-3.86.2/tests/end-2-end/_ensure_requirements_container.py +21 -0
- bec_ipython_client-3.86.2/tests/end-2-end/test_procedures_e2e.py +134 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/tests/end-2-end/test_scans_e2e.py +171 -47
- bec_ipython_client-3.86.2/tests/end-2-end/test_scans_lib_e2e.py +545 -0
- bec_ipython_client-2.21.2/bec_ipython_client/callbacks/move_device.py +0 -157
- bec_ipython_client-2.21.2/tests/client_tests/test_ipython_live_updates.py +0 -159
- bec_ipython_client-2.21.2/tests/client_tests/test_live_table.py +0 -280
- bec_ipython_client-2.21.2/tests/client_tests/test_move_callback.py +0 -170
- bec_ipython_client-2.21.2/tests/end-2-end/test_scans_lib_e2e.py +0 -363
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/__init__.py +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/beamline_mixin.py +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/callbacks/__init__.py +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/high_level_interfaces/__init__.py +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/high_level_interfaces/bec_hli.py +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/high_level_interfaces/spec_hli.py +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/plugins/SLS/__init__.py +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/plugins/SLS/sls_info.py +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/plugins/XTreme/__init__.py +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/plugins/XTreme/x-treme.py +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/plugins/__init__.py +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/plugins/flomni/flomni_config.yaml +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/prettytable.py +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/progressbar.py +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/tests/client_tests/test_beamline_mixins.py +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/tests/client_tests/test_device_progress.py +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/tests/client_tests/test_pretty_table.py +0 -0
- {bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/tests/conftest.py +0 -0
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: bec_ipython_client
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.86.2
|
|
4
4
|
Summary: BEC IPython client
|
|
5
|
-
Project-URL: Bug Tracker, https://
|
|
6
|
-
Project-URL: Homepage, https://
|
|
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.
|
|
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
|
|
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~=
|
|
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,15 +2,18 @@ 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
|
-
from bec_ipython_client import
|
|
11
|
+
from bec_ipython_client import BECIPythonClient
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
@magics_class
|
|
12
15
|
class BECMagics(Magics):
|
|
13
|
-
def __init__(self, shell, client:
|
|
16
|
+
def __init__(self, shell, client: BECIPythonClient):
|
|
14
17
|
super(BECMagics, self).__init__(shell)
|
|
15
18
|
self.client = client
|
|
16
19
|
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
14
|
-
from bec_widgets.cli.client import BECDockArea as _BECDockArea
|
|
15
|
+
from bec_widgets.cli.client_utils import BECGuiClient
|
|
15
16
|
except ImportError:
|
|
16
|
-
|
|
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
|
|
31
|
-
gui = bec.gui =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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:
|
|
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.
|
|
168
|
+
while not scan_request.scan_queue_request.queue:
|
|
152
169
|
time.sleep(0.01)
|
|
153
170
|
|
|
154
|
-
self._current_queue = queue = scan_request.
|
|
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.
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
|
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[
|
|
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 =
|
|
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:
|
{bec_ipython_client-2.21.2 → bec_ipython_client-3.86.2}/bec_ipython_client/callbacks/live_table.py
RENAMED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
"""
|
|
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
|
|
203
|
+
req for req in self.scan_item.queue.request_blocks if req.RID == req_ID
|
|
174
204
|
][0]
|
|
175
|
-
if not request_block
|
|
205
|
+
if not request_block.is_scan:
|
|
176
206
|
break
|
|
177
|
-
if request_block
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
267
|
-
if signal is None
|
|
268
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
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()
|