bec-lib 3.151.3__tar.gz → 3.153.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.
Files changed (154) hide show
  1. {bec_lib-3.151.3 → bec_lib-3.153.0}/PKG-INFO +3 -3
  2. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/acl_login.py +74 -26
  3. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/bec_service.py +14 -15
  4. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/devicemanager.py +3 -1
  5. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/endpoints.py +17 -28
  6. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/messages.py +4 -0
  7. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/queue_items.py +40 -0
  8. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/scan_input_validator.py +1 -1
  9. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/scans.py +17 -3
  10. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/service_config.py +1 -0
  11. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/user_acls_test.py +44 -4
  12. {bec_lib-3.151.3 → bec_lib-3.153.0}/pyproject.toml +3 -3
  13. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_acl_login.py +110 -0
  14. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_bec_logger.py +1 -1
  15. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_bec_service.py +22 -1
  16. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_queue_items.py +67 -0
  17. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_scan_object.py +10 -0
  18. {bec_lib-3.151.3 → bec_lib-3.153.0}/.gitignore +0 -0
  19. {bec_lib-3.151.3 → bec_lib-3.153.0}/README.md +0 -0
  20. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/__init__.py +0 -0
  21. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/_print_versions.py +0 -0
  22. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/actors.py +0 -0
  23. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/alarm_handler.py +0 -0
  24. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/atlas_models.py +0 -0
  25. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/bec_errors.py +0 -0
  26. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/bec_yaml_loader.py +0 -0
  27. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/bl_state_manager.py +0 -0
  28. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/bl_states.py +0 -0
  29. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/builtin_actor_hli.py +0 -0
  30. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/callback_handler.py +0 -0
  31. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/channel_monitor.py +0 -0
  32. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/client.py +0 -0
  33. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/codecs.py +0 -0
  34. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/config_helper.py +0 -0
  35. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/config_values.py +0 -0
  36. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/configs/__init__.py +0 -0
  37. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/configs/demo_config.yaml +0 -0
  38. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/configs/openapi_schema.json +0 -0
  39. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/connector.py +0 -0
  40. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/dap_plugin_objects.py +0 -0
  41. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/dap_plugins.py +0 -0
  42. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/device.py +0 -0
  43. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/file_utils.py +0 -0
  44. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/live_scan_data.py +0 -0
  45. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/lmfit_serializer.py +0 -0
  46. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/logger.py +0 -0
  47. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/macro_update_handler.py +0 -0
  48. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/messaging_hooks.py +0 -0
  49. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/messaging_services.py +0 -0
  50. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/metadata_schema.py +0 -0
  51. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/numpy_encoder.py +0 -0
  52. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/observer.py +0 -0
  53. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/pdf_writer.py +0 -0
  54. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/plugin_helper.py +0 -0
  55. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/procedures/__init__.py +0 -0
  56. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/procedures/helper.py +0 -0
  57. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/procedures/hli.py +0 -0
  58. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/procedures/status.py +0 -0
  59. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/redis_connector/__init__.py +0 -0
  60. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/redis_connector/constants.py +0 -0
  61. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/redis_connector/hli.py +0 -0
  62. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/redis_connector/managed_redis_connection.py +0 -0
  63. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/redis_connector/streams.py +0 -0
  64. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/redis_connector/validation.py +0 -0
  65. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/request_context.py +0 -0
  66. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/request_items.py +0 -0
  67. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/scan_args.py +0 -0
  68. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/scan_data_container.py +0 -0
  69. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/scan_history.py +0 -0
  70. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/scan_items.py +0 -0
  71. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/scan_manager.py +0 -0
  72. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/scan_number_container.py +0 -0
  73. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/scan_repeat.py +0 -0
  74. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/scan_report.py +0 -0
  75. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/scibec_validator.py +0 -0
  76. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/script_executor.py +0 -0
  77. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/serialization.py +0 -0
  78. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/serialization_registry.py +0 -0
  79. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/signature_serializer.py +0 -0
  80. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/tests/__init__.py +0 -0
  81. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/tests/fixtures.py +0 -0
  82. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/tests/test_config.yaml +0 -0
  83. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/tests/test_service_config.yaml +0 -0
  84. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/tests/utils.py +0 -0
  85. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/user_macros.py +0 -0
  86. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/__init__.py +0 -0
  87. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/copier_jinja_filters.py +0 -0
  88. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/import_utils.py +0 -0
  89. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/json_extended.py +0 -0
  90. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/plugin_manager/__init__.py +0 -0
  91. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/plugin_manager/_constants.py +0 -0
  92. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/plugin_manager/_util.py +0 -0
  93. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/plugin_manager/create/__init__.py +0 -0
  94. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/plugin_manager/create/device.py +0 -0
  95. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/plugin_manager/create/scan.py +0 -0
  96. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/plugin_manager/create/templates/v4_scan.py.jinja +0 -0
  97. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/plugin_manager/main.py +0 -0
  98. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/proxy.py +0 -0
  99. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/pydantic_pretty_print.py +0 -0
  100. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/rpc_utils.py +0 -0
  101. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/scan_utils.py +0 -0
  102. {bec_lib-3.151.3 → bec_lib-3.153.0}/bec_lib/utils/threading_utils.py +0 -0
  103. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/__init__.py +0 -0
  104. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/_additional_for_signature_serializer_test.py +0 -0
  105. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/conftest.py +0 -0
  106. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_alarm_handler.py +0 -0
  107. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_atlas_models.py +0 -0
  108. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_beamline_states.py +0 -0
  109. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_bec_messages.py +0 -0
  110. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_callback_handler.py +0 -0
  111. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_channel_monitor.py +0 -0
  112. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_client.py +0 -0
  113. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_config_helper.py +0 -0
  114. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_core_utils.py +0 -0
  115. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_dap_plugins.py +0 -0
  116. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_data_container.py +0 -0
  117. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_device_hashing.py +0 -0
  118. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_device_manager.py +0 -0
  119. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_devices.py +0 -0
  120. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_file_utils.py +0 -0
  121. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_hli_connector_fakeredis.py +0 -0
  122. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_hli_redis_connector.py +0 -0
  123. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_import_utils.py +0 -0
  124. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_jinja_plugins.py +0 -0
  125. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_lmfit_serializer.py +0 -0
  126. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_managed_config_values.py +0 -0
  127. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_managed_redis_connection.py +0 -0
  128. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_messaging_hooks.py +0 -0
  129. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_messaging_service.py +0 -0
  130. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_metadata_schema.py +0 -0
  131. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_observer.py +0 -0
  132. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_pdf_writer.py +0 -0
  133. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_plugin_helper.py +0 -0
  134. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_plugin_manager.py +0 -0
  135. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_plugin_system.py +0 -0
  136. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_pydantic_pretty_print.py +0 -0
  137. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_redis_connector_fakeredis.py +0 -0
  138. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_scan_context.py +0 -0
  139. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_scan_data.py +0 -0
  140. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_scan_history.py +0 -0
  141. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_scan_items.py +0 -0
  142. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_scan_manager.py +0 -0
  143. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_scan_repeat.py +0 -0
  144. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_scan_report.py +0 -0
  145. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_scibec_validator.py +0 -0
  146. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_script_executor.py +0 -0
  147. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_serializer.py +0 -0
  148. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_signature_serializer.py +0 -0
  149. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_signature_serializer_with_future_import.py +0 -0
  150. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_stream_subs.py +0 -0
  151. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_user_macros.py +0 -0
  152. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_utils.py +0 -0
  153. {bec_lib-3.151.3 → bec_lib-3.153.0}/tests/test_yaml_loader.py +0 -0
  154. {bec_lib-3.151.3 → bec_lib-3.153.0}/util_scripts/init_config.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bec_lib
