From 7c500f0148338a995a01798645aab6a4bc16b539 Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 10 Oct 2024 09:21:42 +0100 Subject: [PATCH 01/10] Remove dependency on dls_bluesky_core --- dev-requirements.txt | 17 ----- docs/explanations/lifecycle.md | 2 +- helm/blueapi/values.yaml | 4 -- pyproject.toml | 3 +- src/blueapi/config.py | 2 - src/blueapi/core/bluesky_types.py | 2 +- src/blueapi/startup/example_plans.py | 77 +++++++++++++++++------ tests/unit_tests/core/fake_plan_module.py | 2 +- 8 files changed, 61 insertions(+), 48 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 68b3ecac7..18bd82ab0 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -20,7 +20,6 @@ bluesky-kafka==0.10.0 bluesky-live==0.0.8 bluesky-stomp==0.1.2 boltons==24.0.0 -bump-pydantic==0.8.0 cachetools==5.5.0 caproto==1.1.1 certifi==2024.8.30 @@ -43,21 +42,18 @@ dataclasses-json==0.6.7 decorator==5.1.1 deepmerge==2.0 distlib==0.3.8 -dls-bluesky-core==0.0.4 dls-dodal==1.31.1 dnspython==2.6.1 docopt==0.6.2 doct==1.1.0 docutils==0.21.2 dunamai==1.22.0 -email_validator==2.2.0 entrypoints==0.4 epicscorelibs==7.0.7.99.0.2 event-model==1.21.0 exceptiongroup==1.2.2 executing==2.1.0 fastapi==0.114.2 -fastapi-cli==0.0.5 fasteners==0.19 filelock==3.16.0 flexcache==0.3 @@ -74,7 +70,6 @@ h5py==3.11.0 HeapDict==1.0.1 historydict==1.2.6 httpcore==1.0.5 -httptools==0.6.1 httpx==0.27.2 humanize==4.10.0 identify==2.6.1 @@ -87,7 +82,6 @@ iniconfig==2.0.0 intake==0.6.4 ipython==8.18.0 ipywidgets==8.1.5 -itsdangerous==2.2.0 jedi==0.19.1 Jinja2==3.1.4 jinja2-ansible-filters==1.3.2 @@ -96,8 +90,6 @@ jsonschema-specifications==2023.12.1 jupyterlab_widgets==3.0.13 kiwisolver==1.4.7 ldap3==2.9.1 -libcst==1.4.0 -livereload==2.7.0 locket==1.0.0 lz4==4.3.3 markdown-it-py==3.0.0 @@ -155,7 +147,6 @@ py==1.11.0 pyasn1==0.6.1 pycryptodome==3.20.0 pydantic==2.9.1 -pydantic-extra-types==2.9.0 pydantic-settings==2.5.2 pydantic_core==2.23.3 pydantic_numpy==5.0.2 @@ -170,17 +161,14 @@ pytest-asyncio==0.24.0 pytest-cov==5.0.0 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 -python-multipart==0.0.9 pytz==2024.2 PyYAML==6.0.2 -pyyaml-include==2.1 questionary==2.0.1 redis==5.0.8 redis-json-dict==0.2.0 referencing==0.35.1 requests==2.32.3 responses==0.25.3 -rich==13.7.1 rpds-py==0.20.0 ruamel.yaml==0.18.6 ruamel.yaml.clib==0.2.8 @@ -188,7 +176,6 @@ ruff==0.6.5 scanspec==0.7.2 semver==3.0.2 setuptools-dso==2.11 -shellingham==1.5.4 six==1.16.0 slicerator==1.1.0 smmap==5.0.1 @@ -219,12 +206,10 @@ super-state-machine==2.0.2 tifffile==2024.8.30 tomli==2.0.1 toolz==0.12.1 -tornado==6.4.1 tox==3.28.0 tox-direct==0.4 tqdm==4.66.5 traitlets==5.14.3 -typer==0.12.4 types-mock==5.1.0.20240425 types-PyYAML==6.0.12.20240917 types-requests==2.32.0.20240914 @@ -233,10 +218,8 @@ typing-inspect==0.9.0 typing_extensions==4.12.2 tzdata==2024.1 tzlocal==5.2 -ujson==5.10.0 urllib3==2.2.3 uvicorn==0.30.6 -uvloop==0.19.0 virtualenv==20.26.4 watchfiles==0.24.0 wcwidth==0.2.13 diff --git a/docs/explanations/lifecycle.md b/docs/explanations/lifecycle.md index 862f3a754..98bd23af5 100644 --- a/docs/explanations/lifecycle.md +++ b/docs/explanations/lifecycle.md @@ -7,7 +7,7 @@ of being written, loaded and run. Take the following plan. import bluesky.plans as bp from blueapi.core import MsgGenerator - from dls_bluesky_core.core import inject + from dodal.common import inject from bluesky.protocols import Readable diff --git a/helm/blueapi/values.yaml b/helm/blueapi/values.yaml index d32e9f1eb..a38c3eb39 100644 --- a/helm/blueapi/values.yaml +++ b/helm/blueapi/values.yaml @@ -94,10 +94,6 @@ worker: module: blueapi.startup.example_devices - kind: planFunctions module: blueapi.startup.example_plans - - kind: planFunctions - module: dls_bluesky_core.plans - - kind: planFunctions - module: dls_bluesky_core.stubs stomp: auth: username: guest diff --git a/pyproject.toml b/pyproject.toml index ea16df3a0..75cf808b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,6 @@ dependencies = [ "fastapi>=0.112.0", "uvicorn", "requests", - "dls-bluesky-core", #requires ophyd-async "dls-dodal>=1.31.0", "super-state-machine", # See GH issue 553 "GitPython", @@ -144,5 +143,5 @@ extend-immutable-calls = [ "fastapi.Depends", "fastapi.Body", "fastapi.Task", - "dls_bluesky_core.core.inject", + "dodal.common.inject", ] diff --git a/src/blueapi/config.py b/src/blueapi/config.py index 3502590ba..a6345f71f 100644 --- a/src/blueapi/config.py +++ b/src/blueapi/config.py @@ -51,8 +51,6 @@ class EnvironmentConfig(BlueapiBaseModel): kind=SourceKind.DEVICE_FUNCTIONS, module="blueapi.startup.example_devices" ), Source(kind=SourceKind.PLAN_FUNCTIONS, module="blueapi.startup.example_plans"), - Source(kind=SourceKind.PLAN_FUNCTIONS, module="dls_bluesky_core.plans"), - Source(kind=SourceKind.PLAN_FUNCTIONS, module="dls_bluesky_core.stubs"), ] events: WorkerEventConfig = Field(default_factory=WorkerEventConfig) diff --git a/src/blueapi/core/bluesky_types.py b/src/blueapi/core/bluesky_types.py index 1a2978213..845a9a05d 100644 --- a/src/blueapi/core/bluesky_types.py +++ b/src/blueapi/core/bluesky_types.py @@ -24,7 +24,7 @@ Triggerable, WritesExternalAssets, ) -from dls_bluesky_core.core import MsgGenerator, PlanGenerator +from dodal.common import MsgGenerator, PlanGenerator from ophyd_async.core import Device as AsyncDevice from pydantic import BaseModel, Field diff --git a/src/blueapi/startup/example_plans.py b/src/blueapi/startup/example_plans.py index b3d969521..1b08d9272 100644 --- a/src/blueapi/startup/example_plans.py +++ b/src/blueapi/startup/example_plans.py @@ -1,30 +1,67 @@ +import operator +from collections.abc import Mapping +from functools import reduce +from typing import Annotated, Any + +import bluesky.plans as bp from bluesky.protocols import Movable, Readable -from dls_bluesky_core.core import inject -from dls_bluesky_core.plans import count -from dls_bluesky_core.stubs import move +from cycler import Cycler, cycler +from dodal.common import MsgGenerator +from dodal.plans.data_session_metadata import attach_data_session_metadata_decorator +from pydantic import validate_call +from scanspec.specs import Spec -from blueapi.core import MsgGenerator +""" +Plans related to the use of the `ScanSpec https://github.com/dls-controls/scanspec` +library for constructing arbitrarily complex N-dimensional trajectories, similar to +Diamond's "mapping scans" using ScanPointGenerator. +""" -def stp_snapshot( - detectors: list[Readable], - temperature: Movable = inject("sample_temperature"), - pressure: Movable = inject("sample_pressure"), +@attach_data_session_metadata_decorator() +@validate_call(config={"arbitrary_types_allowed": True}) +def scan( + detectors: Annotated[ + set[Readable], "Set of readable devices, will take a reading at each point" + ], + axes_to_move: Annotated[ + Mapping[str, Movable], "All axes involved in this scan, names and objects" + ], + spec: Annotated[Spec[str], "ScanSpec modelling the path of the scan"], + metadata: Mapping[str, Any] | None = None, ) -> MsgGenerator: + _md = { + "plan_args": { + "detectors": {det.name for det in detectors}, + "axes_to_move": {k: v.name for k, v in axes_to_move.items()}, + "spec": repr(spec), + }, + "plan_name": "scan", + "shape": spec.shape(), + **(metadata or {}), + } + + cycler = _scanspec_to_cycler(spec, axes_to_move) + yield from bp.scan_nd(detectors, cycler, md=_md) + + +def _scanspec_to_cycler(spec: Spec[str], axes: Mapping[str, Movable]) -> Cycler: """ - Moves devices for pressure and temperature (defaults fetched from the context) - and captures a single frame from a collection of devices + Convert a scanspec to a cycler for compatibility with legacy Bluesky plans such as + `bp.scan_nd`. Use the midpoints of the scanspec since cyclers are normally used + for software triggered scans. Args: - detectors (List[Readable]): A list of devices to read while the sample is at STP - temperature (Optional[Movable]): A device controlling temperature of the sample, - defaults to fetching a device name "sample_temperature" from the context - pressure (Optional[Movable]): A device controlling pressure on the sample, - defaults to fetching a device name "sample_pressure" from the context + spec: A scanspec + axes: Names and axes to move + Returns: - MsgGenerator: Plan - Yields: - Iterator[MsgGenerator]: Bluesky messages + Cycler: A new cycler """ - yield from move({temperature: 0, pressure: 10**5}) - yield from count(detectors, 1) + + midpoints = spec.frames().midpoints + midpoints = {axes[name]: points for name, points in midpoints.items()} + + # Need to "add" the cyclers for all the axes together. The code below is + # effectively: cycler(motor1, [...]) + cycler(motor2, [...]) + ... + return reduce(operator.add, (cycler(*args) for args in midpoints.items())) diff --git a/tests/unit_tests/core/fake_plan_module.py b/tests/unit_tests/core/fake_plan_module.py index 5fe5f1279..273b86699 100644 --- a/tests/unit_tests/core/fake_plan_module.py +++ b/tests/unit_tests/core/fake_plan_module.py @@ -1 +1 @@ -from dls_bluesky_core.plans import scan # noqa: F401 +from blueapi.startup.example_plans import scan # noqa: F401 From 0ef13be510057b9994c0191174060565e6a69488 Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 10 Oct 2024 09:21:42 +0100 Subject: [PATCH 02/10] Remove dependency on dls_bluesky_core --- docs/how-to/add-plans-and-devices.md | 2 +- src/blueapi/core/__init__.py | 4 ---- src/blueapi/core/context.py | 2 +- src/blueapi/utils/__init__.py | 1 - tests/unit_tests/core/test_context.py | 2 +- tests/unit_tests/service/test_interface.py | 4 ++-- tests/unit_tests/worker/test_task_worker.py | 3 ++- 7 files changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/how-to/add-plans-and-devices.md b/docs/how-to/add-plans-and-devices.md index a2c12e0b2..5f29865f9 100644 --- a/docs/how-to/add-plans-and-devices.md +++ b/docs/how-to/add-plans-and-devices.md @@ -27,7 +27,7 @@ Plans in Python files look like this: > **_NOTE:_** The type annotations (e.g. `: str`, `: int`, `-> MsgGenerator`) are required as blueapi uses them to generate an API! You can define as many plans as you like in a single Python file or spread them over multiple files. ``` from bluesky.protocols import Readable, Movable - from blueapi.core import MsgGenerator + from dodal.common import MsgGenerator from typing import Mapping, Any def my_plan( diff --git a/src/blueapi/core/__init__.py b/src/blueapi/core/__init__.py index 7b2306b0f..d7c352a74 100644 --- a/src/blueapi/core/__init__.py +++ b/src/blueapi/core/__init__.py @@ -3,9 +3,7 @@ BLUESKY_PROTOCOLS, DataEvent, Device, - MsgGenerator, Plan, - PlanGenerator, WatchableStatus, is_bluesky_compatible_device, is_bluesky_compatible_device_type, @@ -16,8 +14,6 @@ __all__ = [ "Plan", - "PlanGenerator", - "MsgGenerator", "Device", "BLUESKY_PROTOCOLS", "BlueskyContext", diff --git a/src/blueapi/core/context.py b/src/blueapi/core/context.py index 429ef4d77..ad8ac11b8 100644 --- a/src/blueapi/core/context.py +++ b/src/blueapi/core/context.py @@ -15,6 +15,7 @@ ) from bluesky.run_engine import RunEngine +from dodal.common import PlanGenerator from dodal.utils import make_all_devices from ophyd_async.core import NotConnected from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler, create_model @@ -30,7 +31,6 @@ Device, HasName, Plan, - PlanGenerator, is_bluesky_compatible_device, is_bluesky_plan_generator, ) diff --git a/src/blueapi/utils/__init__.py b/src/blueapi/utils/__init__.py index b871f842a..c81762542 100644 --- a/src/blueapi/utils/__init__.py +++ b/src/blueapi/utils/__init__.py @@ -7,7 +7,6 @@ __all__ = [ "handle_all_exceptions", "load_module_all", - "ConfigLoader", "serialize", "BlueapiBaseModel", "BlueapiModelConfig", diff --git a/tests/unit_tests/core/test_context.py b/tests/unit_tests/core/test_context.py index c7b1ad8ef..af17e33b3 100644 --- a/tests/unit_tests/core/test_context.py +++ b/tests/unit_tests/core/test_context.py @@ -5,7 +5,7 @@ import pytest from bluesky.protocols import Descriptor, Movable, Readable, Reading, SyncOrAsync -from dls_bluesky_core.core import MsgGenerator, PlanGenerator, inject +from dodal.common import MsgGenerator, PlanGenerator, inject from ophyd.sim import SynAxis, SynGauss from pydantic import TypeAdapter, ValidationError from pytest import LogCaptureFixture diff --git a/tests/unit_tests/service/test_interface.py b/tests/unit_tests/service/test_interface.py index 86dd2e5c1..257181c3a 100644 --- a/tests/unit_tests/service/test_interface.py +++ b/tests/unit_tests/service/test_interface.py @@ -4,11 +4,11 @@ import pytest from bluesky_stomp.messaging import StompClient +from dodal.common import MsgGenerator from ophyd.sim import SynAxis from stomp.connect import StompConnection11 as Connection from blueapi.config import ApplicationConfig, StompConfig -from blueapi.core import MsgGenerator from blueapi.core.context import BlueskyContext from blueapi.service import interface from blueapi.service.model import DeviceModel, PlanModel, WorkerTask @@ -148,7 +148,7 @@ def test_get_device(context_mock: MagicMock): ) with pytest.raises(KeyError): - assert interface.get_device("non_existing_device") + interface.get_device("non_existing_device") @patch("blueapi.service.interface.context") diff --git a/tests/unit_tests/worker/test_task_worker.py b/tests/unit_tests/worker/test_task_worker.py index 96777db9b..ecd76019c 100644 --- a/tests/unit_tests/worker/test_task_worker.py +++ b/tests/unit_tests/worker/test_task_worker.py @@ -7,9 +7,10 @@ from unittest.mock import MagicMock, patch import pytest +from dodal.common import MsgGenerator from blueapi.config import EnvironmentConfig, Source, SourceKind -from blueapi.core import BlueskyContext, EventStream, MsgGenerator +from blueapi.core import BlueskyContext, EventStream from blueapi.core.bluesky_types import DataEvent from blueapi.worker import ( ProgressEvent, From b3a23486d13cd96a62246b32a3ed434c89a78230 Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 10 Oct 2024 09:21:42 +0100 Subject: [PATCH 03/10] Prevent exposing unintended startup values --- helm/blueapi/values.yaml | 4 ++-- src/blueapi/config.py | 6 ++---- src/blueapi/startup/__init__.py | 27 +++++++++++++++++++++++++++ src/blueapi/startup/example_plans.py | 7 +++++-- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/helm/blueapi/values.yaml b/helm/blueapi/values.yaml index a38c3eb39..e4486e105 100644 --- a/helm/blueapi/values.yaml +++ b/helm/blueapi/values.yaml @@ -91,9 +91,9 @@ worker: env: sources: - kind: deviceFunctions - module: blueapi.startup.example_devices + module: blueapi.startup - kind: planFunctions - module: blueapi.startup.example_plans + module: blueapi.startup stomp: auth: username: guest diff --git a/src/blueapi/config.py b/src/blueapi/config.py index a6345f71f..83054a3f8 100644 --- a/src/blueapi/config.py +++ b/src/blueapi/config.py @@ -47,10 +47,8 @@ class EnvironmentConfig(BlueapiBaseModel): """ sources: list[Source] = [ - Source( - kind=SourceKind.DEVICE_FUNCTIONS, module="blueapi.startup.example_devices" - ), - Source(kind=SourceKind.PLAN_FUNCTIONS, module="blueapi.startup.example_plans"), + Source(kind=SourceKind.DEVICE_FUNCTIONS, module="blueapi.startup"), + Source(kind=SourceKind.PLAN_FUNCTIONS, module="blueapi.startup"), ] events: WorkerEventConfig = Field(default_factory=WorkerEventConfig) diff --git a/src/blueapi/startup/__init__.py b/src/blueapi/startup/__init__.py index e69de29bb..8a5c5733e 100644 --- a/src/blueapi/startup/__init__.py +++ b/src/blueapi/startup/__init__.py @@ -0,0 +1,27 @@ +from .example_devices import ( + current_det, + image_det, + sample_pressure, + sample_temperature, + theta, + unplugged_motor, + x, + x_err, + y, + z, +) +from .example_plans import scan + +__all__ = [ + "x", + "y", + "z", + "theta", + "current_det", + "image_det", + "sample_pressure", + "sample_temperature", + "unplugged_motor", + "x_err", + "scan", +] diff --git a/src/blueapi/startup/example_plans.py b/src/blueapi/startup/example_plans.py index 1b08d9272..bb23b8991 100644 --- a/src/blueapi/startup/example_plans.py +++ b/src/blueapi/startup/example_plans.py @@ -4,7 +4,7 @@ from typing import Annotated, Any import bluesky.plans as bp -from bluesky.protocols import Movable, Readable +from bluesky.protocols import HasName, Movable, Readable from cycler import Cycler, cycler from dodal.common import MsgGenerator from dodal.plans.data_session_metadata import attach_data_session_metadata_decorator @@ -18,6 +18,9 @@ """ +class NamedMovable(HasName, Movable): ... + + @attach_data_session_metadata_decorator() @validate_call(config={"arbitrary_types_allowed": True}) def scan( @@ -25,7 +28,7 @@ def scan( set[Readable], "Set of readable devices, will take a reading at each point" ], axes_to_move: Annotated[ - Mapping[str, Movable], "All axes involved in this scan, names and objects" + Mapping[str, NamedMovable], "All axes involved in this scan, names and objects" ], spec: Annotated[Spec[str], "ScanSpec modelling the path of the scan"], metadata: Mapping[str, Any] | None = None, From 815bfb3987c9fae1a0ff3a10a7870dae49f71149 Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 10 Oct 2024 09:21:42 +0100 Subject: [PATCH 04/10] Restore count --- src/blueapi/startup/__init__.py | 10 +- src/blueapi/startup/example_plans.py | 22 ++++- src/blueapi/startup/example_stubs.py | 137 +++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 src/blueapi/startup/example_stubs.py diff --git a/src/blueapi/startup/__init__.py b/src/blueapi/startup/__init__.py index 8a5c5733e..3942480ce 100644 --- a/src/blueapi/startup/__init__.py +++ b/src/blueapi/startup/__init__.py @@ -10,7 +10,8 @@ y, z, ) -from .example_plans import scan +from .example_plans import count, scan +from .example_stubs import move, move_relative, set_absolute, set_relative, sleep, wait __all__ = [ "x", @@ -24,4 +25,11 @@ "unplugged_motor", "x_err", "scan", + "count", + "move", + "move_relative", + "set_absolute", + "set_relative", + "sleep", + "wait", ] diff --git a/src/blueapi/startup/example_plans.py b/src/blueapi/startup/example_plans.py index bb23b8991..b99912db5 100644 --- a/src/blueapi/startup/example_plans.py +++ b/src/blueapi/startup/example_plans.py @@ -8,7 +8,7 @@ from cycler import Cycler, cycler from dodal.common import MsgGenerator from dodal.plans.data_session_metadata import attach_data_session_metadata_decorator -from pydantic import validate_call +from pydantic import Field, validate_call from scanspec.specs import Spec """ @@ -21,6 +21,9 @@ class NamedMovable(HasName, Movable): ... +PositiveFloat = Annotated[float, Field(gt=0)] + + @attach_data_session_metadata_decorator() @validate_call(config={"arbitrary_types_allowed": True}) def scan( @@ -68,3 +71,20 @@ def _scanspec_to_cycler(spec: Spec[str], axes: Mapping[str, Movable]) -> Cycler: # Need to "add" the cyclers for all the axes together. The code below is # effectively: cycler(motor1, [...]) + cycler(motor2, [...]) + ... return reduce(operator.add, (cycler(*args) for args in midpoints.items())) + + +@attach_data_session_metadata_decorator() +@validate_call(config={"arbitrary_types_allowed": True}) +def count( + detectors: Annotated[ + set[Readable], "Set of readable devices, will take a reading at each point" + ], + num: Annotated[int, "Number of frames to collect", Field(ge=1)] = 1, + delay: Annotated[ + PositiveFloat | list[PositiveFloat] | None, + "Delay between readings: if list, len(delay) == num - 1 and the delay is \ + between each point, if value or None is the delay for every gap", + ] = None, + metadata: Mapping[str, Any] | None = None, +) -> MsgGenerator: + yield from bp.count(detectors, num, delay=delay, md=metadata or {}) diff --git a/src/blueapi/startup/example_stubs.py b/src/blueapi/startup/example_stubs.py new file mode 100644 index 000000000..8778f9dae --- /dev/null +++ b/src/blueapi/startup/example_stubs.py @@ -0,0 +1,137 @@ +import itertools +from collections.abc import Mapping +from typing import Annotated, Any, TypeVar + +import bluesky.plan_stubs as bps +from bluesky.protocols import Movable +from dodal.common import MsgGenerator + +""" +Wrappers for Bluesky built-in plan stubs with type hinting +""" + +Group = Annotated[str, "String identifier used by 'wait' or stubs that await"] +T = TypeVar("T") + + +def set_absolute( + movable: Movable, value: T, group: Group | None = None, wait: bool = False +) -> MsgGenerator: + """ + Set a device, wrapper for `bp.abs_set`. + + Args: + movable (Movable): The device to set + value (T): The new value + group (Optional[Group], optional): The message group to associate with the + setting, for sequencing. Defaults to None. + wait (bool, optional): The group should wait until all setting is complete + (e.g. a motor has finished moving). Defaults to False. + + Returns: + MsgGenerator: Plan + + Yields: + Iterator[MsgGenerator]: Bluesky messages + """ + + yield from bps.abs_set(movable, value, group=group, wait=wait) + + +def set_relative( + movable: Movable, value: T, group: Group | None = None, wait: bool = False +) -> MsgGenerator: + """ + Change a device, wrapper for `bp.rel_set`. + + Args: + movable (Movable): The device to set + value (T): The new value + group (Optional[Group], optional): The message group to associate with the + setting, for sequencing. Defaults to None. + wait (bool, optional): The group should wait until all setting is complete + (e.g. a motor has finished moving). Defaults to False. + + Returns: + MsgGenerator: Plan + + Yields: + Iterator[MsgGenerator]: Bluesky messages + """ + + yield from bps.rel_set(movable, value, group=group, wait=wait) + + +def move(moves: Mapping[Movable, Any], group: Group | None = None) -> MsgGenerator: + """ + Move a device, wrapper for `bp.mv`. + + Args: + moves (Mapping[Movable, Any]): Mapping of Movables to target positions + group (Optional[Group], optional): The message group to associate with the + setting, for sequencing. Defaults to None. + + Returns: + MsgGenerator: Plan + + Yields: + Iterator[MsgGenerator]: Bluesky messages + """ + + yield from bps.mv(*itertools.chain.from_iterable(moves.items()), group=group) + + +def move_relative( + moves: Mapping[Movable, Any], group: Group | None = None +) -> MsgGenerator: + """ + Move a device relative to its current position, wrapper for `bp.mvr`. + + Args: + moves (Mapping[Movable, Any]): Mapping of Movables to target deltas + group (Optional[Group], optional): The message group to associate with the + setting, for sequencing. Defaults to None. + + Returns: + MsgGenerator: Plan + + Yields: + Iterator[MsgGenerator]: Bluesky messages + """ + + yield from bps.mvr(*itertools.chain.from_iterable(moves.items()), group=group) + + +def sleep(time: float) -> MsgGenerator: + """ + Suspend all action for a given time, wrapper for `bp.sleep` + + Args: + time (float): Time to wait in seconds + + Returns: + MsgGenerator: Plan + + Yields: + Iterator[MsgGenerator]: Bluesky messages + """ + + yield from bps.sleep(time) + + +def wait(group: Group | None = None) -> MsgGenerator: + """ + Wait for a group status to complete, wrapper for `bp.wait` + + Args: + group (Optional[Group], optional): The name of the group to wait for, defaults + to None. + + Returns: + MsgGenerator: Plan + + Yields: + Iterator[MsgGenerator]: Bluesky messages + """ + + yield from bps.wait(group) From f8d8fef012c7e6fac28f1afa451697815f329fbb Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 10 Oct 2024 09:21:42 +0100 Subject: [PATCH 05/10] Update for wrapped count plan --- src/blueapi/startup/example_plans.py | 4 +- tests/unit_tests/worker/conftest.py | 14 +++++ tests/unit_tests/worker/test_task_worker.py | 70 ++++++++++++--------- 3 files changed, 56 insertions(+), 32 deletions(-) create mode 100644 tests/unit_tests/worker/conftest.py diff --git a/src/blueapi/startup/example_plans.py b/src/blueapi/startup/example_plans.py index b99912db5..c148ff85a 100644 --- a/src/blueapi/startup/example_plans.py +++ b/src/blueapi/startup/example_plans.py @@ -77,7 +77,9 @@ def _scanspec_to_cycler(spec: Spec[str], axes: Mapping[str, Movable]) -> Cycler: @validate_call(config={"arbitrary_types_allowed": True}) def count( detectors: Annotated[ - set[Readable], "Set of readable devices, will take a reading at each point" + set[Readable], + "Set of readable devices, will take a reading at each point", + Field(min_items=1), ], num: Annotated[int, "Number of frames to collect", Field(ge=1)] = 1, delay: Annotated[ diff --git a/tests/unit_tests/worker/conftest.py b/tests/unit_tests/worker/conftest.py new file mode 100644 index 000000000..36f1cf874 --- /dev/null +++ b/tests/unit_tests/worker/conftest.py @@ -0,0 +1,14 @@ +from pathlib import Path +from unittest.mock import AsyncMock, MagicMock + +import pytest +from ophyd_async.core import PathInfo + + +@pytest.fixture +def updating_path_provider(tmp_path: Path): + updating_mock_path_provider = MagicMock() + updating_mock_path_provider.data_session = AsyncMock(return_value="bar") + updating_mock_path_provider.update = AsyncMock() + updating_mock_path_provider.return_value = PathInfo(tmp_path, "foo", 0) + return updating_mock_path_provider diff --git a/tests/unit_tests/worker/test_task_worker.py b/tests/unit_tests/worker/test_task_worker.py index ecd76019c..3ba567937 100644 --- a/tests/unit_tests/worker/test_task_worker.py +++ b/tests/unit_tests/worker/test_task_worker.py @@ -2,12 +2,14 @@ import threading from collections.abc import Callable, Iterable from concurrent.futures import Future +from pathlib import Path from queue import Full from typing import Any, TypeVar from unittest.mock import MagicMock, patch import pytest from dodal.common import MsgGenerator +from dodal.common.types import UpdatingPathProvider from blueapi.config import EnvironmentConfig, Source, SourceKind from blueapi.core import BlueskyContext, EventStream @@ -308,40 +310,46 @@ def begin_task_and_wait_until_complete( # -def test_worker_and_data_events_produce_in_order(worker: TaskWorker) -> None: - assert_running_count_plan_produces_ordered_worker_and_data_events( - [ - WorkerEvent( - state=WorkerState.RUNNING, - task_status=TaskStatus( - task_id="count", task_complete=False, task_failed=False +def test_worker_and_data_events_produce_in_order( + updating_path_provider: UpdatingPathProvider, tmp_path: Path, worker: TaskWorker +) -> None: + with patch( + "dodal.common.beamlines.beamline_utils.get_path_provider", + return_value=updating_path_provider, + ): + assert_running_count_plan_produces_ordered_worker_and_data_events( + [ + WorkerEvent( + state=WorkerState.RUNNING, + task_status=TaskStatus( + task_id="count", task_complete=False, task_failed=False + ), + errors=[], + warnings=[], ), - errors=[], - warnings=[], - ), - DataEvent(name="start", doc={}), - DataEvent(name="descriptor", doc={}), - DataEvent(name="event", doc={}), - DataEvent(name="stop", doc={}), - WorkerEvent( - state=WorkerState.IDLE, - task_status=TaskStatus( - task_id="count", task_complete=False, task_failed=False + DataEvent(name="start", doc={}), + DataEvent(name="descriptor", doc={}), + DataEvent(name="event", doc={}), + DataEvent(name="stop", doc={}), + WorkerEvent( + state=WorkerState.IDLE, + task_status=TaskStatus( + task_id="count", task_complete=False, task_failed=False + ), + errors=[], + warnings=[], ), - errors=[], - warnings=[], - ), - WorkerEvent( - state=WorkerState.IDLE, - task_status=TaskStatus( - task_id="count", task_complete=True, task_failed=False + WorkerEvent( + state=WorkerState.IDLE, + task_status=TaskStatus( + task_id="count", task_complete=True, task_failed=False + ), + errors=[], + warnings=[], ), - errors=[], - warnings=[], - ), - ], - worker, - ) + ], + worker, + ) def assert_running_count_plan_produces_ordered_worker_and_data_events( From 5b7aca377ded2da31ce94cdf4f3659d27aff248b Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 10 Oct 2024 09:44:28 +0100 Subject: [PATCH 06/10] Allow for Bluesky 1.13 handling requiring Sequence of devices --- src/blueapi/startup/example_plans.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blueapi/startup/example_plans.py b/src/blueapi/startup/example_plans.py index c148ff85a..1f0c8521b 100644 --- a/src/blueapi/startup/example_plans.py +++ b/src/blueapi/startup/example_plans.py @@ -48,7 +48,7 @@ def scan( } cycler = _scanspec_to_cycler(spec, axes_to_move) - yield from bp.scan_nd(detectors, cycler, md=_md) + yield from bp.scan_nd(tuple(detectors), cycler, md=_md) def _scanspec_to_cycler(spec: Spec[str], axes: Mapping[str, Movable]) -> Cycler: @@ -89,4 +89,4 @@ def count( ] = None, metadata: Mapping[str, Any] | None = None, ) -> MsgGenerator: - yield from bp.count(detectors, num, delay=delay, md=metadata or {}) + yield from bp.count(tuple(detectors), num, delay=delay, md=metadata or {}) From 7913ab54681b40a642d45986937ec7b75af1f1be Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 10 Oct 2024 13:10:44 +0100 Subject: [PATCH 07/10] Update dev-requirements --- dev-requirements.txt | 105 ++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 18bd82ab0..2bc91ca92 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,21 +1,20 @@ accessible-pygments==0.0.5 aioca==1.7 aiofiles==24.1.0 -aiohappyeyeballs==2.4.0 -aiohttp==3.10.5 +aiohappyeyeballs==2.4.3 +aiohttp==3.10.9 aiosignal==1.3.1 alabaster==1.0.0 annotated-types==0.7.0 -anyio==4.4.0 +anyio==4.6.0 appdirs==1.4.4 asciitree==0.3.3 asttokens==2.4.1 -async-timeout==4.0.3 attrs==24.2.0 babel==2.16.0 beautifulsoup4==4.12.3 bidict==0.23.1 -bluesky==1.13.0a4 +bluesky==1.13 bluesky-kafka==0.10.0 bluesky-live==0.0.8 bluesky-stomp==0.1.2 @@ -24,7 +23,7 @@ cachetools==5.5.0 caproto==1.1.1 certifi==2024.8.30 cfgv==3.4.0 -charset-normalizer==3.3.2 +charset-normalizer==3.4.0 click==8.1.7 cloudpickle==3.0.0 colorama==0.4.6 @@ -34,31 +33,33 @@ compress-pickle==2.1.0 confluent-kafka==2.5.3 contourpy==1.3.0 copier==9.3.1 -coverage==7.6.1 +coverage==7.6.2 cycler==0.12.1 -dask==2024.9.0 +dask==2024.9.1 databroker==1.2.5 dataclasses-json==0.6.7 decorator==5.1.1 +deepdiff==8.0.1 deepmerge==2.0 -distlib==0.3.8 -dls-dodal==1.31.1 -dnspython==2.6.1 +Deprecated==1.2.14 +distlib==0.3.9 +dls-bluesky-core==0.0.4 +dls-dodal==1.33.0 +dnspython==2.7.0 docopt==0.6.2 doct==1.1.0 docutils==0.21.2 dunamai==1.22.0 entrypoints==0.4 -epicscorelibs==7.0.7.99.0.2 +epicscorelibs==7.0.7.99.1.1 event-model==1.21.0 -exceptiongroup==1.2.2 executing==2.1.0 -fastapi==0.114.2 +fastapi==0.115.0 fasteners==0.19 -filelock==3.16.0 +filelock==3.16.1 flexcache==0.3 flexparser==0.3.1 -fonttools==4.53.1 +fonttools==4.54.1 frozenlist==1.4.1 fsspec==2024.9.0 funcy==2.0 @@ -66,17 +67,17 @@ gitdb==4.0.11 GitPython==3.1.43 graypy==2.1.0 h11==0.14.0 -h5py==3.11.0 +h5py==3.12.1 HeapDict==1.0.1 historydict==1.2.6 -httpcore==1.0.5 +httpcore==1.0.6 httpx==0.27.2 -humanize==4.10.0 +humanize==4.11.0 identify==2.6.1 idna==3.10 imageio==2.35.1 imagesize==1.4.1 -importlib_metadata==8.5.0 +importlib_metadata==8.4.0 importlib_resources==6.4.5 iniconfig==2.0.0 intake==0.6.4 @@ -86,14 +87,14 @@ jedi==0.19.1 Jinja2==3.1.4 jinja2-ansible-filters==1.3.2 jsonschema==4.23.0 -jsonschema-specifications==2023.12.1 +jsonschema-specifications==2024.10.1 jupyterlab_widgets==3.0.13 kiwisolver==1.4.7 ldap3==2.9.1 locket==1.0.0 lz4==4.3.3 markdown-it-py==3.0.0 -MarkupSafe==2.1.5 +MarkupSafe==3.0.1 marshmallow==3.22.0 matplotlib==3.9.2 matplotlib-inline==0.1.7 @@ -111,16 +112,18 @@ myst-parser==4.0.0 networkx==3.3 nodeenv==1.9.1 nose2==0.15.1 -nslsii==0.10.3 -numcodecs==0.13.0 +nslsii==0.10.5 +numcodecs==0.13.1 numpy==1.26.4 opencv-python-headless==4.10.0.84 +opentelemetry-api==1.27.0 ophyd==1.9.0 -ophyd-async==0.5.2 +ophyd-async==0.6.0 +orderly-set==5.2.2 orjson==3.10.7 -p4p==4.1.12 +p4p==4.2.0 packaging==24.1 -pandas==2.2.2 +pandas==2.2.3 parso==0.8.4 partd==1.4.2 pathlib2==2.3.7.post1 @@ -131,29 +134,30 @@ pika==1.3.2 pillow==10.4.0 PIMS==0.7 Pint==0.24.3 -pipdeptree==2.23.3 -platformdirs==4.3.3 +pipdeptree==2.23.4 +platformdirs==4.3.6 pluggy==1.5.0 -plumbum==1.8.3 +plumbum==1.9.0 ply==3.11 -pre-commit==3.8.0 +pre_commit==4.0.1 prettytable==3.11.0 prompt-toolkit==3.0.36 +propcache==0.2.0 psutil==6.0.0 ptyprocess==0.7.0 pure_eval==0.2.3 -pvxslibs==1.3.1 +pvxslibs==1.3.2 py==1.11.0 pyasn1==0.6.1 -pycryptodome==3.20.0 -pydantic==2.9.1 +pycryptodome==3.21.0 +pydantic==2.9.2 pydantic-settings==2.5.2 -pydantic_core==2.23.3 +pydantic_core==2.23.4 pydantic_numpy==5.0.2 pydata-sphinx-theme==0.15.4 pyepics==3.5.7 Pygments==2.18.0 -pymongo==4.8.0 +pymongo==4.10.1 pyOlog==4.5.0 pyparsing==3.1.4 pytest==8.3.3 @@ -164,17 +168,18 @@ python-dotenv==1.0.1 pytz==2024.2 PyYAML==6.0.2 questionary==2.0.1 -redis==5.0.8 -redis-json-dict==0.2.0 +redis==5.1.1 +redis-json-dict==0.2.1 referencing==0.35.1 requests==2.32.3 responses==0.25.3 rpds-py==0.20.0 ruamel.yaml==0.18.6 ruamel.yaml.clib==0.2.8 -ruff==0.6.5 +ruff==0.6.9 scanspec==0.7.2 semver==3.0.2 +setuptools==75.1.0 setuptools-dso==2.11 six==1.16.0 slicerator==1.1.0 @@ -183,7 +188,7 @@ sniffio==1.3.1 snowballstemmer==2.2.0 soupsieve==2.6 Sphinx==8.0.2 -sphinx-autobuild==2024.9.3 +sphinx-autobuild==2024.10.3 sphinx-click==6.0.0 sphinx-copybutton==0.5.2 sphinx_design==0.6.1 @@ -197,15 +202,14 @@ sphinxcontrib-openapi==0.8.4 sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 stack-data==0.6.3 -starlette==0.38.5 +starlette==0.38.6 stomp-py==8.1.2 suitcase-mongo==0.6.0 suitcase-msgpack==0.3.0 suitcase-utils==0.5.4 super-state-machine==2.0.2 -tifffile==2024.8.30 -tomli==2.0.1 -toolz==0.12.1 +tifffile==2024.9.20 +toolz==1.0.0 tox==3.28.0 tox-direct==0.4 tqdm==4.66.5 @@ -216,20 +220,21 @@ types-requests==2.32.0.20240914 types-urllib3==1.26.25.14 typing-inspect==0.9.0 typing_extensions==4.12.2 -tzdata==2024.1 +tzdata==2024.2 tzlocal==5.2 urllib3==2.2.3 -uvicorn==0.30.6 -virtualenv==20.26.4 +uvicorn==0.31.1 +virtualenv==20.26.6 watchfiles==0.24.0 wcwidth==0.2.13 websocket-client==1.8.0 -websockets==13.0.1 +websockets==13.1 widgetsnbextension==4.0.13 workflows==2.27 +wrapt==1.16.0 xarray==2024.9.0 -yarl==1.11.1 +yarl==1.14.0 zarr==2.18.3 zict==2.2.0 zipp==3.20.2 -zocalo==1.1.0 +zocalo==1.1.1 From c3765b893896c2d38be71fd3c76247e06fd25322 Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 10 Oct 2024 13:35:41 +0100 Subject: [PATCH 08/10] Use bluesky.utils MsgGenerator --- README.md | 14 +++++++------- docs/explanations/lifecycle.md | 2 +- docs/how-to/add-plans-and-devices.md | 2 +- src/blueapi/core/bluesky_types.py | 3 ++- src/blueapi/startup/example_plans.py | 2 +- src/blueapi/startup/example_stubs.py | 2 +- tests/unit_tests/core/test_context.py | 3 ++- tests/unit_tests/service/test_interface.py | 2 +- tests/unit_tests/worker/test_task_worker.py | 2 +- 9 files changed, 17 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 5e44be3d9..f0e327c67 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,12 @@ Lightweight bluesky-as-a-service wrapper application. Also usable as a library. -Source | -:---: | :---: -PyPI | `pip install blueapi` -Docker | `docker run ghcr.io/diamondlightsource/blueapi:latest` -Documentation | -Releases | +| Source | | +| :-----------: | :------------------------------------------------------: | +| PyPI | `pip install blueapi` | +| Docker | `docker run ghcr.io/diamondlightsource/blueapi:latest` | +| Documentation | | +| Releases | | This module wraps [bluesky](https://blueskyproject.io/bluesky) plans and devices inside a server and exposes endpoints to send commands/receive data. Useful for @@ -30,7 +30,7 @@ type-annotated plans. For example, take the following plan: ```python import bluesky.plans as bp - from blueapi.core import MsgGenerator + from bluesky.utils import MsgGenerator def my_plan(foo: str, bar: int) -> MsgGenerator: yield from bp.scan(...) diff --git a/docs/explanations/lifecycle.md b/docs/explanations/lifecycle.md index 98bd23af5..2ebffce97 100644 --- a/docs/explanations/lifecycle.md +++ b/docs/explanations/lifecycle.md @@ -6,7 +6,7 @@ of being written, loaded and run. Take the following plan. from typing import Any, List, Mapping, Optional, Union import bluesky.plans as bp - from blueapi.core import MsgGenerator + from bluesky.utils import MsgGenerator from dodal.common import inject from bluesky.protocols import Readable diff --git a/docs/how-to/add-plans-and-devices.md b/docs/how-to/add-plans-and-devices.md index 5f29865f9..87e0f1e9a 100644 --- a/docs/how-to/add-plans-and-devices.md +++ b/docs/how-to/add-plans-and-devices.md @@ -27,7 +27,7 @@ Plans in Python files look like this: > **_NOTE:_** The type annotations (e.g. `: str`, `: int`, `-> MsgGenerator`) are required as blueapi uses them to generate an API! You can define as many plans as you like in a single Python file or spread them over multiple files. ``` from bluesky.protocols import Readable, Movable - from dodal.common import MsgGenerator + from bluesky.utils import MsgGenerator from typing import Mapping, Any def my_plan( diff --git a/src/blueapi/core/bluesky_types.py b/src/blueapi/core/bluesky_types.py index 845a9a05d..690b2ec71 100644 --- a/src/blueapi/core/bluesky_types.py +++ b/src/blueapi/core/bluesky_types.py @@ -24,7 +24,8 @@ Triggerable, WritesExternalAssets, ) -from dodal.common import MsgGenerator, PlanGenerator +from bluesky.utils import MsgGenerator +from dodal.common import PlanGenerator from ophyd_async.core import Device as AsyncDevice from pydantic import BaseModel, Field diff --git a/src/blueapi/startup/example_plans.py b/src/blueapi/startup/example_plans.py index 1f0c8521b..b73d665bb 100644 --- a/src/blueapi/startup/example_plans.py +++ b/src/blueapi/startup/example_plans.py @@ -5,8 +5,8 @@ import bluesky.plans as bp from bluesky.protocols import HasName, Movable, Readable +from bluesky.utils import MsgGenerator from cycler import Cycler, cycler -from dodal.common import MsgGenerator from dodal.plans.data_session_metadata import attach_data_session_metadata_decorator from pydantic import Field, validate_call from scanspec.specs import Spec diff --git a/src/blueapi/startup/example_stubs.py b/src/blueapi/startup/example_stubs.py index 8778f9dae..d8a489411 100644 --- a/src/blueapi/startup/example_stubs.py +++ b/src/blueapi/startup/example_stubs.py @@ -4,7 +4,7 @@ import bluesky.plan_stubs as bps from bluesky.protocols import Movable -from dodal.common import MsgGenerator +from bluesky.utils import MsgGenerator """ Wrappers for Bluesky built-in plan stubs with type hinting diff --git a/tests/unit_tests/core/test_context.py b/tests/unit_tests/core/test_context.py index af17e33b3..02cfd5848 100644 --- a/tests/unit_tests/core/test_context.py +++ b/tests/unit_tests/core/test_context.py @@ -5,7 +5,8 @@ import pytest from bluesky.protocols import Descriptor, Movable, Readable, Reading, SyncOrAsync -from dodal.common import MsgGenerator, PlanGenerator, inject +from bluesky.utils import MsgGenerator +from dodal.common import PlanGenerator, inject from ophyd.sim import SynAxis, SynGauss from pydantic import TypeAdapter, ValidationError from pytest import LogCaptureFixture diff --git a/tests/unit_tests/service/test_interface.py b/tests/unit_tests/service/test_interface.py index 257181c3a..16eb631f1 100644 --- a/tests/unit_tests/service/test_interface.py +++ b/tests/unit_tests/service/test_interface.py @@ -3,8 +3,8 @@ from unittest.mock import MagicMock, Mock, patch import pytest +from bluesky.utils import MsgGenerator from bluesky_stomp.messaging import StompClient -from dodal.common import MsgGenerator from ophyd.sim import SynAxis from stomp.connect import StompConnection11 as Connection diff --git a/tests/unit_tests/worker/test_task_worker.py b/tests/unit_tests/worker/test_task_worker.py index 3ba567937..25cdadf2c 100644 --- a/tests/unit_tests/worker/test_task_worker.py +++ b/tests/unit_tests/worker/test_task_worker.py @@ -8,7 +8,7 @@ from unittest.mock import MagicMock, patch import pytest -from dodal.common import MsgGenerator +from bluesky.utils import MsgGenerator from dodal.common.types import UpdatingPathProvider from blueapi.config import EnvironmentConfig, Source, SourceKind From 100af7d5ab414e8cb490e537cc467f9a03f16af8 Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Thu, 10 Oct 2024 15:22:51 +0100 Subject: [PATCH 09/10] Type checking issue with bluesky 1.13 --- src/blueapi/startup/example_plans.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blueapi/startup/example_plans.py b/src/blueapi/startup/example_plans.py index b73d665bb..dfa80bd27 100644 --- a/src/blueapi/startup/example_plans.py +++ b/src/blueapi/startup/example_plans.py @@ -89,4 +89,4 @@ def count( ] = None, metadata: Mapping[str, Any] | None = None, ) -> MsgGenerator: - yield from bp.count(tuple(detectors), num, delay=delay, md=metadata or {}) + yield from bp.count(tuple(detectors), num, delay=delay or 0, md=metadata or {}) From cd54fafd6f137227c56b50a8fac035a23a5ba11e Mon Sep 17 00:00:00 2001 From: "Ware, Joseph (DLSLtd,RAL,LSCI)" Date: Fri, 11 Oct 2024 10:04:09 +0100 Subject: [PATCH 10/10] Linting for bluesky 1.13 types --- src/blueapi/startup/example_plans.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blueapi/startup/example_plans.py b/src/blueapi/startup/example_plans.py index dfa80bd27..3ff2d2d32 100644 --- a/src/blueapi/startup/example_plans.py +++ b/src/blueapi/startup/example_plans.py @@ -87,6 +87,6 @@ def count( "Delay between readings: if list, len(delay) == num - 1 and the delay is \ between each point, if value or None is the delay for every gap", ] = None, - metadata: Mapping[str, Any] | None = None, + metadata: dict[str, Any] | None = None, ) -> MsgGenerator: yield from bp.count(tuple(detectors), num, delay=delay or 0, md=metadata or {})