mx-bluesky 1.4.1a0__py3-none-any.whl → 1.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. mx_bluesky/_version.py +2 -2
  2. mx_bluesky/beamlines/i04/redis_to_murko_forwarder.py +178 -0
  3. mx_bluesky/beamlines/i24/serial/__init__.py +0 -6
  4. mx_bluesky/beamlines/i24/serial/dcid.py +125 -151
  5. mx_bluesky/beamlines/i24/serial/extruder/EX-gui-edm/DiamondExtruder-I24-py3v1.edl +1 -1
  6. mx_bluesky/beamlines/i24/serial/extruder/i24ssx_Extruder_Collect_py3v2.py +66 -36
  7. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/DiamondChipI24-py3v1.edl +1 -1
  8. mx_bluesky/beamlines/i24/serial/fixed_target/FT-gui-edm/MappingLite-oxford_py3v1.edl +2 -46
  9. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Collect_py3v1.py +74 -120
  10. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_Manager_py3v1.py +58 -66
  11. mx_bluesky/beamlines/i24/serial/fixed_target/i24ssx_Chip_StartUp_py3v1.py +1 -19
  12. mx_bluesky/beamlines/i24/serial/parameters/__init__.py +9 -1
  13. mx_bluesky/beamlines/i24/serial/parameters/constants.py +6 -0
  14. mx_bluesky/beamlines/i24/serial/parameters/experiment_parameters.py +75 -16
  15. mx_bluesky/beamlines/i24/serial/parameters/utils.py +19 -0
  16. mx_bluesky/beamlines/i24/serial/setup_beamline/pv.py +2 -0
  17. mx_bluesky/beamlines/i24/serial/setup_beamline/setup_beamline.py +32 -8
  18. mx_bluesky/beamlines/i24/serial/write_nexus.py +66 -67
  19. mx_bluesky/common/parameters/components.py +3 -3
  20. mx_bluesky/common/parameters/constants.py +5 -0
  21. mx_bluesky/hyperion/device_setup_plans/dcm_pitch_roll_mirror_adjuster.py +21 -31
  22. mx_bluesky/hyperion/device_setup_plans/manipulate_sample.py +6 -6
  23. mx_bluesky/hyperion/device_setup_plans/smargon.py +3 -3
  24. mx_bluesky/hyperion/exceptions.py +13 -1
  25. mx_bluesky/hyperion/experiment_plans/flyscan_xray_centre_plan.py +16 -10
  26. mx_bluesky/hyperion/experiment_plans/grid_detect_then_xray_centre_plan.py +0 -8
  27. mx_bluesky/hyperion/experiment_plans/load_centre_collect_full_plan.py +58 -34
  28. mx_bluesky/hyperion/experiment_plans/oav_snapshot_plan.py +8 -2
  29. mx_bluesky/hyperion/experiment_plans/pin_tip_centring_plan.py +3 -3
  30. mx_bluesky/hyperion/experiment_plans/robot_load_and_change_energy.py +30 -26
  31. mx_bluesky/hyperion/experiment_plans/robot_load_then_centre_plan.py +26 -7
  32. mx_bluesky/hyperion/experiment_plans/rotation_scan_plan.py +0 -7
  33. mx_bluesky/hyperion/experiment_plans/set_energy_plan.py +8 -7
  34. mx_bluesky/hyperion/external_interaction/callbacks/__init__.py +0 -4
  35. mx_bluesky/hyperion/external_interaction/callbacks/__main__.py +4 -0
  36. mx_bluesky/hyperion/external_interaction/callbacks/common/callback_util.py +5 -0
  37. mx_bluesky/hyperion/external_interaction/callbacks/robot_load/ispyb_callback.py +18 -10
  38. mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/__init__.py +0 -0
  39. mx_bluesky/hyperion/external_interaction/callbacks/sample_handling/sample_handling_callback.py +84 -0
  40. mx_bluesky/hyperion/external_interaction/callbacks/xray_centre/ispyb_callback.py +10 -7
  41. mx_bluesky/hyperion/external_interaction/exceptions.py +0 -9
  42. mx_bluesky/hyperion/external_interaction/ispyb/exp_eye_store.py +65 -15
  43. mx_bluesky/hyperion/utils/validation.py +1 -1
  44. {mx_bluesky-1.4.1a0.dist-info → mx_bluesky-1.4.2.dist-info}/METADATA +2 -2
  45. {mx_bluesky-1.4.1a0.dist-info → mx_bluesky-1.4.2.dist-info}/RECORD +49 -46
  46. {mx_bluesky-1.4.1a0.dist-info → mx_bluesky-1.4.2.dist-info}/LICENSE +0 -0
  47. {mx_bluesky-1.4.1a0.dist-info → mx_bluesky-1.4.2.dist-info}/WHEEL +0 -0
  48. {mx_bluesky-1.4.1a0.dist-info → mx_bluesky-1.4.2.dist-info}/entry_points.txt +0 -0
  49. {mx_bluesky-1.4.1a0.dist-info → mx_bluesky-1.4.2.dist-info}/top_level.txt +0 -0