3
- Version: 3.151.3
3
+ Version: 3.153.0
4
4
  Summary: BEC library
5
5
  Project-URL: Bug Tracker, https://github.com/bec-project/bec/issues
6
6
  Project-URL: Homepage, https://github.com/bec-project/bec
@@ -13,7 +13,7 @@ Requires-Dist: beautifulsoup4~=4.14
13
13
  Requires-Dist: fastjsonschema~=2.19
14
14
  Requires-Dist: fpdf2>=2.7.7,~=2.7
15
15
  Requires-Dist: h5py~=3.10
16
- Requires-Dist: hdf5plugin<7.0,>=4.3
16
+ Requires-Dist: hdf5plugin<8.0,>=4.3
17
17
  Requires-Dist: hiredis~=3.0
18
18
  Requires-Dist: lmfit~=1.3
19
19
  Requires-Dist: loguru~=0.7
@@ -45,7 +45,7 @@ Requires-Dist: coverage~=7.0; extra == 'dev'
45
45
  Requires-Dist: fakeredis>=2.23.2,~=2.23; extra == 'dev'
46
46
  Requires-Dist: isort>=5.13.2,~=5.13; extra == 'dev'
47
47
  Requires-Dist: jinja2~=3.1; extra == 'dev'
48
- Requires-Dist: pandas~=2.0; extra == 'dev'
48
+ Requires-Dist: pandas<4,>=2; extra == 'dev'
49
49
  Requires-Dist: pylint<5,>=3; extra == 'dev'
50
50
  Requires-Dist: pytest-random-order~=1.1; extra == 'dev'
51
51
  Requires-Dist: pytest-redis<5,>=3; extra == 'dev'
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import os
4
+ from collections.abc import Iterator
5
+ from contextlib import contextmanager
4
6
  from functools import wraps
5
7
  from getpass import getpass
6
8
  from typing import TYPE_CHECKING, cast
@@ -13,6 +15,7 @@ from rich.table import Table
13
15
  from bec_lib.endpoints import MessageEndpoints
