from api.task import Task, Status, TaskEvent from api.assets import Asset from api.execution_context import ExecutionContext from api.io_port import CompatibilityException, InPort, OutPort class Module(Task): """ Leaf node: atomic executable unit. Executes using its input ports and produces assets on output ports. """ def add_in_port(self, port: InPort): port.parent_task = self self.in_ports.append(port) def add_out_port(self, port: OutPort): port.parent_task = self self.out_ports.append(port) # Hooks def before_run(self, context: ExecutionContext): pass def after_run(self, context: ExecutionContext): pass def on_error(self, error: Exception): pass # Validation def validate(self): super().validate() # Main execution entry point def run(self, context: ExecutionContext): self.before_run(context) if not self.check_inputs_ready(context): self._set_status(Status.PENDING) return self._set_status(Status.RUNNING) self._emit(TaskEvent.ON_START, {"module": self.name}) try: # Gather input data consumed_data = {} for port in self.in_ports: if not port.is_ready(context): raise CompatibilityException( f"Input {port.full_name()} is not ready" ) asset = context.get(port) consumed_data[port.name] = asset.data # Execute module logic result = self.execute(consumed_data) if not isinstance(result, dict): raise ValueError(f"Module '{self.name}' must return dict") # Store outputs for out_port in self.out_ports: if out_port.name not in result: raise ValueError( f"Output '{out_port.name}' missing in module '{self.name}' result" ) asset = Asset( data=result[out_port.name], dtype=out_port.dtype ) context.put(out_port, asset) self._set_status(Status.COMPLETED) self._emit(TaskEvent.ON_FINISH, {"module": self.name}) self.after_run(context) except Exception as e: self._set_status(Status.FAILED) self.on_error(e) self._emit(TaskEvent.ON_ERROR, {"error": str(e)}) raise e # Default execute def execute(self, inputs): return inputs