bec-server 4.1.7__tar.gz → 4.2.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 (207) hide show
  1. {bec_server-4.1.7 → bec_server-4.2.0}/PKG-INFO +2 -2
  2. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/device_server/device_server.py +64 -10
  3. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/direct_scan_worker.py +12 -0
  4. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/scan_actions.py +20 -1
  5. {bec_server-4.1.7 → bec_server-4.2.0}/pyproject.toml +2 -2
  6. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_device_server/test_device_server.py +187 -2
  7. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_scan_actions.py +50 -0
  8. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_direct_scan_worker.py +17 -0
  9. {bec_server-4.1.7 → bec_server-4.2.0}/.gitignore +0 -0
  10. {bec_server-4.1.7 → bec_server-4.2.0}/.zed/debug.json +0 -0
  11. {bec_server-4.1.7 → bec_server-4.2.0}/README.md +0 -0
  12. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/__init__.py +0 -0
  13. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/actors/__init__.py +0 -0
  14. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/actors/actor.py +0 -0
  15. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/actors/builtin_actor_manager.py +0 -0
  16. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/actors/manager.py +0 -0
  17. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/actors/scan_interlock.py +0 -0
  18. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/actors/worker.py +0 -0
  19. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/bec_server_utils/__init__.py +0 -0
  20. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/bec_server_utils/launch.py +0 -0
  21. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/bec_server_utils/service_handler.py +0 -0
  22. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/bec_server_utils/subprocess_launch.py +0 -0
  23. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/bec_server_utils/tmux_launch.py +0 -0
  24. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/data_processing/__init__.py +0 -0
  25. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/data_processing/cli/__init__.py +0 -0
  26. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/data_processing/cli/launch.py +0 -0
  27. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/data_processing/dap_server.py +0 -0
  28. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/data_processing/dap_service.py +0 -0
  29. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/data_processing/dap_service_manager.py +0 -0
  30. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/data_processing/lmfit1d_service.py +0 -0
  31. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/device_server/__init__.py +0 -0
  32. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/device_server/bec_message_handler.py +0 -0
  33. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/device_server/cli/__init__.py +0 -0
  34. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/device_server/cli/launch.py +0 -0
  35. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/device_server/devices/__init__.py +0 -0
  36. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/device_server/devices/config_update_handler.py +0 -0
  37. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/device_server/devices/device_serializer.py +0 -0
  38. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/device_server/devices/devicemanager.py +0 -0
  39. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/device_server/friendly_device_exceptions.py +0 -0
  40. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/device_server/ophyd_callback_monitor.py +0 -0
  41. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/device_server/rpc_handler.py +0 -0
  42. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/device_server/tests/__init__.py +0 -0
  43. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/device_server/tests/utils.py +0 -0
  44. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/file_writer/__init__.py +0 -0
  45. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/file_writer/async_writer.py +0 -0
  46. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/file_writer/cli/__init__.py +0 -0
  47. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/file_writer/cli/launch.py +0 -0
  48. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/file_writer/default_writer.py +0 -0
  49. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/file_writer/file_writer.py +0 -0
  50. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/file_writer/file_writer_manager.py +0 -0
  51. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/file_writer/merged_dicts.py +0 -0
  52. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/procedures/Containerfile.bec_linux_base +0 -0
  53. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/procedures/Containerfile.requirements +0 -0
  54. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/procedures/Containerfile.worker +0 -0
  55. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/procedures/__init__.py +0 -0
  56. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/procedures/_dev_runner.py +0 -0
  57. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/procedures/builtin_procedures.py +0 -0
  58. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/procedures/constants.py +0 -0
  59. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/procedures/container_utils.py +0 -0
  60. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/procedures/container_worker.py +0 -0
  61. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/procedures/manager.py +0 -0
  62. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/procedures/oop_worker_base.py +0 -0
  63. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/procedures/procedure_registry.py +0 -0
  64. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/procedures/protocol.py +0 -0
  65. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/procedures/subprocess_worker.py +0 -0
  66. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/procedures/worker_base.py +0 -0
  67. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_bundler/__init__.py +0 -0
  68. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_bundler/bec_emitter.py +0 -0
  69. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_bundler/cli/__init__.py +0 -0
  70. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_bundler/cli/launch.py +0 -0
  71. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_bundler/emitter.py +0 -0
  72. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_bundler/scan_bundler.py +0 -0
  73. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/__init__.py +0 -0
  74. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/beamline_state_manager.py +0 -0
  75. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/cli/__init__.py +0 -0
  76. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/cli/launch.py +0 -0
  77. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/device_lock_registry.py +0 -0
  78. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/errors.py +0 -0
  79. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/instruction_handler.py +0 -0
  80. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scan_assembler.py +0 -0
  81. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scan_guard.py +0 -0
  82. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scan_manager.py +0 -0
  83. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scan_queue.py +0 -0
  84. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scan_server.py +0 -0
  85. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scan_worker.py +0 -0
  86. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/__init__.py +0 -0
  87. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/acquire.py +0 -0
  88. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/cont_line_scan.py +0 -0
  89. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/device_rpc.py +0 -0
  90. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/fermat_scan.py +0 -0
  91. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/grid_scan.py +0 -0
  92. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/hexagonal_scan.py +0 -0
  93. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/line_scan.py +0 -0
  94. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/line_sweep_scan.py +0 -0
  95. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/list_scan.py +0 -0
  96. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/log_scan.py +0 -0
  97. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/move.py +0 -0
  98. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/multi_region_grid_scan.py +0 -0
  99. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/multi_region_line_scan.py +0 -0
  100. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/path_optimization.py +0 -0
  101. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/position_generators.py +0 -0
  102. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/round_roi_scan.py +0 -0
  103. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/round_scan.py +0 -0
  104. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/scan_argument_modifier.py +0 -0
  105. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/scan_base.py +0 -0
  106. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/scan_components.py +0 -0
  107. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/scan_modifier.py +0 -0
  108. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/scan_status.py +0 -0
  109. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/time_scan.py +0 -0
  110. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/scans/updated_move.py +0 -0
  111. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/tests/__init__.py +0 -0
  112. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/tests/fixtures.py +0 -0
  113. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/tests/scan_fixtures.py +0 -0
  114. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/tests/scan_hook_tests.py +0 -0
  115. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scan_server/tests/utils.py +0 -0
  116. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scihub/__init__.py +0 -0
  117. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scihub/atlas/__init__.py +0 -0
  118. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scihub/atlas/atlas_active_client_emitter.py +0 -0
  119. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scihub/atlas/atlas_connector.py +0 -0
  120. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scihub/atlas/atlas_forwarder.py +0 -0
  121. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scihub/atlas/atlas_metadata_handler.py +0 -0
  122. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scihub/atlas/atlas_plugin_repo_emitter.py +0 -0
  123. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scihub/atlas/config_handler.py +0 -0
  124. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scihub/cli/__init__.py +0 -0
  125. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scihub/cli/launch.py +0 -0
  126. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scihub/scihub.py +0 -0
  127. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scihub/service_handler/__init__.py +0 -0
  128. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/scihub/service_handler/service_handler.py +0 -0
  129. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/test/__init__.py +0 -0
  130. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/test/actor_test_utils.py +0 -0
  131. {bec_server-4.1.7 → bec_server-4.2.0}/bec_server/test/helpers.py +0 -0
  132. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_actors/test_actors.py +0 -0
  133. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_bec_server_utils/test_main.py +0 -0
  134. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_bec_server_utils/test_service_handler.py +0 -0
  135. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_bec_server_utils/test_subprocess_launch.py +0 -0
  136. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_bec_server_utils/test_tmux_launch.py +0 -0
  137. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_data_processing/conftest.py +0 -0
  138. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_data_processing/test_dap_cli_launch.py +0 -0
  139. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_data_processing/test_dap_server.py +0 -0
  140. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_data_processing/test_dap_service_manager.py +0 -0
  141. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_data_processing/test_lmfit1d_service.py +0 -0
  142. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_device_server/conftest.py +0 -0
  143. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_device_server/test_config_handler.py +0 -0
  144. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_device_server/test_device_instructions.py +0 -0
  145. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_device_server/test_device_manager_ds.py +0 -0
  146. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_device_server/test_device_serializer.py +0 -0
  147. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_device_server/test_device_server_cli_launch.py +0 -0
  148. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_device_server/test_ophyd_callback_monitor.py +0 -0
  149. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_device_server/test_rpc_handler.py +0 -0
  150. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_file_writer/conftest.py +0 -0
  151. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_file_writer/test_async_file_writer.py +0 -0
  152. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_file_writer/test_file_writer.py +0 -0
  153. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_file_writer/test_file_writer_cli_launch.py +0 -0
  154. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_file_writer/test_file_writer_manager.py +0 -0
  155. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_bundler/conftest.py +0 -0
  156. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_bundler/test_bec_emitter.py +0 -0
  157. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_bundler/test_scan_bundler.py +0 -0
  158. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_bundler/test_scan_bundler_cli_launch.py +0 -0
  159. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/conftest.py +0 -0
  160. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/__init__.py +0 -0
  161. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_acquire.py +0 -0
  162. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_cont_line_scan.py +0 -0
  163. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_device_rpc_scan.py +0 -0
  164. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_fermat_scan.py +0 -0
  165. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_grid_scan.py +0 -0
  166. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_hexagonal_scan.py +0 -0
  167. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_line_scan.py +0 -0
  168. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_line_sweep_scan.py +0 -0
  169. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_list_scan.py +0 -0
  170. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_log_scan.py +0 -0
  171. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_move_scan.py +0 -0
  172. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_multi_region_grid_scan.py +0 -0
  173. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_multi_region_line_scan.py +0 -0
  174. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_position_generators.py +0 -0
  175. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_relative_scan_return_to_start.py +0 -0
  176. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_round_roi_scan.py +0 -0
  177. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_round_scan.py +0 -0
  178. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_scan_components.py +0 -0
  179. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_scan_status.py +0 -0
  180. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_time_scan.py +0 -0
  181. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/scans/test_updated_move_scan.py +0 -0
  182. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_beamline_state_manager.py +0 -0
  183. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_builtin_actor_manager.py +0 -0
  184. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_container_utils.py +0 -0
  185. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_device_lock_registry.py +0 -0
  186. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_instruction_handler.py +0 -0
  187. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_path_optimization.py +0 -0
  188. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_procedure_container_worker.py +0 -0
  189. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_procedures.py +0 -0
  190. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_scan_argument_modifier.py +0 -0
  191. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_scan_assembler.py +0 -0
  192. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_scan_fixture_imports.py +0 -0
  193. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_scan_guard.py +0 -0
  194. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_scan_interlock.py +0 -0
  195. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_scan_modifier.py +0 -0
  196. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_scan_server_cli_launch.py +0 -0
  197. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_scan_server_queue.py +0 -0
  198. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_scan_server_scan_manager.py +0 -0
  199. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scan_server/test_scan_worker.py +0 -0
  200. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scihub/conftest.py +0 -0
  201. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scihub/test_atlas_active_client_emitter.py +0 -0
  202. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scihub/test_atlas_config_handler.py +0 -0
  203. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scihub/test_atlas_connector.py +0 -0
  204. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scihub/test_atlas_forwarder.py +0 -0
  205. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scihub/test_atlas_metadata_handler.py +0 -0
  206. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scihub/test_atlas_plugin_repo_emitter.py +0 -0
  207. {bec_server-4.1.7 → bec_server-4.2.0}/tests/tests_scihub/test_scihub_cli_launch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: bec-server