14
16
  from bec_lib.logger import bec_logger
15
17
  from bec_lib.redis_connector import RedisConnector
18
+ from bec_lib.service_config import ACLConfig
16
19
  from bec_lib.utils.import_utils import lazy_import_from
17
20
 
18
21
  if TYPE_CHECKING: # pragma: no cover
@@ -87,8 +90,35 @@ class BECAccess:
87
90
  """
88
91
  self.connector.authenticate(username=username, password=token)
89
92
 
93
+ @contextmanager
94
+ def temporary_user(self, *, username: str, token: str | None) -> Iterator[None]:
95
+ """
96
+ Temporarily authenticate as another ACL user and restore the current user on exit.
97
+ """
98
+ credentials = self.current_credentials()
99
+ self.login_with_token(username=username, token=token)
100
+ try:
101
+ yield
102
+ finally:
103
+ self.login_with_token(
104
+ username=credentials["username"] or "default", token=credentials.get("token")
105
+ )
106
+
107
+ def current_credentials(self) -> dict[str, str | None]:
108
+ """
109
+ Return the current Redis ACL credentials for forwarding to linked clients.
110
+ """
111
+ # pylint: disable=protected-access
112
+ conn_kwargs = (
113
+ self.connector._managed_connection._redis_conn.connection_pool.connection_kwargs
114
+ )
115
+ token = conn_kwargs.get("password")
116
+ if token == "null":
117
+ token = None
118
+ return {"username": self.connector.username, "token": token}
119
+
90
120
  def _bec_service_login(
91
- self, prompt_for_acl: bool = False, acl_config: dict | str | None = None
121
+ self, prompt_for_acl: bool = False, acl_config: ACLConfig | dict | str | None = None
92
122
  ) -> None:
93
123
  """
94
124
  Login to Redis using the ACL system. This is the main entry point for the login process, started
@@ -97,9 +127,8 @@ class BECAccess:
97
127
  Args:
98
128
  prompt_for_acl (bool): If True, prompt the user to login using ACL. This is typically only used
99
129
  for user-facing services. Default is False.
100
- acl_config (dict or str): The ACL configuration. If a string is provided, it will be treated as a
101
- path to the configuration file. If a dictionary is provided, it should contain the username and
102
- password for the account.
130
+ acl_config (ACLConfig, dict, str, optional): The ACL configuration. If a string is provided,
131
+ it will be treated as a path to the configuration file.
103
132
  """
104
133
 
105
134
  if not self.connector.redis_server_is_running():
@@ -241,40 +270,58 @@ class BECAccess:
241
270
  password = getpass(f"Enter the token for {selected_account} (hidden): ")
242
271
  return password
243
272
 
244
- def _config_login_successful(self, prompt_for_acl: bool, acl_config: dict | str) -> bool:
273
+ def _config_login_successful(
274
+ self, prompt_for_acl: bool, acl_config: ACLConfig | dict | str | None
275
+ ) -> bool:
245
276
  """
246
277
  Login to Redis using the configuration file.
247
278
 
248
279
  Args:
249
280
  prompt_for_acl (bool): If True, prompt the user to login using ACL. Default is False.
250
- acl_config (dict or str): The ACL configuration. If a string is provided, it will be treated as a path to the configuration file.
281
+ acl_config (ACLConfig, dict, str, optional): The ACL configuration. If a string is provided,
282
+ it will be treated as a path to the configuration file.
251
283
 
252
284
  Returns:
253
285
  bool: True if the login was successful, False otherwise.
