bec-server 4.1.0__tar.gz → 4.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. {bec_server-4.1.0 → bec_server-4.1.2}/PKG-INFO +1 -1
  2. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/file_writer/file_writer_manager.py +60 -56
  3. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/scan_actions.py +1 -0
  4. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/scan_base.py +3 -0
  5. {bec_server-4.1.0 → bec_server-4.1.2}/pyproject.toml +1 -1
  6. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_file_writer/test_file_writer_manager.py +316 -36
  7. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_scan_actions.py +24 -0
  8. {bec_server-4.1.0 → bec_server-4.1.2}/.gitignore +0 -0
  9. {bec_server-4.1.0 → bec_server-4.1.2}/.zed/debug.json +0 -0
  10. {bec_server-4.1.0 → bec_server-4.1.2}/README.md +0 -0
  11. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/__init__.py +0 -0
  12. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/actors/__init__.py +0 -0
  13. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/actors/actor.py +0 -0
  14. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/actors/builtin_actor_manager.py +0 -0
  15. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/actors/manager.py +0 -0
  16. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/actors/scan_interlock.py +0 -0
  17. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/actors/worker.py +0 -0
  18. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/bec_server_utils/__init__.py +0 -0
  19. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/bec_server_utils/launch.py +0 -0
  20. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/bec_server_utils/service_handler.py +0 -0
  21. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/bec_server_utils/subprocess_launch.py +0 -0
  22. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/bec_server_utils/tmux_launch.py +0 -0
  23. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/data_processing/__init__.py +0 -0
  24. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/data_processing/cli/__init__.py +0 -0
  25. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/data_processing/cli/launch.py +0 -0
  26. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/data_processing/dap_server.py +0 -0
  27. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/data_processing/dap_service.py +0 -0
  28. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/data_processing/dap_service_manager.py +0 -0
  29. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/data_processing/lmfit1d_service.py +0 -0
  30. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/device_server/__init__.py +0 -0
  31. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/device_server/bec_message_handler.py +0 -0
  32. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/device_server/cli/__init__.py +0 -0
  33. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/device_server/cli/launch.py +0 -0
  34. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/device_server/device_server.py +0 -0
  35. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/device_server/devices/__init__.py +0 -0
  36. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/device_server/devices/config_update_handler.py +0 -0
  37. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/device_server/devices/device_serializer.py +0 -0
  38. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/device_server/devices/devicemanager.py +0 -0
  39. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/device_server/friendly_device_exceptions.py +0 -0
  40. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/device_server/ophyd_callback_monitor.py +0 -0
  41. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/device_server/rpc_handler.py +0 -0
  42. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/device_server/tests/__init__.py +0 -0
  43. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/device_server/tests/utils.py +0 -0
  44. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/file_writer/__init__.py +0 -0
  45. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/file_writer/async_writer.py +0 -0
  46. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/file_writer/cli/__init__.py +0 -0
  47. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/file_writer/cli/launch.py +0 -0
  48. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/file_writer/default_writer.py +0 -0
  49. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/file_writer/file_writer.py +0 -0
  50. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/file_writer/merged_dicts.py +0 -0
  51. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/procedures/Containerfile.bec_linux_base +0 -0
  52. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/procedures/Containerfile.requirements +0 -0
  53. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/procedures/Containerfile.worker +0 -0
  54. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/procedures/__init__.py +0 -0
  55. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/procedures/_dev_runner.py +0 -0
  56. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/procedures/builtin_procedures.py +0 -0
  57. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/procedures/constants.py +0 -0
  58. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/procedures/container_utils.py +0 -0
  59. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/procedures/container_worker.py +0 -0
  60. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/procedures/manager.py +0 -0
  61. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/procedures/oop_worker_base.py +0 -0
  62. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/procedures/procedure_registry.py +0 -0
  63. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/procedures/protocol.py +0 -0
  64. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/procedures/subprocess_worker.py +0 -0
  65. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/procedures/worker_base.py +0 -0
  66. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_bundler/__init__.py +0 -0
  67. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_bundler/bec_emitter.py +0 -0
  68. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_bundler/cli/__init__.py +0 -0
  69. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_bundler/cli/launch.py +0 -0
  70. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_bundler/emitter.py +0 -0
  71. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_bundler/scan_bundler.py +0 -0
  72. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/__init__.py +0 -0
  73. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/beamline_state_manager.py +0 -0
  74. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/cli/__init__.py +0 -0
  75. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/cli/launch.py +0 -0
  76. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/device_lock_registry.py +0 -0
  77. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/direct_scan_worker.py +0 -0
  78. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/errors.py +0 -0
  79. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/instruction_handler.py +0 -0
  80. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scan_assembler.py +0 -0
  81. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scan_guard.py +0 -0
  82. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scan_manager.py +0 -0
  83. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scan_queue.py +0 -0
  84. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scan_server.py +0 -0
  85. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scan_worker.py +0 -0
  86. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/__init__.py +0 -0
  87. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/acquire.py +0 -0
  88. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/cont_line_scan.py +0 -0
  89. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/device_rpc.py +0 -0
  90. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/fermat_scan.py +0 -0
  91. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/grid_scan.py +0 -0
  92. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/hexagonal_scan.py +0 -0
  93. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/line_scan.py +0 -0
  94. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/line_sweep_scan.py +0 -0
  95. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/list_scan.py +0 -0
  96. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/log_scan.py +0 -0
  97. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/move.py +0 -0
  98. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/multi_region_grid_scan.py +0 -0
  99. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/multi_region_line_scan.py +0 -0
  100. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/path_optimization.py +0 -0
  101. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/position_generators.py +0 -0
  102. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/round_roi_scan.py +0 -0
  103. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/round_scan.py +0 -0
  104. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/scan_argument_modifier.py +0 -0
  105. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/scan_components.py +0 -0
  106. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/scan_modifier.py +0 -0
  107. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/scan_status.py +0 -0
  108. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/time_scan.py +0 -0
  109. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/scans/updated_move.py +0 -0
  110. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/tests/__init__.py +0 -0
  111. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/tests/fixtures.py +0 -0
  112. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/tests/scan_fixtures.py +0 -0
  113. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/tests/scan_hook_tests.py +0 -0
  114. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scan_server/tests/utils.py +0 -0
  115. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scihub/__init__.py +0 -0
  116. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scihub/atlas/__init__.py +0 -0
  117. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scihub/atlas/atlas_active_client_emitter.py +0 -0
  118. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scihub/atlas/atlas_connector.py +0 -0
  119. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scihub/atlas/atlas_forwarder.py +0 -0
  120. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scihub/atlas/atlas_metadata_handler.py +0 -0
  121. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scihub/atlas/atlas_plugin_repo_emitter.py +0 -0
  122. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scihub/atlas/config_handler.py +0 -0
  123. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scihub/cli/__init__.py +0 -0
  124. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scihub/cli/launch.py +0 -0
  125. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scihub/scihub.py +0 -0
  126. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scihub/service_handler/__init__.py +0 -0
  127. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/scihub/service_handler/service_handler.py +0 -0
  128. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/test/__init__.py +0 -0
  129. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/test/actor_test_utils.py +0 -0
  130. {bec_server-4.1.0 → bec_server-4.1.2}/bec_server/test/helpers.py +0 -0
  131. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_actors/test_actors.py +0 -0
  132. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_bec_server_utils/test_main.py +0 -0
  133. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_bec_server_utils/test_service_handler.py +0 -0
  134. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_bec_server_utils/test_subprocess_launch.py +0 -0
  135. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_bec_server_utils/test_tmux_launch.py +0 -0
  136. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_data_processing/conftest.py +0 -0
  137. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_data_processing/test_dap_cli_launch.py +0 -0
  138. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_data_processing/test_dap_server.py +0 -0
  139. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_data_processing/test_dap_service_manager.py +0 -0
  140. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_data_processing/test_lmfit1d_service.py +0 -0
  141. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_device_server/conftest.py +0 -0
  142. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_device_server/test_config_handler.py +0 -0
  143. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_device_server/test_device_instructions.py +0 -0
  144. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_device_server/test_device_manager_ds.py +0 -0
  145. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_device_server/test_device_serializer.py +0 -0
  146. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_device_server/test_device_server.py +0 -0
  147. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_device_server/test_device_server_cli_launch.py +0 -0
  148. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_device_server/test_ophyd_callback_monitor.py +0 -0
  149. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_device_server/test_rpc_handler.py +0 -0
  150. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_file_writer/conftest.py +0 -0
  151. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_file_writer/test_async_file_writer.py +0 -0
  152. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_file_writer/test_file_writer.py +0 -0
  153. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_file_writer/test_file_writer_cli_launch.py +0 -0
  154. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_bundler/conftest.py +0 -0
  155. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_bundler/test_bec_emitter.py +0 -0
  156. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_bundler/test_scan_bundler.py +0 -0
  157. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_bundler/test_scan_bundler_cli_launch.py +0 -0
  158. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/conftest.py +0 -0
  159. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/__init__.py +0 -0
  160. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_acquire.py +0 -0
  161. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_cont_line_scan.py +0 -0
  162. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_device_rpc_scan.py +0 -0
  163. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_fermat_scan.py +0 -0
  164. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_grid_scan.py +0 -0
  165. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_hexagonal_scan.py +0 -0
  166. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_line_scan.py +0 -0
  167. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_line_sweep_scan.py +0 -0
  168. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_list_scan.py +0 -0
  169. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_log_scan.py +0 -0
  170. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_move_scan.py +0 -0
  171. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_multi_region_grid_scan.py +0 -0
  172. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_multi_region_line_scan.py +0 -0
  173. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_position_generators.py +0 -0
  174. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_relative_scan_return_to_start.py +0 -0
  175. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_round_roi_scan.py +0 -0
  176. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_round_scan.py +0 -0
  177. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_scan_components.py +0 -0
  178. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_scan_status.py +0 -0
  179. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_time_scan.py +0 -0
  180. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/scans/test_updated_move_scan.py +0 -0
  181. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_beamline_state_manager.py +0 -0
  182. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_builtin_actor_manager.py +0 -0
  183. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_container_utils.py +0 -0
  184. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_device_lock_registry.py +0 -0
  185. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_direct_scan_worker.py +0 -0
  186. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_instruction_handler.py +0 -0
  187. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_path_optimization.py +0 -0
  188. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_procedure_container_worker.py +0 -0
  189. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_procedures.py +0 -0
  190. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_scan_argument_modifier.py +0 -0
  191. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_scan_assembler.py +0 -0
  192. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_scan_fixture_imports.py +0 -0
  193. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_scan_guard.py +0 -0
  194. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_scan_interlock.py +0 -0
  195. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_scan_modifier.py +0 -0
  196. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_scan_server_cli_launch.py +0 -0
  197. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_scan_server_queue.py +0 -0
  198. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_scan_server_scan_manager.py +0 -0
  199. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scan_server/test_scan_worker.py +0 -0
  200. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scihub/conftest.py +0 -0
  201. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scihub/test_atlas_active_client_emitter.py +0 -0
  202. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scihub/test_atlas_config_handler.py +0 -0
  203. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scihub/test_atlas_connector.py +0 -0
  204. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scihub/test_atlas_forwarder.py +0 -0
  205. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scihub/test_atlas_metadata_handler.py +0 -0
  206. {bec_server-4.1.0 → bec_server-4.1.2}/tests/tests_scihub/test_atlas_plugin_repo_emitter.py +0 -0
  207. {bec_server-4.1.0 → bec_server-4.1.2}/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.0
