ophyd-async 0.12.3__py3-none-any.whl → 0.13.1__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 (39) hide show
  1. ophyd_async/_version.py +16 -3
  2. ophyd_async/core/__init__.py +11 -0
  3. ophyd_async/core/_detector.py +7 -10
  4. ophyd_async/core/_enums.py +21 -0
  5. ophyd_async/core/_signal.py +9 -9
  6. ophyd_async/core/_utils.py +5 -4
  7. ophyd_async/epics/adandor/_andor.py +1 -2
  8. ophyd_async/epics/adaravis/__init__.py +1 -2
  9. ophyd_async/epics/adaravis/_aravis_controller.py +4 -4
  10. ophyd_async/epics/adaravis/_aravis_io.py +2 -12
  11. ophyd_async/epics/adcore/__init__.py +4 -2
  12. ophyd_async/epics/adcore/_core_detector.py +1 -2
  13. ophyd_async/epics/adcore/_core_io.py +60 -8
  14. ophyd_async/epics/adcore/_core_logic.py +4 -4
  15. ophyd_async/epics/adcore/_core_writer.py +10 -7
  16. ophyd_async/epics/adcore/_hdf_writer.py +12 -7
  17. ophyd_async/epics/adcore/_utils.py +38 -0
  18. ophyd_async/epics/adkinetix/_kinetix_io.py +4 -4
  19. ophyd_async/epics/adpilatus/_pilatus.py +2 -6
  20. ophyd_async/epics/advimba/__init__.py +0 -2
  21. ophyd_async/epics/advimba/_vimba_controller.py +6 -9
  22. ophyd_async/epics/advimba/_vimba_io.py +3 -10
  23. ophyd_async/epics/core/_aioca.py +6 -2
  24. ophyd_async/epics/core/_p4p.py +2 -3
  25. ophyd_async/epics/core/_pvi_connector.py +1 -1
  26. ophyd_async/epics/pmac/__init__.py +6 -1
  27. ophyd_async/epics/pmac/_pmac_io.py +1 -0
  28. ophyd_async/epics/pmac/_utils.py +231 -0
  29. ophyd_async/epics/testing/_example_ioc.py +1 -2
  30. ophyd_async/plan_stubs/_nd_attributes.py +11 -37
  31. ophyd_async/plan_stubs/_settings.py +1 -1
  32. ophyd_async/tango/core/_tango_transport.py +2 -2
  33. ophyd_async/testing/_assert.py +6 -6
  34. ophyd_async/testing/_one_of_everything.py +1 -1
  35. {ophyd_async-0.12.3.dist-info → ophyd_async-0.13.1.dist-info}/METADATA +5 -4
  36. {ophyd_async-0.12.3.dist-info → ophyd_async-0.13.1.dist-info}/RECORD +39 -37
  37. {ophyd_async-0.12.3.dist-info → ophyd_async-0.13.1.dist-info}/WHEEL +0 -0
  38. {ophyd_async-0.12.3.dist-info → ophyd_async-0.13.1.dist-info}/licenses/LICENSE +0 -0
  39. {ophyd_async-0.12.3.dist-info → ophyd_async-0.13.1.dist-info}/top_level.txt +0 -0
@@ -1,18 +1,15 @@
1
1
  import asyncio
2
2
 
3
- from ophyd_async.core import (
4
- DetectorTrigger,
5
- TriggerInfo,
6
- )
3
+ from ophyd_async.core import DetectorTrigger, OnOff, TriggerInfo
7
4
  from ophyd_async.epics import adcore
8
5
 
9
- from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaOnOff, VimbaTriggerSource
6
+ from ._vimba_io import VimbaDriverIO, VimbaExposeOutMode, VimbaTriggerSource
10
7
 
11
8
  TRIGGER_MODE = {
12
- DetectorTrigger.INTERNAL: VimbaOnOff.OFF,
13
- DetectorTrigger.CONSTANT_GATE: VimbaOnOff.ON,
14
- DetectorTrigger.VARIABLE_GATE: VimbaOnOff.ON,
15
- DetectorTrigger.EDGE_TRIGGER: VimbaOnOff.ON,
9
+ DetectorTrigger.INTERNAL: OnOff.OFF,
10
+ DetectorTrigger.CONSTANT_GATE: OnOff.ON,
11
+ DetectorTrigger.VARIABLE_GATE: OnOff.ON,
12
+ DetectorTrigger.EDGE_TRIGGER: OnOff.ON,
16
13
  }
