From 93d7d75b0dc07ac4548912acd32edd41187c0d28 Mon Sep 17 00:00:00 2001 From: Julian Stirling Date: Wed, 6 May 2026 10:05:34 +0100 Subject: [PATCH] Create basic gallery class that finds Things that want to show data --- ofm_config_full.json | 1 + ofm_config_simulation.json | 1 + .../things/__init__.py | 7 +++ .../things/camera/__init__.py | 3 +- .../things/gallery.py | 31 ++++++++++++ .../things/smart_scan.py | 3 ++ tests/conftest.py | 20 ++++++++ tests/integration_tests/test_actions.py | 49 ++++++------------- tests/unit_tests/test_gallery.py | 17 +++++++ 9 files changed, 97 insertions(+), 35 deletions(-) create mode 100644 src/openflexure_microscope_server/things/gallery.py create mode 100644 tests/unit_tests/test_gallery.py diff --git a/ofm_config_full.json b/ofm_config_full.json index 27024a2c..16a211fb 100644 --- a/ofm_config_full.json +++ b/ofm_config_full.json @@ -8,6 +8,7 @@ }, "stage": "openflexure_microscope_server.things.stage.sangaboard:SangaboardThing", "autofocus": "openflexure_microscope_server.things.autofocus:AutofocusThing", + "gallery": "openflexure_microscope_server.things.gallery.GalleryThing", "camera_stage_mapping": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper", "system": "openflexure_microscope_server.things.system:OpenFlexureSystem", "illumination": "openflexure_microscope_server.things.illumination:SangaIllumination", diff --git a/ofm_config_simulation.json b/ofm_config_simulation.json index b04988d4..b52ce4d7 100644 --- a/ofm_config_simulation.json +++ b/ofm_config_simulation.json @@ -3,6 +3,7 @@ "camera": "openflexure_microscope_server.things.camera.simulation:SimulatedCamera", "stage": "openflexure_microscope_server.things.stage.dummy:DummyStage", "autofocus": "openflexure_microscope_server.things.autofocus:AutofocusThing", + "gallery": "openflexure_microscope_server.things.gallery.GalleryThing", "camera_stage_mapping": "openflexure_microscope_server.things.camera_stage_mapping:CameraStageMapper", "illumination": "openflexure_microscope_server.things.illumination.SimulatorIllumination", "system": "openflexure_microscope_server.things.system:OpenFlexureSystem", diff --git a/src/openflexure_microscope_server/things/__init__.py b/src/openflexure_microscope_server/things/__init__.py index 75ba3d24..896c2e70 100644 --- a/src/openflexure_microscope_server/things/__init__.py +++ b/src/openflexure_microscope_server/things/__init__.py @@ -15,6 +15,13 @@ class OFMThing(lt.Thing): _data_dir: Optional[str] = None + _show_in_gallery: bool = False + + @property + def show_in_gallery(self) -> bool: + """Whether to show in the Gallery.""" + return self._show_in_gallery + def __enter__(self) -> Self: """Set the data directory when the Thing is entered.""" # Note that the `application_config` was already validated when the diff --git a/src/openflexure_microscope_server/things/camera/__init__.py b/src/openflexure_microscope_server/things/camera/__init__.py index 6b9bc323..96840889 100644 --- a/src/openflexure_microscope_server/things/camera/__init__.py +++ b/src/openflexure_microscope_server/things/camera/__init__.py @@ -25,6 +25,7 @@ from pydantic import BaseModel, Field import labthings_fastapi as lt from labthings_fastapi.types.numpy import NDArray +from openflexure_microscope_server.things import OFMThing from openflexure_microscope_server.things.background_detect import ( BackgroundDetectAlgorithm, ) @@ -164,7 +165,7 @@ class CameraMemoryBuffer: del self._storage[key] -class BaseCamera(lt.Thing): +class BaseCamera(OFMThing): """The base class for all cameras. All cameras must directly inherit from this class. The connection to the camera hardware should be added to the ``__enter__`` method not diff --git a/src/openflexure_microscope_server/things/gallery.py b/src/openflexure_microscope_server/things/gallery.py new file mode 100644 index 00000000..e8767928 --- /dev/null +++ b/src/openflexure_microscope_server/things/gallery.py @@ -0,0 +1,31 @@ +"""Server-side gallery functionality. + +The types of data that are captured and how they display in the gallery are defined by +the Things that capture the Data. This thing just provides a unified way for the gallery +front end to access the data. +""" + +from typing import Mapping, Optional + +import labthings_fastapi as lt + +from openflexure_microscope_server.things import OFMThing + + +class GalleryThing(lt.Thing): + """A Thing for communicating with the front end gallery.""" + + all_ofm_things: Mapping[str, OFMThing] = lt.thing_slot() + + _gallery_providing_things: Optional[Mapping[str, OFMThing]] = None + + @property + def gallery_providing_things(self) -> Mapping[str, OFMThing]: + """All Things that provide data to the gallery.""" + if self._gallery_providing_things is None: + self._gallery_providing_things = { + name: thing + for name, thing in self.all_ofm_things.items() + if thing.show_in_gallery + } + return self._gallery_providing_things diff --git a/src/openflexure_microscope_server/things/smart_scan.py b/src/openflexure_microscope_server/things/smart_scan.py index cb7488fd..1e2ae5d5 100644 --- a/src/openflexure_microscope_server/things/smart_scan.py +++ b/src/openflexure_microscope_server/things/smart_scan.py @@ -176,6 +176,9 @@ class SmartScanThing(OFMThing): In this case it doesn't need to do anything. """ + # Register with gallery. + _show_in_gallery = True + # Note that the default detector name is set at init. This is over written if # setting is loaded from disk. @lt.setting diff --git a/tests/conftest.py b/tests/conftest.py index 68d6a483..f7fb28c1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,8 @@ """Fixtures for all test suites.""" +import json import logging +import os import re from collections.abc import Iterable from contextlib import contextmanager @@ -10,6 +12,24 @@ import pytest from labthings_fastapi.testing import create_thing_without_server +from .shared_utils.lt_test_utils import LabThingsTestEnv + +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +REPO_ROOT = os.path.dirname(THIS_DIR) +SIM_CONFIG = os.path.join(REPO_ROOT, "ofm_config_simulation.json") + + +@pytest.fixture +def simulation_test_env(): + """Yield a server with the configuration from the simulation json.""" + with open(SIM_CONFIG, "r", encoding="utf-8") as f_obj: + config_dict = json.load(f_obj) + with LabThingsTestEnv( + things=config_dict["things"], + application_config=config_dict["application_config"], + ) as env: + yield env + @pytest.fixture def check_side_effect(caplog): diff --git a/tests/integration_tests/test_actions.py b/tests/integration_tests/test_actions.py index 4f2afda0..5872c518 100644 --- a/tests/integration_tests/test_actions.py +++ b/tests/integration_tests/test_actions.py @@ -12,7 +12,6 @@ it has been moved as it artificaially inflated coverage. """ import json -import os import numpy as np import piexif @@ -21,29 +20,11 @@ from PIL import Image import labthings_fastapi as lt -from ..shared_utils.lt_test_utils import LabThingsTestEnv -THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -REPO_ROOT = os.path.dirname(os.path.dirname(THIS_DIR)) -SIM_CONFIG = os.path.join(REPO_ROOT, "ofm_config_simulation.json") - - -@pytest.fixture -def test_env(): - """Yield a server with a very basic configuration.""" - with open(SIM_CONFIG, "r", encoding="utf-8") as f_obj: - config_dict = json.load(f_obj) - with LabThingsTestEnv( - things=config_dict["things"], - application_config=config_dict["application_config"], - ) as env: - yield env - - -def test_autofocus(test_env): +def test_autofocus(simulation_test_env): """Test Fast Autofocus can run doesn't raise an exception.""" - stage = test_env.get_thing_client("stage") - autofocus = test_env.get_thing_client("autofocus") + stage = simulation_test_env.get_thing_client("stage") + autofocus = simulation_test_env.get_thing_client("autofocus") assert stage.position["z"] == 0 # Autofocus 5 times and check each ends within 500 steps for i in range(5): @@ -51,17 +32,17 @@ def test_autofocus(test_env): assert abs(stage.position["z"]) < 500, f"Autofocus failed on iteration {i}" -def test_grab_jpeg(test_env): +def test_grab_jpeg(simulation_test_env): """Check that grab_jpeg returns a blob that can be opened.""" - camera = test_env.get_thing_client("camera") + camera = simulation_test_env.get_thing_client("camera") blob = camera.grab_jpeg() image = Image.open(blob.open()) assert image.size == (820, 616) -def test_capture_jpeg_metadata(test_env): +def test_capture_jpeg_metadata(simulation_test_env): """Check that the position is encoded into the image metadata.""" - camera = test_env.get_thing_client("camera") + camera = simulation_test_env.get_thing_client("camera") blob = camera.capture_jpeg() image = Image.open(blob.open()) exif_dict = piexif.load(image.info["exif"]) @@ -72,9 +53,9 @@ def test_capture_jpeg_metadata(test_env): assert image.size == (820, 616) -def test_stage(test_env): +def test_stage(simulation_test_env): """Test moving the stage forwards and backwards.""" - stage = test_env.get_thing_client("stage") + stage = simulation_test_env.get_thing_client("stage") start = stage.position move = {"x": 1, "y": 2, "z": 3} move_back = {"x": -1, "y": -2, "z": -3} @@ -95,20 +76,20 @@ def test_stage(test_env): assert start["z"] == pos["z"] -def test_capture_array(test_env): +def test_capture_array(simulation_test_env): """Capture array from simulation and check the size is as expected.""" - camera = test_env.get_thing_client("camera") + camera = simulation_test_env.get_thing_client("camera") array = np.asarray(camera.capture_array()) assert array.shape == (616, 820, 3) -def test_camera_stage_mapping_calibration(test_env): +def test_camera_stage_mapping_calibration(simulation_test_env): """Check that camera stage mapping runs and returns the expected result.""" - camera = test_env.get_thing_client("camera") + camera = simulation_test_env.get_thing_client("camera") # Remove camera settling time for speed. camera.settling_time = 0 - csm = test_env.get_thing_client("camera_stage_mapping") - stage = test_env.get_thing_client("stage") + csm = simulation_test_env.get_thing_client("camera_stage_mapping") + stage = simulation_test_env.get_thing_client("stage") # Check it starts uncalibrated assert csm.calibration_required diff --git a/tests/unit_tests/test_gallery.py b/tests/unit_tests/test_gallery.py new file mode 100644 index 00000000..176be45f --- /dev/null +++ b/tests/unit_tests/test_gallery.py @@ -0,0 +1,17 @@ +"""Tests that captures have the expected metadata.""" + + +def test_gallery_thing_finds_all_providers(simulation_test_env): + """Check the gallery identifies the correct Things that will provide data.""" + gallery = simulation_test_env.get_thing_by_name("gallery") + snake_workflow = simulation_test_env.get_thing_by_name("snake_workflow") + smart_scan = simulation_test_env.get_thing_by_name("smart_scan") + camera = simulation_test_env.get_thing_by_name("camera") + + assert snake_workflow not in gallery.all_ofm_things.values() + assert camera in gallery.all_ofm_things.values() + assert smart_scan in gallery.all_ofm_things.values() + + assert snake_workflow not in gallery.gallery_providing_things.values() + assert camera not in gallery.gallery_providing_things.values() + assert smart_scan in gallery.gallery_providing_things.values()