3
+ Version: 4.1.2
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
@@ -4,7 +4,7 @@ import os
4
4
  import threading
5
5
  import time
6
6
  import traceback
7
- from collections import defaultdict
7
+ from collections import defaultdict, deque
8
8
 
9
9
  from bec_lib import messages, plugin_helper
10
10
  from bec_lib.alarm_handler import Alarms
@@ -60,12 +60,23 @@ class ScanStorage:
60
60
  """
61
61
  self.scan_segments[point_id] = data
62
62
 
63
+ @property
64
+ def baseline_ready(self) -> bool:
65
+ """Whether no baseline read was requested or its bundled data has arrived."""
66
+ if self.status_msg is None:
67
+ return True
68
+ return not self.status_msg.info.get("baseline_readout_requested", False) or bool(
69
+ self.baseline
70
+ )
71
+
63
72
  def ready_to_write(self) -> bool:
64
73
  """
65
74
  Check if the scan is ready to be written to file.
66
75
  """
67
76
  if self.forced_finish:
68
77
  return True
78
+ if not self.baseline_ready:
79
+ return False
69
80
  if self.enforce_sync:
70
81
  # wait for all points to be received. Since this method will be called for every
71
82
  # update of the scan segments, we can also accept to write after the scan is finished
@@ -106,8 +117,16 @@ class FileWriterManager(BECService):
106
117
  self.available_beamline_states: list[messages.BeamlineStateConfig] = []
107
118
  self.beamline_state_subscriptions: set[str] = set()
108
119
  self.beamline_states: dict[str, messages.BeamlineStateMessage] = {}
120
+ self.scan_storage: dict[str, ScanStorage] = {}
121
+ self._pending_baseline_writes: dict[str, dict] = {}
122
+ self._pending_file_references: dict[str, dict[str, messages.FileMessage]] = {}
123
+ self._finished_scan_ids: deque[str] = deque(maxlen=1000)
109
124
  self.connector.register(MessageEndpoints.scan_segment(), cb=self._scan_segment_callback)
110
125
  self.connector.register(MessageEndpoints.scan_status(), cb=self._scan_status_callback)
126
+ self.connector.register(MessageEndpoints.scan_baseline(), cb=self._scan_baseline_callback)
127
+ self.connector.register(
128
+ patterns=MessageEndpoints.public_file("*", "*"), cb=self._file_reference_callback
129
+ )
111
130
  self.connector.register(
112
131
  patterns=MessageEndpoints.device_read_configuration("*"), # type: ignore
113
132
  cb=self._device_configuration_callback,
@@ -122,7 +141,6 @@ class FileWriterManager(BECService):
122
141
  )
123
142
  self._run_storage_copy_plugin = self._load_storage_copy_plugin()
124
143
  self.async_writer = None
125
- self.scan_storage: dict[str, ScanStorage] = {}
126
144
  self.file_writer = HDF5FileWriter(self)
127
145
  self.status = messages.BECStatus.RUNNING
128
146
  self.refresh_device_configs()
@@ -141,6 +159,43 @@ class FileWriterManager(BECService):
141
159
  msg = msg.value
142
160
  self.update_scan_storage_with_status(msg)
143
161
 
162
+ def _scan_baseline_callback(self, msg: MessageObject) -> None:
163
+ baseline_msg = msg.value
164
+ with self._lock:
165
+ if baseline_msg.scan_id in self._finished_scan_ids:
166
+ return
167
+ storage = self.scan_storage.get(baseline_msg.scan_id)
168
+ if storage is None:
169
+ self._pending_baseline_writes[baseline_msg.scan_id] = baseline_msg.data
170
+ return
171
+ storage.baseline = baseline_msg.data
172
+ self.check_storage_status(scan_id=baseline_msg.scan_id)
173
+
174
+ def _file_reference_callback(self, msg: MessageObject) -> None:
175
+ _, scan_id, _, name = msg.topic.rsplit("/", 3)
176
+ file_msg = msg.value
177
+ if name == "master" or file_msg.is_master_file:
178
+ return
179
+ with self._lock:
180
+ if scan_id in self._finished_scan_ids:
181
+ return
182
+ storage = self.scan_storage.get(scan_id)
183
+ if storage is None:
184
+ self._pending_file_references.setdefault(scan_id, {})[name] = file_msg
185
+ return
186
+ storage.file_references[name] = file_msg
187
+
188
+ def _get_or_create_scan_storage(self, scan_id: str, scan_number: int | None) -> ScanStorage:
189
+ """Create scan storage and transfer any notifications received before its status or data."""
190
+ with self._lock:
191
+ storage = self.scan_storage.get(scan_id)
192
+ if storage is None:
193
+ storage = ScanStorage(scan_number=scan_number, scan_id=scan_id)
194
+ storage.baseline = self._pending_baseline_writes.pop(scan_id, {})
195
+ storage.file_references = self._pending_file_references.pop(scan_id, {})
196
+ self.scan_storage[scan_id] = storage
197
+ return storage
198
+
144
199
  def _device_configuration_callback(self, msg):
145
200
  topic, msg = msg.topic, msg.value
146
201
  device = topic.split("/")[-1]
@@ -255,10 +310,7 @@ class FileWriterManager(BECService):
255
310
  if scan_id is None:
256
311
  return
257
312
 
258
- if not self.scan_storage.get(scan_id):
259
- self.scan_storage[scan_id] = ScanStorage(
260
- scan_number=msg.content["info"].get("scan_number"), scan_id=scan_id
261
- )
313
+ self._get_or_create_scan_storage(scan_id, msg.content["info"].get("scan_number"))
262
314
 
263
315
  for state in self.beamline_states.values():
264
316
  if state.name not in self.scan_storage[scan_id].beamline_states:
@@ -324,60 +376,13 @@ class FileWriterManager(BECService):
324
376
  scan_id = msg.content.get("scan_id")
325
377
  if scan_id is None:
326
378
  return
327
- if not self.scan_storage.get(scan_id):
328
- self.scan_storage[scan_id] = ScanStorage(
329
- scan_number=msg.metadata.get("scan_number"), scan_id=scan_id
330
- )
379
+ self._get_or_create_scan_storage(scan_id, msg.metadata.get("scan_number"))
331
380
  self.scan_storage[scan_id].append(
332
381
  point_id=msg.content.get("point_id"), data=msg.content.get("data")
333
382
  )
334
383
  logger.debug(msg.content.get("point_id"))
335
384
  self.check_storage_status(scan_id=scan_id)
336
385
 
337
- def update_baseline_reading(self, scan_id: str) -> None:
338
- """
339
- Update the baseline reading for the scan.
340
-
341
- Args:
342
- scan_id (str): Scan ID
343
- """
344
- if not self.scan_storage.get(scan_id):
345
- return
346
- if self.scan_storage[scan_id].baseline:
347
- return
348
- baseline = self.connector.get(MessageEndpoints.public_scan_baseline(scan_id))
349
- if not baseline:
350
- return
351
- self.scan_storage[scan_id].baseline = baseline.content["data"]
352
- return
353
-
354
- def update_file_references(self, scan_id: str) -> None:
355
- """
356
- Update the file references for the scan.
357
- All external files ought to be announced to the endpoint public_file before the scan finishes. This function
358
- retrieves the file references and adds them to the scan storage.
359
-
360
- Args:
361
- scan_id (str): Scan ID
362
- """
363
- if not self.scan_storage.get(scan_id):
364
- return
365
- msgs = self.connector.keys(MessageEndpoints.public_file(scan_id, "*"))
366
- if not msgs:
367
- return
368
-
369
- # extract name from 'public/<scan_id>/file/<name>'
370
- names = [msg.decode().split("/")[-1] for msg in msgs]
371
- file_msgs = [
372
- self.connector.get(MessageEndpoints.public_file(scan_id=scan_id, name=name))
373
- for name in names
374
- ]
375
- if not file_msgs:
376
- return
377
- for name, file_msg in zip(names, file_msgs):
378
- self.scan_storage[scan_id].file_references[name] = file_msg
379
- return
380
-
381
386
  def update_device_configuration(self, device: str, msg: messages.DeviceMessage) -> None:
382
387
  """