17
14
 
18
15
  EXPOSE_OUT_MODE = {
@@ -1,6 +1,6 @@
1
1
  from typing import Annotated as A
2
2
 
3
- from ophyd_async.core import SignalRW, StrictEnum
3
+ from ophyd_async.core import OnOff, SignalRW, StrictEnum
4
4
  from ophyd_async.epics import adcore
5
5
  from ophyd_async.epics.core import PvSuffix
6
6
 
@@ -30,17 +30,10 @@ class VimbaTriggerSource(StrictEnum):
30
30
  class VimbaOverlap(StrictEnum):
31
31
  """Overlap modes for the Vimba detector."""
32
32
 
33
- OFF = "Off"
33
+ OFF = OnOff.OFF.value
34
34
  PREV_FRAME = "PreviousFrame"
35
35
 
36
36
 
37
- class VimbaOnOff(StrictEnum):
38
- """On/Off modes on the Vimba detector."""
39
-
40
- ON = "On"
41
- OFF = "Off"
42
-
43
-
44
37
  class VimbaExposeOutMode(StrictEnum):
45
38
  """Exposure control modes for Vimba detectors."""
46
39
 
@@ -55,6 +48,6 @@ class VimbaDriverIO(adcore.ADBaseIO):
55
48
  SignalRW[VimbaConvertFormat], PvSuffix("ConvertPixelFormat")
56
49
  ]
57
50
  trigger_source: A[SignalRW[VimbaTriggerSource], PvSuffix("TriggerSource")]
58
- trigger_mode: A[SignalRW[VimbaOnOff], PvSuffix("TriggerMode")]
51
+ trigger_mode: A[SignalRW[OnOff], PvSuffix("TriggerMode")]
59
52
  trigger_overlap: A[SignalRW[VimbaOverlap], PvSuffix("TriggerOverlap")]
60
53
  exposure_mode: A[SignalRW[VimbaExposeOutMode], PvSuffix("ExposureMode")]
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ import os
2
3
  import sys
3
4
  import typing
4
5
  from collections.abc import Mapping, Sequence
@@ -40,6 +41,10 @@ from ._util import EpicsSignalBackend, format_datatype, get_supported_values
40
41
  logger = logging.getLogger("ophyd_async")
41
42
 
42
43
 
44
+ def _all_updates() -> bool:
45
+ return os.environ.get("OPHYD_ASYNC_EPICS_CA_KEEP_ALL_UPDATES", "True") == "True"
46
+
47
+
43
48
  def _limits_from_augmented_value(value: AugmentedValue) -> Limits:
44
49
  def get_limits(limit: str) -> LimitsRange | None:
45
50
  low = getattr(value, f"lower_{limit}_limit", nan)
@@ -250,12 +255,11 @@ class CaSignalBackend(EpicsSignalBackend[SignalDatatypeT]):
250
255
  datatype: type[SignalDatatypeT] | None,
251
256
  read_pv: str = "",
252
257
  write_pv: str = "",
253
- all_updates: bool = True,
254
258
  ):
255
259
  self.converter: CaConverter = DisconnectedCaConverter(float, dbr.DBR_DOUBLE)
256
260
  self.initial_values: dict[str, AugmentedValue] = {}
257
261
  self.subscription: Subscription | None = None
258
- self._all_updates = all_updates
262
+ self._all_updates = _all_updates()
259
263
  super().__init__(datatype, read_pv, write_pv)
260
264
 
261
265
  def source(self, name: str, read: bool):