254
286
  """
255
287
 
288
+ if acl_config is None:
289
+ return False
290
+
256
291
  if isinstance(acl_config, str):
257
- if os.path.exists(acl_config) and not prompt_for_acl:
258
- # Load the account information from the .env file
259
- # This is relevant for the BEC services that are not launched by the user
260
- # but are auto-deployed.
261
- account = dotenv_values(acl_config)
262
- user = account.get("REDIS_USER")
263
- password = account.get("REDIS_PASSWORD")
264
- if self._check_redis_auth(user, password):
265
- return True
292
+ env_file = acl_config
293
+ username = None
294
+ password = None
266
295
  elif isinstance(acl_config, dict):
267
- username = acl_config.get("username")
296
+ env_file = acl_config.get("env_file")
297
+ username = acl_config.get("username") or acl_config.get("user")
268
298
  password = acl_config.get("password")
269
- if self._check_redis_auth(username, password):
270
- return True
271
- if not password and prompt_for_acl:
272
- self._user_service_login(username=username)
273
- return True
299
+ elif isinstance(acl_config, ACLConfig):
300
+ env_file = acl_config.env_file
301
+ username = acl_config.user
302
+ password = acl_config.password
274
303
  else:
275
304
  raise ValueError(
276
- "Invalid value for 'acl' in the service config. Must be a dict or a path to a .env file."
305
+ "Invalid value for 'acl' in the service config. Must be an ACLConfig, dict, "
306
+ "or a path to a .env file."
277
307
  )
308
+
309
+ if env_file and not prompt_for_acl and os.path.exists(env_file):
310
+ # Load the account information from the .env file. This is relevant for BEC services
311
+ # that are not launched by the user but are auto-deployed.
312
+ account = dotenv_values(env_file)
313
+ user = account.get("REDIS_USER")
314
+ env_password = account.get("REDIS_PASSWORD")
315
+ if self._check_redis_auth(user, env_password):
316
+ return True
317
+
318
+ if (username or password) and self._check_redis_auth(username, password):
319
+ return True
320
+
321
+ if prompt_for_acl and username:
322
+ self._user_service_login(username=username)
323
+ return True
324
+
278
325
  return False
279
326
 
280
327
  def _default_user_login_successful(self, full_access: bool) -> bool:
@@ -282,7 +329,8 @@ class BECAccess:
282
329
  Login using the default user account.
283
330
 
284
331
  Args:
285
- full_access (bool): If True, the user has to have full access to the Redis
332
+ full_access (bool): If True, the user has sufficient access to perform all user
333
+ operations. If False, the user has limited access or no access to perform user operations.
286
334
 
287
335
  Returns:
288
336
  bool: True if the login was successful, False otherwise.
@@ -292,8 +340,8 @@ class BECAccess:
292
340
  return True
293
341
 
294
342
  try:
295
- self.connector.get(MessageEndpoints.acl_accounts())
296
- # ACLs are enabled but we have full access. No need to login.
343
+ # We check an endpoint on level INFO to verify that the user has sufficient access to perform all user operations.
344
+ self.connector.get(MessageEndpoints.device_config())
297
345
  return True
298
346
  # pylint: disable=broad-except
299
347
  except Exception:
@@ -319,7 +367,7 @@ class BECAccess:
319
367
  raise BECAuthenticationError("Login information not available. Unable to login.")
320
368
  self._atlas_login = self._info.atlas_login
321
369
  if not username and not self._info.atlas_login:
322
- return self.login_with_token(username="user", token=None)
370
+ return self.login_with_token(username="default", token=None)
323
371
  return self.login(username)
324
372
 
325
373
  def _check_redis_auth(self, user: str | None, password: str | None) -> bool:
@@ -6,19 +6,15 @@ from __future__ import annotations
6
6
 
7
7
  import argparse
8
8
  import getpass
9
- import os
10
9
  import socket
11
10
  import sys
12
11
  import threading
13
12
  import time
14
13
  import uuid
15
14
  from dataclasses import asdict, dataclass
16
- from importlib.metadata import PackageNotFoundError
17
- from importlib.metadata import version as importlib_version
18
15
  from typing import TYPE_CHECKING, Any, Literal
19
16
 
20
17
  import psutil
21
- import tomli
22
18
  from rich.console import Console
23
19
  from rich.table import Table
24
20
 
@@ -111,7 +107,7 @@ def parse_cmdline_args(parser=None, config_name: Literal["client", "server"] | s
111
107
  config_file = args.config
112
108
  cli_args = vars(args)
113
109
  user = cli_args.pop("user")
114
- acl_config = {"username": user} if user else {}
110
+ acl_config = {"user": user} if user else None
115
111
  redis_url = args.bec_server
116
112
 
117
113
  if redis_url and config_file:
@@ -134,18 +130,21 @@ def parse_cmdline_args(parser=None, config_name: Literal["client", "server"] | s
134
130
  except ValueError:
135
131
  raise ValueError(f"Invalid port number in Redis URL: {comps[1]}")
136
132
 
137
- service_config = ServiceConfig(
138
- redis=redis_data, cmdline_args=cli_args, acl=acl_config, config_name=config_name
139
- )
133
+ kwargs = {"redis": redis_data, "cmdline_args": cli_args, "config_name": config_name}
134
+ if acl_config:
135
+ kwargs["acl"] = acl_config
136
+ service_config = ServiceConfig(**kwargs)
140
137
  elif config_file:
141
- service_config = ServiceConfig(
142
- config_file, cmdline_args=cli_args, acl=acl_config, config_name=config_name
143
- )
138
+ kwargs = {"cmdline_args": cli_args, "config_name": config_name}
139
+ if acl_config:
140
+ kwargs["acl"] = acl_config
141
+ service_config = ServiceConfig(config_file, **kwargs)
144
142
  else:
145
143
  # If no config file or Redis URL is provided, use the default ServiceConfig
146
- service_config = ServiceConfig(
147
- cmdline_args=cli_args, acl=acl_config, config_name=config_name
148
- )
144
+ kwargs = {"cmdline_args": cli_args, "config_name": config_name}
145
+ if acl_config:
146
+ kwargs["acl"] = acl_config
147
+ service_config = ServiceConfig(**kwargs)
149
148
 
150
149
  return args, extra_args, service_config
151
150
 
@@ -173,7 +172,7 @@ class BECService:
173
172
  else connector
174
173
  )
175
174
  self.acl = BECAccess(self.connector)
176
- self.acl._bec_service_login(prompt_for_acl, self._service_config.config.get("acl"))
175
+ self.acl._bec_service_login(prompt_for_acl, self._service_config.model.acl)
177
176
 
178
177
  self._unique_service = unique_service
179
178
  self.wait_for_server = wait_for_server
@@ -315,7 +315,9 @@ class DeviceContainer(dict):
315
315
  return list(tags)
316
316
 
317
317
  def get_software_triggered_devices(self) -> list:
318
- """get a list of all devices that should receive a software trigger detectors"""
318
+ """
319
+ Get a list of all enabled devices that should receive a software trigger during a scan.
320
+ """
319
321
  # pylint: disable=protected-access
320
322
  devices = [
321
323
  dev for _, dev in self.items() if dev._config.get("softwareTrigger", False) is True
@@ -234,7 +234,7 @@ class MessageEndpoints:
234
234
  Returns:
235
235
  EndpointInfo: Endpoint for device config request.
236
236
  """