383
388
  Update the device configuration. Note that this is a global configuration and not specific to a scan.
@@ -397,8 +402,6 @@ class FileWriterManager(BECService):
397
402
  with self._lock:
398
403
  if not self.scan_storage.get(scan_id):
399
404
  return
400
- self.update_baseline_reading(scan_id)
401
- self.update_file_references(scan_id)
402
405
  if self.scan_storage[scan_id].ready_to_write():
403
406
  self.write_file(scan_id)
404
407
 
@@ -481,6 +484,7 @@ class FileWriterManager(BECService):
481
484
  logger.info(f"Writing to file {file_path} took {time.time() - start_time:.2f} seconds.")
482
485
 
483
486
  self.scan_storage.pop(scan_id)
487
+ self._finished_scan_ids.append(scan_id)
484
488
  self.connector.set_and_publish(
485
489
  MessageEndpoints.public_file(scan_id, "master"),
486
490
  messages.FileMessage(file_path=file_path, done=True, successful=successful),
@@ -614,6 +614,7 @@ class ScanActions:
614
614
  metadata={"device_instr_id": status._device_instr_id, "readout_priority": "baseline"},
615
615
  )
616
616
  self._send(instr)
617
+ self._scan.scan_info.baseline_readout_requested = True
617
618
  if wait:
