Source code for ascribe_link.specimen_store
"""Specimen store — manages a directory of curated specimens.
Each specimen is a subdirectory containing:
specimen.json — metadata (SpecimenMetadata fields)
thumbnail.* — thumbnail image (png/jpg)
<data_file> — mesh or volume data file
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import TYPE_CHECKING
import numpy as np
from ascribe_link.models import SpecimenMetadata, SpecimenType
if TYPE_CHECKING:
from ascribe_link.models import VolumeResult
[docs]
class SpecimenStore:
"""Read-only store backed by a directory of specimen bundles."""
def __init__(self, root: Path) -> None:
self._root = Path(root)
self._cache: dict[str, SpecimenMetadata] = {}
self.reload()
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
@property
def root(self) -> Path:
return self._root
[docs]
def reload(self) -> None:
"""(Re-)scan the specimen directory and rebuild the cache."""
import logging
logger = logging.getLogger(__name__)
self._cache.clear()
logger.info(f"Scanning specimen directory: {self._root.absolute()}")
if not self._root.exists():
logger.warning(f"Specimen directory does not exist: {self._root.absolute()}")
return
if not self._root.is_dir():
logger.warning(f"Specimen path is not a directory: {self._root.absolute()}")
return
children = list(self._root.iterdir())
logger.info(f"Found {len(children)} items in specimen directory")
for child in sorted(children):
logger.debug(f"Checking: {child.name}")
if not child.is_dir():
logger.debug(f" Skipping (not a directory): {child.name}")
continue
meta_path = child / "specimen.json"
if not meta_path.exists():
logger.debug(f" Skipping (no specimen.json): {child.name}")
continue
try:
meta = self._load_metadata(child.name, meta_path)
self._cache[meta.id] = meta
logger.info(f" Loaded specimen: {meta.id} ({meta.display_name})")
except Exception as exc: # noqa: BLE001
logging.getLogger(__name__).warning(
"Skipping specimen %s: %s", child.name, exc
)
def list(self) -> list[SpecimenMetadata]:
import logging
logger = logging.getLogger(__name__)
result = list(self._cache.values())
logger.info(f"SpecimenStore.list() returning {len(result)} specimens")
return result
def get(self, specimen_id: str) -> SpecimenMetadata | None:
return self._cache.get(specimen_id)
def specimen_dir(self, specimen_id: str) -> Path | None:
if specimen_id in self._cache:
return self._root / specimen_id
return None
def data_path(self, specimen_id: str) -> Path | None:
meta = self._cache.get(specimen_id)
if meta and meta.data_file:
p = self._root / specimen_id / meta.data_file
if p.is_file():
return p
return None
def thumbnail_path(self, specimen_id: str) -> Path | None:
meta = self._cache.get(specimen_id)
if meta and meta.thumbnail_file:
p = self._root / specimen_id / meta.thumbnail_file
if p.is_file():
return p
# Fallback: look for any thumbnail.* in the directory
d = self._root / specimen_id
if d.is_dir():
for ext in (".png", ".jpg", ".jpeg", ".webp"):
candidate = d / f"thumbnail{ext}"
if candidate.is_file():
return candidate
return None
# ------------------------------------------------------------------
# Internal
# ------------------------------------------------------------------
@staticmethod
def _load_metadata(dir_name: str, meta_path: Path) -> SpecimenMetadata:
raw = json.loads(meta_path.read_text())
return SpecimenMetadata(
id=raw.get("id", dir_name),
display_name=raw.get("display_name", dir_name),
description=raw.get("description", ""),
type=SpecimenType(raw.get("type", "mesh")),
data_file=raw.get("data_file", ""),
thumbnail_file=raw.get("thumbnail_file", ""),
story_text=raw.get("story_text", []),
tags=raw.get("tags", []),
schema=raw.get("schema"),
function_name=raw.get("function_name"),
)
[docs]
def load_static_volume(spec_dir: Path, data_file: str) -> VolumeResult:
"""Load a static volume specimen from disk (currently .npy + optional .json sidecar).
Parameters
----------
spec_dir : Path
Directory containing the specimen bundle.
data_file : str
Filename (within spec_dir) of the volume data, e.g. "data.npy".
Returns
-------
VolumeResult
"""
from ascribe_link.models import VolumeResult # avoid import cycle
data_path = spec_dir / data_file
if not data_path.exists():
raise FileNotFoundError(f"volume data missing: {data_path}")
if data_path.suffix.lower() != ".npy":
raise ValueError(f"unsupported static volume format: {data_path.suffix}")
arr = np.load(data_path, mmap_mode="r")
if arr.ndim != 3:
raise ValueError(f"static volume must be 3D, got ndim={arr.ndim}")
sidecar = data_path.with_suffix(".json")
spacing: list[float] | None = None
origin: list[float] | None = None
if sidecar.exists():
meta = json.loads(sidecar.read_text())
spacing = meta.get("spacing")
origin = meta.get("origin")
return VolumeResult.from_numpy(np.ascontiguousarray(arr), spacing=spacing, origin=origin)