@@ -10,7 +10,7 @@ import bluesky.preprocessors as bpp
10
10
  import pydantic
11
11
  from blueapi.core import BlueskyContext
12
12
  from bluesky.utils import Msg
13
- from dodal.devices.aperturescatterguard import ApertureScatterguard
13
+ from dodal.devices.aperturescatterguard import ApertureScatterguard, ApertureValue
14
14
  from dodal.devices.attenuator import Attenuator
15
15
  from dodal.devices.dcm import DCM
16
16
  from dodal.devices.focusing_mirror import FocusingMirrorWithStripes, MirrorVoltages
@@ -94,7 +94,11 @@ def take_robot_snapshots(oav: OAV, webcam: Webcam, directory: Path):
94
94
  def prepare_for_robot_load(
95
95
  aperture_scatterguard: ApertureScatterguard, smargon: Smargon
96
96
  ):
97
- yield from bps.trigger(aperture_scatterguard.move_out, group="prepare_robot_load")
97
+ yield from bps.abs_set(
98
+ aperture_scatterguard,
99
+ ApertureValue.ROBOT_LOAD,
100
+ group="prepare_robot_load",
101
+ )
98
102
 
99
103
  yield from bps.mv(smargon.stub_offsets, StubPosition.RESET_TO_ROBOT_LOAD) # type: ignore # See: https://github.com/bluesky/bluesky/issues/1809
100
104
 
@@ -124,11 +128,7 @@ def do_robot_load(
124
128
  group="robot_load",
125
129
  )
126
130
 
127
- if demand_energy_ev:
128
- yield from set_energy_plan(
129
- demand_energy_ev / 1000,
130
- cast(SetEnergyComposite, composite),
131
- )
131
+ yield from set_energy_plan(demand_energy_ev, cast(SetEnergyComposite, composite))
132
132
 
133
133
  yield from bps.wait("robot_load")
134
134
 
