Add All Folders
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
from typing import Any, Dict, Optional
|
||||
from copy import deepcopy
|
||||
from api.io_port import DType
|
||||
|
||||
|
||||
class Asset:
|
||||
"""
|
||||
Represents the output of a Task.
|
||||
|
||||
Stores:
|
||||
- data: actual payload
|
||||
- dtype: semantic type (DType instance)
|
||||
- metadata: optional dictionary
|
||||
"""
|
||||
|
||||
def __init__(self, data: Any, dtype: Optional[DType] = None, metadata: Optional[Dict[str, Any]] = None):
|
||||
self.data = data
|
||||
self.dtype = dtype # DO NOT deepcopy dtype
|
||||
self.metadata = metadata or {}
|
||||
|
||||
# Whether this asset should be preserved after workflow execution
|
||||
self.preserved = True
|
||||
|
||||
# Lifecycle
|
||||
def dismiss(self):
|
||||
"""Mark asset as disposable."""
|
||||
self.preserved = False
|
||||
|
||||
# Utility
|
||||
def clone(self) -> "Asset":
|
||||
"""
|
||||
Create a deep copy of the asset.
|
||||
dtype is NOT deepcopied because DType objects are not deepcopy-safe.
|
||||
"""
|
||||
return Asset(
|
||||
data=deepcopy(self.data),
|
||||
dtype=self.dtype, # keep reference
|
||||
metadata=deepcopy(self.metadata)
|
||||
)
|
||||
|
||||
# Representation
|
||||
def __repr__(self):
|
||||
# Avoid printing full dtype object (may contain nested structures)
|
||||
dtype_name = self.dtype.__class__.__name__ if self.dtype else "None"
|
||||
return f"<Asset dtype={dtype_name} preserved={self.preserved}>"
|
||||
@@ -0,0 +1,82 @@
|
||||
from typing import Dict, Any
|
||||
from api.assets import Asset
|
||||
from api.io_port import InPort, OutPort
|
||||
|
||||
|
||||
class ExecutionContext:
|
||||
"""
|
||||
Runtime data container for a workflow execution.
|
||||
Stores produced assets for OutPorts and allows InPorts to retrieve them.
|
||||
"""
|
||||
|
||||
def __init__(self, execution_id: str | None = None):
|
||||
self.execution_id = execution_id
|
||||
|
||||
# Key: OutPort, Value: Asset
|
||||
self._data: Dict[OutPort, Asset] = {}
|
||||
|
||||
# Arbitrary metadata (optional)
|
||||
self._metadata: Dict[str, Any] = {}
|
||||
|
||||
# Store produced data
|
||||
def put(self, output_port: OutPort, asset: Asset):
|
||||
"""
|
||||
Register an asset produced by an OutPort.
|
||||
Also updates output_port.produced_asset.
|
||||
"""
|
||||
self._data[output_port] = asset
|
||||
output_port.produced_asset = asset
|
||||
|
||||
# Retrieve data
|
||||
def get(self, port):
|
||||
"""
|
||||
Retrieve asset for:
|
||||
- OutPort → return its asset
|
||||
- InPort → return asset from its connected OutPort
|
||||
"""
|
||||
if isinstance(port, OutPort):
|
||||
return self._data.get(port)
|
||||
|
||||
if isinstance(port, InPort):
|
||||
if port.connected_output is None:
|
||||
return None
|
||||
return self._data.get(port.connected_output)
|
||||
|
||||
raise TypeError("ExecutionContext.get() expects InPort or OutPort")
|
||||
|
||||
# Check if data exists
|
||||
def has_data(self, port) -> bool:
|
||||
if isinstance(port, OutPort):
|
||||
return port in self._data
|
||||
|
||||
if isinstance(port, InPort):
|
||||
if port.connected_output is None:
|
||||
return False
|
||||
return port.connected_output in self._data
|
||||
|
||||
return False
|
||||
|
||||
# Metadata management
|
||||
def set_meta(self, key: str, value: Any):
|
||||
self._metadata[key] = value
|
||||
|
||||
def get_meta(self, key: str, default=None):
|
||||
return self._metadata.get(key, default)
|
||||
|
||||
# Debug snapshot
|
||||
def status_view(self):
|
||||
"""
|
||||
Lightweight snapshot of current execution state.
|
||||
Safe for GUI display.
|
||||
"""
|
||||
return {
|
||||
"execution_id": self.execution_id,
|
||||
"assets": {
|
||||
f"{port.parent_task.name}.{port.name}": {
|
||||
"dtype": port.dtype.__class__.__name__,
|
||||
"preserved": asset.preserved,
|
||||
}
|
||||
for port, asset in self._data.items()
|
||||
},
|
||||
"metadata": self._metadata.copy(),
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, Dict, Any, List
|
||||
|
||||
|
||||
# Exceptions
|
||||
class CompatibilityException(Exception):
|
||||
def __init__(self, reason: str):
|
||||
super().__init__(reason)
|
||||
self.reason = reason
|
||||
|
||||
|
||||
# DType (Semantic Contract)
|
||||
class DType(ABC):
|
||||
"""
|
||||
Pure semantic type. Does NOT contain names.
|
||||
Names belong to Ports or CompositePart.
|
||||
"""
|
||||
def __init__(self, metadata: Optional[Dict[str, Any]] = None):
|
||||
self.metadata = metadata or {}
|
||||
self.conditions: Optional[Dict[str, Any]] = None
|
||||
|
||||
@abstractmethod
|
||||
def can_connect_to(self, other: "DType", conditions: Optional[Dict[str, Any]] = None):
|
||||
pass
|
||||
|
||||
def set_conditions(self, conditions: Dict[str, Any]):
|
||||
self.conditions = conditions
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}(metadata={self.metadata})"
|
||||
|
||||
|
||||
# Composite Types
|
||||
class CompositePart:
|
||||
"""
|
||||
Represents one named component inside a CompositeType.
|
||||
Example:
|
||||
CompositePart("image", ImageType("nifti"))
|
||||
"""
|
||||
def __init__(self, name: str, dtype: DType):
|
||||
self.name = name
|
||||
self.dtype = dtype
|
||||
|
||||
def __repr__(self):
|
||||
return f"CompositePart(name={self.name}, dtype={self.dtype})"
|
||||
|
||||
|
||||
class CompositeType(DType):
|
||||
"""
|
||||
CompositeType is a LIST of CompositePart.
|
||||
Order matters. Names are preserved inside CompositePart.
|
||||
"""
|
||||
def __init__(self, parts: List[CompositePart], metadata=None):
|
||||
super().__init__(metadata)
|
||||
self.parts = parts
|
||||
|
||||
def can_connect_to(self, other: "DType", conditions=None):
|
||||
if not isinstance(other, CompositeType):
|
||||
raise CompatibilityException("Expected CompositeType")
|
||||
|
||||
if len(self.parts) != len(other.parts):
|
||||
raise CompatibilityException("CompositeType length mismatch")
|
||||
|
||||
for i in range(len(self.parts)):
|
||||
p1 = self.parts[i]
|
||||
p2 = other.parts[i]
|
||||
|
||||
if p1.name != p2.name:
|
||||
raise CompatibilityException(
|
||||
f"CompositeType part name mismatch: {p1.name} vs {p2.name}"
|
||||
)
|
||||
|
||||
p1.dtype.can_connect_to(p2.dtype, conditions)
|
||||
|
||||
def __repr__(self):
|
||||
return f"CompositeType(parts={self.parts})"
|
||||
|
||||
|
||||
# MaskType:
|
||||
class MaskType(DType):
|
||||
def __init__(self, metadata=None):
|
||||
super().__init__(metadata)
|
||||
|
||||
def can_connect_to(self, other: DType, conditions=None):
|
||||
if not isinstance(other, MaskType):
|
||||
raise CompatibilityException("Target is not a MaskType")
|
||||
|
||||
|
||||
# Image Types:
|
||||
class ImageType(DType):
|
||||
def __init__(self, modality: Optional[str] = None, metadata=None):
|
||||
super().__init__(metadata)
|
||||
self.modality = modality
|
||||
|
||||
def can_connect_to(self, other: DType, conditions=None):
|
||||
if not isinstance(other, ImageType):
|
||||
raise CompatibilityException("Target is not an ImageType")
|
||||
|
||||
if self.modality and other.modality and self.modality != other.modality:
|
||||
raise CompatibilityException(
|
||||
f"Image modality mismatch: {self.modality} vs {other.modality}"
|
||||
)
|
||||
|
||||
|
||||
class NIFTIImageType(ImageType):
|
||||
def __init__(self, metadata=None):
|
||||
super().__init__(modality="nifti", metadata=metadata)
|
||||
|
||||
|
||||
class DICOMImageType(ImageType):
|
||||
def __init__(self, metadata=None):
|
||||
super().__init__(modality="dicom", metadata=metadata)
|
||||
|
||||
|
||||
# Table Types:
|
||||
class TableType(DType):
|
||||
def __init__(self, columns: List[str], metadata=None):
|
||||
super().__init__(metadata)
|
||||
self.columns = columns
|
||||
|
||||
def can_connect_to(self, other: DType, conditions=None):
|
||||
if not isinstance(other, TableType):
|
||||
raise CompatibilityException("Target is not a TableType")
|
||||
|
||||
if self.columns != other.columns:
|
||||
raise CompatibilityException(
|
||||
f"Table schema mismatch: {self.columns} vs {other.columns}"
|
||||
)
|
||||
|
||||
|
||||
class CSVTableType(TableType):
|
||||
def __init__(self, columns: List[str], delimiter: str = ",", encoding: str = "utf-8", metadata=None):
|
||||
super().__init__(columns, metadata)
|
||||
self.delimiter = delimiter
|
||||
self.encoding = encoding
|
||||
|
||||
def can_connect_to(self, other: DType, conditions=None):
|
||||
if not isinstance(other, CSVTableType):
|
||||
raise CompatibilityException("CSV tables can only connect to CSV tables")
|
||||
|
||||
super().can_connect_to(other, conditions)
|
||||
|
||||
if self.delimiter != other.delimiter:
|
||||
raise CompatibilityException(
|
||||
f"CSV delimiter mismatch: {self.delimiter} vs {other.delimiter}"
|
||||
)
|
||||
|
||||
|
||||
# Text Types:
|
||||
class TextType(DType):
|
||||
def __init__(self, language: Optional[str] = None, encoding: Optional[str] = None, metadata=None):
|
||||
super().__init__(metadata)
|
||||
self.language = language
|
||||
self.encoding = encoding
|
||||
|
||||
def can_connect_to(self, other: DType, conditions=None):
|
||||
if not isinstance(other, TextType):
|
||||
raise CompatibilityException("Target is not TextType")
|
||||
|
||||
if self.language and other.language and self.language != other.language:
|
||||
raise CompatibilityException(
|
||||
f"Language mismatch: {self.language} vs {other.language}"
|
||||
)
|
||||
|
||||
if self.encoding and other.encoding and self.encoding != other.encoding:
|
||||
raise CompatibilityException(
|
||||
f"Encoding mismatch: {self.encoding} vs {other.encoding}"
|
||||
)
|
||||
|
||||
|
||||
# Ports:
|
||||
class Port(ABC):
|
||||
def __init__(self, name: str, dtype: DType):
|
||||
self.name = name
|
||||
self.dtype = dtype
|
||||
self.parent_task = None # Set by Workflow builder
|
||||
|
||||
def full_name(self):
|
||||
if self.parent_task:
|
||||
return f"{self.parent_task.name}.{self.name}"
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({self.full_name()}, {self.dtype})"
|
||||
|
||||
|
||||
# OutPort (fan-out supported):
|
||||
class OutPort(Port):
|
||||
def __init__(self, name: str, dtype: DType):
|
||||
super().__init__(name, dtype)
|
||||
self._connections: List["InPort"] = []
|
||||
self.produced_asset = None # Set by ExecutionContext
|
||||
|
||||
@property
|
||||
def connections(self):
|
||||
return list(self._connections)
|
||||
|
||||
def connect(self, in_port: "InPort", conditions: Optional[Dict[str, Any]] = None):
|
||||
# Fan-out allowed → no limit on connections
|
||||
in_port.can_connect_to(self, conditions)
|
||||
|
||||
self._connections.append(in_port)
|
||||
in_port._connected_output = self
|
||||
|
||||
def disconnect(self, in_port: "InPort"):
|
||||
if in_port in self._connections:
|
||||
self._connections.remove(in_port)
|
||||
in_port._connected_output = None
|
||||
|
||||
def get_downstream_ports(self):
|
||||
return list(self._connections)
|
||||
|
||||
|
||||
# InPort:
|
||||
class InPort(Port):
|
||||
def __init__(self, name: str, dtype: DType, required: bool = True):
|
||||
super().__init__(name, dtype)
|
||||
self.required = required
|
||||
self._connected_output: Optional[OutPort] = None
|
||||
|
||||
@property
|
||||
def connected_output(self):
|
||||
return self._connected_output
|
||||
|
||||
def is_connected(self):
|
||||
return self._connected_output is not None
|
||||
|
||||
def is_ready(self, context):
|
||||
"""
|
||||
True only if data exists in ExecutionContext.
|
||||
"""
|
||||
if not self._connected_output:
|
||||
return False
|
||||
return context.has_data(self)
|
||||
|
||||
def can_connect_to(self, out_port: OutPort, conditions: Optional[Dict[str, Any]] = None):
|
||||
self.dtype.can_connect_to(out_port.dtype, conditions)
|
||||
|
||||
def disconnect(self):
|
||||
if self._connected_output:
|
||||
self._connected_output._connections.remove(self)
|
||||
self._connected_output = None
|
||||
|
||||
def get_upstream_port(self):
|
||||
return self._connected_output
|
||||
|
||||
def validate(self):
|
||||
if self.required and not self.is_connected():
|
||||
raise CompatibilityException(f"InPort {self.full_name()} is required but not connected")
|
||||
@@ -0,0 +1,89 @@
|
||||
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
|
||||
@@ -0,0 +1,64 @@
|
||||
from typing import Set
|
||||
from api.task import Task, Status
|
||||
from api.execution_context import ExecutionContext
|
||||
|
||||
|
||||
class Scheduler:
|
||||
"""
|
||||
DAG-based execution scheduler.
|
||||
Decides WHEN a Task can run based on port readiness and orchestrates execution.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._completed: Set[Task] = set()
|
||||
self._failed: Set[Task] = set()
|
||||
|
||||
def run(self, root: Task, context: ExecutionContext):
|
||||
self._execute_task(root, context)
|
||||
|
||||
def _execute_task(self, task: Task, context: ExecutionContext):
|
||||
if task.status in (Status.COMPLETED, Status.FAILED, Status.STOPPED):
|
||||
return
|
||||
|
||||
if hasattr(task, "children"):
|
||||
self._run_workflow(task, context)
|
||||
return
|
||||
|
||||
if not task.check_inputs_ready(context):
|
||||
print(f"[Scheduler] Task '{task.name}' not ready, skipping for now.")
|
||||
return
|
||||
|
||||
try:
|
||||
task._set_status(Status.RUNNING)
|
||||
task.run(context)
|
||||
task._set_status(Status.COMPLETED)
|
||||
self._completed.add(task)
|
||||
except Exception as e:
|
||||
task._set_status(Status.FAILED)
|
||||
self._failed.add(task)
|
||||
print(f"[Scheduler] Task '{task.name}' failed: {e}")
|
||||
raise
|
||||
|
||||
def _run_workflow(self, workflow: Task, context: ExecutionContext):
|
||||
workflow._set_status(Status.RUNNING)
|
||||
|
||||
remaining = set(workflow.children)
|
||||
|
||||
while remaining:
|
||||
progress_made = False
|
||||
|
||||
for child in list(remaining):
|
||||
if child.check_inputs_ready(context):
|
||||
self._execute_task(child, context)
|
||||
remaining.remove(child)
|
||||
progress_made = True
|
||||
|
||||
if not progress_made:
|
||||
raise RuntimeError(
|
||||
f"Workflow '{workflow.name}' deadlock: unresolved dependencies"
|
||||
)
|
||||
|
||||
if any(c.status == Status.FAILED for c in workflow.children):
|
||||
workflow._set_status(Status.FAILED)
|
||||
else:
|
||||
workflow._set_status(Status.COMPLETED)
|
||||
@@ -0,0 +1,78 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from typing import List, Any, Optional
|
||||
|
||||
from api.io_port import InPort, OutPort
|
||||
from api.execution_context import ExecutionContext
|
||||
|
||||
|
||||
class Status(Enum):
|
||||
PENDING = "pending"
|
||||
READY = "ready"
|
||||
RUNNING = "running"
|
||||
PAUSED = "paused"
|
||||
STOPPED = "stopped"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class TaskEvent(Enum):
|
||||
BEFORE_RUN = "before_run"
|
||||
AFTER_RUN = "after_run"
|
||||
ON_ERROR = "on_error"
|
||||
STATUS_CHANGED = "status_changed"
|
||||
ON_START = "on_start"
|
||||
ON_FINISH = "on_finish"
|
||||
|
||||
|
||||
class TaskEventListener:
|
||||
def handle(self, event: TaskEvent, task: "Task", payload: Optional[Any] = None):
|
||||
pass
|
||||
|
||||
|
||||
class Task(ABC):
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.status = Status.PENDING
|
||||
|
||||
self.in_ports: List[InPort] = []
|
||||
self.out_ports: List[OutPort] = []
|
||||
|
||||
self._listeners: List[TaskEventListener] = []
|
||||
self.parent_task: Optional["Task"] = None
|
||||
|
||||
# Event System
|
||||
def add_listener(self, listener: TaskEventListener):
|
||||
self._listeners.append(listener)
|
||||
|
||||
def _emit(self, event: TaskEvent, payload: Optional[Any] = None):
|
||||
for listener in self._listeners:
|
||||
listener.handle(event, self, payload)
|
||||
|
||||
def _set_status(self, new_status: Status):
|
||||
old_status = self.status
|
||||
self.status = new_status
|
||||
|
||||
if old_status != new_status:
|
||||
self._emit(TaskEvent.STATUS_CHANGED, {"from": old_status, "to": new_status})
|
||||
|
||||
# Input Readiness Check
|
||||
def check_inputs_ready(self, context: ExecutionContext) -> bool:
|
||||
for port in self.in_ports:
|
||||
if not port.is_ready(context):
|
||||
return False
|
||||
return True
|
||||
|
||||
# Execution Wrapper
|
||||
def run(self, context: ExecutionContext):
|
||||
"""
|
||||
Base Task.run() SHOULD NOT be used for Module.
|
||||
Module overrides run() completely.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"Task.run() should not be called for Module. Use Module.run() instead."
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def execute(self, context: ExecutionContext):
|
||||
pass
|
||||
@@ -0,0 +1,96 @@
|
||||
from typing import List
|
||||
from api.task import Task, Status, TaskEvent
|
||||
from api.execution_context import ExecutionContext
|
||||
from api.io_port import InPort, CompatibilityException
|
||||
|
||||
|
||||
class Workflow(Task):
|
||||
"""
|
||||
Composite node: contains children Tasks.
|
||||
Executes tasks in dependency order (topological execution).
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
super().__init__(name)
|
||||
self.children: List[Task] = []
|
||||
|
||||
def execute(self, context: ExecutionContext):
|
||||
pass
|
||||
|
||||
def add_child(self, task: Task):
|
||||
task.parent_task = self
|
||||
self.children.append(task)
|
||||
|
||||
def remove_child(self, task: Task):
|
||||
if task in self.children:
|
||||
self.children.remove(task)
|
||||
task.parent_task = None
|
||||
|
||||
def get_children(self):
|
||||
return list(self.children)
|
||||
|
||||
def get_all_tasks(self):
|
||||
tasks = []
|
||||
for child in self.children:
|
||||
tasks.append(child)
|
||||
if isinstance(child, Workflow):
|
||||
tasks.extend(child.get_all_tasks())
|
||||
return tasks
|
||||
|
||||
def validate(self):
|
||||
super().validate()
|
||||
for child in self.children:
|
||||
child.validate()
|
||||
|
||||
def _get_dependencies(self, task: Task) -> List[Task]:
|
||||
deps = []
|
||||
for in_port in task.in_ports:
|
||||
if isinstance(in_port, InPort) and in_port.connected_output:
|
||||
upstream_task = in_port.connected_output.parent_task
|
||||
if upstream_task and upstream_task != task:
|
||||
deps.append(upstream_task)
|
||||
return deps
|
||||
|
||||
def run(self, context: ExecutionContext):
|
||||
self._set_status(Status.RUNNING)
|
||||
self._emit(TaskEvent.ON_START, {"workflow": self.name})
|
||||
|
||||
remaining = set(self.children)
|
||||
completed = set()
|
||||
|
||||
while remaining:
|
||||
progress_made = False
|
||||
|
||||
for task in list(remaining):
|
||||
deps = self._get_dependencies(task)
|
||||
|
||||
if all(dep in completed for dep in deps):
|
||||
if task.check_inputs_ready(context):
|
||||
try:
|
||||
task.run(context)
|
||||
completed.add(task)
|
||||
remaining.remove(task)
|
||||
progress_made = True
|
||||
|
||||
except CompatibilityException as e:
|
||||
task._set_status(Status.FAILED)
|
||||
self._emit(TaskEvent.ON_ERROR, {"error": e.reason, "task": task.name})
|
||||
self._set_status(Status.FAILED)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
task._set_status(Status.FAILED)
|
||||
self._emit(TaskEvent.ON_ERROR, {"error": str(e), "task": task.name})
|
||||
self._set_status(Status.FAILED)
|
||||
return
|
||||
|
||||
if not progress_made:
|
||||
self._set_status(Status.FAILED)
|
||||
self._emit(TaskEvent.ON_ERROR, {"error": "Dependency deadlock detected"})
|
||||
return
|
||||
|
||||
if all(t.status == Status.COMPLETED for t in self.children):
|
||||
self._set_status(Status.COMPLETED)
|
||||
self._emit(TaskEvent.ON_FINISH, {"workflow": self.name})
|
||||
else:
|
||||
self._set_status(Status.FAILED)
|
||||
Reference in New Issue
Block a user