Merge branch 'start-gallery-thing' into 'v3'
Start implementing GalleryThing See merge request openflexure/openflexure-microscope-server!576
This commit is contained in:
commit
906b8be82c
13 changed files with 321 additions and 104 deletions
|
|
@ -15,6 +15,13 @@ class OFMThing(lt.Thing):
|
|||
|
||||
_data_dir: Optional[str] = None
|
||||
|
||||
_show_data_in_gallery: bool = False
|
||||
|
||||
@property
|
||||
def show_data_in_gallery(self) -> bool:
|
||||
"""Whether to show in the Gallery."""
|
||||
return self._show_data_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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
99
src/openflexure_microscope_server/things/gallery.py
Normal file
99
src/openflexure_microscope_server/things/gallery.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
"""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 Any, Mapping, Optional, Protocol, Self, cast, runtime_checkable
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
import labthings_fastapi as lt
|
||||
|
||||
from openflexure_microscope_server.things import OFMThing
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class GalleryCompatibleThing(Protocol):
|
||||
"""Protocol for checking Things using the gallery are complete.
|
||||
|
||||
Note that runtime checkable protocols only check that the methods exist, not the
|
||||
full signatures. This means that anything attempting to define the methods will
|
||||
be picked up, but may throw an error when used.
|
||||
"""
|
||||
|
||||
# Ensure it is a Thing:
|
||||
_thing_server_interface: lt.ThingServerInterface
|
||||
|
||||
# Ensure it is an OFMThing:
|
||||
show_data_in_gallery: bool
|
||||
|
||||
gallery_data_name: str
|
||||
|
||||
gallery_data_schema: type[BaseModel]
|
||||
|
||||
# Ignore D102: No docstrings for the protocol.
|
||||
def get_data_for_gallery(self) -> list[BaseModel]: ... # noqa: D102
|
||||
|
||||
|
||||
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, GalleryCompatibleThing]] = None
|
||||
|
||||
@property
|
||||
def gallery_providing_things(self) -> Mapping[str, GalleryCompatibleThing]:
|
||||
"""All Things that provide data to the gallery."""
|
||||
if self._gallery_providing_things is None:
|
||||
raise RuntimeError(
|
||||
"Cannot access gallery_providing_things before server has started."
|
||||
)
|
||||
return self._gallery_providing_things
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
"""Check for all gallery providing things on server startup."""
|
||||
self._set_gallery_providers()
|
||||
return self
|
||||
|
||||
def _set_gallery_providers(self) -> None:
|
||||
"""Set the mapping of gallery providing things.
|
||||
|
||||
This is called on ``__enter__`` when the server starts.
|
||||
"""
|
||||
gallery_providers = {
|
||||
name: thing
|
||||
for name, thing in self.all_ofm_things.items()
|
||||
if thing.show_data_in_gallery
|
||||
}
|
||||
# cache initial list of keys as it may change in the loop
|
||||
keys = list(gallery_providers.keys())
|
||||
for key in keys:
|
||||
if not isinstance(gallery_providers[key], GalleryCompatibleThing):
|
||||
self.logger.error(
|
||||
f"Data from {key} cannot be shown in gallery as it does not "
|
||||
"provide all necessary properties/methods for the gallery."
|
||||
)
|
||||
gallery_providers.pop(key)
|
||||
|
||||
# Cast the type of each thing to "GalleryCompatibleThing" as other Things have
|
||||
# been popped.
|
||||
self._gallery_providing_things = cast(
|
||||
Mapping[str, GalleryCompatibleThing], gallery_providers
|
||||
)
|
||||
|
||||
@lt.property
|
||||
def list_data(self) -> list[dict[str, Any]]:
|
||||
"""List the data from all registered things.
|
||||
|
||||
Currently only works with `smart_scan` as the UI cards are not customisable.
|
||||
|
||||
This will change to an action (or another type of endpoint) at a
|
||||
later date to enable filtering, and returning only a specific page.
|
||||
"""
|
||||
data_list = []
|
||||
for thing in self.gallery_providing_things.values():
|
||||
data_list += [model.model_dump() for model in thing.get_data_for_gallery()]
|
||||
return data_list
|
||||
|
|
@ -71,16 +71,6 @@ class ActiveScanData(scan_directories.BaseScanData):
|
|||
self.scan_result = result
|
||||
|
||||
|
||||
class ScanListInfo(BaseModel):
|
||||
"""The information to be sent to the Scan List tab."""
|
||||
|
||||
scans: list[scan_directories.ScanInfo]
|
||||
"""The list of scans as ScanInfo objects"""
|
||||
|
||||
ongoing: Optional[str]
|
||||
"""The name of the ongoing scan or None"""
|
||||
|
||||
|
||||
class JPEGBlob(lt.blob.Blob):
|
||||
"""A class representing a JPEG image as a LabThings FastAPI Blob."""
|
||||
|
||||
|
|
@ -176,6 +166,31 @@ class SmartScanThing(OFMThing):
|
|||
In this case it doesn't need to do anything.
|
||||
"""
|
||||
|
||||
# Register with gallery.
|
||||
_show_data_in_gallery = True
|
||||
|
||||
@property
|
||||
def gallery_data_name(self) -> str:
|
||||
"""Name under which data shows up in gallery."""
|
||||
return "Scans"
|
||||
|
||||
@property
|
||||
def gallery_data_schema(self) -> type[scan_directories.ScanInfo]:
|
||||
"""The schema (BaseModel) for passing data to the gallery."""
|
||||
return scan_directories.ScanInfo
|
||||
|
||||
def get_data_for_gallery(self) -> list[scan_directories.ScanInfo]:
|
||||
"""Return all the information from the scan directories.
|
||||
|
||||
It is preferable to use the method rather than calling
|
||||
_scan_dir_manager.all_scans_info() directly as it will handle stopping the json
|
||||
in any ongoing scans being read.
|
||||
"""
|
||||
ongoing_name = None if self._ongoing_scan is None else self.ongoing_scan.name
|
||||
return self._scan_dir_manager.all_scans_info(
|
||||
ongoing=ongoing_name, include_ongoing=False
|
||||
)
|
||||
|
||||
# Note that the default detector name is set at init. This is over written if
|
||||
# setting is loaded from disk.
|
||||
@lt.setting
|
||||
|
|
@ -544,31 +559,6 @@ class SmartScanThing(OFMThing):
|
|||
stitch_automatically: bool = lt.setting(default=True)
|
||||
"""Whether to run a final stitch at the end of a successful scan."""
|
||||
|
||||
def _get_all_scan_info(self) -> list[scan_directories.ScanInfo]:
|
||||
"""Return all the information from the scan directories.
|
||||
|
||||
It is preferable to use the method rather than calling
|
||||
_scan_dir_manager.all_scans_info() directly as it will handle stopping the json
|
||||
in any ongoing scans being read.
|
||||
"""
|
||||
ongoing_name = None if self._ongoing_scan is None else self.ongoing_scan.name
|
||||
return self._scan_dir_manager.all_scans_info(ongoing=ongoing_name)
|
||||
|
||||
@lt.property
|
||||
def scans(self) -> ScanListInfo:
|
||||
"""All the available scans.
|
||||
|
||||
Each scan has a name (which can be used to access it), along with
|
||||
its modified and created times (according to the filesystem) and
|
||||
the number of items in the ``images`` folder. Note that image count
|
||||
uses a regular expression, and changes to the naming scheme will
|
||||
break it.
|
||||
"""
|
||||
return ScanListInfo(
|
||||
scans=self._get_all_scan_info(),
|
||||
ongoing=None if self._ongoing_scan is None else self.ongoing_scan.name,
|
||||
)
|
||||
|
||||
@lt.endpoint(
|
||||
"get",
|
||||
"get_stitch/{scan_name}",
|
||||
|
|
@ -632,8 +622,8 @@ class SmartScanThing(OFMThing):
|
|||
@lt.action
|
||||
def purge_empty_scans(self) -> None:
|
||||
"""Delete all scan folders containing no images at the top level."""
|
||||
# JSON is ignored as it's created before any images are captured
|
||||
for scan_info in self._get_all_scan_info():
|
||||
# Use the scan list (the data read by the gallery) to check for empty scans.
|
||||
for scan_info in self.get_data_for_gallery():
|
||||
if scan_info.number_of_images == 0:
|
||||
self._delete_scan(scan_info.name)
|
||||
|
||||
|
|
@ -748,6 +738,8 @@ class SmartScanThing(OFMThing):
|
|||
"""
|
||||
if self._scan_lock.locked():
|
||||
raise RuntimeError("Can't stitch previous scans while a scan is ongoing")
|
||||
for scan in self._get_all_scan_info():
|
||||
# Use the scan list (the data read by the gallery) to find any scans that
|
||||
# need stitching.
|
||||
for scan in self.get_data_for_gallery():
|
||||
if scan.dzi is None:
|
||||
self.stitch_scan(scan_name=scan.name)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue