- Add DataType enum (SOURCE_ITEMS, TEXT_BUFFER, etc.) - Add inlet_types and outlet_types to Stage - Add _validate_types() for type checking at build time - Update tests with proper type annotations
305 lines
9.5 KiB
Python
305 lines
9.5 KiB
Python
"""
|
|
Pipeline core - Unified Stage abstraction and PipelineContext.
|
|
|
|
This module provides the foundation for a clean, dependency-managed pipeline:
|
|
- Stage: Base class for all pipeline components (sources, effects, displays, cameras)
|
|
- PipelineContext: Dependency injection context for runtime data exchange
|
|
- Capability system: Explicit capability declarations with duck-typing support
|
|
- DataType: PureData-style inlet/outlet typing for validation
|
|
"""
|
|
|
|
from abc import ABC, abstractmethod
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum, auto
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
if TYPE_CHECKING:
|
|
from engine.pipeline.params import PipelineParams
|
|
|
|
|
|
class DataType(Enum):
|
|
"""PureData-style data types for inlet/outlet validation.
|
|
|
|
Each type represents a specific data format that flows through the pipeline.
|
|
This enables compile-time-like validation of connections.
|
|
|
|
Examples:
|
|
SOURCE_ITEMS: List[SourceItem] - raw items from sources
|
|
ITEM_TUPLES: List[tuple] - (title, source, timestamp) tuples
|
|
TEXT_BUFFER: List[str] - rendered ANSI buffer for display
|
|
RAW_TEXT: str - raw text strings
|
|
"""
|
|
|
|
SOURCE_ITEMS = auto() # List[SourceItem] - from DataSource
|
|
ITEM_TUPLES = auto() # List[tuple] - (title, source, ts)
|
|
TEXT_BUFFER = auto() # List[str] - ANSI buffer
|
|
RAW_TEXT = auto() # str - raw text
|
|
ANY = auto() # Accepts any type
|
|
NONE = auto() # No data (terminator)
|
|
|
|
|
|
@dataclass
|
|
class StageConfig:
|
|
"""Configuration for a single stage."""
|
|
|
|
name: str
|
|
category: str
|
|
enabled: bool = True
|
|
optional: bool = False
|
|
params: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
class Stage(ABC):
|
|
"""Abstract base class for all pipeline stages.
|
|
|
|
A Stage is a single component in the rendering pipeline. Stages can be:
|
|
- Sources: Data providers (headlines, poetry, pipeline viz)
|
|
- Effects: Post-processors (noise, fade, glitch, hud)
|
|
- Displays: Output backends (terminal, pygame, websocket)
|
|
- Cameras: Viewport controllers (vertical, horizontal, omni)
|
|
- Overlays: UI elements that compose on top (HUD)
|
|
|
|
Stages declare:
|
|
- capabilities: What they provide to other stages
|
|
- dependencies: What they need from other stages
|
|
- stage_type: Category of stage (source, effect, overlay, display)
|
|
- render_order: Execution order within category
|
|
- is_overlay: If True, output is composited on top, not passed downstream
|
|
|
|
Duck-typing is supported: any class with the required methods can act as a Stage.
|
|
"""
|
|
|
|
name: str
|
|
category: str # "source", "effect", "overlay", "display", "camera"
|
|
optional: bool = False # If True, pipeline continues even if stage fails
|
|
|
|
@property
|
|
def stage_type(self) -> str:
|
|
"""Category of stage for ordering.
|
|
|
|
Valid values: "source", "effect", "overlay", "display", "camera"
|
|
Defaults to category for backwards compatibility.
|
|
"""
|
|
return self.category
|
|
|
|
@property
|
|
def render_order(self) -> int:
|
|
"""Execution order within stage_type group.
|
|
|
|
Higher values execute later. Useful for ordering overlays
|
|
or effects that need specific execution order.
|
|
"""
|
|
return 0
|
|
|
|
@property
|
|
def is_overlay(self) -> bool:
|
|
"""If True, this stage's output is composited on top of the buffer.
|
|
|
|
Overlay stages don't pass their output to the next stage.
|
|
Instead, their output is layered on top of the final buffer.
|
|
Use this for HUD, status displays, and similar UI elements.
|
|
"""
|
|
return False
|
|
|
|
@property
|
|
def inlet_types(self) -> set[DataType]:
|
|
"""Return set of data types this stage accepts.
|
|
|
|
PureData-style inlet typing. If the connected upstream stage's
|
|
outlet_type is not in this set, the pipeline will raise an error.
|
|
|
|
Examples:
|
|
- Source stages: {DataType.NONE} (no input needed)
|
|
- Transform stages: {DataType.ITEM_TUPLES, DataType.TEXT_BUFFER}
|
|
- Display stages: {DataType.TEXT_BUFFER}
|
|
"""
|
|
return {DataType.ANY}
|
|
|
|
@property
|
|
def outlet_types(self) -> set[DataType]:
|
|
"""Return set of data types this stage produces.
|
|
|
|
PureData-style outlet typing. Downstream stages must accept
|
|
this type in their inlet_types.
|
|
|
|
Examples:
|
|
- Source stages: {DataType.SOURCE_ITEMS}
|
|
- Transform stages: {DataType.TEXT_BUFFER}
|
|
- Display stages: {DataType.NONE} (consumes data)
|
|
"""
|
|
return {DataType.ANY}
|
|
|
|
@property
|
|
def capabilities(self) -> set[str]:
|
|
"""Return set of capabilities this stage provides.
|
|
|
|
Examples:
|
|
- "source.headlines"
|
|
- "effect.noise"
|
|
- "display.output"
|
|
- "camera"
|
|
"""
|
|
return {f"{self.category}.{self.name}"}
|
|
|
|
@property
|
|
def dependencies(self) -> set[str]:
|
|
"""Return set of capability names this stage needs.
|
|
|
|
Examples:
|
|
- {"display.output"}
|
|
- {"source.headlines"}
|
|
- {"camera"}
|
|
"""
|
|
return set()
|
|
|
|
def init(self, ctx: "PipelineContext") -> bool:
|
|
"""Initialize stage with pipeline context.
|
|
|
|
Args:
|
|
ctx: PipelineContext for accessing services
|
|
|
|
Returns:
|
|
True if initialization succeeded, False otherwise
|
|
"""
|
|
return True
|
|
|
|
@abstractmethod
|
|
def process(self, data: Any, ctx: "PipelineContext") -> Any:
|
|
"""Process input data and return output.
|
|
|
|
Args:
|
|
data: Input data from previous stage (or initial data for first stage)
|
|
ctx: PipelineContext for accessing services and state
|
|
|
|
Returns:
|
|
Processed data for next stage
|
|
"""
|
|
...
|
|
|
|
def cleanup(self) -> None: # noqa: B027
|
|
"""Clean up resources when pipeline shuts down."""
|
|
pass
|
|
|
|
def get_config(self) -> StageConfig:
|
|
"""Return current configuration of this stage."""
|
|
return StageConfig(
|
|
name=self.name,
|
|
category=self.category,
|
|
optional=self.optional,
|
|
)
|
|
|
|
def set_enabled(self, enabled: bool) -> None:
|
|
"""Enable or disable this stage."""
|
|
self._enabled = enabled # type: ignore[attr-defined]
|
|
|
|
def is_enabled(self) -> bool:
|
|
"""Check if stage is enabled."""
|
|
return getattr(self, "_enabled", True)
|
|
|
|
|
|
@dataclass
|
|
class StageResult:
|
|
"""Result of stage processing, including success/failure info."""
|
|
|
|
success: bool
|
|
data: Any
|
|
error: str | None = None
|
|
stage_name: str = ""
|
|
|
|
|
|
class PipelineContext:
|
|
"""Dependency injection context passed through the pipeline.
|
|
|
|
Provides:
|
|
- services: Named services (display, config, event_bus, etc.)
|
|
- state: Runtime state shared between stages
|
|
- params: PipelineParams for animation-driven config
|
|
|
|
Services can be injected at construction time or lazily resolved.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
services: dict[str, Any] | None = None,
|
|
initial_state: dict[str, Any] | None = None,
|
|
):
|
|
self.services: dict[str, Any] = services or {}
|
|
self.state: dict[str, Any] = initial_state or {}
|
|
self._params: PipelineParams | None = None
|
|
|
|
# Lazy resolvers for common services
|
|
self._lazy_resolvers: dict[str, Callable[[], Any]] = {
|
|
"config": self._resolve_config,
|
|
"event_bus": self._resolve_event_bus,
|
|
}
|
|
|
|
def _resolve_config(self) -> Any:
|
|
from engine.config import get_config
|
|
|
|
return get_config()
|
|
|
|
def _resolve_event_bus(self) -> Any:
|
|
from engine.eventbus import get_event_bus
|
|
|
|
return get_event_bus()
|
|
|
|
def get(self, key: str, default: Any = None) -> Any:
|
|
"""Get a service or state value by key.
|
|
|
|
First checks services, then state, then lazy resolution.
|
|
"""
|
|
if key in self.services:
|
|
return self.services[key]
|
|
if key in self.state:
|
|
return self.state[key]
|
|
if key in self._lazy_resolvers:
|
|
try:
|
|
return self._lazy_resolvers[key]()
|
|
except Exception:
|
|
return default
|
|
return default
|
|
|
|
def set(self, key: str, value: Any) -> None:
|
|
"""Set a service or state value."""
|
|
self.services[key] = value
|
|
|
|
def set_state(self, key: str, value: Any) -> None:
|
|
"""Set a runtime state value."""
|
|
self.state[key] = value
|
|
|
|
def get_state(self, key: str, default: Any = None) -> Any:
|
|
"""Get a runtime state value."""
|
|
return self.state.get(key, default)
|
|
|
|
@property
|
|
def params(self) -> "PipelineParams | None":
|
|
"""Get current pipeline params (for animation)."""
|
|
return self._params
|
|
|
|
@params.setter
|
|
def params(self, value: "PipelineParams") -> None:
|
|
"""Set pipeline params (from animation controller)."""
|
|
self._params = value
|
|
|
|
def has_capability(self, capability: str) -> bool:
|
|
"""Check if a capability is available."""
|
|
return capability in self.services or capability in self._lazy_resolvers
|
|
|
|
|
|
class StageError(Exception):
|
|
"""Raised when a stage fails to process."""
|
|
|
|
def __init__(self, stage_name: str, message: str, is_optional: bool = False):
|
|
self.stage_name = stage_name
|
|
self.message = message
|
|
self.is_optional = is_optional
|
|
super().__init__(f"Stage '{stage_name}' failed: {message}")
|
|
|
|
|
|
def create_stage_error(
|
|
stage_name: str, error: Exception, is_optional: bool = False
|
|
) -> StageError:
|
|
"""Helper to create a StageError from an exception."""
|
|
return StageError(stage_name, str(error), is_optional)
|