618
619
  status.wait()
619
620
  return status
@@ -116,6 +116,9 @@ class ScanInfo(BaseModel):
116
116
  metadata: Annotated[dict, Field(description="Additional metadata for the scan")] = {}
117
117
 
118
118
  # progress tracking
119
+ baseline_readout_requested: Annotated[
120
+ bool, Field(description="Whether a read of nonempty baseline devices was requested.")
121
+ ] = False
119
122
  num_monitored_readouts: Annotated[
120
123
  int,
121
124
  Field(
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bec-server"
7
- version = "4.1.0"
7
+ version = "4.1.2"
8
8
  description = "BEC server"
9
9
  requires-python = ">=3.11"
10
10
  classifiers = [
@@ -1,5 +1,6 @@
1
1
  # pylint: skip-file
2
2
  import os
3
+ import threading
3
4
  import time
4
5
  from unittest import mock
5
6
 
@@ -201,15 +202,11 @@ def test_check_storage_status(file_writer_manager_mock, scan_storage_mock):
201
202
 
202
203
  with (
203
204
  mock.patch.object(scan_storage_mock, "ready_to_write") as mock_ready_to_write,
204
- mock.patch.object(file_manager, "update_baseline_reading") as mock_update_baseline,
205
- mock.patch.object(file_manager, "update_file_references") as mock_update_file_references,
206
205
  mock.patch.object(file_manager, "write_file") as mock_write_file,
207
206
  ):
208
207
  mock_ready_to_write.return_value = True
209
208
  file_manager.check_storage_status(scan_id="scan_id")
210
209
  assert mock_ready_to_write.called
211
- assert mock_update_baseline.call_args == mock.call("scan_id")
212
- assert mock_update_file_references.call_args == mock.call("scan_id")
213
210
  assert mock_write_file.call_args == mock.call("scan_id")
214
211
 
215
212
 
@@ -331,18 +328,6 @@ def test_write_file_renames_tmp_file_on_exception(file_writer_manager_mock, scan
331
328
  mock_rename.assert_called_once_with(tmp_file_path, final_file_path)
332
329
 
333
330
 
334
- def test_update_baseline_reading(file_writer_manager_mock, scan_storage_mock):
335
- file_manager = file_writer_manager_mock
336
- file_manager.scan_storage["scan_id"] = scan_storage_mock
337
- with mock.patch.object(file_manager, "connector") as mock_connector:
338
- mock_connector.get.return_value = messages.ScanBaselineMessage(
339
- scan_id="scan_id", data={"data": "data"}
340
- )
341
- file_manager.update_baseline_reading("scan_id")
342
- assert file_manager.scan_storage["scan_id"].baseline == {"data": "data"}
343
- mock_connector.get.assert_called_once_with(MessageEndpoints.public_scan_baseline("scan_id"))
344
-
345
-
346
331
  def test_scan_storage_append(scan_storage_mock):
347
332
  storage = scan_storage_mock
348
333
  storage.append(1, {"data": "data"})
@@ -358,21 +343,6 @@ def test_scan_storage_ready_to_write(scan_storage_mock):
358
343
  assert storage.ready_to_write() is True
359
344
 
360
345
 
361
- def test_update_file_references(file_writer_manager_mock):
362
- file_manager = file_writer_manager_mock
363
- with mock.patch.object(file_manager, "connector") as mock_connector:
364
- file_manager.update_file_references("scan_id")
365
- mock_connector.keys.assert_not_called()
366
-
367
-
368
- def test_update_file_references_gets_keys(file_writer_manager_mock, scan_storage_mock):
369
- file_manager = file_writer_manager_mock
370
- file_manager.scan_storage["scan_id"] = scan_storage_mock
371
- with mock.patch.object(file_manager, "connector") as mock_connector:
372
- file_manager.update_file_references("scan_id")
373
- mock_connector.keys.assert_called_once_with(MessageEndpoints.public_file("scan_id", "*"))
374
-
375
-
376
346
  def test_update_scan_storage_with_status_ignores_none(file_writer_manager_mock):
377
347
  file_manager = file_writer_manager_mock
378
348
  file_manager.update_scan_storage_with_status(
@@ -397,11 +367,7 @@ def test_update_scan_storage_with_status_waits_for_v4_sync_segments(file_writer_
397
367
  info={"scan_number": 1, "monitor_sync": None},
398
368
  )
399
369
 
400
- with (
401
- mock.patch.object(file_manager, "update_baseline_reading"),
402
- mock.patch.object(file_manager, "update_file_references"),
403
- mock.patch.object(file_manager, "write_file") as write_file,
404
- ):
370
+ with (mock.patch.object(file_manager, "write_file") as write_file,):
405
371
  file_manager.update_scan_storage_with_status(msg)
406
372
 
407
373
  assert storage.enforce_sync is True
@@ -559,3 +525,317 @@ def test_file_writer_manager_removes_beamline_state_subscription(file_writer_man
559
525
  file_manager._update_available_beamline_states({"data": msg})
560
526
  assert "State1" not in file_manager.beamline_state_subscriptions
561
527
  assert "State1" not in file_manager.beamline_states
528
+
529
+
530
+ @pytest.mark.parametrize("enforce_sync,monitored", [(True, ["samx"]), (True, []), (False, [])])
531
+ def test_ready_to_write_waits_for_baseline(enforce_sync, monitored):
532
+ storage = ScanStorage(1, "scan_id")
533
+ storage.status_msg = messages.ScanStatusMessage(
534
+ scan_id="scan_id",
535
+ status="closed",
536
+ info={"baseline_readout_requested": True},
537
+ readout_priority={"monitored": monitored, "baseline": ["samz"]},
538
+ )
539
+ storage.enforce_sync = enforce_sync
540
+ storage.scan_finished = True
541
+ storage.num_monitored_readouts = 0
542
+
543
+ assert storage.baseline_ready is False
544
+ assert storage.ready_to_write() is False
545
+
546
+ storage.baseline = {"samz": {"samz": {"value": 42, "timestamp": 1}}}
547
+
548
+ assert storage.baseline_ready is True
549
+ assert storage.ready_to_write() is True
550
+
551
+
552
+ @pytest.mark.parametrize("arrival_order", ["baseline_first", "baseline_last"])
553
+ def test_baseline_arrival_finalizes_scan_once(file_writer_manager_mock, arrival_order):
554
+ manager = file_writer_manager_mock
555
+ manager.scan_storage["scan_id"] = ScanStorage(1, "scan_id")
556
+ baseline_msg = messages.ScanBaselineMessage(
557
+ scan_id="scan_id", data={"samz": {"samz": {"value": 42, "timestamp": 1}}}
558
+ )
559
+ baseline_event = MessageObject(
560
+ topic=MessageEndpoints.scan_baseline().endpoint, value=baseline_msg
561
+ )
562
+ closed_msg = messages.ScanStatusMessage(
563
+ scan_id="scan_id",
564
+ status="closed",
565
+ scan_number=1,
566
+ scan_type="software_triggered",
567
+ num_points=1,
568
+ num_monitored_readouts=1,
569
+ readout_priority={"monitored": ["samx"], "baseline": ["samz"]},
570
+ info={"scan_number": 1, "monitor_sync": "bec", "baseline_readout_requested": True},
571
+ )
572
+ segment = messages.ScanMessage(
573
+ scan_id="scan_id", point_id=0, data={"samx": {"samx": {"value": 1, "timestamp": 1}}}
574
+ )
575
+ written_baselines = []
576
+
577
+ def write_file(scan_id):
578
+ written_baselines.append(manager.scan_storage.pop(scan_id).baseline)
579
+ manager._finished_scan_ids.append(scan_id)
580
+
581
+ with (
582
+ mock.patch.object(manager.connector, "get", return_value=None) as get,
583
+ mock.patch.object(manager, "write_file", side_effect=write_file) as write,
584
+ ):
585
+ if arrival_order == "baseline_first":
586
+ manager._scan_baseline_callback(baseline_event)
587
+ write.assert_not_called()
588
+
589
+ manager.insert_to_scan_storage(segment)
590
+ write.assert_not_called()
591
+ manager.update_scan_storage_with_status(closed_msg)
592
+
593
+ if arrival_order == "baseline_last":
594
+ write.assert_not_called()
595
+ manager._scan_baseline_callback(baseline_event)
596
+
597
+ write.assert_called_once_with("scan_id")
598
+ assert written_baselines == [baseline_msg.data]
599
+ get.assert_not_called()
600
+ assert "scan_id" not in manager.scan_storage
601
+
602
+ manager._scan_baseline_callback(baseline_event)
603
+ write.assert_called_once_with("scan_id")
604
+ assert "scan_id" not in manager.scan_storage
605
+ assert manager._pending_baseline_writes == {}
606
+
607
+
608
+ def test_baseline_callback_does_not_create_scan_storage(file_writer_manager_mock):
609
+ manager = file_writer_manager_mock
610
+ baseline = messages.ScanBaselineMessage(scan_id="unknown_scan", data={"samz": {}})
611
+ with mock.patch.object(manager, "write_file") as write:
612
+ manager._scan_baseline_callback(
613
+ MessageObject(topic=MessageEndpoints.scan_baseline().endpoint, value=baseline)
614
+ )
615
+ write.assert_not_called()
616
+ assert manager.scan_storage == {}
617
+ assert manager._pending_baseline_writes == {"unknown_scan": baseline.data}
618
+
619
+
620
+ @pytest.mark.parametrize("status", ["aborted", "halted", "user_completed"])
621
+ def test_interrupted_scan_does_not_wait_for_baseline(file_writer_manager_mock, status):
622
+ manager = file_writer_manager_mock
623
+ storage = ScanStorage(1, "scan_id")
624
+ manager.scan_storage["scan_id"] = storage
625
+ status_msg = messages.ScanStatusMessage(
626
+ scan_id="scan_id",
627
+ status=status,
628
+ scan_number=1,
629
+ scan_type="software_triggered",
630
+ num_monitored_readouts=1,
631
+ readout_priority={"monitored": ["samx"], "baseline": ["samz"]},
632
+ info={"scan_number": 1, "monitor_sync": "bec", "baseline_readout_requested": True},
633
+ )
634
+ with (
635
+ mock.patch.object(manager.connector, "get", return_value=None),
636
+ mock.patch.object(manager, "write_file") as write,
637
+ ):
638
+ manager.update_scan_storage_with_status(status_msg)
639
+ assert storage.baseline_ready is False
640
+ write.assert_called_once_with("scan_id")
641
+
642
+
643
+ @pytest.mark.parametrize("baseline_devices", [[], ["samz"]])
644
+ @pytest.mark.parametrize("requested_info", [{}, {"baseline_readout_requested": False}])
645
+ def test_scan_without_baseline_request_does_not_wait_for_baseline(
646
+ file_writer_manager_mock, baseline_devices, requested_info
647
+ ):
648
+ manager = file_writer_manager_mock
649
+ manager.scan_storage["scan_id"] = ScanStorage(1, "scan_id")
650
+ closed_msg = messages.ScanStatusMessage(
651
+ scan_id="scan_id",
652
+ status="closed",
653
+ scan_number=1,
654
+ scan_type="software_triggered",
655
+ num_monitored_readouts=0,
656
+ readout_priority={"monitored": [], "baseline": baseline_devices},
657
+ info={"scan_number": 1, "monitor_sync": "bec", **requested_info},
658
+ )
659
+ with (
660
+ mock.patch.object(manager.connector, "get", return_value=None),
661
+ mock.patch.object(manager, "write_file") as write,
662
+ ):
663
+ manager.update_scan_storage_with_status(closed_msg)
664
+ write.assert_called_once_with("scan_id")
665
+
666
+
667
+ @pytest.mark.parametrize("first_update", ["status", "segment"])
668
+ def test_pending_notifications_are_flushed_when_storage_is_created(
669
+ file_writer_manager_mock, first_update
670
+ ):
671
+ manager = file_writer_manager_mock
672
+ baseline = messages.ScanBaselineMessage(
673
+ scan_id="scan_id", data={"samz": {"samz": {"value": 42, "timestamp": 1}}}
674
+ )
675
+ files = {
676
+ name: messages.FileMessage(file_path=f"/{name}.h5", done=True, successful=True)
677
+ for name in ["eiger", "pilatus"]
678
+ }
679
+ manager._scan_baseline_callback(
680
+ MessageObject(topic=MessageEndpoints.scan_baseline().endpoint, value=baseline)
681
+ )
682
+ for name, file_msg in files.items():
683
+ manager._file_reference_callback(
684
+ MessageObject(
685
+ topic=MessageEndpoints.public_file("scan_id", name).endpoint, value=file_msg
686
+ )
687
+ )
688
+ manager._pending_baseline_writes["other_scan"] = {"samz": {}}
689
+ manager._pending_file_references["other_scan"] = {"eiger": files["eiger"]}
690
+ assert manager.scan_storage == {}
691
+
692
+ with mock.patch.object(manager, "write_file") as write:
693
+ if first_update == "status":
694
+ manager.update_scan_storage_with_status(
695
+ messages.ScanStatusMessage(
696
+ scan_id="scan_id",
697
+ status="paused",
698
+ info={"scan_number": 1},
699
+ readout_priority={"baseline": ["samz"]},
700
+ )
701
+ )
702
+ else:
703
+ manager.insert_to_scan_storage(
704
+ messages.ScanMessage(
705
+ scan_id="scan_id", point_id=0, data={}, metadata={"scan_number": 1}
706
+ )
707
+ )
708
+ write.assert_not_called()
709
+
710
+ storage = manager.scan_storage["scan_id"]
711
+ assert storage.baseline == baseline.data
712
+ assert storage.file_references == files
713
+ assert "scan_id" not in manager._pending_baseline_writes
714
+ assert "scan_id" not in manager._pending_file_references
715
+ assert manager._pending_baseline_writes == {"other_scan": {"samz": {}}}
716
+ assert manager._pending_file_references == {"other_scan": {"eiger": files["eiger"]}}
717
+
718
+
719
+ def test_baseline_before_first_status_allows_closed_scan_to_finalize(file_writer_manager_mock):
720
+ manager = file_writer_manager_mock
721
+ baseline = messages.ScanBaselineMessage(scan_id="scan_id", data={"samz": {}})
722
+ manager._scan_baseline_callback(
723
+ MessageObject(topic=MessageEndpoints.scan_baseline().endpoint, value=baseline)
724
+ )
725
+ with mock.patch.object(manager, "write_file") as write:
726
+ manager.update_scan_storage_with_status(
727
+ messages.ScanStatusMessage(
728
+ scan_id="scan_id",
729
+ status="closed",
730
+ scan_type="software_triggered",
731
+ num_monitored_readouts=0,
732
+ readout_priority={"monitored": [], "baseline": ["samz"]},
733
+ info={"scan_number": 1, "monitor_sync": "bec", "baseline_readout_requested": True},
734
+ )
735
+ )
736
+ write.assert_called_once_with("scan_id")
737
+ assert manager.scan_storage["scan_id"].baseline == baseline.data
738
+ assert manager._pending_baseline_writes == {}
739
+
740
+
741
+ def test_file_reference_updates_active_storage(file_writer_manager_mock):
742
+ manager = file_writer_manager_mock
743
+ manager.scan_storage["scan_id"] = ScanStorage(1, "scan_id")
744
+ first = messages.FileMessage(file_path="/eiger.h5", done=False, successful=False)
745
+ finished = messages.FileMessage(file_path="/eiger.h5", done=True, successful=True)
746
+ for file_msg in [first, finished]:
747
+ manager._file_reference_callback(
748
+ MessageObject(
749
+ topic=MessageEndpoints.public_file("scan_id", "eiger").endpoint, value=file_msg
750
+ )
751
+ )
752
+ assert manager.scan_storage["scan_id"].file_references == {"eiger": finished}
753
+ assert manager._pending_file_references == {}
754
+
755
+
756
+ @pytest.mark.parametrize(
757
+ "name,is_master_file", [("master", False), ("master", True), ("other", True)]
758
+ )
759
+ def test_master_file_notifications_are_not_buffered(file_writer_manager_mock, name, is_master_file):
760
+ manager = file_writer_manager_mock
761
+ file_msg = messages.FileMessage(
762
+ file_path="/master.h5", done=True, successful=True, is_master_file=is_master_file
763
+ )
764
+ manager._file_reference_callback(
765
+ MessageObject(topic=MessageEndpoints.public_file("scan_id", name).endpoint, value=file_msg)
766
+ )
767
+ assert manager.scan_storage == {}
768
+ assert manager._pending_file_references == {}
769
+
770
+
771
+ def test_finished_scan_does_not_buffer_late_notifications(
772
+ file_writer_manager_mock, scan_storage_mock
773
+ ):
774
+ manager = file_writer_manager_mock
775
+ manager.scan_storage["scan_id"] = scan_storage_mock
776
+ with (
777
+ mock.patch(
778
+ "bec_server.file_writer.file_writer_manager.get_full_path", return_value="scan.h5"
779
+ ),
780
+ mock.patch.object(manager.file_writer, "write"),
781
+ ):
782
+ manager.write_file("scan_id")
783
+
784
+ baseline = messages.ScanBaselineMessage(scan_id="scan_id", data={"samz": {}})
785
+ file_msg = messages.FileMessage(file_path="/eiger.h5", done=True, successful=True)
786
+ manager._scan_baseline_callback(
787
+ MessageObject(topic=MessageEndpoints.scan_baseline().endpoint, value=baseline)
788
+ )
789
+ manager._file_reference_callback(
790
+ MessageObject(
791
+ topic=MessageEndpoints.public_file("scan_id", "eiger").endpoint, value=file_msg
792
+ )
793
+ )
794
+ assert manager.scan_storage == {}
795
+ assert manager._pending_baseline_writes == {}
796
+ assert manager._pending_file_references == {}
797
+
798
+
799
+ def test_subscriptions_buffer_early_notifications(file_writer_manager_mock, connected_connector):
800
+ manager = file_writer_manager_mock
801
+ received = threading.Event()
802
+ baseline = messages.ScanBaselineMessage(scan_id="scan_id", data={"samz": {}})
803
+ file_msg = messages.FileMessage(file_path="/eiger.h5", done=True, successful=True)
804
+
805
+ def file_reference_callback(msg):
806
+ manager._file_reference_callback(msg)
807
+ received.set()
808
+
809
+ connected_connector.register(
810
+ MessageEndpoints.scan_baseline(), cb=manager._scan_baseline_callback
811
+ )
812
+ connected_connector.register(
813
+ patterns=MessageEndpoints.public_file("*", "*"), cb=file_reference_callback
814
+ )
815
+ pipe = connected_connector.pipeline()
816
+ connected_connector.set_and_publish(MessageEndpoints.scan_baseline(), baseline, pipe=pipe)
817
+ connected_connector.set_and_publish(
818
+ MessageEndpoints.public_file("scan_id", "eiger"), file_msg, pipe=pipe
819
+ )
820
+ pipe.execute()
821
+ assert received.wait(5), "File reference subscription did not deliver its notification"
822
+ assert manager.scan_storage == {}
823
+ assert manager._pending_baseline_writes == {"scan_id": baseline.data}
824
+ assert manager._pending_file_references == {"scan_id": {"eiger": file_msg}}
825
+
826
+ with mock.patch.object(manager, "write_file") as write:
827
+ manager.update_scan_storage_with_status(
828
+ messages.ScanStatusMessage(
829
+ scan_id="scan_id",
830
+ status="closed",
831
+ scan_type="software_triggered",
832
+ num_monitored_readouts=0,
833
+ readout_priority={"monitored": [], "baseline": ["samz"]},
834
+ info={"scan_number": 1, "monitor_sync": "bec", "baseline_readout_requested": True},
835
+ )
836
+ )
837
+ write.assert_called_once_with("scan_id")
838
+ assert manager.scan_storage["scan_id"].baseline == baseline.data
839
+ assert manager.scan_storage["scan_id"].file_references == {"eiger": file_msg}
840
+ assert manager._pending_baseline_writes == {}
841
+ assert manager._pending_file_references == {}
@@ -13,6 +13,7 @@ from bec_lib.messaging_services import NotificationMessageObject
13
13
  from bec_lib.tests.fixtures import dm_with_devices # noqa: F401
14
14
  from bec_lib.tests.utils import ConnectorMock
15
15
  from bec_lib.utils.scan_utils import compose_cli_input_from_scan_info
16
+ from bec_server.file_writer.file_writer_manager import ScanStorage
16
17
  from bec_server.scan_server.instruction_handler import InstructionHandler
17
18
  from bec_server.scan_server.scans.scan_base import ScanBase, ScanInfo, ScanType
18
19
  from bec_server.scan_server.scans.scan_status import ScanStatus
@@ -388,6 +389,7 @@ def test_build_scan_status_message(action_context):
388
389
  assert msg.dataset_number == 2
389
390
  assert msg.num_points == 3
390
391
  assert msg.num_monitored_readouts == 0
392
+ assert msg.info["baseline_readout_requested"] is False
391
393
  assert msg.scan_type == "software_triggered"
392
394
  assert msg.scan_parameters == {
393
395
  "exp_time": 0.1,
@@ -576,6 +578,27 @@ def test_read_actions_emit_expected_messages_and_point_ids(action_context):
576
578
  assert monitored_msg_2.metadata["device_instr_id"] == monitored_status_2._device_instr_id
577
579
 
578
580
 
581
+ @pytest.mark.parametrize("read_baseline", [False, True])
582
+ def test_baseline_request_in_scan_status_controls_file_readiness(action_context, read_baseline):
583
+ ctx = action_context()
584
+ _set_readout_priority(ctx, baseline=["samz"], monitored=["samx"])
585
+ ctx.actions.read_monitored_devices(wait=False)
586
+ assert ctx.scan.scan_info.baseline_readout_requested is False
587
+ if read_baseline:
588
+ ctx.actions.read_baseline_devices(wait=False)
589
+
590
+ closed_msg = ctx.actions._build_scan_status_message("closed")
591
+ assert closed_msg.info["baseline_readout_requested"] is read_baseline
592
+ storage = ScanStorage(1, closed_msg.scan_id)
593
+ storage.status_msg = closed_msg
594
+ storage.scan_finished = True
595
+ storage.num_monitored_readouts = 1
596
+ storage.append(0, {"samx": {}})
597
+ assert storage.ready_to_write() is (not read_baseline)
598
+ storage.baseline = {"samz": {}}
599
+ assert storage.ready_to_write() is True
600
+
601
+
579
602
  def test_empty_read_and_trigger_actions_return_done_status(action_context):
580
603
  ctx = action_context()
581
604
  ctx.scan.scan_info.metadata["RID"] = "rid-123"
@@ -589,6 +612,7 @@ def test_empty_read_and_trigger_actions_return_done_status(action_context):
589
612
  trigger_status = ctx.actions.trigger_all_devices(wait=False)
590
613
 
591
614
  assert baseline_status.done
615
+ assert ctx.scan.scan_info.baseline_readout_requested is False
592
616
  assert monitored_status.done
593
617
  assert trigger_status.done
594
618
  assert not _sent_device_instructions(ctx, "read")
File without changes
File without changes
File without changes