@@ -22,7 +22,6 @@ from ophyd_async.core import (
22
22
  SignalDatatype,
23
23
  SignalDatatypeT,
24
24
  SignalMetadata,
25
- StrictEnum,
26
25
  Table,
27
26
  get_enum_cls,
28
27
  get_unique,
@@ -82,7 +81,7 @@ def _metadata_from_value(datatype: type[SignalDatatype], value: Any) -> SignalMe
82
81
  if (limits := _limits_from_value(value)) and specifier[-1] in _number_specifiers:
83
82
  metadata["limits"] = limits
84
83
  # Get choices from display or value
85
- if datatype is str or issubclass(datatype, StrictEnum):
84
+ if datatype is str or get_enum_cls(datatype) is not None:
86
85
  if hasattr(display_data, "choices"):
87
86
  metadata["choices"] = display_data.choices
88
87
  elif hasattr(value_data, "choices"):
@@ -327,7 +326,7 @@ def context() -> Context:
327
326
  async def pvget_with_timeout(pv: str, timeout: float) -> Any:
328
327
  try:
329
328
  return await asyncio.wait_for(context().get(pv), timeout=timeout)
330
- except asyncio.TimeoutError as exc:
329
+ except TimeoutError as exc:
331
330
  logger.debug(f"signal pva://{pv} timed out", exc_info=True)
332
331
  raise NotConnected(f"pva://{pv}") from exc
333
332
 
@@ -6,13 +6,13 @@ from ophyd_async.core import (
6
6
  Device,
7
7
  DeviceConnector,
8
8
  DeviceFiller,
9
+ LazyMock,
9
10
  Signal,
10
11
  SignalR,
11
12
  SignalRW,
12
13
  SignalW,
13
14
  SignalX,
14
15
  )
15
- from ophyd_async.core._utils import LazyMock
16
16
 
17
17
  from ._epics_connector import fill_backend_with_prefix
18
18
  from ._signal import PvaSignalBackend, pvget_with_timeout
@@ -1,3 +1,8 @@
1
1
  from ._pmac_io import PmacAxisAssignmentIO, PmacCoordIO, PmacIO, PmacTrajectoryIO
2
2
 
3
- __all__ = ["PmacAxisAssignmentIO", "PmacCoordIO", "PmacIO", "PmacTrajectoryIO"]
3
+ __all__ = [
4
+ "PmacAxisAssignmentIO",
5
+ "PmacCoordIO",
6
+ "PmacIO",
7
+ "PmacTrajectoryIO",
8
+ ]
@@ -69,6 +69,7 @@ class PmacCoordIO(Device):
69
69
 
70
70
  def __init__(self, prefix: str, name: str = "") -> None:
71
71
  self.defer_moves = epics_signal_rw(bool, f"{prefix}DeferMoves")
72
+ self.cs_port = epics_signal_r(str, f"{prefix}Port")
72
73
  self.cs_axis_setpoint = DeviceVector(
73
74
  {
74
75
  i + 1: epics_signal_rw(np.float64, f"{prefix}M{i + 1}:DirectDemand")
@@ -0,0 +1,231 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from collections.abc import Sequence
5
+ from dataclasses import dataclass
6
+
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+ from scanspec.core import Slice
10
+
11
+ from ophyd_async.core import error_if_none, gather_dict
12
+ from ophyd_async.epics.motor import Motor
13
+
14
+ from ._pmac_io import CS_LETTERS, PmacIO
15
+
16
+ # PMAC durations are in milliseconds
17
+ # We must convert from scanspec durations (seconds) to milliseconds
18
+ # PMAC motion program multiples durations by 0.001
19
+ # (see https://github.com/DiamondLightSource/pmac/blob/afe81f8bb9179c3a20eff351f30bc6cfd1539ad9/pmacApp/pmc/trajectory_scan_code_ppmac.pmc#L241)
20
+ # Therefore, we must divide scanspec durations by 10e-6
21
+ TICK_S = 0.000001
22
+
23
+
24
+ @dataclass
25
+ class _Trajectory:
26
+ positions: dict[Motor, np.ndarray]
27
+ velocities: dict[Motor, np.ndarray]
28
+ user_programs: npt.NDArray[np.int32]
29
+ durations: npt.NDArray[np.float64]
30
+
31
+ @classmethod
32
+ def from_slice(cls, slice: Slice[Motor], ramp_up_time: float) -> _Trajectory:
33
+ """Parse a trajectory with no gaps from a slice.
34
+
35
+ :param slice: Information about a series of scan frames along a number of axes
36
+ :param ramp_up_duration: Time required to ramp up to speed
37
+ :param ramp_down: Booleon representing if we ramp down or not
38
+ :returns Trajectory: Data class representing our parsed trajectory
39
+ :raises RuntimeError: Slice must have no gaps and a duration array
40
+ """
41
+ slice_duration = error_if_none(slice.duration, "Slice must have a duration")
42
+
43
+ # Check if any gaps other than initial gap.
44
+ if any(slice.gap[1:]):
45
+ raise RuntimeError(
46
+ f"Cannot parse trajectory with gaps. Slice has gaps: {slice.gap}"
47
+ )
48
+
49
+ scan_size = len(slice)
50
+ motors = slice.axes()
51
+
52
+ positions: dict[Motor, npt.NDArray[np.float64]] = {}
53
+ velocities: dict[Motor, npt.NDArray[np.float64]] = {}
54
+
55
+ # Initialise arrays
56
+ positions = {motor: np.empty(2 * scan_size + 1, float) for motor in motors}
57
+ velocities = {motor: np.empty(2 * scan_size + 1, float) for motor in motors}
58
+ durations: npt.NDArray[np.float64] = np.empty(2 * scan_size + 1, float)
59
+ user_programs: npt.NDArray[np.int32] = np.ones(2 * scan_size + 1, float)
60
+ user_programs[-1] = 8
61
+
62
+ # Ramp up time for start of collection window
63
+ durations[0] = int(ramp_up_time / TICK_S)
64
+ # Half the time per point
65
+ durations[1:] = np.repeat(slice_duration / (2 * TICK_S), 2)
66
+
67
+ # Fill profile assuming no gaps
68
+ # Excluding starting points, we begin at our next frame
69
+ half_durations = slice_duration / 2
70
+ for motor in motors:
71
+ # Set the first position to be lower bound, then
72
+ # alternate mid and upper as the upper and lower
73
+ # bounds of neighbouring points are the same as gap is false
74
+ positions[motor][0] = slice.lower[motor][0]
75
+ positions[motor][1::2] = slice.midpoints[motor]
76
+ positions[motor][2::2] = slice.upper[motor]
77
+ # For velocities we will need the relative distances
78
+ mid_to_upper_velocities = (
79
+ slice.upper[motor] - slice.midpoints[motor]
80
+ ) / half_durations
81
+ lower_to_mid_velocities = (
82
+ slice.midpoints[motor] - slice.lower[motor]
83
+ ) / half_durations
84
+ # First velocity is the lower -> mid of first point
85
+ velocities[motor][0] = lower_to_mid_velocities[0]
86
+ # For the midpoints, we take the average of the
87
+ # lower -> mid and mid-> upper velocities of the same point
88
+ velocities[motor][1::2] = (
89
+ lower_to_mid_velocities + mid_to_upper_velocities
90
+ ) / 2
91
+ # For the upper points, we need to take the average of the
92
+ # mid -> upper velocity of the previous point and
93
+ # lower -> mid velocity of the current point
94
+ velocities[motor][2:-1:2] = (
95
+ mid_to_upper_velocities[:-1] + lower_to_mid_velocities[1:]
96
+ ) / 2
97
+ # For the last velocity take the mid to upper velocity
98
+ velocities[motor][-1] = mid_to_upper_velocities[-1]
99
+
100
+ return cls(
101
+ positions=positions,
102
+ velocities=velocities,
103
+ user_programs=user_programs,
104
+ durations=durations,
105
+ )
106
+
107
+
108
+ @dataclass
109
+ class _PmacMotorInfo:
110
+ cs_port: str
111
+ cs_number: int
112
+ motor_cs_index: dict[Motor, int]
113
+ motor_acceleration_rate: dict[Motor, float]
114
+
115
+ @classmethod
116
+ async def from_motors(cls, pmac: PmacIO, motors: Sequence[Motor]) -> _PmacMotorInfo:
117
+ """Creates a _PmacMotorInfo instance based on a controller and list of motors.
118
+
119
+ :param pmac: The PMAC device
120
+ :param motors: Sequence of motors involved in trajectory
121
+ :raises RuntimeError:
122
+ if motors do not share common CS port or CS number, or if
123
+ motors do not have unique CS index assignments
124
+ :returns:
125
+ _PmacMotorInfo instance with motor's common CS port and CS number, and
126
+ dictionaries of motor's to their unique CS index and accelerate rate
127
+
128
+ """
129
+ assignments = {
130
+ motor: pmac.assignment[pmac.motor_assignment_index[motor]]
131
+ for motor in motors
132
+ }
133
+
134
+ cs_ports, cs_numbers, cs_axes, velocities, accls = await asyncio.gather(
135
+ gather_dict(
136
+ {motor: assignments[motor].cs_port.get_value() for motor in motors}
137
+ ),
138
+ gather_dict(
139
+ {motor: assignments[motor].cs_number.get_value() for motor in motors}
140
+ ),
141
+ gather_dict(
142
+ {
143
+ motor: assignments[motor].cs_axis_letter.get_value()
144
+ for motor in motors
145
+ }
146
+ ),
147
+ gather_dict({motor: motor.max_velocity.get_value() for motor in motors}),
148
+ gather_dict(
149
+ {motor: motor.acceleration_time.get_value() for motor in motors}
150
+ ),
151
+ )
152
+
153
+ # check if the values in cs_port and cs_number are the same
154
+ cs_ports = set(cs_ports.values())
155
+
156
+ if len(cs_ports) != 1:
157
+ raise RuntimeError(
158
+ "Failed to fetch common CS port."
159
+ "Motors passed are assigned to multiple CS ports:"
160
+ f"{list(cs_ports)}"
161
+ )
162
+
163
+ cs_port = cs_ports.pop()
164
+
165
+ cs_numbers = set(cs_numbers.values())
166
+ if len(cs_numbers) != 1:
167
+ raise RuntimeError(
168
+ "Failed to fetch common CS number."
169
+ "Motors passed are assigned to multiple CS numbers:"
170
+ f"{list(cs_numbers)}"
171
+ )
172
+
173
+ cs_number = cs_numbers.pop()
174
+
175
+ motor_cs_index = {}
176
+ for motor in cs_axes:
177
+ try:
178
+ if not cs_axes[motor]:
179
+ raise ValueError
180
+ motor_cs_index[motor] = CS_LETTERS.index(cs_axes[motor])
181
+ except ValueError as err:
182
+ raise ValueError(
183
+ "Failed to get motor CS index. "
184
+ f"Motor {motor.name} assigned to '{cs_axes[motor]}' "
185
+ f"but must be assignmed to '{CS_LETTERS}"
186
+ ) from err
187
+ if len(set(motor_cs_index.values())) != len(motor_cs_index.items()):
188
+ raise RuntimeError(
189
+ "Failed to fetch distinct CS Axes."
190
+ "Motors passed are assigned to the same CS Axis"
191
+ f"{list(motor_cs_index)}"
192
+ )
193
+
194
+ motor_acceleration_rate = {
195
+ motor: float(velocities[motor]) / float(accls[motor])
196
+ for motor in velocities
197
+ }
198
+
199
+ return _PmacMotorInfo(
200
+ cs_port, cs_number, motor_cs_index, motor_acceleration_rate
201
+ )
202
+
203
+
204
+ def calculate_ramp_position_and_duration(
205
+ slice: Slice[Motor], motor_info: _PmacMotorInfo, is_up: bool
206
+ ) -> tuple[dict[Motor, float], float]:
207
+ if slice.duration is None:
208
+ raise ValueError("Slice must have a duration")
209
+
210
+ scan_axes = slice.axes()
211
+ idx = 0 if is_up else -1
212
+
213
+ velocities: dict[Motor, float] = {}
214
+ ramp_times: list[float] = []
215
+ for axis in scan_axes:
216
+ velocity = (slice.upper[axis][idx] - slice.lower[axis][idx]) / slice.duration[
217
+ idx
218
+ ]
219
+ velocities[axis] = velocity
220
+ ramp_times.append(abs(velocity) / motor_info.motor_acceleration_rate[axis])
221
+
222
+ max_ramp_time = max(ramp_times)
223
+
224
+ motor_to_ramp_position = {}
225
+ sign = -1 if is_up else 1
226
+ for axis, v in velocities.items():
227
+ ref_pos = slice.lower[axis][0] if is_up else slice.upper[axis][-1]
228
+ displacement = 0.5 * v * max_ramp_time
229
+ motor_to_ramp_position[axis] = ref_pos + sign * displacement
230
+
231
+ return (motor_to_ramp_position, max_ramp_time)
@@ -4,8 +4,7 @@ from typing import Annotated as A
4
4
 
5
5
  import numpy as np
6
6
 
7
- from ophyd_async.core import Array1D, SignalR, SignalRW, StrictEnum, Table
8
- from ophyd_async.core._utils import SubsetEnum
7
+ from ophyd_async.core import Array1D, SignalR, SignalRW, StrictEnum, SubsetEnum, Table
9
8
  from ophyd_async.epics.core import EpicsDevice, PvSuffix
10
9
 
11
10
  from ._utils import TestingIOC, generate_random_pv_prefix
@@ -1,57 +1,31 @@
1
1
  from collections.abc import Sequence
2
- from xml.etree import ElementTree as ET
3
2
 
4
3
  import bluesky.plan_stubs as bps
5
4
 
6
- from ophyd_async.core import Device
7
5
  from ophyd_async.epics.adcore import (
6
+ AreaDetector,
8
7
  NDArrayBaseIO,
9
8
  NDAttributeDataType,
10
9
  NDAttributeParam,
11
10
  NDAttributePv,
12
11
  NDFileHDFIO,
12
+ ndattributes_to_xml,
13
13
  )
14
14
 
15
15
 
16
16
  def setup_ndattributes(
17
- device: NDArrayBaseIO, ndattributes: Sequence[NDAttributePv | NDAttributeParam]
17
+ device: NDArrayBaseIO, ndattributes: Sequence[NDAttributeParam | NDAttributePv]
18
18
  ):
19
- """Set up attributes on NdArray devices."""
20
- root = ET.Element("Attributes")
21
-
22
- for ndattribute in ndattributes:
23
- if isinstance(ndattribute, NDAttributeParam):
24
- ET.SubElement(
25
- root,
26
- "Attribute",
27
- name=ndattribute.name,
28
- type="PARAM",
29
- source=ndattribute.param,
30
- addr=str(ndattribute.addr),
31
- datatype=ndattribute.datatype.value,
32
- description=ndattribute.description,
33
- )
34
- elif isinstance(ndattribute, NDAttributePv):
35
- ET.SubElement(
36
- root,
37
- "Attribute",
38
- name=ndattribute.name,
39
- type="EPICS_PV",
40
- source=ndattribute.signal.source.split("ca://")[-1],
41
- dbrtype=ndattribute.dbrtype.value,
42
- description=ndattribute.description,
43
- )
44
- else:
45
- raise ValueError(
46
- f"Invalid type for ndattributes: {type(ndattribute)}. "
47
- "Expected NDAttributePv or NDAttributeParam."
48
- )
49
- xml_text = ET.tostring(root, encoding="unicode")
50
- yield from bps.abs_set(device.nd_attributes_file, xml_text, wait=True)
19
+ xml = ndattributes_to_xml(ndattributes)
20
+ yield from bps.abs_set(
21
+ device.nd_attributes_file,
22
+ xml,
23
+ wait=True,
24
+ )
51
25
 
52
26
 
53
- def setup_ndstats_sum(detector: Device):
54
- """Set up nd stats for a detector."""
27
+ def setup_ndstats_sum(detector: AreaDetector):
28
+ """Set up nd stats sum nd attribute for a detector."""
55
29
  hdf = getattr(detector, "fileio", None)
56
30
  if not isinstance(hdf, NDFileHDFIO):
57
31
  msg = (
@@ -13,10 +13,10 @@ from ophyd_async.core import (
13
13
  Settings,
14
14
  SettingsProvider,
15
15
  SignalRW,
16
+ Table,
16
17
  walk_config_signals,
17
18
  walk_rw_signals,
18
19
  )
19
- from ophyd_async.core._table import Table
20
20
 
21
21
  from ._utils import T
22
22
  from ._wait_for_awaitable import wait_for_awaitable
@@ -212,7 +212,7 @@ class AttributeProxy(TangoProxy):
212
212
 
213
213
  task = asyncio.create_task(_write())
214
214
  await asyncio.wait_for(task, timeout)
215
- except asyncio.TimeoutError as te:
215
+ except TimeoutError as te:
216
216
  raise TimeoutError(f"{self._name} attr put failed: Timeout") from te
217
217
  except DevFailed as de:
218
218
  raise RuntimeError(
@@ -451,7 +451,7 @@ class CommandProxy(TangoProxy):
451
451
  timestamp=time.time(),
452
452
  alarm_severity=0,
453
453
  )
454
- except asyncio.TimeoutError as te:
454
+ except TimeoutError as te:
455
455
  raise TimeoutError(f"{self._name} command failed: Timeout") from te
456
456
  except DevFailed as de:
457
457
  raise RuntimeError(
@@ -21,11 +21,11 @@ from ophyd_async.core import (
21
21
  from ._utils import T
22
22
 
23
23
 
24
- def partial_reading(val: Any) -> dict[str, Any]:
25
- """Helper function for building expected reading or configuration dicts.
24
+ def partial_reading(val: Any) -> Mapping[str, Any]:
25
+ """Helper function for building expected reading or configuration mapping.
26
26
 
27
- :param val: Value to be wrapped in dict with "value" as the key.
28
- :return: The dict that has wrapped the val with key "value".
27
+ :param val: Value to be wrapped in mapping with "value" as the key.
28
+ :return: The mapping that has wrapped the val with key "value".
29
29
  """
30
30
  return {"value": val}
31
31
 
@@ -100,7 +100,7 @@ def _assert_readings_approx_equal(
100
100
 
101
101
  async def assert_configuration(
102
102
  configurable: AsyncConfigurable,
103
- expected_configuration: dict[str, dict[str, Any]],
103
+ expected_configuration: Mapping[str, Mapping[str, Any]],
104
104
  full_match: bool = True,
105
105
  ) -> None:
106
106
  """Assert that a configurable Device has the given configuration.
@@ -108,7 +108,7 @@ async def assert_configuration(
108
108
  :param configurable:
109
109
  Device with an async ``read_configuration()`` method to get the
110
110
  configuration from.
111
- :param configuration: The expected configuration from the configurable.
111
+ :param expected_configuration: The expected configuration from the configurable.
112
112
  :param full_match: if expected_reading keys set is same as actual keys set.
113
113
  true: exact match
114
114
  false: expected_reading keys is subset of actual reading keys
@@ -6,6 +6,7 @@ import numpy as np
6
6
  from ophyd_async.core import (
7
7
  Array1D,
8
8
  Device,
9
+ DeviceVector,
9
10
  DTypeScalar_co,
10
11
  SignalRW,
11
12
  StandardReadable,
@@ -15,7 +16,6 @@ from ophyd_async.core import (
15
16
  soft_signal_rw,
16
17
  )
17
18
  from ophyd_async.core import StandardReadableFormat as Format
18
- from ophyd_async.core._device import DeviceVector
19
19
 
20
20
 
21
21
  class ExampleEnum(StrictEnum):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ophyd-async
3
- Version: 0.12.3
3
+ Version: 0.13.1
4
4
  Summary: Asynchronous Bluesky hardware abstraction code, compatible with control systems like EPICS and Tango
5
5
  Author-email: Tom Cobb <tom.cobb@diamond.ac.uk>
6
6
  License: BSD 3-Clause License
@@ -35,10 +35,10 @@ License: BSD 3-Clause License
35
35
  Project-URL: GitHub, https://github.com/bluesky/ophyd-async
36
36
  Classifier: Development Status :: 3 - Alpha
37
37
  Classifier: License :: OSI Approved :: BSD License
38
- Classifier: Programming Language :: Python :: 3.10
39
38
  Classifier: Programming Language :: Python :: 3.11
40
39
  Classifier: Programming Language :: Python :: 3.12
41
- Requires-Python: >=3.10
40
+ Classifier: Programming Language :: Python :: 3.13
41
+ Requires-Python: >=3.11
42
42
  Description-Content-Type: text/markdown
43
43
  License-File: LICENSE
44
44
  Requires-Dist: numpy
@@ -49,6 +49,7 @@ Requires-Dist: colorlog
49
49
  Requires-Dist: pydantic>=2.0
50
50
  Requires-Dist: pydantic-numpy
51
51
  Requires-Dist: stamina>=23.1.0
52
+ Requires-Dist: scanspec>=1.0a1
52
53
  Provides-Extra: sim
53
54
  Requires-Dist: h5py; extra == "sim"
54
55
  Provides-Extra: ca
@@ -56,7 +57,7 @@ Requires-Dist: aioca>=2.0a4; extra == "ca"
56
57
  Provides-Extra: pva
57
58
  Requires-Dist: p4p>=4.2.0; extra == "pva"
58
59
  Provides-Extra: tango
59
- Requires-Dist: pytango==10.0.0; extra == "tango"
60
+ Requires-Dist: pytango==10.0.2; extra == "tango"
60
61
  Provides-Extra: demo
61
62
  Requires-Dist: ipython; extra == "demo"
62
63
  Requires-Dist: matplotlib; extra == "demo"