Source code for bioimageflow_core.tool

"""Worker-safe tool base classes."""

from dataclasses import dataclass
from enum import Enum
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, Optional


[docs] class Category(str, Enum): """High-level functional category for a tool.""" CONVERSION = "conversion" IMAGE_PROCESSING = "image_processing" SEGMENTATION = "segmentation" REGISTRATION = "registration" SPECTRAL_ANALYSIS = "spectral_analysis" TRACKING = "tracking" MEASUREMENT = "measurement" SPOT_DETECTION = "spot_detection" DECONVOLUTION = "deconvolution" RESTORATION = "restoration" COLOCALIZATION = "colocalization" STITCHING = "stitching" CLASSIFICATION = "classification" UTILITIES = "utilities"
if TYPE_CHECKING: class Template(Path): """Type-only facade for ProcessingTool output path templates.""" pattern: str def __new__(cls, pattern: str) -> "Template": ... else:
[docs] @dataclass(frozen=True) class Template: """Explicit marker for ProcessingTool output path templates.""" pattern: str def __post_init__(self) -> None: if not isinstance(self.pattern, str): raise TypeError("Template pattern must be a string.") if self.pattern == "": raise ValueError("Template pattern must not be empty.") def __str__(self) -> str: return self.pattern
[docs] class IOModel: """Lightweight declarative base for tool Inputs/Outputs.""" @classmethod def _get_all_annotations(cls) -> dict[str, Any]: """Walk the MRO to collect annotations from all ancestor classes.""" annotations: dict[str, Any] = {} for klass in reversed(cls.__mro__): annotations.update(getattr(klass, '__annotations__', {})) return annotations
[docs] def __init__(self, **kwargs: Any) -> None: all_annotations = self._get_all_annotations() unknown = set(kwargs) - set(all_annotations) if unknown: raise TypeError(f"Unknown fields: {unknown}") for name in all_annotations: if name in kwargs: setattr(self, name, kwargs[name]) elif hasattr(self.__class__, name): setattr(self, name, getattr(self.__class__, name)) else: raise TypeError(f"Missing required field: '{name}'")
def __repr__(self) -> str: fields = {k: getattr(self, k) for k in self._get_all_annotations()} return f"{self.__class__.__name__}({fields})"
[docs] class BaseTool: """ Common base for all tools. Provides identity and Inputs. __call__ is NOT defined here — each subclass defines its own. """ display_name: ClassVar[str] = "" documentation: ClassVar[str] = "" category: ClassVar[Optional[Category]] = None tags: ClassVar[list[str]] = [] Inputs: ClassVar[type[IOModel]] = IOModel Outputs: ClassVar[Optional[type[IOModel]]] = None
[docs] def __init__(self) -> None: pass
[docs] class ProcessingTool(BaseTool): """Tool that processes data in an isolated Wetlands environment.""" environment: ClassVar[Any] Outputs: ClassVar[Optional[type[IOModel]]] resources: ClassVar[Any] = None run_empty_batch: ClassVar[bool] = False empty_batch_anchor_inputs: ClassVar[tuple[str, ...]] = () zero_row_scalar_outputs: ClassVar[dict[str, Any]] = {} def __init_subclass__(cls, **kwargs: Any) -> None: super().__init_subclass__(**kwargs) # Only validate leaf concrete classes that define Outputs on themselves has_own_outputs = 'Outputs' in cls.__dict__ if not has_own_outputs: return # Check that at least one of process_row or process_batch is overridden has_process_row = cls.process_row is not ProcessingTool.process_row has_process_batch = cls.process_batch is not ProcessingTool.process_batch if not has_process_row and not has_process_batch: raise TypeError( f"{cls.__name__} must implement process_row or process_batch" ) def __call__( self, *, name: Optional[str] = None, output_templates: Optional[dict[str, str]] = None, **kwargs: Any, ) -> Any: """Create a graph node. No computation occurs.""" try: from bioimageflow.node import Node except ImportError: raise RuntimeError( f"{type(self).__name__}.__call__() requires the bioimageflow " f"orchestrator package. This method is not available in worker " f"environments — use process_row/process_batch instead." ) return Node( tool=self, kwargs=kwargs, name=name, output_templates=output_templates, )
[docs] def process_row(self, arguments: Any, *, context: Any = None) -> Any: """Process a single row. Override in subclasses.""" raise NotImplementedError( f"{type(self).__name__} must implement process_row or process_batch." )
[docs] def process_batch(self, arguments_list: list[Any], *, context: Any = None) -> Any: """Process all rows at once. Override for batch processing.""" raise NotImplementedError