Add All Folders

This commit is contained in:
soorena62
2026-02-08 04:38:10 +03:30
commit 9bb57e4799
191 changed files with 8469 additions and 0 deletions
View File
+45
View File
@@ -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}>"
+82
View File
@@ -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(),
}
+249
View File
@@ -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")
+89
View File
@@ -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
+64
View File
@@ -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)
+78
View File
@@ -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
+96
View File
@@ -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)