3
- Version: 4.1.7
3
+ Version: 4.2.0
4
4
  Summary: BEC server
5
5
  Project-URL: Bug Tracker, https://github.com/bec-project/bec/issues
6
6
  Project-URL: Homepage, https://github.com/bec-project/bec
@@ -14,7 +14,7 @@ Requires-Dist: libtmux~=0.37
14
14
  Requires-Dist: lmfit>=1.3.3,~=1.3
15
15
  Requires-Dist: msgpack~=1.0
16
16
  Requires-Dist: numpy<3.0,>=1.24
17
- Requires-Dist: ophyd-devices>=1.47.1
17
+ Requires-Dist: ophyd-devices>=1.51.0
18
18
  Requires-Dist: ophyd~=1.9
19
19
  Requires-Dist: podman>=5.4
20
20
  Requires-Dist: psutil
@@ -10,7 +10,7 @@ from concurrent.futures import ThreadPoolExecutor
10
10
  from typing import TYPE_CHECKING, Any
11
11
 
12
12
  import ophyd
13
- from ophyd import DeviceStatus, Kind, OphydObject, Staged, StatusBase
13
+ from ophyd import Device, DeviceStatus, Kind, OphydObject, Staged, StatusBase
14
14
  from ophyd.utils import errors as ophyd_errors
