Source code for eoio.processors.processor_pipeline

"""eoio.processors.processor_pipeline

Returns
-------
xarray.Dataset
    Processed xarray Dataset.

Raises
------
ProcessorPipelineError
    Raised when invalid processor specified.
    Raised when error occurred in the specified processors during the run.

"""

from typing import Any, Dict, List, Tuple, Union
from eoio.processors.registry import PROCESSOR_REGISTRY
import xarray as xr
from processor_tools import Context
import logging

logger = logging.getLogger(__name__)


[docs] class ProcessorPipelineError(ValueError): """ Raised when a processor is invalid or fails during execution. """
[docs] class ProcessorPipeline: """ Sequential processor pipeline that applies registered EOIO processors to an xarray.Dataset. The pipeline performs the following steps: - validates that the requested processors exist in the registry - instantiates them with their respective parameters - runs each processor in sequence Parameters -------------------- :param processor_params: Either a dict or a list of single-key dicts mapping processor names to their parameter dictionaries. The list form allows the same processor to appear more than once with different parameters. The on_missing parameter can be used to specify how to handle errors for each processor, supported values are: - "error" (default): processor pipeline is stopped returning most recent successful processed dataset - "skip": processor pipeline omits processor when an error occurs Keys must exist in `PROCESSOR_REGISTRY`. :param context: Processing context (reader info, metadata view, logger, etc.). Example ------- # Dict form (each processor once): processor_params = { "processor_name_0": {param_0:..., param_1:...., "on_missing":"skip"}, "processor_name_1": {param_0:..., param_1:...., "on_missing":"error"} } # List form (allows repeated processors): processor_params = [ {"interpolate": {param_0:..., "on_missing": "skip"}}, {"interpolate": {param_0:..., "on_missing": "error"}}, ] """
[docs] def __init__( self, processor_params: Union[Dict[str, Dict[str, Any]], List[Dict[str, Any]]], context: Dict[str, Any] | Context, ) -> None: self.steps: List[Tuple[str, Dict[str, Any]]] = self._normalise(processor_params) self.context: Dict[str, Any] = context self.processors: list[Any] = [] self.registry_lc = {name.lower(): name for name in PROCESSOR_REGISTRY} self._validate_processor_names() self._instantiate_processors()
@staticmethod def _normalise( processor_params: Union[Dict[str, Dict[str, Any]], List[Dict[str, Any]]], ) -> List[Tuple[str, Dict[str, Any]]]: """Normalise dict or list-of-single-key-dicts to an ordered list of (name, params) tuples.""" if isinstance(processor_params, dict): return [(name, params) for name, params in processor_params.items()] steps = [] for item in processor_params: if not isinstance(item, dict) or len(item) != 1: raise ProcessorPipelineError( "Each entry in a list-form processors definition must be a single-key dict, " f'e.g. {{"interpolate": {{...}}}}. Got: {item!r}' ) name, params = next(iter(item.items())) steps.append((name, params)) return steps def _validate_processor_names(self) -> None: """Ensure all referenced processors exist in the registry.""" for processor_name, _ in self.steps: if processor_name.lower() not in self.registry_lc: raise ProcessorPipelineError( f"Unknown processor: {processor_name}, available processors are {sorted(PROCESSOR_REGISTRY.keys())}" ) def _instantiate_processors(self) -> None: """Create processor instances with parameters.""" for processor_name, params in self.steps: registry_name = self.registry_lc[processor_name.lower()] cls = PROCESSOR_REGISTRY[registry_name] processor_instance = cls(params, self.context) self.processors.append(processor_instance)
[docs] def run(self, ds: xr.Dataset) -> xr.Dataset: """ Run all processors sequentially. :param ds: Input dataset. :return: The processed xarray dataset. :raises ProcessorPipelineError: If a processor fails and its `on_missing` is `"error"`. """ logger.info(msg="Starting processor pipeline...") current_ds = ds for i, (processor, (step_name, _)) in enumerate(zip(self.processors, self.steps)): try: logger.info(f"Running processor {i + 1}: {step_name}") current_ds = processor.run(current_ds) except Exception as e: params = getattr(processor, "params", {}) # check if params exists on_missing = str(object=params.get("on_missing", "error")).lower() # "error" | "skip" logger.error(msg=f"Error in processor {step_name}: {e}") if on_missing == "skip": logger.warning(msg=f"Skipping processor {step_name} (index {i}) due to error: {e}") # Continue without altering current_ds continue # Default behaviour: raise a pipeline error raise ProcessorPipelineError(f"Error in {step_name}: {e}") from e logger.info("Pipeline completed successfully.") return current_ds