From a2e86d090885cc31aeeba42421b88e5db09b2d84 Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Thu, 15 Jun 2023 14:15:25 +0000 Subject: [PATCH 01/22] Merge eiger changes from tickit/Exts-Logic --- src/tickit_devices/eiger/__init__.py | 5 ++--- src/tickit_devices/eiger/eiger.py | 3 +++ src/tickit_devices/eiger/filewriter/filewriter_config.py | 2 +- src/tickit_devices/eiger/monitor/monitor_config.py | 2 +- src/tickit_devices/eiger/stream/stream_config.py | 2 +- tests/eiger/test_eiger_filewriter_config.py | 2 +- tests/eiger/test_eiger_filewriter_status.py | 2 +- tests/eiger/test_eiger_monitor_config.py | 2 +- tests/eiger/test_eiger_monitor_status.py | 2 +- tests/eiger/test_eiger_settings.py | 2 +- tests/eiger/test_eiger_status.py | 2 +- tests/eiger/test_eiger_stream_config.py | 2 +- tests/eiger/test_eiger_stream_status.py | 2 +- 13 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/tickit_devices/eiger/__init__.py b/src/tickit_devices/eiger/__init__.py index 6bb33489..6ae79baa 100644 --- a/src/tickit_devices/eiger/__init__.py +++ b/src/tickit_devices/eiger/__init__.py @@ -2,9 +2,8 @@ from tickit.core.components.component import Component, ComponentConfig from tickit.core.components.device_simulation import DeviceSimulation - -from tickit_devices.eiger.eiger import EigerDevice -from tickit_devices.eiger.eiger_adapters import EigerRESTAdapter, EigerZMQAdapter +from tickit.devices.eiger.eiger import EigerDevice +from tickit.devices.eiger.eiger_adapters import EigerRESTAdapter, EigerZMQAdapter @dataclass diff --git a/src/tickit_devices/eiger/eiger.py b/src/tickit_devices/eiger/eiger.py index e783f3e2..6f38dab5 100644 --- a/src/tickit_devices/eiger/eiger.py +++ b/src/tickit_devices/eiger/eiger.py @@ -41,6 +41,9 @@ class EigerDevice(Device): _num_frames_left: int _data_queue: Queue + _num_frames_left: int + _data_queue: Queue + #: An empty typed mapping of input values Inputs: TypedDict = TypedDict("Inputs", {"trigger": bool}, total=False) #: A typed mapping containing the 'value' output value diff --git a/src/tickit_devices/eiger/filewriter/filewriter_config.py b/src/tickit_devices/eiger/filewriter/filewriter_config.py index b4b47bfb..8a7ac44f 100644 --- a/src/tickit_devices/eiger/filewriter/filewriter_config.py +++ b/src/tickit_devices/eiger/filewriter/filewriter_config.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field, fields from typing import Any -from tickit_devices.eiger.eiger_schema import rw_bool, rw_int, rw_str +from tickit.devices.eiger.eiger_schema import rw_bool, rw_int, rw_str @dataclass diff --git a/src/tickit_devices/eiger/monitor/monitor_config.py b/src/tickit_devices/eiger/monitor/monitor_config.py index 3b4bd0ed..9c3bd5ec 100644 --- a/src/tickit_devices/eiger/monitor/monitor_config.py +++ b/src/tickit_devices/eiger/monitor/monitor_config.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field, fields from typing import Any -from tickit_devices.eiger.eiger_schema import rw_int, rw_str +from tickit.devices.eiger.eiger_schema import rw_int, rw_str @dataclass diff --git a/src/tickit_devices/eiger/stream/stream_config.py b/src/tickit_devices/eiger/stream/stream_config.py index 0581f871..0eec1e3b 100644 --- a/src/tickit_devices/eiger/stream/stream_config.py +++ b/src/tickit_devices/eiger/stream/stream_config.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field, fields from typing import Any -from tickit_devices.eiger.eiger_schema import rw_str +from tickit.devices.eiger.eiger_schema import rw_str @dataclass diff --git a/tests/eiger/test_eiger_filewriter_config.py b/tests/eiger/test_eiger_filewriter_config.py index 13e12ca8..911fc22c 100644 --- a/tests/eiger/test_eiger_filewriter_config.py +++ b/tests/eiger/test_eiger_filewriter_config.py @@ -1,6 +1,6 @@ import pytest -from tickit_devices.eiger.filewriter.filewriter_config import FileWriterConfig +from tickit.devices.eiger.filewriter.filewriter_config import FileWriterConfig # # # # # Eiger FileWriterConfig Tests # # # # # diff --git a/tests/eiger/test_eiger_filewriter_status.py b/tests/eiger/test_eiger_filewriter_status.py index 9650ea1b..0429b570 100644 --- a/tests/eiger/test_eiger_filewriter_status.py +++ b/tests/eiger/test_eiger_filewriter_status.py @@ -1,6 +1,6 @@ import pytest -from tickit_devices.eiger.filewriter.filewriter_status import FileWriterStatus +from tickit.devices.eiger.filewriter.filewriter_status import FileWriterStatus # # # # # Eiger FileWriterStatus Tests # # # # # diff --git a/tests/eiger/test_eiger_monitor_config.py b/tests/eiger/test_eiger_monitor_config.py index f4149fbe..013d4a3b 100644 --- a/tests/eiger/test_eiger_monitor_config.py +++ b/tests/eiger/test_eiger_monitor_config.py @@ -1,6 +1,6 @@ import pytest -from tickit_devices.eiger.monitor.monitor_config import MonitorConfig +from tickit.devices.eiger.monitor.monitor_config import MonitorConfig # # # # # Eiger MonitorConfig Tests # # # # # diff --git a/tests/eiger/test_eiger_monitor_status.py b/tests/eiger/test_eiger_monitor_status.py index 573f854c..9642fc3e 100644 --- a/tests/eiger/test_eiger_monitor_status.py +++ b/tests/eiger/test_eiger_monitor_status.py @@ -1,6 +1,6 @@ import pytest -from tickit_devices.eiger.monitor.monitor_status import MonitorStatus +from tickit.devices.eiger.monitor.monitor_status import MonitorStatus # # # # # Eiger MonitorStatus Tests # # # # # diff --git a/tests/eiger/test_eiger_settings.py b/tests/eiger/test_eiger_settings.py index ad6fcfa1..47dedfde 100644 --- a/tests/eiger/test_eiger_settings.py +++ b/tests/eiger/test_eiger_settings.py @@ -1,6 +1,6 @@ import pytest -from tickit_devices.eiger.eiger_settings import EigerSettings, KA_Energy +from tickit.devices.eiger.eiger_settings import EigerSettings, KA_Energy # # # # # EigerStatus Tests # # # # # diff --git a/tests/eiger/test_eiger_status.py b/tests/eiger/test_eiger_status.py index 956b94f7..41c0d222 100644 --- a/tests/eiger/test_eiger_status.py +++ b/tests/eiger/test_eiger_status.py @@ -1,6 +1,6 @@ import pytest -from tickit_devices.eiger.eiger_status import EigerStatus +from tickit.devices.eiger.eiger_status import EigerStatus # # # # # EigerStatus Tests # # # # # diff --git a/tests/eiger/test_eiger_stream_config.py b/tests/eiger/test_eiger_stream_config.py index 868ccea4..4b6b79d6 100644 --- a/tests/eiger/test_eiger_stream_config.py +++ b/tests/eiger/test_eiger_stream_config.py @@ -1,6 +1,6 @@ import pytest -from tickit_devices.eiger.stream.stream_config import StreamConfig +from tickit.devices.eiger.stream.stream_config import StreamConfig # # # # # Eiger StreamConfig Tests # # # # # diff --git a/tests/eiger/test_eiger_stream_status.py b/tests/eiger/test_eiger_stream_status.py index 347d5bc5..e7789ef6 100644 --- a/tests/eiger/test_eiger_stream_status.py +++ b/tests/eiger/test_eiger_stream_status.py @@ -1,6 +1,6 @@ import pytest -from tickit_devices.eiger.stream.stream_status import StreamStatus +from tickit.devices.eiger.stream.stream_status import StreamStatus # # # # # Eiger StreamStatus Tests # # # # # From 2f8d800c8b7af86dc58eac78474d1986a7cbd1b2 Mon Sep 17 00:00:00 2001 From: Abigail Emery Date: Fri, 16 Jun 2023 13:30:23 +0000 Subject: [PATCH 02/22] Add test to flag missing __init__.py files in tickit directory --- tests/test_package_structure.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/test_package_structure.py diff --git a/tests/test_package_structure.py b/tests/test_package_structure.py new file mode 100644 index 00000000..74fa6008 --- /dev/null +++ b/tests/test_package_structure.py @@ -0,0 +1,20 @@ +import os +from pathlib import Path + +import tickit_devices + + +def test_all_ticket_folders_are_packages() -> None: + top_level = Path(tickit_devices.__file__).parent + _assert_are_packages(top_level) + + +def _assert_are_packages(top_level: Path) -> None: + non_packages = [] + for dirpath, _, _ in os.walk(top_level): + init = Path(dirpath) / "__init__.py" + if not init.exists(): + non_packages += [dirpath] + assert ( + not non_packages + ), f"The following directories need an __init__.py: {non_packages}" \ No newline at end of file From a3dced9815c9d1bf1da8932ec9d47bca761d6510 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Mon, 3 Jul 2023 10:43:30 +0100 Subject: [PATCH 03/22] Adapt to WIP Http Adapter PR --- src/tickit_devices/eiger/__init__.py | 5 +++-- src/tickit_devices/eiger/eiger_adapters.py | 6 ++++++ tests/eiger/test_eiger_stream_status.py | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/tickit_devices/eiger/__init__.py b/src/tickit_devices/eiger/__init__.py index 6ae79baa..6bb33489 100644 --- a/src/tickit_devices/eiger/__init__.py +++ b/src/tickit_devices/eiger/__init__.py @@ -2,8 +2,9 @@ from tickit.core.components.component import Component, ComponentConfig from tickit.core.components.device_simulation import DeviceSimulation -from tickit.devices.eiger.eiger import EigerDevice -from tickit.devices.eiger.eiger_adapters import EigerRESTAdapter, EigerZMQAdapter + +from tickit_devices.eiger.eiger import EigerDevice +from tickit_devices.eiger.eiger_adapters import EigerRESTAdapter, EigerZMQAdapter @dataclass diff --git a/src/tickit_devices/eiger/eiger_adapters.py b/src/tickit_devices/eiger/eiger_adapters.py index 6e7c085c..a1dc1c74 100644 --- a/src/tickit_devices/eiger/eiger_adapters.py +++ b/src/tickit_devices/eiger/eiger_adapters.py @@ -45,7 +45,13 @@ async def get_config(self, request: web.Request) -> web.Response: return web.json_response(data) +<<<<<<< HEAD @HttpEndpoint.put(f"/{DETECTOR_API}" + "/config/{parameter_name}") +======= + @HttpEndpoint.put( + f"/{DETECTOR_API}" + "/config/{parameter_name}", include_json=True + ) +>>>>>>> 5fe979d (Adapt to WIP Http Adapter PR) async def put_config(self, request: web.Request) -> web.Response: """A HTTP Endpoint for setting configuration variables for the Eiger. diff --git a/tests/eiger/test_eiger_stream_status.py b/tests/eiger/test_eiger_stream_status.py index e7789ef6..347d5bc5 100644 --- a/tests/eiger/test_eiger_stream_status.py +++ b/tests/eiger/test_eiger_stream_status.py @@ -1,6 +1,6 @@ import pytest -from tickit.devices.eiger.stream.stream_status import StreamStatus +from tickit_devices.eiger.stream.stream_status import StreamStatus # # # # # Eiger StreamStatus Tests # # # # # From f275d0a5b6eae97fb78ab9ef6aa821be5b1414f4 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Mon, 3 Jul 2023 12:46:55 +0100 Subject: [PATCH 04/22] Fix tests --- src/tickit_devices/eiger/eiger_adapters.py | 6 ------ src/tickit_devices/eiger/filewriter/filewriter_config.py | 2 +- src/tickit_devices/eiger/monitor/monitor_config.py | 2 +- src/tickit_devices/eiger/stream/stream_config.py | 2 +- tests/eiger/test_eiger_filewriter_config.py | 2 +- tests/eiger/test_eiger_filewriter_status.py | 2 +- tests/eiger/test_eiger_monitor_config.py | 2 +- tests/eiger/test_eiger_monitor_status.py | 2 +- tests/eiger/test_eiger_settings.py | 2 +- tests/eiger/test_eiger_status.py | 2 +- tests/eiger/test_eiger_stream_config.py | 2 +- 11 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/tickit_devices/eiger/eiger_adapters.py b/src/tickit_devices/eiger/eiger_adapters.py index a1dc1c74..6e7c085c 100644 --- a/src/tickit_devices/eiger/eiger_adapters.py +++ b/src/tickit_devices/eiger/eiger_adapters.py @@ -45,13 +45,7 @@ async def get_config(self, request: web.Request) -> web.Response: return web.json_response(data) -<<<<<<< HEAD @HttpEndpoint.put(f"/{DETECTOR_API}" + "/config/{parameter_name}") -======= - @HttpEndpoint.put( - f"/{DETECTOR_API}" + "/config/{parameter_name}", include_json=True - ) ->>>>>>> 5fe979d (Adapt to WIP Http Adapter PR) async def put_config(self, request: web.Request) -> web.Response: """A HTTP Endpoint for setting configuration variables for the Eiger. diff --git a/src/tickit_devices/eiger/filewriter/filewriter_config.py b/src/tickit_devices/eiger/filewriter/filewriter_config.py index 8a7ac44f..b4b47bfb 100644 --- a/src/tickit_devices/eiger/filewriter/filewriter_config.py +++ b/src/tickit_devices/eiger/filewriter/filewriter_config.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field, fields from typing import Any -from tickit.devices.eiger.eiger_schema import rw_bool, rw_int, rw_str +from tickit_devices.eiger.eiger_schema import rw_bool, rw_int, rw_str @dataclass diff --git a/src/tickit_devices/eiger/monitor/monitor_config.py b/src/tickit_devices/eiger/monitor/monitor_config.py index 9c3bd5ec..3b4bd0ed 100644 --- a/src/tickit_devices/eiger/monitor/monitor_config.py +++ b/src/tickit_devices/eiger/monitor/monitor_config.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field, fields from typing import Any -from tickit.devices.eiger.eiger_schema import rw_int, rw_str +from tickit_devices.eiger.eiger_schema import rw_int, rw_str @dataclass diff --git a/src/tickit_devices/eiger/stream/stream_config.py b/src/tickit_devices/eiger/stream/stream_config.py index 0eec1e3b..0581f871 100644 --- a/src/tickit_devices/eiger/stream/stream_config.py +++ b/src/tickit_devices/eiger/stream/stream_config.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field, fields from typing import Any -from tickit.devices.eiger.eiger_schema import rw_str +from tickit_devices.eiger.eiger_schema import rw_str @dataclass diff --git a/tests/eiger/test_eiger_filewriter_config.py b/tests/eiger/test_eiger_filewriter_config.py index 911fc22c..13e12ca8 100644 --- a/tests/eiger/test_eiger_filewriter_config.py +++ b/tests/eiger/test_eiger_filewriter_config.py @@ -1,6 +1,6 @@ import pytest -from tickit.devices.eiger.filewriter.filewriter_config import FileWriterConfig +from tickit_devices.eiger.filewriter.filewriter_config import FileWriterConfig # # # # # Eiger FileWriterConfig Tests # # # # # diff --git a/tests/eiger/test_eiger_filewriter_status.py b/tests/eiger/test_eiger_filewriter_status.py index 0429b570..9650ea1b 100644 --- a/tests/eiger/test_eiger_filewriter_status.py +++ b/tests/eiger/test_eiger_filewriter_status.py @@ -1,6 +1,6 @@ import pytest -from tickit.devices.eiger.filewriter.filewriter_status import FileWriterStatus +from tickit_devices.eiger.filewriter.filewriter_status import FileWriterStatus # # # # # Eiger FileWriterStatus Tests # # # # # diff --git a/tests/eiger/test_eiger_monitor_config.py b/tests/eiger/test_eiger_monitor_config.py index 013d4a3b..f4149fbe 100644 --- a/tests/eiger/test_eiger_monitor_config.py +++ b/tests/eiger/test_eiger_monitor_config.py @@ -1,6 +1,6 @@ import pytest -from tickit.devices.eiger.monitor.monitor_config import MonitorConfig +from tickit_devices.eiger.monitor.monitor_config import MonitorConfig # # # # # Eiger MonitorConfig Tests # # # # # diff --git a/tests/eiger/test_eiger_monitor_status.py b/tests/eiger/test_eiger_monitor_status.py index 9642fc3e..573f854c 100644 --- a/tests/eiger/test_eiger_monitor_status.py +++ b/tests/eiger/test_eiger_monitor_status.py @@ -1,6 +1,6 @@ import pytest -from tickit.devices.eiger.monitor.monitor_status import MonitorStatus +from tickit_devices.eiger.monitor.monitor_status import MonitorStatus # # # # # Eiger MonitorStatus Tests # # # # # diff --git a/tests/eiger/test_eiger_settings.py b/tests/eiger/test_eiger_settings.py index 47dedfde..ad6fcfa1 100644 --- a/tests/eiger/test_eiger_settings.py +++ b/tests/eiger/test_eiger_settings.py @@ -1,6 +1,6 @@ import pytest -from tickit.devices.eiger.eiger_settings import EigerSettings, KA_Energy +from tickit_devices.eiger.eiger_settings import EigerSettings, KA_Energy # # # # # EigerStatus Tests # # # # # diff --git a/tests/eiger/test_eiger_status.py b/tests/eiger/test_eiger_status.py index 41c0d222..956b94f7 100644 --- a/tests/eiger/test_eiger_status.py +++ b/tests/eiger/test_eiger_status.py @@ -1,6 +1,6 @@ import pytest -from tickit.devices.eiger.eiger_status import EigerStatus +from tickit_devices.eiger.eiger_status import EigerStatus # # # # # EigerStatus Tests # # # # # diff --git a/tests/eiger/test_eiger_stream_config.py b/tests/eiger/test_eiger_stream_config.py index 4b6b79d6..868ccea4 100644 --- a/tests/eiger/test_eiger_stream_config.py +++ b/tests/eiger/test_eiger_stream_config.py @@ -1,6 +1,6 @@ import pytest -from tickit.devices.eiger.stream.stream_config import StreamConfig +from tickit_devices.eiger.stream.stream_config import StreamConfig # # # # # Eiger StreamConfig Tests # # # # # From c1b122c9a0364f9b71d9ebda674f1b0b9b157338 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Mon, 3 Jul 2023 13:50:16 +0100 Subject: [PATCH 05/22] Begin removing private calls from tests --- tests/eiger/test_eiger.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/eiger/test_eiger.py b/tests/eiger/test_eiger.py index c4e3eea2..4924c93c 100644 --- a/tests/eiger/test_eiger.py +++ b/tests/eiger/test_eiger.py @@ -24,6 +24,10 @@ def test_starting_state_is_na(eiger: EigerDevice): assert_in_state(eiger, State.NA) +def test_starting_state_is_na(eiger: EigerDevice): + assert_in_state(eiger, State.NA) + + @pytest.mark.asyncio async def test_initialize(eiger: EigerDevice): await eiger.initialize() @@ -115,6 +119,9 @@ async def test_armed_eiger_starts_series(eiger: EigerDevice, mock_stream: Mock): await eiger.arm() mock_stream.begin_series.assert_called_once_with(eiger.settings, 1) + eiger.update(SimTime(0.0), {}) + eiger.update(SimTime(0.0), {}) + @pytest.mark.asyncio async def test_disarmed_eiger_starts_and_ends_series( From aa491c623717c0a0c7a51a5376eb7aff9cdc2d45 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Wed, 5 Jul 2023 15:34:22 +0100 Subject: [PATCH 06/22] Ensure interrupts in correct place --- src/tickit_devices/eiger/eiger_inspector.py | 59 +++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/tickit_devices/eiger/eiger_inspector.py diff --git a/src/tickit_devices/eiger/eiger_inspector.py b/src/tickit_devices/eiger/eiger_inspector.py new file mode 100644 index 00000000..ea2e4b23 --- /dev/null +++ b/src/tickit_devices/eiger/eiger_inspector.py @@ -0,0 +1,59 @@ +import asyncio +import json +from pprint import pprint + +import aiozmq +import zmq +from aiohttp import ClientResponse, ClientSession + + +async def zmq_listen(ready: asyncio.Event) -> None: + addr = "tcp://127.0.0.1:9999" + socket = await aiozmq.create_zmq_stream(zmq.PULL, connect=addr) + try: + print(f"Connected to {addr}") + ready.set() + while True: + msg = await socket.read() + print("Received new message:") + formatted = json.loads(msg) + pprint(formatted) + finally: + socket.close() + + +async def setup_eiger(session: ClientSession) -> None: + print("Eiger setup") + url_base = "http://localhost:8081/detector/api/1.8.0" + async with session.put(f"{url_base}/command/initialize") as response: + verify_sequence(response) + + print("Initialized") + async with session.put( + f"{url_base}/config/trigger_mode", json={"value": "ints"} + ) as response: + verify_sequence(response) + print("Trigger mode set") + async with session.put(f"{url_base}/command/arm") as response: + verify_sequence(response) + print("Armed") + async with session.put(f"{url_base}/command/trigger") as resonse: + verify_sequence(resonse) + + +def verify_sequence(response: ClientResponse) -> None: + if response.status != 200: + raise Exception(response.status) + + +async def main() -> None: + ready = asyncio.Event() + listen = asyncio.create_task(zmq_listen(ready)) + await ready.wait() + async with ClientSession() as session: + await setup_eiger(session) + await listen + + +if __name__ == "__main__": + asyncio.run(main()) From 442e7200071945a884ff165a54d37bed650edf05 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Thu, 6 Jul 2023 16:19:11 +0100 Subject: [PATCH 07/22] Rationalise stream messages into a schema --- src/tickit_devices/eiger/data/schema.py | 44 +++++++++++++++++++ src/tickit_devices/eiger/eiger.py | 5 +++ .../eiger/stream/eiger_stream.py | 7 +++ 3 files changed, 56 insertions(+) diff --git a/src/tickit_devices/eiger/data/schema.py b/src/tickit_devices/eiger/data/schema.py index de9f20d5..25db40ed 100644 --- a/src/tickit_devices/eiger/data/schema.py +++ b/src/tickit_devices/eiger/data/schema.py @@ -1,6 +1,14 @@ +<<<<<<< HEAD from typing import Any, Dict, Iterable, Tuple, Union from pydantic.v1 import BaseModel +======= +import json +from dataclasses import dataclass +from typing import Any, Dict, Iterable, List, Optional, Tuple, Union + +from pydantic import BaseModel, Field +>>>>>>> b9b2e0b (Rationalise stream messages into a schema) from zmq import Frame Json = Dict[str, Any] @@ -36,12 +44,42 @@ class AcquisitionDetailsHeader(BaseModel): type: str +<<<<<<< HEAD class ImageHeader(BaseModel): """Sent before a detector image blob. Metadata about the acquisition operation. """ +======= + details = [self.flat_field, self.pixel_mask, self.countrate] + for detail in details: + if detail: + yield from detail.to_message() + + +DEFAULT_HEADER_TYPE = "dheader-1.0" + + +class AcquisitionSeriesHeader(BaseModel): + header_detail: str + series: int + htype: str = DEFAULT_HEADER_TYPE + + +class AcquisitionSeriesFooter(BaseModel): + series: int + htype: str = "dseries_end-1.0" + + +class AcquisitionDetailsHeader(BaseModel): + htype: str = DEFAULT_HEADER_TYPE + shape: Tuple[int, int] + type: str + + +class ImageHeader(BaseModel): +>>>>>>> b9b2e0b (Rationalise stream messages into a schema) frame: int hash: str series: int @@ -49,11 +87,14 @@ class ImageHeader(BaseModel): class ImageCharacteristicsHeader(BaseModel): +<<<<<<< HEAD """Sent before a detector image blob. Metadata about the image. """ +======= +>>>>>>> b9b2e0b (Rationalise stream messages into a schema) encoding: str shape: Tuple[int, int] size: int @@ -62,11 +103,14 @@ class ImageCharacteristicsHeader(BaseModel): class ImageConfigHeader(BaseModel): +<<<<<<< HEAD """Sent before a detector image blob. Describes the metrics on the image acquisition. """ +======= +>>>>>>> b9b2e0b (Rationalise stream messages into a schema) real_time: float start_time: float stop_time: float diff --git a/src/tickit_devices/eiger/eiger.py b/src/tickit_devices/eiger/eiger.py index 6f38dab5..b9856c2a 100644 --- a/src/tickit_devices/eiger/eiger.py +++ b/src/tickit_devices/eiger/eiger.py @@ -14,6 +14,11 @@ from tickit_devices.eiger.monitor.monitor_config import MonitorConfig from tickit_devices.eiger.monitor.monitor_status import MonitorStatus from tickit_devices.eiger.stream.eiger_stream import EigerStream +<<<<<<< HEAD +======= +from tickit_devices.eiger.stream.stream_config import StreamConfig +from tickit_devices.eiger.stream.stream_status import StreamStatus +>>>>>>> b9b2e0b (Rationalise stream messages into a schema) from .eiger_status import EigerStatus, State diff --git a/src/tickit_devices/eiger/stream/eiger_stream.py b/src/tickit_devices/eiger/stream/eiger_stream.py index acdc477d..bcacce97 100644 --- a/src/tickit_devices/eiger/stream/eiger_stream.py +++ b/src/tickit_devices/eiger/stream/eiger_stream.py @@ -1,3 +1,4 @@ +import json import logging from queue import Queue from typing import Any, Iterable, Mapping, TypedDict, Union @@ -5,6 +6,7 @@ from pydantic.v1 import BaseModel from tickit.core.typedefs import SimTime from typing_extensions import TypedDict +from zmq import Frame from tickit_devices.eiger.data.dummy_image import Image from tickit_devices.eiger.data.schema import ( @@ -24,6 +26,9 @@ _Message = Union[BaseModel, Mapping[str, Any], bytes] +_Sendable = Union[bytes, Frame, memoryview] +_Message = Union[_Sendable, str, Mapping[str, Any], BaseModel] + class EigerStream: """Simulation of an Eiger stream.""" @@ -34,6 +39,8 @@ class EigerStream: _message_buffer: Queue[_Message] + _message_buffer: Queue[_Sendable] + #: An empty typed mapping of input values Inputs: TypedDict = TypedDict("Inputs", {}) #: A typed mapping containing the 'value' output value From daf5ef6712a7f52810decff536027722e1150701 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Fri, 7 Jul 2023 15:00:32 +0100 Subject: [PATCH 08/22] Remove inspector class --- src/tickit_devices/eiger/eiger_inspector.py | 59 --------------------- 1 file changed, 59 deletions(-) delete mode 100644 src/tickit_devices/eiger/eiger_inspector.py diff --git a/src/tickit_devices/eiger/eiger_inspector.py b/src/tickit_devices/eiger/eiger_inspector.py deleted file mode 100644 index ea2e4b23..00000000 --- a/src/tickit_devices/eiger/eiger_inspector.py +++ /dev/null @@ -1,59 +0,0 @@ -import asyncio -import json -from pprint import pprint - -import aiozmq -import zmq -from aiohttp import ClientResponse, ClientSession - - -async def zmq_listen(ready: asyncio.Event) -> None: - addr = "tcp://127.0.0.1:9999" - socket = await aiozmq.create_zmq_stream(zmq.PULL, connect=addr) - try: - print(f"Connected to {addr}") - ready.set() - while True: - msg = await socket.read() - print("Received new message:") - formatted = json.loads(msg) - pprint(formatted) - finally: - socket.close() - - -async def setup_eiger(session: ClientSession) -> None: - print("Eiger setup") - url_base = "http://localhost:8081/detector/api/1.8.0" - async with session.put(f"{url_base}/command/initialize") as response: - verify_sequence(response) - - print("Initialized") - async with session.put( - f"{url_base}/config/trigger_mode", json={"value": "ints"} - ) as response: - verify_sequence(response) - print("Trigger mode set") - async with session.put(f"{url_base}/command/arm") as response: - verify_sequence(response) - print("Armed") - async with session.put(f"{url_base}/command/trigger") as resonse: - verify_sequence(resonse) - - -def verify_sequence(response: ClientResponse) -> None: - if response.status != 200: - raise Exception(response.status) - - -async def main() -> None: - ready = asyncio.Event() - listen = asyncio.create_task(zmq_listen(ready)) - await ready.wait() - async with ClientSession() as session: - await setup_eiger(session) - await listen - - -if __name__ == "__main__": - asyncio.run(main()) From 15821a6487f6d3f2023380617e05f1f036ba382d Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Thu, 13 Jul 2023 09:30:16 +0100 Subject: [PATCH 09/22] Fix imports and formatting --- src/tickit_devices/eiger/stream/eiger_stream.py | 2 -- tests/test_package_structure.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/tickit_devices/eiger/stream/eiger_stream.py b/src/tickit_devices/eiger/stream/eiger_stream.py index bcacce97..ec166d02 100644 --- a/src/tickit_devices/eiger/stream/eiger_stream.py +++ b/src/tickit_devices/eiger/stream/eiger_stream.py @@ -1,4 +1,3 @@ -import json import logging from queue import Queue from typing import Any, Iterable, Mapping, TypedDict, Union @@ -6,7 +5,6 @@ from pydantic.v1 import BaseModel from tickit.core.typedefs import SimTime from typing_extensions import TypedDict -from zmq import Frame from tickit_devices.eiger.data.dummy_image import Image from tickit_devices.eiger.data.schema import ( diff --git a/tests/test_package_structure.py b/tests/test_package_structure.py index 74fa6008..4b60242b 100644 --- a/tests/test_package_structure.py +++ b/tests/test_package_structure.py @@ -17,4 +17,4 @@ def _assert_are_packages(top_level: Path) -> None: non_packages += [dirpath] assert ( not non_packages - ), f"The following directories need an __init__.py: {non_packages}" \ No newline at end of file + ), f"The following directories need an __init__.py: {non_packages}" From 897ab480d2cd7f0cd22c2eebe53ad669bcc599da Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Fri, 14 Jul 2023 15:54:22 +0100 Subject: [PATCH 10/22] Delete package structure test --- tests/test_package_structure.py | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 tests/test_package_structure.py diff --git a/tests/test_package_structure.py b/tests/test_package_structure.py deleted file mode 100644 index 4b60242b..00000000 --- a/tests/test_package_structure.py +++ /dev/null @@ -1,20 +0,0 @@ -import os -from pathlib import Path - -import tickit_devices - - -def test_all_ticket_folders_are_packages() -> None: - top_level = Path(tickit_devices.__file__).parent - _assert_are_packages(top_level) - - -def _assert_are_packages(top_level: Path) -> None: - non_packages = [] - for dirpath, _, _ in os.walk(top_level): - init = Path(dirpath) / "__init__.py" - if not init.exists(): - non_packages += [dirpath] - assert ( - not non_packages - ), f"The following directories need an __init__.py: {non_packages}" From 76e7c2b6c1c173ca782f8ded199176e319abe669 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Fri, 21 Jul 2023 11:45:15 +0100 Subject: [PATCH 11/22] Convert eiger schema to pydantic --- src/tickit_devices/eiger/eiger_adapters.py | 28 ++++++++---- src/tickit_devices/eiger/eiger_schema.py | 53 ++++++++++------------ 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/src/tickit_devices/eiger/eiger_adapters.py b/src/tickit_devices/eiger/eiger_adapters.py index 6e7c085c..0abf0ce6 100644 --- a/src/tickit_devices/eiger/eiger_adapters.py +++ b/src/tickit_devices/eiger/eiger_adapters.py @@ -41,7 +41,13 @@ async def get_config(self, request: web.Request) -> web.Response: data = construct_value(self.device.settings, param) else: - data = serialize(Value("None", "string", access_mode="None")) + data = serialize( + Value( + value="None", + value_type="string", + access_mode="None", + ) + ) return web.json_response(data) @@ -97,7 +103,13 @@ async def get_status(self, request: web.Request) -> web.Response: data = construct_value(self.device.status, param) else: - data = serialize(Value("None", "string", access_mode="None")) + data = serialize( + Value( + value="None", + value_type="string", + access_mode="None", + ) + ) return web.json_response(data) @@ -141,7 +153,7 @@ async def initialize_eiger(self, request: web.Request) -> web.Response: await self.device.initialize() LOGGER.debug("Initializing Eiger...") - return web.json_response(serialize(SequenceComplete(1))) + return web.json_response(serialize(SequenceComplete(sequence_id=1))) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/arm", interrupt=True) async def arm_eiger(self, request: web.Request) -> web.Response: @@ -157,7 +169,7 @@ async def arm_eiger(self, request: web.Request) -> web.Response: await self.device.arm() LOGGER.debug("Arming Eiger...") - return web.json_response(serialize(SequenceComplete(2))) + return web.json_response(serialize(SequenceComplete(sequence_id=2))) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/disarm", interrupt=True) async def disarm_eiger(self, request: web.Request) -> web.Response: @@ -173,7 +185,7 @@ async def disarm_eiger(self, request: web.Request) -> web.Response: await self.device.disarm() LOGGER.debug("Disarming Eiger...") - return web.json_response(serialize(SequenceComplete(3))) + return web.json_response(serialize(SequenceComplete(sequence_id=3))) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/trigger", interrupt=False) async def trigger_eiger(self, request: web.Request) -> web.Response: @@ -192,7 +204,7 @@ async def trigger_eiger(self, request: web.Request) -> web.Response: await self.raise_interrupt() await self.device.finished_aquisition.wait() - return web.json_response(serialize(SequenceComplete(4))) + return web.json_response(serialize(SequenceComplete(sequence_id=4))) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/cancel", interrupt=True) async def cancel_eiger(self, request: web.Request) -> web.Response: @@ -208,7 +220,7 @@ async def cancel_eiger(self, request: web.Request) -> web.Response: await self.device.cancel() LOGGER.debug("Cancelling Eiger...") - return web.json_response(serialize(SequenceComplete(5))) + return web.json_response(serialize(SequenceComplete(sequence_id=5))) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/abort", interrupt=True) async def abort_eiger(self, request: web.Request) -> web.Response: @@ -224,7 +236,7 @@ async def abort_eiger(self, request: web.Request) -> web.Response: await self.device.abort() LOGGER.debug("Aborting Eiger...") - return web.json_response(serialize(SequenceComplete(6))) + return web.json_response(serialize(SequenceComplete(sequence_id=6))) @HttpEndpoint.get(f"/{STREAM_API}" + "/status/{param}") async def get_stream_status(self, request: web.Request) -> web.Response: diff --git a/src/tickit_devices/eiger/eiger_schema.py b/src/tickit_devices/eiger/eiger_schema.py index 1e59833a..883f8a8f 100644 --- a/src/tickit_devices/eiger/eiger_schema.py +++ b/src/tickit_devices/eiger/eiger_schema.py @@ -4,10 +4,12 @@ from functools import partial from typing import Any, Generic, List, Mapping, Optional, TypeVar -from apischema import serialized -from apischema.fields import with_fields_set -from apischema.metadata import skip -from apischema.serialization import serialize +from pydantic.v1 import BaseModel, Field + +# from apischema import serialized +# from apischema.fields import with_fields_set +# from apischema.metadata import skip +# from apischema.serialization import serialize T = TypeVar("T") @@ -105,14 +107,12 @@ class ValueType(Enum): ) -@with_fields_set -@dataclass -class Value(Generic[T]): +class Value(BaseModel, Generic[T]): """Schema for a value to be returned by the API. Most fields are optional.""" value: T value_type: str - access_mode: Optional[str] = None + access_mode: Optional[AccessMode] = None unit: Optional[str] = None min: Optional[T] = None max: Optional[T] = None @@ -124,34 +124,27 @@ def construct_value(obj, param): # noqa: D103 meta = obj[param]["metadata"] if "allowed_values" in meta: - data = serialize( - Value( - value, - meta["value_type"].value, - access_mode=meta["access_mode"].value, - allowed_values=meta["allowed_values"], - ) - ) + data = Value( + value=value, + value_type=meta["value_type"].value, + access_mode=meta["access_mode"].value, + allowed_values=meta["allowed_values"], + ).dict() else: - data = serialize( - Value( - value, - meta["value_type"].value, - access_mode=meta["access_mode"].value, - ) - ) + data = Value( + value=value, + value_type=meta["value_type"].value, + access_mode=meta["access_mode"].value, + ).dict() return data -@dataclass -class SequenceComplete: +class SequenceComplete(BaseModel): """Schema for confirmation returned by operations that do not return values.""" - _sequence_id: int = field(default=1, metadata=skip, init=True, repr=False) + sequence_id: int = Field(default=1, alias="sequence id") - @serialized("sequence id") # type: ignore - @property - def sequence_id(self) -> int: # noqa: D102 - return self._sequence_id + class Config: + allow_population_by_field_name = True From 60ee76f9739d5c638a64086c74ed55f044b3885e Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Fri, 21 Jul 2023 13:04:30 +0100 Subject: [PATCH 12/22] Fix sequence complete alias --- src/tickit_devices/eiger/eiger_adapters.py | 43 +++++++++++++++++----- src/tickit_devices/eiger/eiger_schema.py | 22 ++++++++--- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/tickit_devices/eiger/eiger_adapters.py b/src/tickit_devices/eiger/eiger_adapters.py index 0abf0ce6..369823f8 100644 --- a/src/tickit_devices/eiger/eiger_adapters.py +++ b/src/tickit_devices/eiger/eiger_adapters.py @@ -1,13 +1,19 @@ import logging +from typing import Any, Dict, List, Union from aiohttp import web -from apischema import serialize +from pydantic import BaseModel from tickit.adapters.httpadapter import HttpAdapter from tickit.adapters.interpreters.endpoints.http_endpoint import HttpEndpoint from tickit.adapters.zeromq.push_adapter import ZeroMqPushAdapter from tickit_devices.eiger.eiger import EigerDevice -from tickit_devices.eiger.eiger_schema import SequenceComplete, Value, construct_value +from tickit_devices.eiger.eiger_schema import ( + AccessMode, + SequenceComplete, + Value, + construct_value, +) from tickit_devices.eiger.eiger_status import State API_VERSION = "1.8.0" @@ -45,7 +51,7 @@ async def get_config(self, request: web.Request) -> web.Response: Value( value="None", value_type="string", - access_mode="None", + access_mode=AccessMode.NONE, ) ) @@ -107,7 +113,7 @@ async def get_status(self, request: web.Request) -> web.Response: Value( value="None", value_type="string", - access_mode="None", + access_mode=AccessMode.NONE, ) ) @@ -153,7 +159,7 @@ async def initialize_eiger(self, request: web.Request) -> web.Response: await self.device.initialize() LOGGER.debug("Initializing Eiger...") - return web.json_response(serialize(SequenceComplete(sequence_id=1))) + return web.json_response(serialize(SequenceComplete.number(1))) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/arm", interrupt=True) async def arm_eiger(self, request: web.Request) -> web.Response: @@ -169,7 +175,7 @@ async def arm_eiger(self, request: web.Request) -> web.Response: await self.device.arm() LOGGER.debug("Arming Eiger...") - return web.json_response(serialize(SequenceComplete(sequence_id=2))) + return web.json_response(serialize(SequenceComplete.number(2))) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/disarm", interrupt=True) async def disarm_eiger(self, request: web.Request) -> web.Response: @@ -185,7 +191,7 @@ async def disarm_eiger(self, request: web.Request) -> web.Response: await self.device.disarm() LOGGER.debug("Disarming Eiger...") - return web.json_response(serialize(SequenceComplete(sequence_id=3))) + return web.json_response(serialize(SequenceComplete.number(3))) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/trigger", interrupt=False) async def trigger_eiger(self, request: web.Request) -> web.Response: @@ -204,7 +210,7 @@ async def trigger_eiger(self, request: web.Request) -> web.Response: await self.raise_interrupt() await self.device.finished_aquisition.wait() - return web.json_response(serialize(SequenceComplete(sequence_id=4))) + return web.json_response(serialize(SequenceComplete.number(4))) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/cancel", interrupt=True) async def cancel_eiger(self, request: web.Request) -> web.Response: @@ -220,7 +226,7 @@ async def cancel_eiger(self, request: web.Request) -> web.Response: await self.device.cancel() LOGGER.debug("Cancelling Eiger...") - return web.json_response(serialize(SequenceComplete(sequence_id=5))) + return web.json_response(serialize(SequenceComplete.number(5))) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/abort", interrupt=True) async def abort_eiger(self, request: web.Request) -> web.Response: @@ -236,7 +242,7 @@ async def abort_eiger(self, request: web.Request) -> web.Response: await self.device.abort() LOGGER.debug("Aborting Eiger...") - return web.json_response(serialize(SequenceComplete(sequence_id=6))) + return web.json_response(serialize(SequenceComplete.number(6))) @HttpEndpoint.get(f"/{STREAM_API}" + "/status/{param}") async def get_stream_status(self, request: web.Request) -> web.Response: @@ -437,3 +443,20 @@ def after_update(self) -> None: """Updates IOC values immediately following a device update.""" buffered_data = self.device.stream.consume_data() self.send_message_sequence_soon([list(buffered_data)]) + + +def serialize( + obj: Union[str, float, bool, List, Dict, BaseModel] +) -> Union[str, float, bool, List, Dict]: + """Shortcut to serialize pydantic base models to dictionaries. + + Args: + obj: Serializable object + + Returns: + Union[str, float, bool, List, Dict]: Serialized object + """ + if isinstance(obj, BaseModel): + return obj.model_dump() + else: + return obj diff --git a/src/tickit_devices/eiger/eiger_schema.py b/src/tickit_devices/eiger/eiger_schema.py index 883f8a8f..970215d4 100644 --- a/src/tickit_devices/eiger/eiger_schema.py +++ b/src/tickit_devices/eiger/eiger_schema.py @@ -1,16 +1,10 @@ import logging -from dataclasses import dataclass, field from enum import Enum from functools import partial from typing import Any, Generic, List, Mapping, Optional, TypeVar from pydantic.v1 import BaseModel, Field -# from apischema import serialized -# from apischema.fields import with_fields_set -# from apischema.metadata import skip -# from apischema.serialization import serialize - T = TypeVar("T") LOGGER = logging.getLogger(__name__) @@ -37,6 +31,7 @@ class AccessMode(Enum): READ_ONLY: str = "r" WRITE_ONLY: str = "w" READ_WRITE: str = "rw" + NONE: str = "None" class ValueType(Enum): @@ -146,5 +141,20 @@ class SequenceComplete(BaseModel): sequence_id: int = Field(default=1, alias="sequence id") + @classmethod + def number(cls, number: int) -> "SequenceComplete": + """Create a new completion document with the given ID. + + This function exists as a workaround for mypy ignoring aliases. + See https://github.com/pydantic/pydantic/discussions/2889 + + Args: + number: The sequence ID + + Returns: + SequenceComplete: Document describing a completed sequence of operations + """ + return SequenceComplete(sequence_id=number) # type: ignore + class Config: allow_population_by_field_name = True From f73dbabc4c14b8bf9b4c3d14534c501946f10c30 Mon Sep 17 00:00:00 2001 From: DiamondJoseph <53935796+DiamondJoseph@users.noreply.github.com> Date: Fri, 21 Jul 2023 11:46:13 +0100 Subject: [PATCH 13/22] Update eiger_adapters.py --- src/tickit_devices/eiger/eiger_adapters.py | 60 +++++++++------------- 1 file changed, 24 insertions(+), 36 deletions(-) diff --git a/src/tickit_devices/eiger/eiger_adapters.py b/src/tickit_devices/eiger/eiger_adapters.py index 369823f8..5b83e3e5 100644 --- a/src/tickit_devices/eiger/eiger_adapters.py +++ b/src/tickit_devices/eiger/eiger_adapters.py @@ -2,7 +2,7 @@ from typing import Any, Dict, List, Union from aiohttp import web -from pydantic import BaseModel +from pydantic.v1 import BaseModel from tickit.adapters.httpadapter import HttpAdapter from tickit.adapters.interpreters.endpoints.http_endpoint import HttpEndpoint from tickit.adapters.zeromq.push_adapter import ZeroMqPushAdapter @@ -47,15 +47,9 @@ async def get_config(self, request: web.Request) -> web.Response: data = construct_value(self.device.settings, param) else: - data = serialize( - Value( - value="None", - value_type="string", - access_mode=AccessMode.NONE, - ) - ) + data = Value(value="None", value_type="string", access_mode=AccessMode.NONE) - return web.json_response(data) + return web.json_response(data.dict()) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/config/{parameter_name}") async def put_config(self, request: web.Request) -> web.Response: @@ -71,11 +65,11 @@ async def put_config(self, request: web.Request) -> web.Response: """ param = request.match_info["parameter_name"] - response = await request.json() + response = await request.dict() if self.device.get_state() is not State.IDLE: LOGGER.warning("Eiger not initialized or is currently running.") - return web.json_response(serialize([])) + return web.json_response([]) elif ( hasattr(self.device.settings, param) and self.device.get_state() is State.IDLE @@ -87,10 +81,10 @@ async def put_config(self, request: web.Request) -> web.Response: self.device.settings[param] = attr LOGGER.debug("Set " + str(param) + " to " + str(attr)) - return web.json_response(serialize([param])) + return web.json_response([param]) else: LOGGER.debug("Eiger has no config variable: " + str(param)) - return web.json_response(serialize([])) + return web.json_response([]) @HttpEndpoint.get(f"/{DETECTOR_API}" + "/status/{status_param}") async def get_status(self, request: web.Request) -> web.Response: @@ -109,15 +103,9 @@ async def get_status(self, request: web.Request) -> web.Response: data = construct_value(self.device.status, param) else: - data = serialize( - Value( - value="None", - value_type="string", - access_mode=AccessMode.NONE, - ) - ) + data = Value(value="None", value_type="string", access_mode=AccessMode.NONE) - return web.json_response(data) + return web.json_response(data.dict()) @HttpEndpoint.get(f"/{DETECTOR_API}" + "/status/board_000/{status_param}") async def get_board_000_status(self, request: web.Request) -> web.Response: @@ -159,7 +147,7 @@ async def initialize_eiger(self, request: web.Request) -> web.Response: await self.device.initialize() LOGGER.debug("Initializing Eiger...") - return web.json_response(serialize(SequenceComplete.number(1))) + return web.json_response(SequenceComplete(sequence_id=1).dict()) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/arm", interrupt=True) async def arm_eiger(self, request: web.Request) -> web.Response: @@ -175,7 +163,7 @@ async def arm_eiger(self, request: web.Request) -> web.Response: await self.device.arm() LOGGER.debug("Arming Eiger...") - return web.json_response(serialize(SequenceComplete.number(2))) + return web.json_response(SequenceComplete(sequence_id=2).dict()) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/disarm", interrupt=True) async def disarm_eiger(self, request: web.Request) -> web.Response: @@ -191,7 +179,7 @@ async def disarm_eiger(self, request: web.Request) -> web.Response: await self.device.disarm() LOGGER.debug("Disarming Eiger...") - return web.json_response(serialize(SequenceComplete.number(3))) + return web.json_response(SequenceComplete(sequence_id=3).dict()) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/trigger", interrupt=False) async def trigger_eiger(self, request: web.Request) -> web.Response: @@ -210,7 +198,7 @@ async def trigger_eiger(self, request: web.Request) -> web.Response: await self.raise_interrupt() await self.device.finished_aquisition.wait() - return web.json_response(serialize(SequenceComplete.number(4))) + return web.json_response(SequenceComplete(sequence_id=4).dict()) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/cancel", interrupt=True) async def cancel_eiger(self, request: web.Request) -> web.Response: @@ -226,7 +214,7 @@ async def cancel_eiger(self, request: web.Request) -> web.Response: await self.device.cancel() LOGGER.debug("Cancelling Eiger...") - return web.json_response(serialize(SequenceComplete.number(5))) + return web.json_response(SequenceComplete(sequence_id=5).dict()) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/abort", interrupt=True) async def abort_eiger(self, request: web.Request) -> web.Response: @@ -242,7 +230,7 @@ async def abort_eiger(self, request: web.Request) -> web.Response: await self.device.abort() LOGGER.debug("Aborting Eiger...") - return web.json_response(serialize(SequenceComplete.number(6))) + return web.json_response(SequenceComplete(sequence_id=6).dict()) @HttpEndpoint.get(f"/{STREAM_API}" + "/status/{param}") async def get_stream_status(self, request: web.Request) -> web.Response: @@ -292,7 +280,7 @@ async def put_stream_config(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - response = await request.json() + response = await request.dict() if hasattr(self.device.stream.config, param): attr = response["value"] @@ -302,10 +290,10 @@ async def put_stream_config(self, request: web.Request) -> web.Response: self.device.stream.config[param] = attr LOGGER.debug("Set " + str(param) + " to " + str(attr)) - return web.json_response(serialize([param])) + return web.json_response([param]) else: LOGGER.debug("Eiger has no config variable: " + str(param)) - return web.json_response(serialize([])) + return web.json_response([]) @HttpEndpoint.get(f"/{MONITOR_API}" + "/config/{param}") async def get_monitor_config(self, request: web.Request) -> web.Response: @@ -338,7 +326,7 @@ async def put_monitor_config(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - response = await request.json() + response = await request.dict() if hasattr(self.device.monitor_config, param): attr = response["value"] @@ -348,10 +336,10 @@ async def put_monitor_config(self, request: web.Request) -> web.Response: self.device.monitor_config[param] = attr LOGGER.debug("Set " + str(param) + " to " + str(attr)) - return web.json_response(serialize([param])) + return web.json_response([param]) else: LOGGER.debug("Eiger has no config variable: " + str(param)) - return web.json_response(serialize([])) + return web.json_response([]) @HttpEndpoint.get(f"/{MONITOR_API}" + "/status/{param}") async def get_monitor_status(self, request: web.Request) -> web.Response: @@ -401,7 +389,7 @@ async def put_filewriter_config(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - response = await request.json() + response = await request.dict() if hasattr(self.device.filewriter_config, param): attr = response["value"] @@ -411,10 +399,10 @@ async def put_filewriter_config(self, request: web.Request) -> web.Response: self.device.filewriter_config[param] = attr LOGGER.debug("Set " + str(param) + " to " + str(attr)) - return web.json_response(serialize([param])) + return web.json_response([param]) else: LOGGER.debug("Eiger has no config variable: " + str(param)) - return web.json_response(serialize([])) + return web.json_response([]) @HttpEndpoint.get(f"/{FILEWRITER_API}" + "/status/{param}") async def get_filewriter_status(self, request: web.Request) -> web.Response: From 9319e973766fa02427cd1559708dd89c813e0a44 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Fri, 21 Jul 2023 13:41:49 +0100 Subject: [PATCH 14/22] Fix pydantic stuff --- src/tickit_devices/eiger/eiger_adapters.py | 50 ++++++++-------------- src/tickit_devices/eiger/eiger_schema.py | 33 ++++++++------ src/tickit_devices/eiger/eiger_status.py | 2 +- src/tickit_devices/utils.py | 34 +++++++++++++++ tests/eiger/test_eiger_schema.py | 21 +++++++++ tests/eiger/test_eiger_system.py | 2 +- 6 files changed, 96 insertions(+), 46 deletions(-) create mode 100644 src/tickit_devices/utils.py create mode 100644 tests/eiger/test_eiger_schema.py diff --git a/src/tickit_devices/eiger/eiger_adapters.py b/src/tickit_devices/eiger/eiger_adapters.py index 5b83e3e5..0164c252 100644 --- a/src/tickit_devices/eiger/eiger_adapters.py +++ b/src/tickit_devices/eiger/eiger_adapters.py @@ -15,6 +15,7 @@ construct_value, ) from tickit_devices.eiger.eiger_status import State +from tickit_devices.utils import serialize API_VERSION = "1.8.0" DETECTOR_API = f"detector/api/{API_VERSION}" @@ -47,9 +48,11 @@ async def get_config(self, request: web.Request) -> web.Response: data = construct_value(self.device.settings, param) else: - data = Value(value="None", value_type="string", access_mode=AccessMode.NONE) + data = serialize( + Value(value="None", value_type="string", access_mode=AccessMode.NONE) + ) - return web.json_response(data.dict()) + return web.json_response(data) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/config/{parameter_name}") async def put_config(self, request: web.Request) -> web.Response: @@ -65,7 +68,7 @@ async def put_config(self, request: web.Request) -> web.Response: """ param = request.match_info["parameter_name"] - response = await request.dict() + response = await request.json() if self.device.get_state() is not State.IDLE: LOGGER.warning("Eiger not initialized or is currently running.") @@ -103,9 +106,11 @@ async def get_status(self, request: web.Request) -> web.Response: data = construct_value(self.device.status, param) else: - data = Value(value="None", value_type="string", access_mode=AccessMode.NONE) + data = serialize( + Value(value="None", value_type="string", access_mode=AccessMode.NONE) + ) - return web.json_response(data.dict()) + return web.json_response(data) @HttpEndpoint.get(f"/{DETECTOR_API}" + "/status/board_000/{status_param}") async def get_board_000_status(self, request: web.Request) -> web.Response: @@ -147,7 +152,7 @@ async def initialize_eiger(self, request: web.Request) -> web.Response: await self.device.initialize() LOGGER.debug("Initializing Eiger...") - return web.json_response(SequenceComplete(sequence_id=1).dict()) + return web.json_response(serialize(SequenceComplete.number(1))) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/arm", interrupt=True) async def arm_eiger(self, request: web.Request) -> web.Response: @@ -163,7 +168,7 @@ async def arm_eiger(self, request: web.Request) -> web.Response: await self.device.arm() LOGGER.debug("Arming Eiger...") - return web.json_response(SequenceComplete(sequence_id=2).dict()) + return web.json_response(serialize(SequenceComplete.number(2))) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/disarm", interrupt=True) async def disarm_eiger(self, request: web.Request) -> web.Response: @@ -179,7 +184,7 @@ async def disarm_eiger(self, request: web.Request) -> web.Response: await self.device.disarm() LOGGER.debug("Disarming Eiger...") - return web.json_response(SequenceComplete(sequence_id=3).dict()) + return web.json_response(serialize(SequenceComplete.number(3))) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/trigger", interrupt=False) async def trigger_eiger(self, request: web.Request) -> web.Response: @@ -198,7 +203,7 @@ async def trigger_eiger(self, request: web.Request) -> web.Response: await self.raise_interrupt() await self.device.finished_aquisition.wait() - return web.json_response(SequenceComplete(sequence_id=4).dict()) + return web.json_response(serialize(SequenceComplete.number(4))) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/cancel", interrupt=True) async def cancel_eiger(self, request: web.Request) -> web.Response: @@ -214,7 +219,7 @@ async def cancel_eiger(self, request: web.Request) -> web.Response: await self.device.cancel() LOGGER.debug("Cancelling Eiger...") - return web.json_response(SequenceComplete(sequence_id=5).dict()) + return web.json_response(serialize(SequenceComplete.number(5))) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/abort", interrupt=True) async def abort_eiger(self, request: web.Request) -> web.Response: @@ -230,7 +235,7 @@ async def abort_eiger(self, request: web.Request) -> web.Response: await self.device.abort() LOGGER.debug("Aborting Eiger...") - return web.json_response(SequenceComplete(sequence_id=6).dict()) + return web.json_response(serialize(SequenceComplete.number(6))) @HttpEndpoint.get(f"/{STREAM_API}" + "/status/{param}") async def get_stream_status(self, request: web.Request) -> web.Response: @@ -280,7 +285,7 @@ async def put_stream_config(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - response = await request.dict() + response = await request.json() if hasattr(self.device.stream.config, param): attr = response["value"] @@ -326,7 +331,7 @@ async def put_monitor_config(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - response = await request.dict() + response = await request.json() if hasattr(self.device.monitor_config, param): attr = response["value"] @@ -389,7 +394,7 @@ async def put_filewriter_config(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - response = await request.dict() + response = await request.json() if hasattr(self.device.filewriter_config, param): attr = response["value"] @@ -431,20 +436,3 @@ def after_update(self) -> None: """Updates IOC values immediately following a device update.""" buffered_data = self.device.stream.consume_data() self.send_message_sequence_soon([list(buffered_data)]) - - -def serialize( - obj: Union[str, float, bool, List, Dict, BaseModel] -) -> Union[str, float, bool, List, Dict]: - """Shortcut to serialize pydantic base models to dictionaries. - - Args: - obj: Serializable object - - Returns: - Union[str, float, bool, List, Dict]: Serialized object - """ - if isinstance(obj, BaseModel): - return obj.model_dump() - else: - return obj diff --git a/src/tickit_devices/eiger/eiger_schema.py b/src/tickit_devices/eiger/eiger_schema.py index 970215d4..59e82a6f 100644 --- a/src/tickit_devices/eiger/eiger_schema.py +++ b/src/tickit_devices/eiger/eiger_schema.py @@ -5,6 +5,8 @@ from pydantic.v1 import BaseModel, Field +from tickit_devices.utils import serialize + T = TypeVar("T") LOGGER = logging.getLogger(__name__) @@ -25,7 +27,7 @@ def field_config(**kwargs) -> Mapping[str, Any]: return dict(**kwargs) -class AccessMode(Enum): +class AccessMode(str, Enum): """Possible access modes for field metadata.""" READ_ONLY: str = "r" @@ -34,7 +36,7 @@ class AccessMode(Enum): NONE: str = "None" -class ValueType(Enum): +class ValueType(str, Enum): """Possible value types for field metadata.""" FLOAT: str = "float" @@ -119,19 +121,23 @@ def construct_value(obj, param): # noqa: D103 meta = obj[param]["metadata"] if "allowed_values" in meta: - data = Value( - value=value, - value_type=meta["value_type"].value, - access_mode=meta["access_mode"].value, - allowed_values=meta["allowed_values"], - ).dict() + data = serialize( + Value( + value=value, + value_type=meta["value_type"].value, + access_mode=meta["access_mode"].value, + allowed_values=meta["allowed_values"], + ) + ) else: - data = Value( - value=value, - value_type=meta["value_type"].value, - access_mode=meta["access_mode"].value, - ).dict() + data = serialize( + Value( + value=value, + value_type=meta["value_type"].value, + access_mode=meta["access_mode"].value, + ) + ) return data @@ -158,3 +164,4 @@ def number(cls, number: int) -> "SequenceComplete": class Config: allow_population_by_field_name = True + fields = {"sequence_id": "sequence id"} diff --git a/src/tickit_devices/eiger/eiger_status.py b/src/tickit_devices/eiger/eiger_status.py index 74a96ec4..932e0553 100644 --- a/src/tickit_devices/eiger/eiger_status.py +++ b/src/tickit_devices/eiger/eiger_status.py @@ -6,7 +6,7 @@ from .eiger_schema import ro_str_list, rw_datetime, rw_float, rw_state -class State(Enum): +class State(str, Enum): """Possible states of the Eiger detector.""" NA = "na" diff --git a/src/tickit_devices/utils.py b/src/tickit_devices/utils.py new file mode 100644 index 00000000..075a0dee --- /dev/null +++ b/src/tickit_devices/utils.py @@ -0,0 +1,34 @@ +from typing import Any, List, Mapping, Union + +from pydantic.v1 import BaseModel + +_Serialized = Union[str, float, int, bool, List[Any], Mapping[str, Any]] +_Serializable = Union[_Serialized, BaseModel] + + +def serialize(document: _Serializable) -> _Serialized: + """Helper to serialize using pydantic base models + + Args: + document: A JSON-serializable document or base model + + Raises: + TypeError: If the document cannot be serialized + + Returns: + _Serialized: A JSON-serializable document + """ + + if ( + isinstance(document, str) + or isinstance(document, float) + or isinstance(document, int) + or isinstance(document, bool) + or isinstance(document, list) + or isinstance(document, dict) + ): + return document + elif isinstance(document, BaseModel): + return document.dict(by_alias=True) + else: + raise TypeError(f"Document {document} is of unrecognized type {type(document)}") diff --git a/tests/eiger/test_eiger_schema.py b/tests/eiger/test_eiger_schema.py new file mode 100644 index 00000000..b743279d --- /dev/null +++ b/tests/eiger/test_eiger_schema.py @@ -0,0 +1,21 @@ +import pytest + +from tickit_devices.eiger.eiger_schema import SequenceComplete +from tickit_devices.utils import serialize + + +@pytest.mark.parametrize("sequence_id", [1, 2]) +def test_sequence_complete_uses_alternative_constructor(sequence_id: int) -> None: + complete = SequenceComplete.number(sequence_id) + assert complete.sequence_id == sequence_id + + +@pytest.mark.parametrize("sequence_id", [1, 2]) +def test_sequence_complete_uses_space_in_field_name(sequence_id: int) -> None: + complete = SequenceComplete.number(sequence_id) + assert serialize(complete)["sequence id"] == sequence_id + + +def test_sequence_complete_uses_alias_only() -> None: + complete = SequenceComplete.number(1) + assert "sequence_id" not in serialize(complete).keys() diff --git a/tests/eiger/test_eiger_system.py b/tests/eiger/test_eiger_system.py index 5233ad29..0535edc9 100644 --- a/tests/eiger/test_eiger_system.py +++ b/tests/eiger/test_eiger_system.py @@ -47,7 +47,7 @@ async def get_status(status, expected): DETECTOR_URL + f"command/{key}", timeout=REQUEST_TIMEOUT, ) as response: - assert value == (await response.json()) + assert value == (await response.json()), key # Check status await get_status(status="doesnt_exist", expected="None") From 4deaee1f0d5dc980f5b7d372a1b416b060f7bb7d Mon Sep 17 00:00:00 2001 From: DiamondJoseph <53935796+DiamondJoseph@users.noreply.github.com> Date: Fri, 21 Jul 2023 11:46:13 +0100 Subject: [PATCH 15/22] Update eiger_adapters.py --- src/tickit_devices/eiger/eiger_adapters.py | 47 +++++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/src/tickit_devices/eiger/eiger_adapters.py b/src/tickit_devices/eiger/eiger_adapters.py index 0164c252..950d40f0 100644 --- a/src/tickit_devices/eiger/eiger_adapters.py +++ b/src/tickit_devices/eiger/eiger_adapters.py @@ -2,7 +2,10 @@ from typing import Any, Dict, List, Union from aiohttp import web +<<<<<<< HEAD from pydantic.v1 import BaseModel +======= +>>>>>>> e21fdaa (Update eiger_adapters.py) from tickit.adapters.httpadapter import HttpAdapter from tickit.adapters.interpreters.endpoints.http_endpoint import HttpEndpoint from tickit.adapters.zeromq.push_adapter import ZeroMqPushAdapter @@ -48,11 +51,15 @@ async def get_config(self, request: web.Request) -> web.Response: data = construct_value(self.device.settings, param) else: +<<<<<<< HEAD data = serialize( Value(value="None", value_type="string", access_mode=AccessMode.NONE) ) +======= + data = Value(value="None", value_type="string", access_mode="None") +>>>>>>> e21fdaa (Update eiger_adapters.py) - return web.json_response(data) + return web.json_response(data.dict()) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/config/{parameter_name}") async def put_config(self, request: web.Request) -> web.Response: @@ -68,7 +75,7 @@ async def put_config(self, request: web.Request) -> web.Response: """ param = request.match_info["parameter_name"] - response = await request.json() + response = await request.dict() if self.device.get_state() is not State.IDLE: LOGGER.warning("Eiger not initialized or is currently running.") @@ -106,11 +113,15 @@ async def get_status(self, request: web.Request) -> web.Response: data = construct_value(self.device.status, param) else: +<<<<<<< HEAD data = serialize( Value(value="None", value_type="string", access_mode=AccessMode.NONE) ) +======= + data = Value(value="None", value_type="string", access_mode="None") +>>>>>>> e21fdaa (Update eiger_adapters.py) - return web.json_response(data) + return web.json_response(data.dict()) @HttpEndpoint.get(f"/{DETECTOR_API}" + "/status/board_000/{status_param}") async def get_board_000_status(self, request: web.Request) -> web.Response: @@ -152,7 +163,11 @@ async def initialize_eiger(self, request: web.Request) -> web.Response: await self.device.initialize() LOGGER.debug("Initializing Eiger...") +<<<<<<< HEAD return web.json_response(serialize(SequenceComplete.number(1))) +======= + return web.json_response(SequenceComplete(sequence_id=1).dict()) +>>>>>>> e21fdaa (Update eiger_adapters.py) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/arm", interrupt=True) async def arm_eiger(self, request: web.Request) -> web.Response: @@ -168,7 +183,11 @@ async def arm_eiger(self, request: web.Request) -> web.Response: await self.device.arm() LOGGER.debug("Arming Eiger...") +<<<<<<< HEAD return web.json_response(serialize(SequenceComplete.number(2))) +======= + return web.json_response(SequenceComplete(sequence_id=2).dict()) +>>>>>>> e21fdaa (Update eiger_adapters.py) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/disarm", interrupt=True) async def disarm_eiger(self, request: web.Request) -> web.Response: @@ -184,7 +203,11 @@ async def disarm_eiger(self, request: web.Request) -> web.Response: await self.device.disarm() LOGGER.debug("Disarming Eiger...") +<<<<<<< HEAD return web.json_response(serialize(SequenceComplete.number(3))) +======= + return web.json_response(SequenceComplete(sequence_id=3).dict()) +>>>>>>> e21fdaa (Update eiger_adapters.py) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/trigger", interrupt=False) async def trigger_eiger(self, request: web.Request) -> web.Response: @@ -203,7 +226,11 @@ async def trigger_eiger(self, request: web.Request) -> web.Response: await self.raise_interrupt() await self.device.finished_aquisition.wait() +<<<<<<< HEAD return web.json_response(serialize(SequenceComplete.number(4))) +======= + return web.json_response(SequenceComplete(sequence_id=4).dict()) +>>>>>>> e21fdaa (Update eiger_adapters.py) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/cancel", interrupt=True) async def cancel_eiger(self, request: web.Request) -> web.Response: @@ -219,7 +246,11 @@ async def cancel_eiger(self, request: web.Request) -> web.Response: await self.device.cancel() LOGGER.debug("Cancelling Eiger...") +<<<<<<< HEAD return web.json_response(serialize(SequenceComplete.number(5))) +======= + return web.json_response(SequenceComplete(sequence_id=5).dict()) +>>>>>>> e21fdaa (Update eiger_adapters.py) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/abort", interrupt=True) async def abort_eiger(self, request: web.Request) -> web.Response: @@ -235,7 +266,11 @@ async def abort_eiger(self, request: web.Request) -> web.Response: await self.device.abort() LOGGER.debug("Aborting Eiger...") +<<<<<<< HEAD return web.json_response(serialize(SequenceComplete.number(6))) +======= + return web.json_response(SequenceComplete(sequence_id=6).dict()) +>>>>>>> e21fdaa (Update eiger_adapters.py) @HttpEndpoint.get(f"/{STREAM_API}" + "/status/{param}") async def get_stream_status(self, request: web.Request) -> web.Response: @@ -285,7 +320,7 @@ async def put_stream_config(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - response = await request.json() + response = await request.dict() if hasattr(self.device.stream.config, param): attr = response["value"] @@ -331,7 +366,7 @@ async def put_monitor_config(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - response = await request.json() + response = await request.dict() if hasattr(self.device.monitor_config, param): attr = response["value"] @@ -394,7 +429,7 @@ async def put_filewriter_config(self, request: web.Request) -> web.Response: """ param = request.match_info["param"] - response = await request.json() + response = await request.dict() if hasattr(self.device.filewriter_config, param): attr = response["value"] From 34e601046c921f61be372636db8cb79d9c0f24f7 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Fri, 21 Jul 2023 13:46:38 +0100 Subject: [PATCH 16/22] Fix imports and mypy --- tests/eiger/test_eiger_schema.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/eiger/test_eiger_schema.py b/tests/eiger/test_eiger_schema.py index b743279d..6955074d 100644 --- a/tests/eiger/test_eiger_schema.py +++ b/tests/eiger/test_eiger_schema.py @@ -13,9 +13,13 @@ def test_sequence_complete_uses_alternative_constructor(sequence_id: int) -> Non @pytest.mark.parametrize("sequence_id", [1, 2]) def test_sequence_complete_uses_space_in_field_name(sequence_id: int) -> None: complete = SequenceComplete.number(sequence_id) - assert serialize(complete)["sequence id"] == sequence_id + serialized = serialize(complete) + assert isinstance(serialized, dict) + assert serialized["sequence id"] == sequence_id def test_sequence_complete_uses_alias_only() -> None: complete = SequenceComplete.number(1) - assert "sequence_id" not in serialize(complete).keys() + serialized = serialize(complete) + assert isinstance(serialized, dict) + assert "sequence_id" not in serialized.keys() From ac0ae2aa2b3a7321e09cc38d708f20d556b45f80 Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Mon, 24 Jul 2023 15:29:46 +0100 Subject: [PATCH 17/22] Add system tests in line with IOC requests --- src/tickit_devices/eiger/eiger_schema.py | 24 +++++------ src/tickit_devices/eiger/eiger_settings.py | 1 + tests/eiger/test_eiger_system.py | 47 ++++++++++++++++++++++ 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/src/tickit_devices/eiger/eiger_schema.py b/src/tickit_devices/eiger/eiger_schema.py index 59e82a6f..9346a68e 100644 --- a/src/tickit_devices/eiger/eiger_schema.py +++ b/src/tickit_devices/eiger/eiger_schema.py @@ -116,27 +116,23 @@ class Value(BaseModel, Generic[T]): allowed_values: Optional[List[str]] = None -def construct_value(obj, param): # noqa: D103 +def construct_value(obj, param) -> Value: # noqa: D103 value = obj[param]["value"] meta = obj[param]["metadata"] if "allowed_values" in meta: - data = serialize( - Value( - value=value, - value_type=meta["value_type"].value, - access_mode=meta["access_mode"].value, - allowed_values=meta["allowed_values"], - ) + data = Value( + value=value, + value_type=meta["value_type"].value, + access_mode=meta["access_mode"].value, + allowed_values=meta["allowed_values"], ) else: - data = serialize( - Value( - value=value, - value_type=meta["value_type"].value, - access_mode=meta["access_mode"].value, - ) + data = Value( + value=value, + value_type=meta["value_type"].value, + access_mode=meta["access_mode"].value, ) return data diff --git a/src/tickit_devices/eiger/eiger_settings.py b/src/tickit_devices/eiger/eiger_settings.py index fec60af9..697a0114 100644 --- a/src/tickit_devices/eiger/eiger_settings.py +++ b/src/tickit_devices/eiger/eiger_settings.py @@ -112,6 +112,7 @@ class EigerSettings: trigger_mode: str = field( default="exts", metadata=rw_str(allowed_values=["exts", "ints", "exte", "inte"]) ) + trigger_start_delay: float = field(default=0.0, metadata=rw_float()) two_theta_increment: float = field(default=0.0, metadata=rw_float()) two_theta_start: float = field(default=0.0, metadata=rw_float()) wavelength: float = field(default=1.0, metadata=rw_float()) diff --git a/tests/eiger/test_eiger_system.py b/tests/eiger/test_eiger_system.py index 0535edc9..23fecbdb 100644 --- a/tests/eiger/test_eiger_system.py +++ b/tests/eiger/test_eiger_system.py @@ -1,5 +1,8 @@ +from datetime import datetime + import aiohttp import pytest +from pydantic.v1 import parse_obj_as DETECTOR_URL = "http://localhost:8081/detector/api/1.8.0/" FILE_WRITER_URL = "http://localhost:8081/filewriter/api/1.8.0/" @@ -55,6 +58,14 @@ async def get_status(status, expected): await get_status(status="board_000/doesnt_exist", expected="None") await get_status(status="builder/dcu_buffer_free", expected=0.5) await get_status(status="builder/doesnt_exist", expected="None") + async with session.get( + DETECTOR_URL + "status/time", + timeout=REQUEST_TIMEOUT, + ) as response: + assert response.status == 200 + value = (await response.json())["value"] + eiger_time = parse_obj_as(datetime, value) + assert isinstance(eiger_time, datetime) # Test Eiger in IDLE state await get_status(status="state", expected="idle") @@ -88,6 +99,24 @@ async def get_status(status, expected): ) as response: assert (await response.json()) == ["element"] + async with session.get( + DETECTOR_URL + "config/frame_time", + timeout=REQUEST_TIMEOUT, + ) as response: + assert response.status == 200 + data = await response.json() + assert data["value"] == 0.12 + assert data["access_mode"] == "rw" + + async with session.put( + DETECTOR_URL + "config/frame_time", + headers=headers, + json={"value": 0.1}, + timeout=REQUEST_TIMEOUT, + ) as response: + assert response.status == 200 + assert (await response.json()) == ["frame_time"] + async with session.get( DETECTOR_URL + "config/photon_energy", timeout=REQUEST_TIMEOUT, @@ -116,6 +145,24 @@ async def get_status(status, expected): ) as response: assert [] == (await response.json()) + async with session.get( + DETECTOR_URL + "config/trigger_start_delay", + timeout=REQUEST_TIMEOUT, + ) as response: + assert response.status == 200 + data = await response.json() + assert data["value"] == 0.0 + assert data["access_mode"] == "rw" + + async with session.put( + DETECTOR_URL + "config/trigger_start_delay", + headers=headers, + json={"value": 0.1}, + timeout=REQUEST_TIMEOUT, + ) as response: + assert response.status == 200 + assert (await response.json()) == ["trigger_start_delay"] + # Test filewriter, monitor and stream endpoints async with session.get( FILE_WRITER_URL + "status/state", From 8ae8017b557aaf6229cbb69a06906caa287ce41c Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Mon, 24 Jul 2023 16:29:20 +0100 Subject: [PATCH 18/22] Fix schema --- src/tickit_devices/eiger/data/schema.py | 44 ------------------------- 1 file changed, 44 deletions(-) diff --git a/src/tickit_devices/eiger/data/schema.py b/src/tickit_devices/eiger/data/schema.py index 25db40ed..de9f20d5 100644 --- a/src/tickit_devices/eiger/data/schema.py +++ b/src/tickit_devices/eiger/data/schema.py @@ -1,14 +1,6 @@ -<<<<<<< HEAD from typing import Any, Dict, Iterable, Tuple, Union from pydantic.v1 import BaseModel -======= -import json -from dataclasses import dataclass -from typing import Any, Dict, Iterable, List, Optional, Tuple, Union - -from pydantic import BaseModel, Field ->>>>>>> b9b2e0b (Rationalise stream messages into a schema) from zmq import Frame Json = Dict[str, Any] @@ -44,42 +36,12 @@ class AcquisitionDetailsHeader(BaseModel): type: str -<<<<<<< HEAD class ImageHeader(BaseModel): """Sent before a detector image blob. Metadata about the acquisition operation. """ -======= - details = [self.flat_field, self.pixel_mask, self.countrate] - for detail in details: - if detail: - yield from detail.to_message() - - -DEFAULT_HEADER_TYPE = "dheader-1.0" - - -class AcquisitionSeriesHeader(BaseModel): - header_detail: str - series: int - htype: str = DEFAULT_HEADER_TYPE - - -class AcquisitionSeriesFooter(BaseModel): - series: int - htype: str = "dseries_end-1.0" - - -class AcquisitionDetailsHeader(BaseModel): - htype: str = DEFAULT_HEADER_TYPE - shape: Tuple[int, int] - type: str - - -class ImageHeader(BaseModel): ->>>>>>> b9b2e0b (Rationalise stream messages into a schema) frame: int hash: str series: int @@ -87,14 +49,11 @@ class ImageHeader(BaseModel): class ImageCharacteristicsHeader(BaseModel): -<<<<<<< HEAD """Sent before a detector image blob. Metadata about the image. """ -======= ->>>>>>> b9b2e0b (Rationalise stream messages into a schema) encoding: str shape: Tuple[int, int] size: int @@ -103,14 +62,11 @@ class ImageCharacteristicsHeader(BaseModel): class ImageConfigHeader(BaseModel): -<<<<<<< HEAD """Sent before a detector image blob. Describes the metrics on the image acquisition. """ -======= ->>>>>>> b9b2e0b (Rationalise stream messages into a schema) real_time: float start_time: float stop_time: float From 9485eda76b4042054e901838b932299bbfa3ba2e Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Mon, 24 Jul 2023 16:29:58 +0100 Subject: [PATCH 19/22] Fix eiger imports --- src/tickit_devices/eiger/eiger.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/tickit_devices/eiger/eiger.py b/src/tickit_devices/eiger/eiger.py index b9856c2a..6f38dab5 100644 --- a/src/tickit_devices/eiger/eiger.py +++ b/src/tickit_devices/eiger/eiger.py @@ -14,11 +14,6 @@ from tickit_devices.eiger.monitor.monitor_config import MonitorConfig from tickit_devices.eiger.monitor.monitor_status import MonitorStatus from tickit_devices.eiger.stream.eiger_stream import EigerStream -<<<<<<< HEAD -======= -from tickit_devices.eiger.stream.stream_config import StreamConfig -from tickit_devices.eiger.stream.stream_status import StreamStatus ->>>>>>> b9b2e0b (Rationalise stream messages into a schema) from .eiger_status import EigerStatus, State From 3b6bd9d49b9a6aa81dff36470284a21b299cc5aa Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Mon, 24 Jul 2023 16:30:30 +0100 Subject: [PATCH 20/22] Remove duplicate variables --- src/tickit_devices/eiger/eiger.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/tickit_devices/eiger/eiger.py b/src/tickit_devices/eiger/eiger.py index 6f38dab5..e783f3e2 100644 --- a/src/tickit_devices/eiger/eiger.py +++ b/src/tickit_devices/eiger/eiger.py @@ -41,9 +41,6 @@ class EigerDevice(Device): _num_frames_left: int _data_queue: Queue - _num_frames_left: int - _data_queue: Queue - #: An empty typed mapping of input values Inputs: TypedDict = TypedDict("Inputs", {"trigger": bool}, total=False) #: A typed mapping containing the 'value' output value From cdecffa827ea251187b7b2ca7d61669f742eb86f Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Mon, 24 Jul 2023 16:31:26 +0100 Subject: [PATCH 21/22] Fix more merge slush --- src/tickit_devices/eiger/eiger_adapters.py | 35 ---------------------- 1 file changed, 35 deletions(-) diff --git a/src/tickit_devices/eiger/eiger_adapters.py b/src/tickit_devices/eiger/eiger_adapters.py index 950d40f0..9504222a 100644 --- a/src/tickit_devices/eiger/eiger_adapters.py +++ b/src/tickit_devices/eiger/eiger_adapters.py @@ -2,10 +2,7 @@ from typing import Any, Dict, List, Union from aiohttp import web -<<<<<<< HEAD from pydantic.v1 import BaseModel -======= ->>>>>>> e21fdaa (Update eiger_adapters.py) from tickit.adapters.httpadapter import HttpAdapter from tickit.adapters.interpreters.endpoints.http_endpoint import HttpEndpoint from tickit.adapters.zeromq.push_adapter import ZeroMqPushAdapter @@ -51,13 +48,9 @@ async def get_config(self, request: web.Request) -> web.Response: data = construct_value(self.device.settings, param) else: -<<<<<<< HEAD data = serialize( Value(value="None", value_type="string", access_mode=AccessMode.NONE) ) -======= - data = Value(value="None", value_type="string", access_mode="None") ->>>>>>> e21fdaa (Update eiger_adapters.py) return web.json_response(data.dict()) @@ -113,13 +106,9 @@ async def get_status(self, request: web.Request) -> web.Response: data = construct_value(self.device.status, param) else: -<<<<<<< HEAD data = serialize( Value(value="None", value_type="string", access_mode=AccessMode.NONE) ) -======= - data = Value(value="None", value_type="string", access_mode="None") ->>>>>>> e21fdaa (Update eiger_adapters.py) return web.json_response(data.dict()) @@ -163,11 +152,7 @@ async def initialize_eiger(self, request: web.Request) -> web.Response: await self.device.initialize() LOGGER.debug("Initializing Eiger...") -<<<<<<< HEAD return web.json_response(serialize(SequenceComplete.number(1))) -======= - return web.json_response(SequenceComplete(sequence_id=1).dict()) ->>>>>>> e21fdaa (Update eiger_adapters.py) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/arm", interrupt=True) async def arm_eiger(self, request: web.Request) -> web.Response: @@ -183,11 +168,7 @@ async def arm_eiger(self, request: web.Request) -> web.Response: await self.device.arm() LOGGER.debug("Arming Eiger...") -<<<<<<< HEAD return web.json_response(serialize(SequenceComplete.number(2))) -======= - return web.json_response(SequenceComplete(sequence_id=2).dict()) ->>>>>>> e21fdaa (Update eiger_adapters.py) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/disarm", interrupt=True) async def disarm_eiger(self, request: web.Request) -> web.Response: @@ -203,11 +184,7 @@ async def disarm_eiger(self, request: web.Request) -> web.Response: await self.device.disarm() LOGGER.debug("Disarming Eiger...") -<<<<<<< HEAD return web.json_response(serialize(SequenceComplete.number(3))) -======= - return web.json_response(SequenceComplete(sequence_id=3).dict()) ->>>>>>> e21fdaa (Update eiger_adapters.py) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/trigger", interrupt=False) async def trigger_eiger(self, request: web.Request) -> web.Response: @@ -226,11 +203,7 @@ async def trigger_eiger(self, request: web.Request) -> web.Response: await self.raise_interrupt() await self.device.finished_aquisition.wait() -<<<<<<< HEAD return web.json_response(serialize(SequenceComplete.number(4))) -======= - return web.json_response(SequenceComplete(sequence_id=4).dict()) ->>>>>>> e21fdaa (Update eiger_adapters.py) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/cancel", interrupt=True) async def cancel_eiger(self, request: web.Request) -> web.Response: @@ -246,11 +219,7 @@ async def cancel_eiger(self, request: web.Request) -> web.Response: await self.device.cancel() LOGGER.debug("Cancelling Eiger...") -<<<<<<< HEAD return web.json_response(serialize(SequenceComplete.number(5))) -======= - return web.json_response(SequenceComplete(sequence_id=5).dict()) ->>>>>>> e21fdaa (Update eiger_adapters.py) @HttpEndpoint.put(f"/{DETECTOR_API}" + "/command/abort", interrupt=True) async def abort_eiger(self, request: web.Request) -> web.Response: @@ -266,11 +235,7 @@ async def abort_eiger(self, request: web.Request) -> web.Response: await self.device.abort() LOGGER.debug("Aborting Eiger...") -<<<<<<< HEAD return web.json_response(serialize(SequenceComplete.number(6))) -======= - return web.json_response(SequenceComplete(sequence_id=6).dict()) ->>>>>>> e21fdaa (Update eiger_adapters.py) @HttpEndpoint.get(f"/{STREAM_API}" + "/status/{param}") async def get_stream_status(self, request: web.Request) -> web.Response: From cd4fc85d34e057ef11f67142ad892d94fe12905e Mon Sep 17 00:00:00 2001 From: Callum Forrester Date: Mon, 24 Jul 2023 16:32:53 +0100 Subject: [PATCH 22/22] Remove dependency on apischema --- pyproject.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f3b54956..299fef11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,8 +15,7 @@ dependencies = [ "tickit==0.2.3", "typing_extensions", "softioc", - "pydantic>1", - "apischema" + "pydantic>1" ] dynamic = ["version"] license.file = "LICENSE"