"""Installer runner that executes milestones sequentially.""" from __future__ import annotations from typing import Callable, Iterable from .milestone import Milestone, ValidationResult from .state import InstallerState EventCallback = Callable[..., None] class InstallerRunner: """Executes milestones sequentially and emits lifecycle events.""" def __init__( self, milestones: Iterable[Milestone], state: InstallerState | None = None ): self._milestones = {m.id: m for m in milestones} self._order = [m.id for m in milestones] self.state = state or InstallerState() self._listeners: dict[str, list[EventCallback]] = { "on_enter_milestone": [], "on_validation_error": [], "on_progress": [], "on_complete": [], } def on(self, event: str, callback: EventCallback) -> None: if event not in self._listeners: raise ValueError(f"Unknown event: {event}") self._listeners[event].append(callback) def emit(self, event: str, *args) -> None: for cb in self._listeners.get(event, []): cb(*args) def run(self, start_id: str | None = None) -> bool: if not self._order: self.emit("on_complete", None, self.state) return True current_id = start_id or self.state.current_id or self._order[0] while current_id: milestone = self._milestones.get(current_id) if milestone is None: self.state.errors.append(f"Unknown milestone id: {current_id}") self.emit( "on_validation_error", None, self.state, [self.state.errors[-1]] ) return False self.state.current_id = current_id self.state.history.append(current_id) self.emit("on_enter_milestone", milestone, self.state) errors = milestone.validate(self.state) error_list = _normalize_validation(errors) if error_list: self.state.errors.extend(error_list) self.emit("on_validation_error", milestone, self.state, error_list) return False def _progress(message: str, percent: float | None = None) -> None: if percent is not None: self.state.progress[current_id] = percent self.emit("on_progress", milestone, self.state, message, percent) milestone.execute(self.state, _progress) self.state.mark_completed(current_id) next_id = milestone.next(self.state) if not next_id: self.emit("on_complete", milestone, self.state) return True current_id = next_id self.emit("on_complete", None, self.state) return True def _normalize_validation(result: ValidationResult) -> list[str]: if result is None: return [] if isinstance(result, str): return [result] return [str(item) for item in result]