@@ -214,24 +214,28 @@ def robot_load_and_change_energy_plan(
214
214
  yield from prepare_for_robot_load(
215
215
  composite.aperture_scatterguard, composite.smargon
216
216
  )
217
- yield from bpp.run_wrapper(
218
- robot_load_and_snapshots(
219
- composite,
220
- sample_location,
221
- params.snapshot_directory,
222
- params.thawing_time,
223
- params.demand_energy_ev,
224
- ),
225
- md={
226
- "subplan_name": CONST.PLAN.ROBOT_LOAD,
227
- "metadata": {
228
- "visit": params.visit,
229
- "sample_id": params.sample_id,
230
- "sample_puck": sample_location.puck,
231
- "sample_pin": sample_location.pin,
217
+
218
+ yield from bpp.set_run_key_wrapper(
219
+ bpp.run_wrapper(
220
+ robot_load_and_snapshots(
221
+ composite,
222
+ sample_location,
223
+ params.snapshot_directory,
224
+ params.thawing_time,
225
+ params.demand_energy_ev,
226
+ ),
227
+ md={
228
+ "subplan_name": CONST.PLAN.ROBOT_LOAD,
229
+ "metadata": {
230
+ "visit": params.visit,
231
+ "sample_id": params.sample_id,
232
+ "sample_puck": sample_location.puck,
233
+ "sample_pin": sample_location.pin,
234
+ },
235
+ "activate_callbacks": [
236
+ "RobotLoadISPyBCallback",
237
+ ],
232
238
  },
233
- "activate_callbacks": [
234
- "RobotLoadISPyBCallback",
235
- ],
236
- },
239
+ ),
240
+ CONST.PLAN.ROBOT_LOAD_AND_SNAPSHOTS,
237
241
  )
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from math import isclose
3
4
  from typing import cast
4
5
 
5
6
  import bluesky.preprocessors as bpp
6
7
  import pydantic
7
8
  from blueapi.core import BlueskyContext
9
+ from bluesky import plan_stubs as bps
8
10
  from bluesky.utils import MsgGenerator
9
11
  from dodal.devices.aperturescatterguard import ApertureScatterguard
10
12
  from dodal.devices.attenuator import Attenuator
@@ -56,6 +58,10 @@ from mx_bluesky.hyperion.experiment_plans.robot_load_and_change_energy import (
56
58
  pin_already_loaded,
57
59
  robot_load_and_change_energy_plan,
58
60
  )
61
+ from mx_bluesky.hyperion.experiment_plans.set_energy_plan import (
62
+ SetEnergyComposite,
63
+ set_energy_plan,
64
+ )
59
65
  from mx_bluesky.hyperion.parameters.constants import CONST
60
66
 
61
67
 
@@ -170,20 +176,33 @@ def robot_load_then_xray_centre(
170
176
  yield from pin_already_loaded(composite.robot, sample_location)
171
177
  )
172
178
 
173
- doing_chi_change = parameters.chi_start_deg is not None
179
+ current_chi = yield from bps.rd(composite.smargon.chi)
180
+ LOGGER.info(f"Read back current smargon chi of {current_chi} degrees.")
181
+ doing_chi_change = parameters.chi_start_deg is not None and not isclose(
182
+ current_chi, parameters.chi_start_deg, abs_tol=0.001
183
+ )
174
184
 
175
185
  if doing_sample_load:
186
+ LOGGER.info("Pin not loaded, loading and centring")
176
187
  plan = _robot_load_then_flyscan_plan(
177
188
  composite,
178
189
  parameters,
179
190
  )
180
- LOGGER.info("Pin not loaded, loading and centring")
181
- elif doing_chi_change:
182
- plan = _flyscan_plan_from_robot_load_params(composite, parameters)
183
- LOGGER.info("Pin already loaded but chi changed so centring")
184
191
  else:
185
- LOGGER.info("Pin already loaded and chi not changed so doing nothing")
186
- return
192
+ # Robot load normally sets the energy so we should do this explicitly if no load is
193
+ # being done
194
+ demand_energy_ev = parameters.demand_energy_ev
195
+ LOGGER.info(f"Setting the energy to {demand_energy_ev}eV")
196
+ yield from set_energy_plan(
197
+ demand_energy_ev, cast(SetEnergyComposite, composite)
198
+ )
199
+
200
+ if doing_chi_change:
201
+ plan = _flyscan_plan_from_robot_load_params(composite, parameters)
202
+ LOGGER.info("Pin already loaded but chi changed so centring")
203
+ else:
204
+ LOGGER.info("Pin already loaded and chi not changed so doing nothing")
205
+ return
187
206
 
188
207
  detector_params = yield from fill_in_energy_if_not_supplied(
189
208
  composite.dcm, parameters.detector_params
@@ -331,13 +331,6 @@ def _move_and_rotation(
331
331
  yield from setup_beamline_for_OAV(
332
332
  composite.smargon, composite.backlight, composite.aperture_scatterguard
333
333
  )
334
- yield from bps.wait(group=CONST.WAIT.READY_FOR_OAV)
335
- if params.selected_aperture:
336
- yield from bps.abs_set(
337
- composite.aperture_scatterguard.aperture_outside_beam,
338
- params.selected_aperture,
339
- group=CONST.WAIT.ROTATION_READY_FOR_DC,
340
- )
341
334
  yield from oav_snapshot_plan(composite, params, oav_params)
342
335
  yield from rotation_scan_plan(
343
336
  composite,
@@ -48,12 +48,13 @@ def _set_energy_plan(
48
48
 
49
49
 
50
50
  def set_energy_plan(
51
- energy_kev,
51
+ energy_ev: float | None,
52
52
  composite: SetEnergyComposite,
53
53
  ):
54
- yield from transmission_and_xbpm_feedback_for_collection_wrapper(
55
- _set_energy_plan(energy_kev, composite),
56
- composite.xbpm_feedback,
57
- composite.attenuator,
58
- DESIRED_TRANSMISSION_FRACTION,
59
- )
54
+ if energy_ev:
55
+ yield from transmission_and_xbpm_feedback_for_collection_wrapper(
56
+ _set_energy_plan(energy_ev / 1000, composite),
57
+ composite.xbpm_feedback,
58
+ composite.attenuator,
59
+ DESIRED_TRANSMISSION_FRACTION,
60
+ )
@@ -4,7 +4,3 @@ execution of an experimental plan.
4
4
 
5
5
  Callbacks used for the Hyperion fast grid scan are prefixed with 'FGS'.
6
6
  """
7
-
8
- from .__main__ import main
9
-
10
- __all__ = ["main"]
@@ -20,6 +20,9 @@ from mx_bluesky.hyperion.external_interaction.callbacks.rotation.ispyb_callback
20
20
  from mx_bluesky.hyperion.external_interaction.callbacks.rotation.nexus_callback import (
21
21
  RotationNexusFileCallback,
22
22
  )
23
+ from mx_bluesky.hyperion.external_interaction.callbacks.sample_handling.sample_handling_callback import (
24
+ SampleHandlingCallback,
25
+ )
23
26
  from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import (
24
27
  GridscanISPyBCallback,
25
28
  )
@@ -49,6 +52,7 @@ def setup_callbacks():
49
52
  RotationISPyBCallback(emit=zocalo),
50
53
  LogUidTaggingCallback(),
51
54
  RobotLoadISPyBCallback(),
55
+ SampleHandlingCallback(),
52
56
  ]
53
57
 
54
58
 
@@ -11,6 +11,9 @@ from mx_bluesky.hyperion.external_interaction.callbacks.rotation.ispyb_callback
11
11
  from mx_bluesky.hyperion.external_interaction.callbacks.rotation.nexus_callback import (
12
12
  RotationNexusFileCallback,
13
13
  )
14
+ from mx_bluesky.hyperion.external_interaction.callbacks.sample_handling.sample_handling_callback import (
15
+ SampleHandlingCallback,
16
+ )
14
17
  from mx_bluesky.hyperion.external_interaction.callbacks.xray_centre.ispyb_callback import (
15
18
  GridscanISPyBCallback,
16
19
  )
@@ -53,6 +56,7 @@ def create_load_centre_collect_callbacks() -> (
53
56
  RobotLoadISPyBCallback,
54
57
  RotationNexusFileCallback,
55
58
  RotationISPyBCallback,
59
+ SampleHandlingCallback,
56
60
  ]
57
61
  ):
58
62
  return (
@@ -61,4 +65,5 @@ def create_load_centre_collect_callbacks() -> (
61
65
  RobotLoadISPyBCallback(),
62
66
  RotationNexusFileCallback(),
63
67
  RotationISPyBCallback(emit=ZocaloCallback()),
68
+ SampleHandlingCallback(),
64
69
  )
@@ -2,8 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
- from event_model.documents import EventDescriptor
6
-
7
5
  from mx_bluesky.hyperion.external_interaction.callbacks.common.ispyb_mapping import (
8
6
  get_proposal_and_session_from_visit_string,
9
7
  )
@@ -11,6 +9,7 @@ from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback i
11
9
  PlanReactiveCallback,
12
10
  )
13
11
  from mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store import (
12
+ BLSampleStatus,
14
13
  ExpeyeInteraction,
15
14
  RobotActionID,
16
15
  )
@@ -25,6 +24,7 @@ class RobotLoadISPyBCallback(PlanReactiveCallback):
25
24
  def __init__(self) -> None:
26
25
  ISPYB_LOGGER.debug("Initialising ISPyB Robot Load Callback")
27
26
  super().__init__(log=ISPYB_LOGGER)
27
+ self._metadata: dict | None = None
28
28
  self.run_uid: str | None = None
29
29
  self.descriptors: dict[str, EventDescriptor] = {}
30
30
  self.action_id: RobotActionID | None = None
@@ -35,16 +35,17 @@ class RobotLoadISPyBCallback(PlanReactiveCallback):
35
35
  if doc.get("subplan_name") == CONST.PLAN.ROBOT_LOAD:
36
36
  ISPYB_LOGGER.debug(f"ISPyB robot load callback received: {doc}")
37
37
  self.run_uid = doc.get("uid")
38
- assert isinstance(metadata := doc.get("metadata"), dict)
38
+ self._metadata = doc.get("metadata")
39
+ assert isinstance(self._metadata, dict)
39
40
  proposal, session = get_proposal_and_session_from_visit_string(
40
- metadata["visit"]
41
+ self._metadata["visit"]
41
42
  )
42
43
  self.action_id = self.expeye.start_load(
43
44
  proposal,
44
45
  session,
45
- metadata["sample_id"],
46
- metadata["sample_puck"],
47
- metadata["sample_pin"],
46
+ self._metadata["sample_id"],
47
+ self._metadata["sample_puck"],
48
+ self._metadata["sample_pin"],
48
49
  )
49
50
  return super().activity_gated_start(doc)
50
51
 
@@ -77,10 +78,17 @@ class RobotLoadISPyBCallback(PlanReactiveCallback):
77
78
  assert (
78
79
  self.action_id is not None
79
80
  ), "ISPyB Robot load callback stop called unexpectedly"
80
- exit_status = (
81
- doc.get("exit_status") or "Exit status not available in stop document!"
82
- )
81
+ exit_status = doc.get("exit_status")
82
+ assert exit_status, "Exit status not available in stop document!"
83
+ assert self._metadata, "Metadata not received before stop document."
83
84
  reason = doc.get("reason") or "OK"
85
+
84
86
  self.expeye.end_load(self.action_id, exit_status, reason)
87
+ self.expeye.update_sample_status(
88
+ self._metadata["sample_id"],
89
+ BLSampleStatus.LOADED
90
+ if exit_status == "success"
91
+ else BLSampleStatus.ERROR_BEAMLINE,
92
+ )
85
93
  self.action_id = None
86
94
  return super().activity_gated_stop(doc)
@@ -0,0 +1,84 @@
1
+ import dataclasses
2
+ from collections.abc import Generator
3
+ from functools import partial
4
+ from typing import Any
5
+
6
+ import bluesky.plan_stubs as bps
7
+ from bluesky.preprocessors import contingency_wrapper
8
+ from bluesky.utils import Msg, make_decorator
9
+ from event_model import Event, EventDescriptor, RunStart
10
+
11
+ from mx_bluesky.hyperion.exceptions import CrystalNotFoundException, SampleException
12
+ from mx_bluesky.hyperion.external_interaction.callbacks.common.abstract_event import (
13
+ AbstractEvent,
14
+ )
15
+ from mx_bluesky.hyperion.external_interaction.callbacks.plan_reactive_callback import (
16
+ PlanReactiveCallback,
17
+ )
18
+ from mx_bluesky.hyperion.external_interaction.ispyb.exp_eye_store import (
19
+ BLSampleStatus,
20
+ ExpeyeInteraction,
21
+ )
22
+ from mx_bluesky.hyperion.log import ISPYB_LOGGER
23
+ from mx_bluesky.hyperion.parameters.constants import CONST
24
+
25
+ # TODO remove this event-raising shenanigans once
26
+ # https://github.com/bluesky/bluesky/issues/1829 is addressed
27
+
28
+
29
+ @dataclasses.dataclass(frozen=True)
30
+ class _ExceptionEvent(AbstractEvent):
31
+ exception_type: str
32
+
33
+
34
+ def _exception_interceptor(exception: Exception) -> Generator[Msg, Any, Any]:
35
+ yield from bps.create(CONST.DESCRIPTORS.SAMPLE_HANDLING_EXCEPTION)
36
+ yield from bps.read(_ExceptionEvent(type(exception).__name__))
37
+ yield from bps.save()
38
+
39
+
40
+ sample_handling_callback_decorator = make_decorator(
41
+ partial(contingency_wrapper, except_plan=_exception_interceptor)
42
+ )
43
+
44
+
45
+ class SampleHandlingCallback(PlanReactiveCallback):
46
+ """Intercepts exceptions from experiment plans and updates the ISPyB BLSampleStatus
47
+ field according to the type of exception raised."""
48
+
49
+ def __init__(self):
50
+ super().__init__(log=ISPYB_LOGGER)
51
+ self._sample_id: int | None = None
52
+ self._descriptor: str | None = None
53
+
54
+ def activity_gated_start(self, doc: RunStart):
55
+ if not self._sample_id:
56
+ sample_id = doc.get("metadata", {}).get("sample_id")
57
+ self.log.info(f"Recording sample ID at run start {sample_id}")
58
+ self._sample_id = sample_id
59
+
60
+ def activity_gated_descriptor(self, doc: EventDescriptor) -> EventDescriptor | None:
61
+ if doc.get("name") == CONST.DESCRIPTORS.SAMPLE_HANDLING_EXCEPTION:
62
+ self._descriptor = doc["uid"]
63
+ return super().activity_gated_descriptor(doc)
64
+
65
+ def activity_gated_event(self, doc: Event) -> Event | None:
66
+ if doc["descriptor"] == self._descriptor:
67
+ exception_type = doc["data"]["exception_type"]
68
+ self.log.info(
69
+ f"Sample handling callback intercepted exception of type {exception_type}"
70
+ )
71
+ self._record_exception(exception_type)
72
+ return doc
73
+
74
+ def _record_exception(self, exception_type: str):
75
+ expeye = ExpeyeInteraction()
76
+ assert self._sample_id, "Unable to record exception due to no sample ID"
77
+ sample_status = self._decode_sample_status(exception_type)
78
+ expeye.update_sample_status(self._sample_id, sample_status)
79
+
80
+ def _decode_sample_status(self, exception_type: str) -> BLSampleStatus:
81
+ match exception_type:
82
+ case SampleException.__name__ | CrystalNotFoundException.__name__:
83
+ return BLSampleStatus.ERROR_SAMPLE
84
+ return BLSampleStatus.ERROR_BEAMLINE
@@ -53,13 +53,16 @@ if TYPE_CHECKING:
53
53
 
54
54
 
55
55
  def ispyb_activation_wrapper(plan_generator: MsgGenerator, parameters):
56
- return bpp.run_wrapper(
57
- plan_generator,
58
- md={
59
- "activate_callbacks": ["GridscanISPyBCallback"],
60
- "subplan_name": CONST.PLAN.GRID_DETECT_AND_DO_GRIDSCAN,
61
- "hyperion_parameters": parameters.model_dump_json(),
62
- },
56
+ return bpp.set_run_key_wrapper(
57
+ bpp.run_wrapper(
58
+ plan_generator,
59
+ md={
60
+ "activate_callbacks": ["GridscanISPyBCallback"],
61
+ "subplan_name": CONST.PLAN.GRID_DETECT_AND_DO_GRIDSCAN,
62
+ "hyperion_parameters": parameters.model_dump_json(),
63
+ },
64
+ ),
65
+ CONST.PLAN.ISPYB_ACTIVATION,
63
66
  )
64
67
 
65
68
 
@@ -1,13 +1,4 @@
1
- from mx_bluesky.hyperion.exceptions import WarningException
2
-
3
-
4
1
  class ISPyBDepositionNotMade(Exception):
5
2
  """Raised when the ISPyB or Zocalo callbacks can't access ISPyB deposition numbers."""
6
3
 
7
4
  pass
8
-
9
-
10
- class NoCentreFoundException(WarningException):
11
- """Error for if zocalo is unable to find the centre during a gridscan."""
12
-
13
- pass
@@ -1,4 +1,6 @@
1
1
  import configparser
2
+ from dataclasses import dataclass
3
+ from enum import StrEnum
2
4
 
3
5
  from requests import patch, post
4
6
  from requests.auth import AuthBase
@@ -29,20 +31,44 @@ def _get_base_url_and_token() -> tuple[str, str]:
29
31
  return expeye_config["url"], expeye_config["token"]
30
32
 
31
33
 
34
+ def _send_and_get_response(auth, url, data, send_func) -> dict:
35
+ response = send_func(url, auth=auth, json=data)
36
+ if not response.ok:
37
+ raise ISPyBDepositionNotMade(f"Could not write {data} to {url}: {response}")
38
+ return response.json()
39
+
40
+
41
+ @dataclass
42
+ class BLSample:
43
+ container_id: int
44
+ bl_sample_id: int
45
+ bl_sample_status: str | None
46
+
47
+
48
+ class BLSampleStatus(StrEnum):
49
+ # The sample has been loaded
50
+ LOADED = "LOADED"
51
+ # Problem with the sample e.g. pin too long/short
52
+ ERROR_SAMPLE = "ERROR - sample"
53
+ # Any other general error
54
+ ERROR_BEAMLINE = "ERROR - beamline"
55
+
56
+
57
+ assert all(
58
+ len(value) <= 20 for value in BLSampleStatus
59
+ ), "Column size limit of 20 for BLSampleStatus"
60
+
61
+
32
62
  class ExpeyeInteraction:
63
+ """Exposes functionality from the Expeye core API"""
64
+
33
65
  CREATE_ROBOT_ACTION = "/proposals/{proposal}/sessions/{visit_number}/robot-actions"
34
66
  UPDATE_ROBOT_ACTION = "/robot-actions/{action_id}"
35
67
 
36
68
  def __init__(self) -> None:
37
69
  url, token = _get_base_url_and_token()
38
- self.base_url = url
39
- self.auth = BearerAuth(token)
40
-
41
- def _send_and_get_response(self, url, data, send_func) -> dict:
42
- response = send_func(url, auth=self.auth, json=data)
43
- if not response.ok:
44
- raise ISPyBDepositionNotMade(f"Could not write {data} to {url}: {response}")
45
- return response.json()
70
+ self._base_url = url
71
+ self._auth = BearerAuth(token)
46
72
 
47
73
  def start_load(
48
74
  self,
@@ -66,7 +92,7 @@ class ExpeyeInteraction:
66
92
  Returns:
67
93
  RobotActionID: The id of the robot load action that is created
68
94
  """
69
- url = self.base_url + self.CREATE_ROBOT_ACTION.format(
95
+ url = self._base_url + self.CREATE_ROBOT_ACTION.format(
70
96
  proposal=proposal_reference, visit_number=visit_number
71
97
  )
72
98
 
@@ -77,7 +103,7 @@ class ExpeyeInteraction:
77
103
  "containerLocation": container_location,
78
104
  "dewarLocation": dewar_location,
79
105
  }
80
- response = self._send_and_get_response(url, data, post)
106
+ response = _send_and_get_response(self._auth, url, data, post)
81
107
  return response["robotActionId"]
82
108
 
83
109
  def update_barcode_and_snapshots(
@@ -95,14 +121,14 @@ class ExpeyeInteraction:
95
121
  snapshot_before_path (str): Path to the snapshot before robot load
96
122
  snapshot_after_path (str): Path to the snapshot after robot load
97
123
  """
98
- url = self.base_url + self.UPDATE_ROBOT_ACTION.format(action_id=action_id)
124
+ url = self._base_url + self.UPDATE_ROBOT_ACTION.format(action_id=action_id)
99
125
 
100
126
  data = {
101
127
  "sampleBarcode": barcode,
102
128
  "xtalSnapshotBefore": snapshot_before_path,
103
129
  "xtalSnapshotAfter": snapshot_after_path,
104
130
  }
105
- self._send_and_get_response(url, data, patch)
131
+ _send_and_get_response(self._auth, url, data, patch)
106
132
 
107
133
  def end_load(self, action_id: RobotActionID, status: str, reason: str):
108
134
  """Finish an existing robot action, providing final information about how it went
@@ -113,13 +139,37 @@ class ExpeyeInteraction:
113
139
  otherwise error
114
140
  reason (str): If the status is in error than the reason for that error
115
141
  """
116
- url = self.base_url + self.UPDATE_ROBOT_ACTION.format(action_id=action_id)
142
+ url = self._base_url + self.UPDATE_ROBOT_ACTION.format(action_id=action_id)
117
143
 
118
144
  run_status = "SUCCESS" if status == "success" else "ERROR"
119
145
 
120
146
  data = {
121
147
  "endTimestamp": get_current_time_string(),
122
148
  "status": run_status,
123
- "message": reason,
149
+ "message": reason[:255] if reason else "",
124
150
  }
125
- self._send_and_get_response(url, data, patch)
151
+ _send_and_get_response(self._auth, url, data, patch)
152
+
153
+ def update_sample_status(
154
+ self, bl_sample_id: int, bl_sample_status: BLSampleStatus
155
+ ) -> BLSample:
156
+ """Update the blSampleStatus of a sample.
157
+ Args:
158
+ bl_sample_id: The sample ID
159
+ bl_sample_status: The sample status
160
+ status_message: An optional message
161
+ Returns:
162
+ The updated sample
163
+ """
164
+ data = {"blSampleStatus": (str(bl_sample_status))}
165
+ response = _send_and_get_response(
166
+ self._auth, self._base_url + f"/samples/{bl_sample_id}", data, patch
167
+ )
168
+ return self._sample_from_json(response)
169
+
170
+ def _sample_from_json(self, response) -> BLSample:
171
+ return BLSample(
172
+ bl_sample_id=response["blSampleId"],
173
+ bl_sample_status=response["blSampleStatus"],
174
+ container_id=response["containerId"],
175
+ )
@@ -9,7 +9,7 @@ import bluesky.preprocessors as bpp
9
9
  from bluesky.run_engine import RunEngine
10
10
  from dodal.beamlines import i03
11
11
  from dodal.devices.oav.oav_parameters import OAVConfig
12
- from ophyd_async.core import set_mock_value
12
+ from ophyd_async.testing import set_mock_value
13
13
 
14
14
  from mx_bluesky.hyperion.device_setup_plans.read_hardware_for_setup import (
15
15
  read_hardware_during_collection,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mx-bluesky
3
- Version: 1.4.1a0
3
+ Version: 1.4.2
4
4
  Summary: Bluesky tools for MX Beamlines at DLS
5
5
  Author-email: Dominic Oram <dominic.oram@diamond.ac.uk>
6
6
  License: Apache License
@@ -239,7 +239,7 @@ Requires-Dist: daq-config-server>=0.1.1
239
239
  Requires-Dist: ophyd==1.9.0
240
240
  Requires-Dist: ophyd-async>=0.8a5
241
241
  Requires-Dist: bluesky>=1.13.0a4
242
- Requires-Dist: dls-dodal==1.36.1a
242
+ Requires-Dist: dls-dodal==1.36.3
243
243
  Provides-Extra: dev
244
244
  Requires-Dist: black; extra == "dev"
245
245
  Requires-Dist: build; extra == "dev"