197 lines
5.8 KiB
Python
197 lines
5.8 KiB
Python
"""Pipeline abstraction for composable data processing stages."""
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Dict, Iterator, List, Optional, TypeVar, Generic
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
T = TypeVar("T")
|
|
R = TypeVar("R")
|
|
|
|
|
|
class Stage(ABC, Generic[T, R]):
|
|
"""Abstract base class for pipeline stages.
|
|
|
|
A Stage represents a single processing step that transforms input data
|
|
and can be composed with other stages to form a pipeline.
|
|
"""
|
|
|
|
@property
|
|
@abstractmethod
|
|
def name(self) -> str:
|
|
"""Return the stage name for logging/debugging."""
|
|
pass
|
|
|
|
@abstractmethod
|
|
def process(self, input_data: T) -> R:
|
|
"""Process input data and return transformed output."""
|
|
pass
|
|
|
|
def __repr__(self) -> str:
|
|
return f"{self.__class__.__name__}(name='{self.name}')"
|
|
|
|
|
|
@dataclass
|
|
class PipelineConfig:
|
|
"""Configuration for pipeline execution.
|
|
|
|
Attributes:
|
|
name: Pipeline name for identification.
|
|
enable_logging: Enable per-stage logging.
|
|
continue_on_error: Continue processing if a stage fails.
|
|
error_threshold: Maximum errors before stopping (-1 for unlimited).
|
|
"""
|
|
|
|
name: str = "pipeline"
|
|
enable_logging: bool = True
|
|
continue_on_error: bool = True
|
|
error_threshold: int = -1
|
|
|
|
|
|
class Pipeline(Generic[T]):
|
|
"""Composable pipeline for sequential data processing.
|
|
|
|
Example::
|
|
|
|
pipeline = Pipeline(config=PipelineConfig(name="data-prep"))
|
|
pipeline.add_stage(TextNormalizationStage(normalizer))
|
|
pipeline.add_stage(TokenizationStage(tokenizer))
|
|
pipeline.add_stage(PackingStage(packer))
|
|
|
|
results = pipeline.run(input_data)
|
|
"""
|
|
|
|
def __init__(self, config: Optional[PipelineConfig] = None):
|
|
self.config = config or PipelineConfig()
|
|
self._stages: List[Stage] = []
|
|
self._error_count: int = 0
|
|
|
|
def add_stage(self, stage: Stage) -> "Pipeline":
|
|
"""Add a stage to the pipeline (fluent interface)."""
|
|
self._stages.append(stage)
|
|
return self
|
|
|
|
def add_stages(self, *stages: Stage) -> "Pipeline":
|
|
"""Add multiple stages at once."""
|
|
self._stages.extend(stages)
|
|
return self
|
|
|
|
def run(self, input_data: List[T]) -> List[Any]:
|
|
"""Run all stages sequentially on input data.
|
|
|
|
Args:
|
|
input_data: List of input items to process.
|
|
|
|
Returns:
|
|
List of processed results.
|
|
"""
|
|
if self.config.enable_logging:
|
|
logger.info(f"Starting pipeline '{self.config.name}' with {len(self._stages)} stages")
|
|
|
|
results = input_data
|
|
for stage in self._stages:
|
|
if self.config.enable_logging:
|
|
logger.info(f"Running stage: {stage.name}")
|
|
|
|
new_results = []
|
|
for item in results:
|
|
if self._should_stop():
|
|
break
|
|
|
|
try:
|
|
result = stage.process(item)
|
|
if result is not None:
|
|
new_results.append(result)
|
|
except Exception as e:
|
|
self._handle_error(stage, item, e)
|
|
|
|
results = new_results
|
|
|
|
if self.config.enable_logging:
|
|
logger.info(f"Pipeline '{self.config.name}' completed: {len(results)} items")
|
|
|
|
return results
|
|
|
|
def run_stream(self, input_data: List[T]) -> Iterator[Any]:
|
|
"""Run pipeline as a generator for memory-efficient processing.
|
|
|
|
Args:
|
|
input_data: List of input items to process.
|
|
|
|
Yields:
|
|
Processed results one at a time.
|
|
"""
|
|
for stage in self._stages:
|
|
if self.config.enable_logging:
|
|
logger.info(f"Running stage: {stage.name}")
|
|
|
|
for item in input_data:
|
|
if self._should_stop():
|
|
return
|
|
|
|
try:
|
|
result = stage.process(item)
|
|
if result is not None:
|
|
yield result
|
|
except Exception as e:
|
|
self._handle_error(stage, item, e)
|
|
|
|
def _should_stop(self) -> bool:
|
|
"""Check if pipeline should stop processing."""
|
|
if self.config.error_threshold < 0:
|
|
return False
|
|
return self._error_count >= self.config.error_threshold
|
|
|
|
def _handle_error(self, stage: Stage, item: Any, error: Exception) -> None:
|
|
"""Handle processing error."""
|
|
self._error_count += 1
|
|
error_msg = f"Error in stage '{stage.name}': {error}"
|
|
if self.config.continue_on_error:
|
|
logger.warning(error_msg)
|
|
else:
|
|
raise RuntimeError(error_msg) from error
|
|
|
|
def __repr__(self) -> str:
|
|
stage_names = [s.name for s in self._stages]
|
|
return f"Pipeline(name='{self.config.name}', stages={stage_names})"
|
|
|
|
|
|
# ── Common Stage Implementations ──────────────────────────────────────────────
|
|
|
|
@dataclass
|
|
class TransformStage(Stage):
|
|
"""Stage that applies a transformation function.
|
|
|
|
Attributes:
|
|
transform: Callable that transforms input to output.
|
|
name: Stage name.
|
|
"""
|
|
|
|
transform: callable
|
|
name: str
|
|
_name: str = field(init=False, repr=False, compare=False, hash=False)
|
|
|
|
def __post_init__(self):
|
|
self._name = self.name
|
|
|
|
def process(self, input_data: T) -> R:
|
|
return self.transform(input_data)
|
|
|
|
|
|
@dataclass
|
|
class FilterStage(Stage):
|
|
"""Stage that filters items based on a predicate.
|
|
|
|
Attributes:
|
|
predicate: Callable that returns True to keep item.
|
|
name: Stage name.
|
|
"""
|
|
|
|
predicate: callable
|
|
name: str
|
|
|
|
def process(self, input_data: T) -> Optional[T]:
|
|
return input_data if self.predicate(input_data) else None
|