237
- endpoint = f"{EndpointType.ADMIN.value}/devices/config_request"
237
+ endpoint = f"{EndpointType.USER.value}/devices/config_request"
238
238
  return EndpointInfo(
239
239
  endpoint=endpoint, message_type=messages.DeviceConfigMessage, message_op=MessageOp.SEND
240
240
  )
@@ -703,7 +703,7 @@ class MessageEndpoints:
703
703
  Returns:
704
704
  EndpointInfo: Endpoint for stopping devices.
705
705
  """
706
- endpoint = f"{EndpointType.INFO.value}/queue/stop_devices"
706
+ endpoint = f"{EndpointType.USER.value}/queue/stop_devices"
707
707
  return EndpointInfo(
708
708
  endpoint=endpoint, message_type=messages.VariableMessage, message_op=MessageOp.SEND
709
709
  )
@@ -849,17 +849,6 @@ class MessageEndpoints:
849
849
  message_op=MessageOp.SET_PUBLISH,
850
850
  )
851
851
 
852
- @staticmethod
853
- def bluesky_events() -> str:
854
- """
855
- Endpoint for bluesky events. This endpoint is used by the scan bundler to
856
- publish the bluesky events using a direct msgpack dump of the bluesky event.
857
-
858
- Returns:
859
- str: Endpoint for bluesky events.
860
- """
861
- return f"{EndpointType.INFO.value}/scans/bluesky-events"
862
-
863
852
  @staticmethod
864
853
  def scan_segment():
865
854
  """
@@ -1084,7 +1073,7 @@ class MessageEndpoints:
1084
1073
  Returns:
1085
1074
  EndpointInfo: Endpoint for log.
1086
1075
  """
1087
- endpoint = f"{EndpointType.INFO.value}/log"
1076
+ endpoint = f"{EndpointType.USER.value}/log"
1088
1077
  return EndpointInfo(
1089
1078
  endpoint=endpoint, message_type=messages.LogMessage, message_op=MessageOp.STREAM
1090
1079
  )
@@ -1626,7 +1615,7 @@ class MessageEndpoints:
1626
1615
  Returns:
1627
1616
  EndpointInfo: Endpoint for procedure queue updates for given queue.
1628
1617
  """
1629
- endpoint = f"{EndpointType.INFO.value}/procedures/logs/{queue}"
1618
+ endpoint = f"{EndpointType.USER.value}/procedures/logs/{queue}"
1630
1619
  return EndpointInfo(
1631
1620
  endpoint=endpoint, message_type=messages.RawMessage, message_op=MessageOp.STREAM
1632
1621
  )
@@ -1682,7 +1671,7 @@ class MessageEndpoints:
1682
1671
 
1683
1672
  @staticmethod
1684
1673
  def modify_interlock_table():