15
15
 
16
16
  from bec_lib import messages
@@ -23,7 +23,7 @@ from bec_lib.logger import bec_logger
23
23
  from bec_lib.messages import BECStatus
24
24
  from bec_lib.serialization import json_ext
25
25
  from bec_lib.utils.rpc_utils import rgetattr
26
- from bec_server.device_server.devices.devicemanager import DeviceManagerDS
26
+ from bec_server.device_server.devices.devicemanager import BECMessageSignal, DeviceManagerDS
27
27
  from bec_server.device_server.friendly_device_exceptions import reformat_known_device_exceptions
28
28
  from bec_server.device_server.ophyd_callback_monitor import OphydCallbackMonitor
29
29
  from bec_server.device_server.rpc_handler import RPCHandler
@@ -566,11 +566,14 @@ class DeviceServer(BECService):
566
566
 
567
567
  """
568
568
  action = None
569
+ instructions = msg
569
570
  try:
570
- instructions = msg
571
+ action = instructions.content["action"]
572
+ if action == "broadcast_scan_device_info" and not instructions.content["device"]:
573
+ self._broadcast_scan_device_info(instructions)
574
+ return
571
575
  if not instructions.content["device"]:
572
576
  return
573
- action = instructions.content["action"]
574
577
  self.assert_device_is_valid(instructions)
575
578
  if action != "rpc":
576
579
  # rpc has its own error handling
@@ -595,6 +598,8 @@ class DeviceServer(BECService):
595
598
  self._unstage_device(instructions)
596
599
  elif action == "pre_scan":
597
600
  self._pre_scan(instructions)
601
+ elif action == "broadcast_scan_device_info":
602
+ self._broadcast_scan_device_info(instructions)
598
603
  else:
599
604
  logger.warning(f"Received unknown device instruction: {instructions}")
600
605
  except ophyd_errors.LimitError as limit_error:
@@ -606,10 +611,10 @@ class DeviceServer(BECService):
606
611
  exception_type=limit_error.__class__.__name__,
607
612
  device=self.get_device_from_exception(limit_error),
608
613
  )
614
+
609
615
  self._ensure_request_registered(instructions)
610
- self.requests_handler.set_finished(
611
- instructions.metadata["device_instr_id"], success=False, error_info=error_info
612
- )
616
+ if diid := instructions.metadata.get("device_instr_id"):
617
+ self.requests_handler.set_finished(diid, success=False, error_info=error_info)
613
618
 
614
619
  logger.error(content)
615
620
  except Exception as exc: # pylint: disable=broad-except
@@ -631,9 +636,9 @@ class DeviceServer(BECService):
631
636
  self.rpc_handler.send_rpc_exception(exc, instructions)
632
637
  else:
633
638
  logger.error(content)
634
- self.requests_handler.set_finished(
635
- instructions.metadata["device_instr_id"], success=False, error_info=error_info
636
- )
639
+
640
+ if diid := instructions.metadata.get("device_instr_id"):
641
+ self.requests_handler.set_finished(diid, success=False, error_info=error_info)
637
642
 
638
643
  @staticmethod
639
644
  def get_device_from_exception(exc: Exception) -> str | None:
@@ -1123,3 +1128,52 @@ class DeviceServer(BECService):
1123
1128
  self.requests_handler.add_status_object(instr.metadata["device_instr_id"], status)
1124
1129
 
1125
1130
  self.requests_handler.patch_num_status_objects(instr, num_status_objects)
1131
+
1132
+ def _broadcast_scan_device_info(self, instr: messages.DeviceInstructionMessage) -> None:
1133
+ """
1134
+ Broadcast a snapshot of device runtime information for the scan.
1135
+
1136
+ Args:
1137
+ instr (messages.DeviceInstructionMessage): The request containing the devices
1138
+ and scan ID.
1139
+ """
1140
+ instr_id = instr.metadata["device_instr_id"]
1141
+ self.requests_handler.add_request(instr, num_status_objects=0)
1142
+
1143
+ devices = instr.content["device"]
1144
+ if not isinstance(devices, list):
1145
+ devices = [devices]
1146
+
1147
+ device_info: dict[str, messages.DeviceRuntimeInfo] = {}
1148
+ scan_id = instr.parameter.get("scan_id")
1149
+ if scan_id is None:
1150
+ self.requests_handler.set_finished(instr_id, success=True)
1151
+ return
1152
+
1153
+ for dev in devices:
1154
+ if dev not in self.device_manager.devices:
1155
+ logger.warning(f"Device {dev} not found in device manager. Skipping.")
1156
+ continue
1157
+ obj = self.device_manager.devices.get(dev).obj
1158
+
1159
+ signal_info: dict[str, messages.SignalInfo] = {}
1160
+ if isinstance(obj, Device):
1161
+ for _, sub_name, item in obj.walk_signals():
1162
+ if not isinstance(item, BECMessageSignal):
1163
+ continue
1164
+ signal_info[sub_name] = item.signal_info
1165
+
1166
+ if not signal_info:
1167
+ continue
1168
+ device_info[dev] = messages.DeviceRuntimeInfo(signal_info=signal_info)
1169
+
1170
+ self.connector.xadd(
1171
+ MessageEndpoints.scan_device_info(),
1172
+ {
1173
+ "data": messages.ScanDeviceInfoMessage(
1174
+ devices=device_info, scan_id=scan_id, metadata=instr.metadata
1175
+ )
1176
+ },
1177
+ max_size=10,
1178
+ )
1179
+ self.requests_handler.set_finished(instr_id, success=True)
@@ -87,6 +87,7 @@ class DirectScanWorker:
87
87
  raise ScanAbortion(f"Scan is missing required method: {step}")
88
88
  self.check_for_interruption()
89
89
  method()
90
+ self.run_post_scan_step(step, scan)
90
91
  except ScanAbortion as exc:
91
92
  if not self._prepare_exception_cleanup(queue, exc):
92
93
  return
@@ -272,3 +273,14 @@ class DirectScanWorker:
272
273
  self.worker.parent.queue_manager.queues[self.worker.queue_name].abort()
273
274
  self.reset()
274
275
  self.worker.status = InstructionQueueStatus.RUNNING
276
+
277
+ def run_post_scan_step(self, step: str, scan: ScanBase) -> None:
278
+ """
279
+ Run the post-scan step for the given scan step.
280
+
281
+ Args:
282
+ step (str): The scan step to run the post-scan method for.
283
+ scan (ScanBase): The scan object to run the post-scan method on.
284
+ """
285
+ if step == "stage":
286
+ scan.actions._broadcast_scan_device_info()
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import functools
4
4
  import os
5
- import time
6
5
  import uuid
7
6
  from collections.abc import Iterable
8
7
  from string import Template
@@ -1529,3 +1528,23 @@ class ScanActions:
1529
1528
  raise ValueError(
1530
1529
  f"Invalid template variable: {exc} in the file base path. Please check your service config."
1531
1530
  ) from exc
1531
+
1532
+ def _broadcast_scan_device_info(self) -> None:
1533
+ """
1534
+ Emit a device instruction to capture runtime information for this scan.
1535
+ All currently locked devices will be included in this update.
1536
+
1537
+ Note that this is an internal method and should not be called directly by scan implementations.
1538
+ """
1539
+ devices = self.get_owned_device_locks()
1540
+ if not self._scan.is_scan or self._scan.scan_info.scan_id is None:
1541
+ return
1542
+ status = self._create_status(name="broadcast_scan_device_info")
1543
+ instr = messages.DeviceInstructionMessage(
1544
+ device=devices,
1545
+ action="broadcast_scan_device_info",
1546
+ parameter={"scan_id": self._scan.scan_info.scan_id},
1547
+ metadata={"device_instr_id": status._device_instr_id},
1548
+ )
1549
+ self._send(instr)
1550
+ status.wait()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bec-server"
7
- version = "4.1.7"
7
+ version = "4.2.0"
8
8
  description = "BEC server"
9
9
  requires-python = ">=3.11"
10
10
  classifiers = [
@@ -20,7 +20,7 @@ dependencies = [
20
20
  "msgpack~=1.0",
21
21
  "numpy>=1.24, <3.0",
22
22
  "ophyd~=1.9",
23
- "ophyd_devices>=1.47.1",
23
+ "ophyd_devices>=1.51.0",
24
24
  "podman>=5.4",
25
25
  "psutil",
26
26
  "pydantic~=2.8",
@@ -7,9 +7,10 @@ from unittest.mock import ANY, patch
7
7
  import numpy as np
8
8
  import pytest
9
9
  from loguru import logger
10
- from ophyd import Device, DeviceStatus, Kind, Staged
10
+ from ophyd import Component as Cpt
11
+ from ophyd import Device, DeviceStatus, Kind, Signal, Staged
11
12
  from ophyd.utils import errors as ophyd_errors
12
- from ophyd_devices import StatusBase
13
+ from ophyd_devices import PreviewSignal, StatusBase
13
14
 
14
15
  from bec_lib import messages
15
16
  from bec_lib.alarm_handler import Alarms
@@ -1026,6 +1027,190 @@ def test_handle_device_instructions_rpc(device_server_mock, instructions):
1026
1027
  update_device_metadata_mock.assert_called_once_with(instructions)
1027
1028
 
1028
1029
 
1030
+ @pytest.mark.parametrize("device", ["samx", []])
1031
+ def test_handle_device_instructions_broadcast_scan_device_info(device_server_mock, device):
1032
+ instructions = messages.DeviceInstructionMessage(
1033
+ device=device,
1034
+ action="broadcast_scan_device_info",
1035
+ parameter={"scan_id": "scan-id-1"},
1036
+ metadata={"RID": "rid-1", "device_instr_id": "broadcast-diid"},
1037
+ )
1038
+
1039
+ with mock.patch.object(
1040
+ device_server_mock,
1041
+ "assert_device_is_valid",
1042
+ wraps=device_server_mock.assert_device_is_valid,
1043
+ ) as valid_mock:
1044
+ with mock.patch.object(
1045
+ device_server_mock,
1046
+ "assert_device_is_enabled",
1047
+ wraps=device_server_mock.assert_device_is_enabled,
1048
+ ) as enabled_mock:
1049
+ with mock.patch.object(
1050
+ device_server_mock, "_broadcast_scan_device_info"
1051
+ ) as broadcast_mock:
1052
+ device_server_mock.handle_device_instructions(instructions)
1053
+
1054
+ broadcast_mock.assert_called_once_with(instructions)
1055
+ if device:
1056
+ valid_mock.assert_called_once_with(instructions)
1057
+ enabled_mock.assert_called_once_with(instructions)
1058
+ else:
1059
+ valid_mock.assert_not_called()
1060
+ enabled_mock.assert_not_called()
1061
+
1062
+
1063
+ def test_broadcast_scan_device_info_publishes_device_records(device_server_mock):
1064
+ class Detector(Device):
1065
+ preview = Cpt(PreviewSignal, ndim=2)
1066
+ plain = Cpt(Signal, value=0)
1067
+
1068
+ detector = Detector(name="eiger")
1069
+ detector.preview.signal_info.acquisition_group = "fly-scan"
1070
+ detector.preview.signal_info.use_alias = True
1071
+ detector.preview.signal_info.signal_metadata["processing"] = {"rotation": 90}
1072
+ expected_info = detector.preview.signal_info
1073
+
1074
+ device_server_mock.device_manager.devices = {
1075
+ "eiger": SimpleNamespace(obj=detector),
1076
+ "samx": SimpleNamespace(obj=Signal(name="samx", value=0)),
1077
+ }
1078
+ device_server_mock.connector.xadd = mock.MagicMock()
1079
+ instructions = messages.DeviceInstructionMessage(
1080
+ device=["eiger", "samx"],
1081
+ action="broadcast_scan_device_info",
1082
+ parameter={"scan_id": "scan-id-1"},
1083
+ metadata={"RID": "rid-1", "device_instr_id": "broadcast-diid"},
1084
+ )
1085
+
1086
+ with mock.patch.object(device_server_mock.connector, "send") as send_mock:
1087
+ device_server_mock._broadcast_scan_device_info(instructions)
1088
+
1089
+ device_server_mock.connector.xadd.assert_called_once()
1090
+ endpoint, payload = device_server_mock.connector.xadd.call_args.args
1091
+ msg = payload["data"]
1092
+ assert endpoint == MessageEndpoints.scan_device_info()
1093
+ assert device_server_mock.connector.xadd.call_args.kwargs == {"max_size": 10}
1094
+ assert msg == messages.ScanDeviceInfoMessage(
1095
+ scan_id="scan-id-1",
1096
+ devices={"eiger": messages.DeviceRuntimeInfo(signal_info={"preview": expected_info})},
1097
+ metadata={"RID": "rid-1", "device_instr_id": "broadcast-diid"},
1098
+ )
1099
+ responses = [call.args[1] for call in send_mock.call_args_list]
1100
+ assert [response.status for response in responses] == ["running", "completed"]
1101
+ assert all(response.instruction_id == "broadcast-diid" for response in responses)
1102
+ assert all(response.instruction == instructions for response in responses)
1103
+ assert device_server_mock.requests_handler.get_request("broadcast-diid") is None
1104
+
1105
+
1106
+ def test_broadcast_scan_device_info_uses_runtime_metadata(device_server_mock):
1107
+ class SubDetector(Device):
1108
+ preview = Cpt(PreviewSignal, ndim=2)
1109
+
1110
+ class Detector(Device):
1111
+ sub = Cpt(SubDetector)
1112
+ plain = Cpt(Signal, value=0)
1113
+
1114
+ detector = Detector(name="eiger")
1115
+ signal = detector.sub.preview
1116
+ runtime_info = messages.SignalInfo(
1117
+ saved=False,
1118
+ ndim=2,
1119
+ role="preview",
1120
+ enabled=False,
1121
+ use_alias=True,
1122
+ signals=[("image", Kind.hinted.value)],
1123
+ signal_metadata={"units": "counts", "processing": {"rotation": 90}},
1124
+ correlation_group="fly-scan",
1125
+ )
1126
+ signal.signal_info = runtime_info
1127
+ signal.describe = mock.Mock(side_effect=AssertionError("Use the live signal_info model"))
1128
+
1129
+ device_server_mock.device_manager.devices = {"eiger": SimpleNamespace(obj=detector)}
1130
+ device_server_mock.connector.xadd = mock.MagicMock()
1131
+ instructions = messages.DeviceInstructionMessage(
1132
+ device="eiger",
1133
+ action="broadcast_scan_device_info",
1134
+ parameter={"scan_id": "scan-id-1"},
1135
+ metadata={"device_instr_id": "broadcast-diid"},
1136
+ )
1137
+
1138
+ device_server_mock._broadcast_scan_device_info(instructions)
1139
+
1140
+ msg = device_server_mock.connector.xadd.call_args.args[1]["data"]
1141
+ snapshot = msg.devices["eiger"].signal_info["sub.preview"]
1142
+ assert snapshot == runtime_info
1143
+ assert snapshot is runtime_info
1144
+
1145
+
1146
+ @pytest.mark.parametrize("devices", [[], ["samx"], ["samx", "empty"]])
1147
+ def test_broadcast_scan_device_info_omits_empty_devices(device_server_mock, devices):
1148
+ class EmptyDetector(Device):
1149
+ plain = Cpt(Signal, value=0)
1150
+
1151
+ device_server_mock.device_manager.devices = {
1152
+ "samx": SimpleNamespace(obj=Signal(name="samx", value=0), enabled=True),
1153
+ "empty": SimpleNamespace(obj=EmptyDetector(name="empty"), enabled=True),
1154
+ }
1155
+ device_server_mock.connector.xadd = mock.MagicMock()
1156
+ instructions = messages.DeviceInstructionMessage(
1157
+ device=devices,
1158
+ action="broadcast_scan_device_info",
1159
+ parameter={"scan_id": "scan-id-1"},
1160
+ metadata={"RID": "rid-1", "device_instr_id": "broadcast-diid"},
1161
+ )
1162
+
1163
+ with mock.patch.object(device_server_mock.connector, "send") as send_mock:
1164
+ device_server_mock.handle_device_instructions(instructions)
1165
+
1166
+ device_server_mock.connector.xadd.assert_called_once()
1167
+ endpoint, payload = device_server_mock.connector.xadd.call_args.args
1168
+ assert endpoint == MessageEndpoints.scan_device_info()
1169
+ assert payload["data"] == messages.ScanDeviceInfoMessage(
1170
+ scan_id="scan-id-1", devices={}, metadata=instructions.metadata
1171
+ )
1172
+ assert [call.args[1].status for call in send_mock.call_args_list] == ["running", "completed"]
1173
+ assert device_server_mock.requests_handler.get_request("broadcast-diid") is None
1174
+
1175
+
1176
+ def test_broadcast_scan_device_info_resolves_failure_when_publish_raises(device_server_mock):
1177
+ instructions = messages.DeviceInstructionMessage(
1178
+ device=[],
1179
+ action="broadcast_scan_device_info",
1180
+ parameter={"scan_id": "scan-id-1"},
1181
+ metadata={"RID": "rid-1", "device_instr_id": "broadcast-diid"},
1182
+ )
1183
+ device_server_mock.connector.xadd = mock.MagicMock(side_effect=RuntimeError("publish failed"))
1184
+
1185
+ with mock.patch.object(device_server_mock.connector, "send") as send_mock:
1186
+ device_server_mock.handle_device_instructions(instructions)
1187
+
1188
+ responses = [call.args[1] for call in send_mock.call_args_list]
1189
+ assert [response.status for response in responses] == ["running", "error"]
1190
+ assert responses[-1].error_info.exception_type == "RuntimeError"
1191
+ assert device_server_mock.requests_handler.get_request("broadcast-diid") is None
1192
+
1193
+
1194
+ def test_broadcast_scan_device_info_skips_without_scan_id(device_server_mock):
1195
+ device_server_mock.connector.xadd = mock.MagicMock()
1196
+ instructions = messages.DeviceInstructionMessage(
1197
+ device="samx",
1198
+ action="broadcast_scan_device_info",
1199
+ parameter={},
1200
+ metadata={"RID": "rid-1", "device_instr_id": "broadcast-diid"},
1201
+ )
1202
+
1203
+ with mock.patch.object(device_server_mock.connector, "send") as send_mock:
1204
+ device_server_mock._broadcast_scan_device_info(instructions)
1205
+
1206
+ device_server_mock.connector.xadd.assert_not_called()
1207
+ responses = [call.args[1] for call in send_mock.call_args_list]
1208
+ assert [response.status for response in responses] == ["running", "completed"]
1209
+ assert all(response.instruction_id == "broadcast-diid" for response in responses)
1210
+ assert all(response.instruction == instructions for response in responses)
1211
+ assert device_server_mock.requests_handler.get_request("broadcast-diid") is None
1212
+
1213
+
1029
1214
  @pytest.mark.parametrize(
1030
1215
  "instructions",
1031
1216
  [
@@ -442,6 +442,56 @@ def test_device_instruction_actions_emit_expected_messages(action_context):
442
442
  assert unstage_msg.metadata["device_instr_id"] == unstage_status._device_instr_id
443
443
 
444
444
 
445
+ def test_broadcast_scan_device_info_emits_owned_devices(action_context):
446
+ ctx = action_context()
447
+ ctx.scan.scan_info.metadata["RID"] = "rid-123"
448
+ ctx.device_manager.parent.device_lock_registry = mock.MagicMock()
449
+ ctx.device_manager.parent.device_lock_registry.get_owned_devices.return_value = ["samy", "samx"]
450
+ status = mock.MagicMock(_device_instr_id="diid-123")
451
+ ctx.actions._create_status = mock.MagicMock(return_value=status)
452
+
453
+ ctx.actions._broadcast_scan_device_info()
454
+
455
+ msg = _last_device_instruction(ctx, "broadcast_scan_device_info")
456
+ assert msg.device == ["samx", "samy"]
457
+ assert msg.parameter == {"scan_id": "scan-id-test"}
458
+ assert msg.metadata["device_instr_id"] == "diid-123"
459
+ assert msg.metadata["RID"] == "rid-123"
460
+ assert msg.metadata["scan_id"] == "scan-id-test"
461
+ status.wait.assert_called_once_with()
462
+
463
+
464
+ def test_broadcast_scan_device_info_emits_without_owned_devices(action_context):
465
+ ctx = action_context()
466
+ ctx.scan.scan_info.metadata["RID"] = "rid-123"
467
+ ctx.device_manager.parent.device_lock_registry = mock.MagicMock()
468
+ ctx.device_manager.parent.device_lock_registry.get_owned_devices.return_value = []
469
+ status = mock.MagicMock(_device_instr_id="diid-empty")
470
+ ctx.actions._create_status = mock.MagicMock(return_value=status)
471
+
472
+ ctx.actions._broadcast_scan_device_info()
473
+
474
+ msg = _last_device_instruction(ctx, "broadcast_scan_device_info")
475
+ assert msg.device == []
476
+ assert msg.parameter == {"scan_id": "scan-id-test"}
477
+ assert msg.metadata["device_instr_id"] == "diid-empty"
478
+ status.wait.assert_called_once_with()
479
+
480
+
481
+ @pytest.mark.parametrize(("is_scan", "scan_id"), [(False, "scan-id-test"), (True, None)])
482
+ def test_broadcast_scan_device_info_skips_when_not_scan_or_missing_scan_id(
483
+ action_context, is_scan, scan_id
484
+ ):
485
+ ctx = action_context()
486
+ ctx.scan.is_scan = is_scan
487
+ ctx.scan.scan_info.scan_id = scan_id
488
+ ctx.actions._send = mock.MagicMock()
489
+
490
+ ctx.actions._broadcast_scan_device_info()
491
+
492
+ ctx.actions._send.assert_not_called()
493
+
494
+
445
495
  def test_device_instruction_actions_use_dotted_name_for_nested_devices(action_context):
446
496
  ctx = action_context()
447
497
  setpoint = ctx.device_manager.devices.samx.setpoint
@@ -173,6 +173,7 @@ def make_scan(direct_worker_context):
173
173
  scan.scan_info.metadata["RID"] = "rid-1"
174
174
  scan.actions._send_scan_status = mock.MagicMock()
175
175
  scan.actions.send_client_info = mock.MagicMock()
176
+ scan.actions._broadcast_scan_device_info = mock.MagicMock()
176
177
  scan._shutdown_event = mock.MagicMock()
177
178
  return scan
178
179
 
@@ -301,6 +302,22 @@ def test_run_executes_full_scan_sequence_in_order(direct_worker_context, make_sc
301
302
  assert direct_worker_context.direct_worker.scan is None
302
303
 
303
304
 
305
+ def test_run_broadcasts_scan_device_info_after_stage(direct_worker_context, make_scan):
306
+ called_steps = []
307
+ scan = make_scan(called_steps=called_steps)
308
+ direct_worker_context.queue.active_scan = scan
309
+ direct_worker_context.scan_worker.current_instruction_queue_item = direct_worker_context.queue
310
+ direct_worker_context.device_manager._rpc_method = mock.MagicMock(return_value=mock.MagicMock())
311
+ scan.actions._broadcast_scan_device_info = mock.MagicMock(
312
+ side_effect=lambda: called_steps.append("broadcast_scan_device_info")
313
+ )
314
+
315
+ direct_worker_context.direct_worker.run(scan)
316
+
317
+ scan.actions._broadcast_scan_device_info.assert_called_once_with()
318
+ assert called_steps[:4] == ["prepare_scan", "open_scan", "stage", "broadcast_scan_device_info"]
319
+
320
+
304
321
  def test_run_initializes_scan_before_scan_sequence(direct_worker_context, make_scan):
305
322
  called_steps = []
306
323
  scan = make_scan(called_steps=called_steps)
File without changes
File without changes
File without changes