bec-ipython-client 3.64.5__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.

.gitignore CHANGED
@@ -9,6 +9,9 @@
9
9
  **/*.egg*
10
10
  **/*.env
11
11
 
12
+ # bec_widgets saved profiles
13
+ widgets_settings/profiles/*
14
+
12
15
  # recovery_config files
13
16
  recovery_config_*
14
17
 
PKG-INFO CHANGED
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_ipython_client
3
- Version: 3.64.5
3
+ Version: 3.84.0
4
4
  Summary: BEC IPython client
5
5
  Project-URL: Bug Tracker, https://github.com/bec-project/bec/issues
6
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
@@ -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
@@ -29,7 +34,7 @@ class LiveUpdatesDeviceProgress(LiveUpdatesTable):
29
34
  self._print_client_msgs_asap()
30
35
  self._print_client_msgs_all()
31
36
 
32
- def _update_progressbar(self, progressbar: ScanProgressBar, device_names: str) -> bool:
37
+ def _update_progressbar(self, progressbar: ScanProgressBar, device_names: list[str]) -> bool:
33
38
  """Update the progressbar based on the device status message
34
39
 
35
40
  Args:
@@ -41,17 +46,17 @@ class LiveUpdatesDeviceProgress(LiveUpdatesTable):
41
46
  self.check_alarms()
42
47
  status = self.bec.connector.get(MessageEndpoints.device_progress(device_names[0]))
43
48
  if not status:
44
- logger.debug("waiting for new data point")
49
+ logger.trace("waiting for new data point")
45
50
  time.sleep(0.1)
46
51
  return False
47
52
  if status.metadata.get("scan_id") != self.scan_item.scan_id:
48
- logger.debug("waiting for new data point")
53
+ logger.trace("waiting for new data point")
49
54
  time.sleep(0.1)
50
55
  return False
51
56
 
52
57
  point_id = status.content.get("value")
53
58
  if point_id is None:
54
- logger.debug("waiting for new data point")
59
+ logger.trace("waiting for new data point")
55
60
  time.sleep(0.1)
56
61
  return False
57
62
 
@@ -49,7 +49,7 @@ class IPythonLiveUpdates:
49
49
  """
50
50
  scan_type = self._active_request.content["scan_type"]
51
51
  if scan_type in ["open_scan_def", "close_scan_def"]:
52
- self._process_instruction({"scan_progress": 0})
52
+ self._process_instruction({"scan_progress": {"points": 0, "show_table": True}})
53
53
  return
54
54
  if scan_type == "close_scan_group":
55
55
  return
@@ -151,11 +151,14 @@ class IPythonLiveUpdates:
151
151
  scan_request = ScanRequestMixin(self.client, request.metadata["RID"])
152
152
  scan_request.wait()
153
153
 
154
+ # After .wait, we can be sure that the queue item is available, so we can
155
+ assert scan_request.scan_queue_request is not None
156
+
154
157
  # get the corresponding queue item
155
- while not scan_request.request_storage.storage[-1].queue:
158
+ while not scan_request.scan_queue_request.queue:
156
159
  time.sleep(0.01)
157
160
 
158
- self._current_queue = queue = scan_request.request_storage.storage[-1].queue
161
+ self._current_queue = queue = scan_request.scan_queue_request.queue
159
162
  self._request_block_id = req_id = self._active_request.metadata.get("RID")
160
163
 
161
164
  while queue.status not in ["COMPLETED", "ABORTED", "HALTED"]:
@@ -223,6 +226,8 @@ class IPythonLiveUpdates:
223
226
  flush=True,
224
227
  )
225
228
  available_blocks = self._available_req_blocks(queue, request)
229
+ if not available_blocks:
230
+ return False
226
231
  req_block = available_blocks[self._request_block_index[req_id]]
227
232
  if req_block["content"]["scan_type"] in [
228
233
  "open_scan_def",
@@ -78,7 +78,7 @@ class LiveUpdatesTable(LiveUpdatesBase):
78
78
  if self.scan_item.status == "closed":
79
79
  break
80
80
  if queue_pos is None:
81
- logger.debug(f"Could not find queue entry for scan_id {self.scan_item.scan_id}")
81
+ logger.trace(f"Could not find queue entry for scan_id {self.scan_item.scan_id}")
82
82
  continue
83
83
  if queue_pos == 0:
84
84
  break
@@ -171,6 +171,13 @@ class LiveUpdatesTable(LiveUpdatesBase):
171
171
 
172
172
  def core(self):
173
173
  """core function to run the live updates for the table"""
174
+ self._wait_for_report_instructions()
175
+ show_table = self.report_instruction[self.REPORT_TYPE].get("show_table", True)
176
+ self._print_table_data = show_table
177
+ self._run_update(self.report_instruction[self.REPORT_TYPE]["points"])
178
+
179
+ def _wait_for_report_instructions(self):
180
+ """wait until the report instructions are available"""
174
181
  req_ID = self.scan_queue_request.requestID
175
182
  while True:
176
183
  request_block = [
@@ -182,8 +189,6 @@ class LiveUpdatesTable(LiveUpdatesBase):
182
189
  break
183
190
  self.check_alarms()
184
191
 
185
- self._run_update(self.report_instruction[self.REPORT_TYPE])
186
-
187
192
  def _run_update(self, target_num_points: int):
188
193
  """run the update loop with the progress bar
189
194
 
@@ -211,7 +216,7 @@ class LiveUpdatesTable(LiveUpdatesBase):
211
216
  self.bec.callbacks.poll()
212
217
  self.scan_item.poll_callbacks()
213
218
  else:
214
- logger.debug("waiting for new data point")
219
+ logger.trace("waiting for new data point")
215
220
  time.sleep(0.1)
216
221
 
217
222
  if not self.scan_item.num_points:
@@ -1,31 +1,110 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import threading
3
4
  from collections.abc import Callable
4
- from typing import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING, cast
5
6
 
6
7
  import numpy as np
7
8
 
8
9
  from bec_ipython_client.progressbar import DeviceProgressBar
10
+ from bec_lib import messages
9
11
  from bec_lib.endpoints import MessageEndpoints
12
+ from bec_lib.redis_connector import MessageObject
10
13
 
11
14
  from .utils import LiveUpdatesBase, check_alarms
12
15
 
13
16
  if TYPE_CHECKING:
14
- from bec_lib import messages
15
17
  from bec_lib.client import BECClient
16
18
  from bec_lib.devicemanager import DeviceManagerBase
17
19
 
18
20
 
19
- class ReadbackDataMixin:
20
- def __init__(self, device_manager: DeviceManagerBase, devices: list) -> None:
21
- """Mixin to get the current device values and request-done messages.
21
+ class ReadbackDataHandler:
22
+ """Helper class to get the current device values and request-done messages."""
23
+
24
+ def __init__(
25
+ self, device_manager: DeviceManagerBase, devices: list[str], request_id: str
26
+ ) -> None:
27
+ """Helper class to get the current device values and request-done messages.
22
28
 
23
29
  Args:
24
30
  device_manager (DeviceManagerBase): device manager
25
31
  devices (list): list of devices to monitor
32
+ request_id (str): request ID
26
33
  """
27
34
  self.device_manager = device_manager
28
35
  self.devices = devices
36
+ self.connector = device_manager.connector
37
+ self.request_id = request_id
38
+ self._devices_received = {dev: False for dev in devices}
39
+ self.data: dict[str, messages.DeviceMessage] = {}
40
+ self._devices_done_state: dict[str, tuple[bool, bool]] = {
41
+ dev: (False, False) for dev in devices
42
+ }
43
+ self.requests_done = threading.Event()
44
+ self._register_callbacks()
45
+
46
+ def _register_callbacks(self):
47
+ """register callbacks for device readback messages."""
48
+ for dev in self.devices:
49
+ self.connector.register(
50
+ MessageEndpoints.device_readback(dev), cb=self.on_readback, parent=self, device=dev
51
+ )
52
+ self.connector.register(
53
+ MessageEndpoints.device_req_status(self.request_id),
54
+ cb=self.on_req_status,
55
+ from_start=True,
56
+ parent=self,
57
+ )
58
+
59
+ def _unregister_callbacks(self):
60
+ """unregister callbacks for device readback messages."""
61
+ for dev in self.devices:
62
+ self.connector.unregister(MessageEndpoints.device_readback(dev), cb=self.on_readback)
63
+ self.connector.unregister(
64
+ MessageEndpoints.device_req_status(self.request_id), cb=self.on_req_status
65
+ )
66
+
67
+ @staticmethod
68
+ def on_req_status(
69
+ msg_obj: dict[str, messages.DeviceReqStatusMessage], parent: ReadbackDataHandler
70
+ ):
71
+ """Callback for device request status messages to track which devices are done.
72
+
73
+ Args:
74
+ msg_obj (dict[str, messages.DeviceReqStatusMessage]): message object or device request status message
75
+ parent (ReadbackDataHandler): parent instance
76
+ """
77
+ # pylint: disable=protected-access
78
+ msg = msg_obj["data"]
79
+ if msg.request_id != parent.request_id:
80
+ return
81
+ device = msg.device
82
+ parent._devices_done_state[device] = (True, msg.success)
83
+
84
+ if (
85
+ all(done for done, _ in parent._devices_done_state.values())
86
+ and not parent.requests_done.is_set()
87
+ ):
88
+ parent._on_request_done()
89
+
90
+ @staticmethod
91
+ def on_readback(msg_obj: MessageObject, parent: ReadbackDataHandler, device: str):
92
+ """Callback for updating device readback data.
93
+
94
+ Args:
95
+ msg_obj (MessageObject): message object
96
+ parent (ReadbackDataHandler): parent instance
97
+ device (str): device name
98
+ """
99
+ # pylint: disable=protected-access
100
+ msg: messages.DeviceMessage = cast(messages.DeviceMessage, msg_obj.value)
101
+ parent._devices_received[device] = True
102
+ parent.data[device] = msg
103
+
104
+ def _on_request_done(self):
105
+ """Callback for when all requests are done."""
106
+ self.requests_done.set()
107
+ self._unregister_callbacks()
29
108
 
30
109
  def get_device_values(self) -> list:
31
110
  """get the current device values
@@ -35,57 +114,55 @@ class ReadbackDataMixin:
35
114
  """
36
115
  values = []
37
116
  for dev in self.devices:
38
- val = self.device_manager.devices[dev].read(cached=True)
39
- if not val:
40
- values.append(np.nan)
41
- continue
117
+ val = self.data.get(dev)
118
+ if val is None:
119
+ signal_data = self.device_manager.devices[dev].read(cached=True)
120
+ else:
121
+ signal_data = val.signals
42
122
  # pylint: disable=protected-access
43
123
  hints = self.device_manager.devices[dev]._hints
44
124
  # if we have hints, use them to get the value, otherwise just use the first value
45
125
  if hints:
46
- values.append(val.get(hints[0]).get("value"))
126
+ values.append(signal_data.get(hints[0]).get("value"))
47
127
  else:
48
- values.append(val.get(list(val.keys())[0]).get("value"))
128
+ values.append(signal_data.get(list(signal_data.keys())[0]).get("value"))
49
129
  return values
50
130
 
51
- def get_request_done_msgs(self):
52
- """get all request-done messages"""
53
- pipe = self.device_manager.connector.pipeline()
54
- for dev in self.devices:
55
- self.device_manager.connector.get(MessageEndpoints.device_req_status(dev), pipe)
56
- return self.device_manager.connector.execute_pipeline(pipe)
131
+ def done(self) -> bool:
132
+ """check if all devices are done
57
133
 
58
- def wait_for_RID(self, request: messages.ScanQueueMessage) -> None:
59
- """wait for the readback's metadata to match the request ID
134
+ Returns:
135
+ bool: True if all devices are done, False otherwise
136
+ """
137
+ return self.requests_done.is_set()
60
138
 
61
- Args:
62
- request (messages.ScanQueueMessage): request message
139
+ def device_states(self) -> dict[str, tuple[bool, bool]]:
63
140
  """
64
- while True:
65
- msgs = [
66
- self.device_manager.connector.get(MessageEndpoints.device_readback(dev))
67
- for dev in self.devices
68
- ]
69
- if all(msg.metadata.get("RID") == request.metadata["RID"] for msg in msgs if msg):
70
- break
71
- check_alarms(self.device_manager.parent)
141
+ Return the current device done states.
142
+
143
+ Returns:
144
+ dict: dictionary with device names as keys and tuples of (done, success) as values
145
+ """
146
+ return self._devices_done_state
72
147
 
73
148
 
74
149
  class LiveUpdatesReadbackProgressbar(LiveUpdatesBase):
75
150
  """Live feedback on motor movements using a progressbar.
76
151
 
77
152
  Args:
78
- dm (DeviceManagerBase): device_manager
79
- request (ScanQueueMessage): request that should be monitored
153
+ bec (BECClient): BECClient instance
154
+ report_instruction (list, optional): report instruction for the scan. Defaults to None.
155
+ request (messages.ScanQueueMessage, optional): scan queue request message. Defaults to None.
156
+ callbacks (list[Callable], optional): list of callbacks to register. Defaults to None.
80
157
 
81
158
  """
82
159
 
83
160
  def __init__(
84
161
  self,
85
162
  bec: BECClient,
86
- report_instruction: list = None,
87
- request: messages.ScanQueueMessage = None,
88
- callbacks: list[Callable] = None,
163
+ report_instruction: list | None = None,
164
+ request: messages.ScanQueueMessage | None = None,
165
+ callbacks: list[Callable] | None = None,
89
166
  ) -> None:
90
167
  super().__init__(
91
168
  bec, report_instruction=report_instruction, request=request, callbacks=callbacks
@@ -97,18 +174,20 @@ class LiveUpdatesReadbackProgressbar(LiveUpdatesBase):
97
174
 
98
175
  def core(self):
99
176
  """core function to monitor the device values and update the progressbar accordingly."""
100
- data_source = ReadbackDataMixin(self.bec.device_manager, self.devices)
177
+ request_id = self.request.metadata["RID"]
178
+ if self.report_instruction:
179
+ self.devices = self.report_instruction["readback"]["devices"]
180
+ request_id = self.report_instruction["readback"]["RID"]
181
+ data_source = ReadbackDataHandler(self.bec.device_manager, self.devices, request_id)
101
182
  start_values = data_source.get_device_values()
102
183
  self.wait_for_request_acceptance()
103
- data_source.wait_for_RID(self.request)
184
+
104
185
  if self.report_instruction:
105
- self.devices = self.report_instruction["readback"]["devices"]
106
186
  target_values = self.report_instruction["readback"]["end"]
107
187
 
108
188
  start_instr = self.report_instruction["readback"].get("start")
109
189
  if start_instr:
110
190
  start_values = self.report_instruction["readback"]["start"]
111
- data_source = ReadbackDataMixin(self.bec.device_manager, self.devices)
112
191
  else:
113
192
  target_values = [
114
193
  x for xs in self.request.content["parameter"]["args"].values() for x in xs
@@ -119,33 +198,16 @@ class LiveUpdatesReadbackProgressbar(LiveUpdatesBase):
119
198
  with DeviceProgressBar(
120
199
  self.devices, start_values=start_values, target_values=target_values
121
200
  ) as progress:
122
- req_done = False
123
- while not progress.finished or not req_done:
201
+
202
+ while not progress.finished or not data_source.done():
124
203
  check_alarms(self.bec)
125
204
 
126
205
  values = data_source.get_device_values()
127
206
  progress.update(values=values)
128
207
  self._print_client_msgs_asap()
129
208
 
130
- msgs = data_source.get_request_done_msgs()
131
- request_ids = [
132
- msg.metadata["RID"] if (msg and msg.metadata.get("RID")) else None
133
- for msg in msgs
134
- ]
135
-
136
- if self.report_instruction:
137
- compare_rids = set([self.report_instruction["readback"]["RID"]])
138
- else:
139
- compare_rids = set([self.request.metadata["RID"]])
140
- if set(request_ids) != set(compare_rids):
141
- progress.sleep()
142
- continue
143
-
144
- req_done = True
145
- for dev, msg in zip(self.devices, msgs):
146
- if not msg:
147
- continue
148
- if msg.content.get("success", False):
209
+ for dev, (done, success) in data_source.device_states().items():
210
+ if done and success:
149
211
  progress.set_finished(dev)
150
212
  # pylint: disable=protected-access
151
213
  progress._progress.refresh()
@@ -138,22 +138,22 @@ class ScanRequestMixin:
138
138
  Returns:
139
139
  RequestItem: scan queue request
140
140
  """
141
- logger.debug("Waiting for request ID")
141
+ logger.trace("Waiting for request ID")
142
142
  start = time.time()
143
143
  while self.request_storage.find_request_by_ID(self.RID) is None:
144
144
  time.sleep(0.1)
145
145
  check_alarms(self.bec)
146
- logger.debug(f"Waiting for request ID finished after {time.time()-start} s.")
146
+ logger.trace(f"Waiting for request ID finished after {time.time()-start} s.")
147
147
  return self.request_storage.find_request_by_ID(self.RID)
148
148
 
149
149
  def _wait_for_scan_request_decision(self):
150
150
  """wait for a scan queuest decision"""
151
- logger.debug("Waiting for decision")
151
+ logger.trace("Waiting for decision")
152
152
  start = time.time()
153
153
  while self.scan_queue_request.decision_pending:
154
154
  time.sleep(0.1)
155
155
  check_alarms(self.bec)
156
- logger.debug(f"Waiting for decision finished after {time.time()-start} s.")
156
+ logger.trace(f"Waiting for decision finished after {time.time()-start} s.")
157
157
 
158
158
  def wait(self):
159
159
  """wait for the request acceptance"""
@@ -1,4 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  import argparse
4
+ import collections
5
+ import functools
2
6
  import os
3
7
  import sys
4
8
  from importlib.metadata import version
@@ -7,16 +11,18 @@ from typing import Iterable, Literal, Tuple
7
11
  import IPython
8
12
  import redis
9
13
  import redis.exceptions
10
- import requests
11
14
  from IPython.terminal.ipapp import TerminalIPythonApp
12
15
  from IPython.terminal.prompts import Prompts, Token
16
+ from pydantic import ValidationError
17
+ from rich.console import Console
18
+ from rich.panel import Panel
19
+ from rich.text import Text
13
20
 
14
21
  from bec_ipython_client.beamline_mixin import BeamlineMixin
15
22
  from bec_ipython_client.bec_magics import BECMagics
16
23
  from bec_ipython_client.callbacks.ipython_live_updates import IPythonLiveUpdates
17
24
  from bec_ipython_client.signals import ScanInterruption, SigintHandler
18
25
  from bec_lib import plugin_helper
19
- from bec_lib.acl_login import BECAuthenticationError
20
26
  from bec_lib.alarm_handler import AlarmBase
21
27
  from bec_lib.bec_errors import DeviceConfigError
22
28
  from bec_lib.bec_service import parse_cmdline_args
@@ -25,6 +31,7 @@ from bec_lib.client import BECClient
25
31
  from bec_lib.logger import bec_logger
26
32
  from bec_lib.redis_connector import RedisConnector
27
33
  from bec_lib.service_config import ServiceConfig
34
+ from bec_lib.utils.pydantic_pretty_print import pretty_print_pydantic_validation_error
28
35
 
29
36
  logger = bec_logger.logger
30
37
 
@@ -79,6 +86,7 @@ class BECIPythonClient:
79
86
  self._client.callbacks.register(
80
87
  event_type=EventType.NAMESPACE_UPDATE, callback=self._update_namespace_callback
81
88
  )
89
+ self._alarm_history = collections.deque(maxlen=100)
82
90
 
83
91
  def __getattr__(self, name):
84
92
  return getattr(self._client, name)
@@ -136,7 +144,7 @@ class BECIPythonClient:
136
144
  self._refresh_ipython_username()
137
145
  self._load_magics()
138
146
  self._ip.events.register("post_run_cell", log_console)
139
- self._ip.set_custom_exc((Exception,), _ip_exception_handler) # register your handler
147
+ self._ip.set_custom_exc((Exception,), self._create_exception_handler())
140
148
  # represent objects using __str__, if overwritten, otherwise use __repr__
141
149
  self._ip.display_formatter.formatters["text/plain"].for_type(
142
150
  object,
@@ -187,10 +195,66 @@ class BECIPythonClient:
187
195
  pass
188
196
  self._client.shutdown()
189
197
 
198
+ def _create_exception_handler(self):
199
+ return functools.partial(_ip_exception_handler, parent=self)
200
+
201
+ def show_last_alarm(self, offset: int = 0):
202
+ """
203
+ Show the last alarm raised in this session with rich formatting.
204
+ """
205
+ try:
206
+ alarm: AlarmBase = self._alarm_history[-1 - offset][1]
207
+ except IndexError:
208
+ print("No alarm has been raised in this session.")
209
+ return
190
210
 
191
- def _ip_exception_handler(self, etype, evalue, tb, tb_offset=None):
192
- if issubclass(etype, (AlarmBase, ScanInterruption, DeviceConfigError)):
193
- print(f"\x1b[31m BEC alarm:\x1b[0m {evalue}")
211
+ console = Console()
212
+
213
+ # --- HEADER ---
214
+ header = Text()
215
+ header.append("Alarm Raised\n", style="bold red")
216
+ header.append(f"Severity: {alarm.severity.name}\n", style="bold")
217
+ header.append(f"Type: {alarm.alarm_type}\n", style="bold")
218
+ if alarm.alarm.info.device:
219
+ header.append(f"Device: {alarm.alarm.info.device}\n", style="bold")
220
+
221
+ console.print(Panel(header, title="Alarm Info", border_style="red", expand=False))
222
+
223
+ # --- SHOW SUMMARY
224
+ if alarm.alarm.info.compact_error_message:
225
+ console.print(
226
+ Panel(
227
+ Text(alarm.alarm.info.compact_error_message, style="yellow"),
228
+ title="Summary",
229
+ border_style="yellow",
230
+ expand=False,
231
+ )
232
+ )
233
+
234
+ # --- SHOW FULL TRACEBACK
235
+ tb_str = alarm.alarm.info.error_message
236
+ if tb_str:
237
+ try:
238
+ console.print(tb_str)
239
+ except Exception:
240
+ # fallback in case msg is not a traceback
241
+ console.print(Panel(tb_str, title="Message", border_style="cyan"))
242
+
243
+
244
+ def _ip_exception_handler(
245
+ self, etype, evalue, tb, tb_offset=None, parent: BECIPythonClient = None, **kwargs
246
+ ):
247
+ if issubclass(etype, AlarmBase):
248
+ parent._alarm_history.append((etype, evalue, tb, tb_offset))
249
+ print("\x1b[31m BEC alarm:\x1b[0m")
250
+ evalue.pretty_print()
251
+ print("For more details, use 'bec.show_last_alarm()'")
252
+ return
253
+ if issubclass(etype, ValidationError):
254
+ pretty_print_pydantic_validation_error(evalue)
255
+ return
256
+ if issubclass(etype, (ScanInterruption, DeviceConfigError)):
257
+ print(f"\x1b[31m {evalue.__class__.__name__}:\x1b[0m {evalue}")
194
258
  return
195
259
  if issubclass(etype, redis.exceptions.NoPermissionError):
196
260
  # pylint: disable=protected-access
@@ -220,9 +284,14 @@ class BECClientPrompt(Prompts):
220
284
  next_scan_number = str(self.client.queue.next_scan_number)
221
285
  except Exception:
222
286
  next_scan_number = "?"
287
+
288
+ if self.client.active_account:
289
+ username = f"{self.client.active_account} | {self.username}"
290
+ else:
291
+ username = self.username
223
292
  return [
224
293
  (status_led, "\u2022"),
225
- (Token.Prompt, " " + self.username),
294
+ (Token.Prompt, " " + username), # BEC ACL username and pgroup
226
295
  (Token.Prompt, "@" + self.session_name),
227
296
  (Token.Prompt, " ["),
228
297
  (Token.PromptNum, str(self.shell.execution_count)),
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_ipython_client
3
- Version: 3.64.5
3
+ Version: 3.84.0
4
4
  Summary: BEC IPython client
5
5
  Project-URL: Bug Tracker, https://github.com/bec-project/bec/issues
6
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