1685
- endpoint = f"{EndpointType.INTERNAL.value}/actor/builtin/scan_interlock/table_mod"
1674
+ endpoint = f"{EndpointType.USER.value}/actor/builtin/scan_interlock/table_mod"
1686
1675
  return EndpointInfo(
1687
1676
  endpoint=endpoint,
1688
1677
  message_type=messages.ScanInterlockModifyStateTableMessage,
@@ -1700,7 +1689,7 @@ class MessageEndpoints:
1700
1689
 
1701
1690
  @staticmethod
1702
1691
  def scan_interlock_enabled():
1703
- endpoint = f"{EndpointType.INFO.value}/actor/builtin/scan_interlock/config/enabled"
1692
+ endpoint = f"{EndpointType.USER.value}/actor/builtin/scan_interlock/config/enabled"
1704
1693
  return EndpointInfo(
1705
1694
  endpoint=endpoint,
1706
1695
  message_type=messages.BoolConfigDefaultFalse,
@@ -1710,7 +1699,7 @@ class MessageEndpoints:
1710
1699
  @staticmethod
1711
1700
  def scan_interlock_trigger_setting():
1712
1701
  """Whether to restart the scan when the interlock is triggered."""
1713
- endpoint = f"{EndpointType.INFO.value}/actor/builtin/scan_interlock/config/trigger_setting"
1702
+ endpoint = f"{EndpointType.USER.value}/actor/builtin/scan_interlock/config/trigger_setting"
1714
1703
  return EndpointInfo(
1715
1704
  endpoint=endpoint,
1716
1705
  message_type=messages.ScanInterlockTriggerSettingMessage,
@@ -1738,18 +1727,18 @@ class MessageEndpoints:
1738
1727
  )
1739
1728
 
1740
1729
  @staticmethod
1741
- def gui_acl(gui_id: str):
1730
+ def acl_session(session_id: str):
1742
1731
  """
1743
- Endpoint for exchanging GUI ACL information. This endpoint is used by the CLI or GUI to exchange
1732
+ Endpoint for exchanging ACL session information. This endpoint is used by the CLI or GUI to exchange
1744
1733
  updates on the required ACL user. It uses a messages.CredentialsMessage message.
1745
1734
 
1746
1735
  Args:
1747
- gui_id (str): GUI ID.
1736
+ session_id (str): Session ID.
1748
1737
  Returns:
1749
- EndpointInfo: Endpoint for GUI ACL.
1738
+ EndpointInfo: Endpoint for ACL session.
1750
1739
  """
1751
1740
 
1752
- endpoint = f"{EndpointType.USER.value}/gui/acl/{gui_id}"
1741
+ endpoint = f"{EndpointType.USER.value}/acl/{session_id}"
1753
1742
  return EndpointInfo(
1754
1743
  endpoint=endpoint, message_type=messages.CredentialsMessage, message_op=MessageOp.SEND
1755
1744
  )
@@ -1812,7 +1801,7 @@ class MessageEndpoints:
1812
1801
  Returns:
1813
1802
  EndpointInfo: Endpoint for script execution info.
1814
1803
  """
1815
- endpoint = f"{EndpointType.INFO.value}/scripts/execution_info/{script_id}"
1804
+ endpoint = f"{EndpointType.USER.value}/scripts/execution_info/{script_id}"
1816
1805
  return EndpointInfo(
1817
1806
  endpoint=endpoint,
1818
1807
  message_type=messages.ScriptExecutionInfoMessage,
@@ -1828,7 +1817,7 @@ class MessageEndpoints:
1828
1817
  Returns:
1829
1818
  EndpointInfo: Endpoint for script content.
1830
1819
  """
1831
- endpoint = f"{EndpointType.INFO.value}/scripts/content/{script_id}"
1820
+ endpoint = f"{EndpointType.USER.value}/scripts/content/{script_id}"
1832
1821
  return EndpointInfo(
1833
1822
  endpoint=endpoint, message_type=messages.VariableMessage, message_op=MessageOp.KEY_VALUE
1834
1823
  )
@@ -1859,7 +1848,7 @@ class MessageEndpoints:
1859
1848
  Returns:
1860
1849
  EndpointInfo: Endpoint for beamline state.
1861
1850
  """
1862
- endpoint = f"{EndpointType.INFO.value}/beamline_state/{state_name}"
1851
+ endpoint = f"{EndpointType.USER.value}/beamline_state/{state_name}"
1863
1852
  return EndpointInfo(
1864
1853
  endpoint=endpoint,
1865
1854
  message_type=messages.BeamlineStateMessage,
@@ -1875,7 +1864,7 @@ class MessageEndpoints:
1875
1864
  Returns:
1876
1865
  EndpointInfo: Endpoint for beamline state updates.
1877
1866
  """
1878
- endpoint = f"{EndpointType.INFO.value}/available_beamline_states"
1867
+ endpoint = f"{EndpointType.USER.value}/available_beamline_states"
1879
1868
  return EndpointInfo(
1880
1869
  endpoint=endpoint,
1881
1870
  message_type=messages.AvailableBeamlineStatesMessage,
@@ -2073,7 +2062,7 @@ class MessageEndpoints:
2073
2062
  Returns:
2074
2063
  EndpointInfo: Endpoint for dynamic metric publishing.
2075
2064
  """
2076
- endpoint = f"{EndpointType.INFO.value}/dynamic_metrics/{group_name}"
2065
+ endpoint = f"{EndpointType.USER.value}/dynamic_metrics/{group_name}"
2077
2066
  return EndpointInfo(
2078
2067
  endpoint=endpoint,
2079
2068
  message_type=messages.DynamicMetricMessage,
@@ -415,6 +415,8 @@ class RequestBlock(BaseModel):
415
415
  scan_number (int | None): Scan number if applicable
416
416
  scan_id (str | None): Scan ID if applicable
417
417
  report_instructions (list[dict] | None): List of report instructions for the scan, if any
418
+ owned_device_locks (list[str]): List of device locks owned by the request
419
+ pending_device_locks (list[str]): List of device locks pending for the request
418
420
 
419
421
  """
420
422
 
@@ -427,6 +429,8 @@ class RequestBlock(BaseModel):
427
429
  scan_number: int | None
428
430
  scan_id: str | None
429
431
  report_instructions: list[dict] | None = None
432
+ owned_device_locks: list[str] = Field(default_factory=list)
433
+ pending_device_locks: list[str] = Field(default_factory=list)
430
434
 
431
435
 
432
436
  class QueueInfoEntry(BaseModel):
@@ -54,6 +54,7 @@ class QueueItem:
54
54
  self.active_request_block = active_request_block
55
55
  self.scan_ids = scan_id
56
56
  self.reason = reason
57
+ self._lock = threading.RLock()
57
58
  if client_messages is None:
58
59
  client_messages = []
59
60
  self.client_messages = client_messages
@@ -85,6 +86,44 @@ class QueueItem:
85
86
  def status(self):
86
87
  return self._status
87
88
 
89
+ @threadlocked
90
+ def _get_device_lock_state(self) -> tuple[list[str], list[str]]:
91
+ """
92
+ Return a snapshot of owned and pending device locks.
93
+
94
+ Returns:
95
+ tuple[list[str], list[str]]: A tuple containing two lists:
96
+ - The first list contains the names of devices whose locks are currently owned.
97
+ - The second list contains the names of devices whose locks are pending.
98
+ """
99
+ owned_device_locks = sorted(
100
+ {
101
+ device
102
+ for request_block in self.request_blocks
103
+ for device in request_block.owned_device_locks
104
+ }
105
+ )
106
+ pending_device_locks = sorted(
107
+ {
108
+ device
109
+ for request_block in self.request_blocks
110
+ for device in request_block.pending_device_locks
111
+ }
112
+ )
113
+ return owned_device_locks, pending_device_locks
114
+
115
+ def get_device_lock_state(self) -> tuple[list[str], list[str]]:
116
+ """
117
+ Refresh and return a consistent snapshot of device-lock state.
118
+
119
+ Returns:
120
+ tuple[list[str], list[str]]: A tuple containing two lists:
121
+ - The first list contains the names of devices whose locks are currently owned.
122
+ - The second list contains the names of devices whose locks are pending.
123
+ """
124
+ self._update_with_buffer()
125
+ return self._get_device_lock_state()
126
+
88
127
  def _update_with_buffer(self):
89
128
  current_queue = self.scan_manager.queue_storage.current_scan_queue
90
129
  if not current_queue:
@@ -103,6 +142,7 @@ class QueueItem:
103
142
  self.update_queue_item(queue_item.info)
104
143
  return
105
144
 
145
+ @threadlocked
106
146
  def update_queue_item(self, queue_item: messages.QueueInfoEntry):
107
147
  """update the queue item"""
108
148
  self.request_blocks = queue_item.request_blocks
@@ -13,7 +13,7 @@ from bec_lib.scan_args import ScanArgument
13
13
  from bec_lib.signature_serializer import deserialize_dtype, dict_to_signature
14
14
 
15
15
  # Internal scan kwargs that are automatically added to the scan request
16
- INTERNAL_SCAN_KWARGS = {"user_metadata", "system_config", "scan_queue"}
16
+ INTERNAL_SCAN_KWARGS = {"user_metadata", "system_config", "scan_queue", "monitored", "on_request"}
17
17
 
18
18
 
19
19
  class ScanInputValidator:
@@ -58,6 +58,7 @@ class ScanObject:
58
58
  hide_report: bool = False,
59
59
  metadata: dict | None = None,
60
60
  monitored: list[str | DeviceBase] | None = None,
61
+ on_request: list[str | DeviceBase] | None = None,
61
62
  file_suffix: str | None = None,
62
63
  file_directory: str | None = None,
63
64
  scan_queue: str | None = None,
@@ -72,7 +73,8 @@ class ScanObject:
72
73
  async_callback: Asynchronous callback function
73
74
  hide_report: Hide the report
74
75
  metadata: Metadata dictionary
75
- monitored: List of monitored devices
76
+ monitored: List of devices to be put on the readout priority `monitored`
77
+ on_request: List of devices to be put on the readout priority `on_request`
76
78
  file_suffix: File suffix for the scan data
77
79
  file_directory: File directory for the scan data
78
80
  scan_queue: Scan queue name. If None, the default queue will be used.
@@ -110,13 +112,25 @@ class ScanObject:
110
112
  monitored = [monitored]
111
113
  for mon_device in monitored:
112
114
  if isinstance(mon_device, str):
113
- mon_device = self.client.device_manager.devices.get(mon_device)
114
- if not mon_device:
115
+ mon_device_obj = self.client.device_manager.devices.get(mon_device)
116
+ if not mon_device_obj:
115
117
  raise RuntimeError(
116
118
  f"Specified monitored device {mon_device} does not exist in the current device configuration."
117
119
  )
118
120
  kwargs["monitored"] = monitored
119
121
 
122
+ if on_request is not None:
123
+ if not isinstance(on_request, list):
124
+ on_request = [on_request]
125
+ for or_device in on_request:
126
+ if isinstance(or_device, str):
127
+ or_device_obj = self.client.device_manager.devices.get(or_device)
128
+ if not or_device_obj:
129
+ raise RuntimeError(
130
+ f"Specified on_request device {or_device} does not exist in the current device configuration."
131
+ )
132
+ kwargs["on_request"] = on_request
133
+
120
134
  sys_config = sys_config.model_dump()
121
135
  # pylint: disable=protected-access
122
136
  if scans._scan_group:
@@ -83,6 +83,7 @@ class ACLConfig(BaseModel):
83
83
 
84
84
  env_file: str = Field(default_factory=lambda: os.path.join(DEFAULT_BASE_PATH, ".bec_acl.env"))
85
85
  user: str | None = None
86
+ password: str | None = None
86
87
 
87
88
 
88
89
  class ProcedureConfig(BaseModel):
@@ -1,7 +1,7 @@
1
1
  import sys
2
2
  from typing import Literal
3
3
 
4
- from bec_lib.endpoints import MessageEndpoints
4
+ from bec_lib.endpoints import EndpointType, MessageEndpoints
5
5
  from bec_lib.redis_connector import RedisConnector
6
6
 
7
7
 
@@ -12,12 +12,20 @@ class BECAccessDemo: # pragma: no cover
12
12
  self.connector = connector
13
13
  else:
14
14
  self.connector = RedisConnector("localhost:6379")
15
- self.connector.authenticate(**self._find_admin_account())
15
+ admin_account = self._find_admin_account()
16
+ if admin_account:
17
+ self.connector.authenticate(**admin_account)
16
18
  self.username = "user"
17
19
  self.admin_username = "admin"
18
20
  self.deployment_id = "test_deployment"
19
21
 
20
- def _find_admin_account(self) -> dict[str, str]:
22
+ def _find_admin_account(self) -> dict[str, str] | None:
23
+ try:
24
+ self.connector.acl_list()
25
+ return None
26
+ except Exception:
27
+ pass
28
+
21
29
  for user, token in [("default", "null"), ("admin", "admin")]:
22
30
  try:
23
31
  self.connector.authenticate(username=user, password=token)
@@ -149,6 +157,30 @@ class BECAccessDemo: # pragma: no cover
149
157
  reset_keys=True,
150
158
  )
151
159
 
160
+ def set_default_non_admin(self):
161
+ """Grant the default user access to all non-admin endpoint namespaces."""
162
+ read_write_namespaces = [
163
+ EndpointType.PUBLIC.value,
164
+ EndpointType.PERSONAL.value,
165
+ EndpointType.USER.value,
166
+ ]
167
+ read_only_namespaces = [EndpointType.INFO.value]
168
+ self.connector._managed_connection._redis_conn.acl_setuser(
169
+ "default",
170
+ enabled=True,
171
+ nopass=True,
172
+ categories=["+@all", "-@dangerous"],
173
+ keys=[f"%RW~{namespace}/*" for namespace in read_write_namespaces]
174
+ + [f"%R~{namespace}/*" for namespace in read_only_namespaces],
175
+ channels=[
176
+ f"{namespace}/*" for namespace in read_write_namespaces + read_only_namespaces
177
+ ]
178
+ + [MessageEndpoints.public_file("*", "*").endpoint],
179
+ commands=["+keys"],
180
+ reset_channels=True,
181
+ reset_keys=True,
182
+ )
183
+
152
184
 
153
185
  def _main(
154
186
  mode: str, connector: RedisConnector | None = None, shutdown: bool = True
@@ -167,6 +199,14 @@ def _main(
167
199
  print(
168
200
  "Enabled mode admin. Please make sure to place the .bec_acl.env file in the root directory and restart the bec server."
169
201
  )
202
+ case "non_admin":
203
+ demo.reset()
204
+ demo.add_user()
205
+ demo.add_admin()
206
+ demo.set_default_non_admin()
207
+ print(
208
+ "Enabled mode non_admin. The default user can access all non-admin namespaces. Please restart the bec server."
209
+ )
170
210
  case _:
171
211
  raise ValueError(f"Invalid mode: {mode}")
172
212
 
@@ -185,6 +225,6 @@ if __name__ == "__main__": # pragma: no cover
185
225
  args = parser.parse_args()
186
226
 
187
227
  if args.mode is None:
188
- args.mode = "default"
228
+ args.mode = "non_admin"
189
229
 
190
230
  _main("default" if args.reset else args.mode)