Add All Folders
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
Description:
|
||||
# In this project, using Pytest, the API layer codes of the Radioma Desktop Ver 2.0 project were tested with 20 tests.
|
||||
|
||||
Dependencies:
|
||||
# pip install pytest
|
||||
|
||||
Running:
|
||||
# pytest test rnd/run_rnd_workflow
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
class Asset:
|
||||
"""
|
||||
Represents the output of a Task.
|
||||
"""
|
||||
def __init__(self, data, type_name: str):
|
||||
self.data = data
|
||||
self.type_name = type_name
|
||||
self.preserved = True
|
||||
|
||||
def dismiss(self):
|
||||
self.preserved = False
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Asset type={self.type_name}>"
|
||||
@@ -0,0 +1,53 @@
|
||||
from typing import Dict, Any
|
||||
from radiuma_api.asset import Asset
|
||||
from radiuma_api.io_port import InPort
|
||||
from radiuma_api.io_port import OutPort
|
||||
# Write Codes Here:
|
||||
|
||||
|
||||
class ExecutionContext:
|
||||
"""
|
||||
Holds runtime execution data for a Workflow run.
|
||||
"""
|
||||
def __init__(self, execution_id: str | None = None):
|
||||
self.execution_id = execution_id
|
||||
self._data: Dict[OutPort, Asset] = {}
|
||||
self._metadata: Dict[str, Any] = {}
|
||||
|
||||
# Output Data Recording
|
||||
def put(self, output_port: OutPort, asset: Asset):
|
||||
"""Register an asset produced by an OutputPort."""
|
||||
self._data[output_port] = asset
|
||||
|
||||
# Getting Data For InputPort
|
||||
def get(self, input_port: InPort):
|
||||
"""Retrieve the asset connected to this InputPort."""
|
||||
if input_port.connected_output is None:
|
||||
return None
|
||||
return self._data.get(input_port.connected_output)
|
||||
|
||||
# Check Data Availability
|
||||
def has_data(self, input_port: InPort):
|
||||
return self.get(input_port) is not None
|
||||
|
||||
# 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)
|
||||
|
||||
# Status View
|
||||
def status_view(self):
|
||||
"""Return a lightweight snapshot of current execution state."""
|
||||
return {
|
||||
"execution_id": self.execution_id,
|
||||
"assets": {
|
||||
f"{port.parent_task.name}.{port.name}": {
|
||||
"contract": repr(port.contract),
|
||||
"asset_type": asset.type_name
|
||||
}
|
||||
for port, asset in self._data.items()
|
||||
},
|
||||
"metadata": self._metadata.copy(),
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, Dict, Any, List
|
||||
# Codes Go Below:
|
||||
|
||||
|
||||
# Errors:
|
||||
class CompatibilityException(Exception):
|
||||
def __init__(self, reason: str):
|
||||
super().__init__(reason)
|
||||
self.reason = reason
|
||||
|
||||
|
||||
# DType (Semantic Contract):
|
||||
class DType(ABC):
|
||||
def __init__(self, metadata: Optional[Dict[str, Any]] = None):
|
||||
self.metadata = metadata or {}
|
||||
|
||||
@abstractmethod
|
||||
def can_connect_to(self, other: "DType", conditions: Optional[Dict[str, Any]] = None):
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}(metadata={self.metadata})"
|
||||
|
||||
|
||||
# COMPOSITE TYPE:
|
||||
class CompositeType(DType):
|
||||
"""
|
||||
Represents a semantic bundle of multiple named DTypes.
|
||||
Example:
|
||||
CompositeType({
|
||||
"sheet1": TableType(...),
|
||||
"sheet2": TextType(...),
|
||||
"image": ImageType(...)
|
||||
})
|
||||
"""
|
||||
def __init__(self, parts: Dict[str, DType], metadata=None):
|
||||
super().__init__(metadata)
|
||||
self.parts = parts # Dict[str, DType]
|
||||
|
||||
def can_connect_to(self, other: "DType", conditions=None):
|
||||
if not isinstance(other, CompositeType):
|
||||
raise CompatibilityException("Expected CompositeType")
|
||||
|
||||
if self.parts.keys() != other.parts.keys():
|
||||
raise CompatibilityException("CompositeType Keys Mismatch")
|
||||
|
||||
for key in self.parts:
|
||||
self.parts[key].can_connect_to(other.parts[key], conditions)
|
||||
|
||||
def __repr__(self):
|
||||
return f"CompositeType({self.parts})"
|
||||
|
||||
|
||||
# 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)
|
||||
|
||||
def can_connect_to(self, other: DType, conditions=None):
|
||||
super().can_connect_to(other, conditions)
|
||||
# NIFTI-specific rules here
|
||||
|
||||
|
||||
class DICOMImageType(ImageType):
|
||||
def __init__(self, metadata=None):
|
||||
super().__init__(modality="dicom", metadata=metadata)
|
||||
|
||||
def can_connect_to(self, other: DType, conditions=None):
|
||||
super().can_connect_to(other, conditions)
|
||||
# DICOM-specific rules here
|
||||
|
||||
|
||||
# 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 Type:
|
||||
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
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({self.name}, {self.dtype})"
|
||||
|
||||
|
||||
class OutPort(Port):
|
||||
def __init__(self, name: str, dtype: DType):
|
||||
super().__init__(name, dtype)
|
||||
self._connections: List["InPort"] = []
|
||||
|
||||
@property
|
||||
def connections(self):
|
||||
return list(self._connections)
|
||||
|
||||
def connect(self, in_port: "InPort", conditions: Optional[Dict[str, Any]] = None):
|
||||
if len(self._connections):
|
||||
raise CompatibilityException("OutPort max connections reached")
|
||||
|
||||
in_port.can_connect_to(self, conditions)
|
||||
|
||||
self._connections.append(in_port)
|
||||
in_port._connected_output = self
|
||||
|
||||
|
||||
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_ready(self):
|
||||
return self._connected_output is not None
|
||||
|
||||
def can_connect_to(self, out_port: OutPort, conditions: Optional[Dict[str, Any]] = None):
|
||||
self.dtype.can_connect_to(out_port.dtype, conditions)
|
||||
@@ -0,0 +1,34 @@
|
||||
from radiuma_api.task import Task, Status, TaskEvent
|
||||
from radiuma_api.asset import Asset
|
||||
from radiuma_api.execution_context import ExecutionContext
|
||||
from radiuma_api.io_port import CompatibilityException
|
||||
# Write Codes Here:
|
||||
|
||||
|
||||
class Module(Task):
|
||||
"""Leaf node: atomic executable unit."""
|
||||
def run(self, context: ExecutionContext):
|
||||
if not self.check_inputs_ready(context):
|
||||
self._set_status(Status.PENDING)
|
||||
return
|
||||
|
||||
self._set_status(Status.RUNNING)
|
||||
|
||||
try:
|
||||
consumed_data = [
|
||||
port.connected_output.produced_asset.data
|
||||
for port in self.inputs
|
||||
if port.is_ready()
|
||||
]
|
||||
for output_port in self.outputs:
|
||||
asset = Asset(
|
||||
data=f"{self.name} processed {consumed_data}",
|
||||
type_name=output_port.type_spec
|
||||
)
|
||||
context.put(output_port, asset)
|
||||
|
||||
self._set_status(Status.COMPLETED)
|
||||
|
||||
except CompatibilityException as e:
|
||||
self._set_status(Status.FAILED)
|
||||
self._emit(TaskEvent.ON_ERROR, {"error": e.reason})
|
||||
@@ -0,0 +1,85 @@
|
||||
from typing import Set
|
||||
from radiuma_api.task import Task, Status
|
||||
from radiuma_api.execution_context import ExecutionContext
|
||||
# Write Codes Here:
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Entry point: execute a Workflow or Task graph.
|
||||
"""
|
||||
self._execute_task(root, context)
|
||||
|
||||
def _execute_task(self, task: Task, context: ExecutionContext):
|
||||
"""
|
||||
Execute a task respecting DAG dependencies.
|
||||
If 'task' is a Workflow (has children), delegate to _run_workflow.
|
||||
Otherwise, run the leaf task when inputs are ready.
|
||||
"""
|
||||
# Skip finished tasks
|
||||
if task.status in (Status.COMPLETED, Status.FAILED, Status.STOPPED):
|
||||
return
|
||||
|
||||
# Workflow (composite)
|
||||
if hasattr(task, "children"):
|
||||
self._run_workflow(task, context)
|
||||
return
|
||||
|
||||
# Leaf Task: must have inputs ready
|
||||
if not task.check_inputs_ready(context):
|
||||
# Optional: diagnostic log
|
||||
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)
|
||||
# Optional: diagnostic log
|
||||
print(f"[Scheduler] Task '{task.name}' failed: {e}")
|
||||
# Re-raise to surface error if caller needs to handle
|
||||
raise
|
||||
|
||||
def _run_workflow(self, workflow: Task, context: ExecutionContext):
|
||||
"""
|
||||
Execute children respecting dependencies.
|
||||
Progress until all children are executed or a deadlock is detected.
|
||||
"""
|
||||
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:
|
||||
# Deadlock: dependencies not resolvable with current data
|
||||
raise RuntimeError(
|
||||
f"Workflow '{workflow.name}' deadlock: unresolved dependencies"
|
||||
)
|
||||
|
||||
# Final workflow status based on children results
|
||||
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,83 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import Enum
|
||||
from typing import List, Any, Optional
|
||||
from radiuma_api.io_port import InPort
|
||||
from radiuma_api.io_port import OutPort
|
||||
from radiuma_api.execution_context import ExecutionContext
|
||||
# Write Codes Here:
|
||||
|
||||
|
||||
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"
|
||||
|
||||
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.inputs: List[InPort] = []
|
||||
self.outputs: List[OutPort] = []
|
||||
self._listeners: List[TaskEventListener] = []
|
||||
|
||||
# Events
|
||||
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})
|
||||
|
||||
# Lifecycle
|
||||
def pause(self):
|
||||
if self.status == Status.RUNNING:
|
||||
self._set_status(Status.PAUSED)
|
||||
|
||||
def resume(self):
|
||||
if self.status == Status.PAUSED:
|
||||
self._set_status(Status.READY)
|
||||
|
||||
def stop(self):
|
||||
if self.status in (Status.RUNNING, Status.PAUSED, Status.READY):
|
||||
self._set_status(Status.STOPPED)
|
||||
|
||||
# Ports
|
||||
def _add_input_port(self, port: InPort):
|
||||
port.parent_task = self
|
||||
self.inputs.append(port)
|
||||
|
||||
def _add_output_port(self, port: OutPort):
|
||||
port.parent_task = self
|
||||
self.outputs.append(port)
|
||||
|
||||
def check_inputs_ready(self, context: ExecutionContext) -> bool:
|
||||
"""Check if all required inputs have data in ExecutionContext."""
|
||||
for in_port in self.inputs:
|
||||
if in_port.required and not context.has_data(in_port):
|
||||
return False
|
||||
return True
|
||||
|
||||
# Runnable
|
||||
@abstractmethod
|
||||
def run(self, context: ExecutionContext):
|
||||
pass
|
||||
@@ -0,0 +1,44 @@
|
||||
from typing import List
|
||||
from radiuma_api.task import Task, Status, TaskEvent
|
||||
from radiuma_api.execution_context import ExecutionContext
|
||||
from radiuma_api.scheduler import Scheduler
|
||||
from radiuma_api.io_port import CompatibilityException
|
||||
# Write Codes Here:
|
||||
|
||||
|
||||
class Workflow(Task):
|
||||
"""Composite node: aggregates Tasks (Modules or other Workflows)."""
|
||||
|
||||
def __init__(self, name: str):
|
||||
super().__init__(name)
|
||||
self.children: List[Task] = []
|
||||
|
||||
def add_child(self, task: Task):
|
||||
task.parent_task = self
|
||||
self.children.append(task)
|
||||
|
||||
def run(self, context: ExecutionContext):
|
||||
"""Delegate execution of children to Scheduler."""
|
||||
self._set_status(Status.RUNNING)
|
||||
scheduler = Scheduler()
|
||||
|
||||
for child in self.children:
|
||||
if self.status == Status.STOPPED:
|
||||
break
|
||||
|
||||
try:
|
||||
if child.check_inputs_ready(context):
|
||||
scheduler.run(child, context)
|
||||
else:
|
||||
print(f"Task {child.name} not ready, skipping.")
|
||||
except CompatibilityException as e:
|
||||
self._emit(TaskEvent.ON_ERROR, {"error": e.reason, "task": child.name})
|
||||
child._set_status(Status.FAILED)
|
||||
|
||||
# Final status evaluation
|
||||
if all(c.status == Status.COMPLETED for c in self.children):
|
||||
self._set_status(Status.COMPLETED)
|
||||
elif any(c.status == Status.FAILED for c in self.children):
|
||||
self._set_status(Status.FAILED)
|
||||
else:
|
||||
self._set_status(Status.STOPPED)
|
||||
@@ -0,0 +1,69 @@
|
||||
from radiuma_api.workflow import Workflow
|
||||
from radiuma_api.execution_context import ExecutionContext
|
||||
from radiuma_api.io_port import InPort, OutPort, ImageType, TextType
|
||||
from radiuma_api.module import Module
|
||||
from radiuma_api.task import Status
|
||||
from radiuma_api.asset import Asset
|
||||
# Codes Go Below:
|
||||
|
||||
|
||||
class Reader(Module):
|
||||
def __init__(self):
|
||||
super().__init__("Reader")
|
||||
self._add_output_port(OutPort("images", ImageType()))
|
||||
|
||||
def run(self, context):
|
||||
print("[Reader] Loading images...")
|
||||
context.put(self.outputs[0], Asset(["img1", "img2"], "images"))
|
||||
self._set_status(Status.COMPLETED)
|
||||
|
||||
|
||||
class Extractor(Module):
|
||||
def __init__(self):
|
||||
super().__init__("Extractor")
|
||||
self._add_input_port(InPort("images", ImageType()))
|
||||
self._add_output_port(OutPort("features", TextType()))
|
||||
|
||||
def run(self, context):
|
||||
imgs = context.get(self.inputs[0]).data
|
||||
print(f"[Extractor] Extracting features from: {imgs}")
|
||||
feats = [{"img": img, "feat": f"feature_of_{img}"} for img in imgs]
|
||||
context.put(self.outputs[0], Asset(feats, "features"))
|
||||
self._set_status(Status.COMPLETED)
|
||||
|
||||
|
||||
class Writer(Module):
|
||||
def __init__(self):
|
||||
super().__init__("Writer")
|
||||
self._add_input_port(InPort("features", TextType()))
|
||||
|
||||
def run(self, context):
|
||||
feats = context.get(self.inputs[0]).data
|
||||
print("[Writer] Saving features:")
|
||||
for f in feats:
|
||||
print(" ", f)
|
||||
self._set_status(Status.COMPLETED)
|
||||
|
||||
|
||||
def main():
|
||||
ctx = ExecutionContext()
|
||||
wf = Workflow("RND_Workflow")
|
||||
|
||||
r = Reader()
|
||||
e = Extractor()
|
||||
w = Writer()
|
||||
|
||||
r.outputs[0].connect(e.inputs[0])
|
||||
e.outputs[0].connect(w.inputs[0])
|
||||
|
||||
wf.add_child(r)
|
||||
wf.add_child(e)
|
||||
wf.add_child(w)
|
||||
|
||||
print("=== Running Workflow ===")
|
||||
wf.run(ctx)
|
||||
print("=== Workflow Completed ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,43 @@
|
||||
from radiuma_api.io_port import (
|
||||
CompositeType,
|
||||
ImageType,
|
||||
TextType,
|
||||
TableType,
|
||||
CompatibilityException
|
||||
)
|
||||
|
||||
def test_composite_type_success():
|
||||
t1 = CompositeType({
|
||||
"img": ImageType(),
|
||||
"meta": TextType(language="en")
|
||||
})
|
||||
|
||||
t2 = CompositeType({
|
||||
"img": ImageType(),
|
||||
"meta": TextType(language="en")
|
||||
})
|
||||
|
||||
# Must succeed
|
||||
t1.can_connect_to(t2)
|
||||
|
||||
|
||||
def test_composite_type_key_mismatch():
|
||||
t1 = CompositeType({"img": ImageType()})
|
||||
t2 = CompositeType({"image": ImageType()})
|
||||
|
||||
try:
|
||||
t1.can_connect_to(t2)
|
||||
assert False, "Expected CompositeType key mismatch"
|
||||
except CompatibilityException:
|
||||
assert True
|
||||
|
||||
|
||||
def test_composite_type_internal_type_mismatch():
|
||||
t1 = CompositeType({"img": ImageType(modality="nifti")})
|
||||
t2 = CompositeType({"img": ImageType(modality="dicom")})
|
||||
|
||||
try:
|
||||
t1.can_connect_to(t2)
|
||||
assert False, "Expected internal DType mismatch"
|
||||
except CompatibilityException:
|
||||
assert True
|
||||
@@ -0,0 +1,17 @@
|
||||
from radiuma_api.execution_context import ExecutionContext
|
||||
from radiuma_api.io_port import OutPort, InPort, ImageType
|
||||
from radiuma_api.asset import Asset
|
||||
# Codes Go Below:
|
||||
|
||||
|
||||
def test_context_put_get():
|
||||
ctx = ExecutionContext()
|
||||
outp = OutPort("o", ImageType())
|
||||
inp = InPort("i", ImageType())
|
||||
|
||||
outp.connect(inp)
|
||||
|
||||
asset = Asset(["img1"], "images")
|
||||
ctx.put(outp, asset)
|
||||
|
||||
assert ctx.get(inp).data == ["img1"]
|
||||
@@ -0,0 +1,39 @@
|
||||
from radiuma_api.io_port import InPort, OutPort, ImageType, TextType, CompatibilityException
|
||||
# Codes Go Below:
|
||||
|
||||
|
||||
def test_compatible_ports():
|
||||
"""Ports with the same DType must connect successfully."""
|
||||
outp = OutPort("o", ImageType())
|
||||
inp = InPort("i", ImageType())
|
||||
|
||||
outp.connect(inp)
|
||||
|
||||
assert inp.connected_output == outp
|
||||
|
||||
|
||||
def test_incompatible_ports():
|
||||
"""Connecting mismatched DTypes must raise CompatibilityException."""
|
||||
outp = OutPort("o", ImageType())
|
||||
inp = InPort("i", TextType())
|
||||
|
||||
try:
|
||||
outp.connect(inp)
|
||||
assert False, "Expected CompatibilityException"
|
||||
except CompatibilityException:
|
||||
assert True
|
||||
|
||||
|
||||
def test_outport_single_connection_limit():
|
||||
"""OutPort must not allow more than one connection."""
|
||||
outp = OutPort("o", ImageType())
|
||||
inp1 = InPort("i1", ImageType())
|
||||
inp2 = InPort("i2", ImageType())
|
||||
|
||||
outp.connect(inp1)
|
||||
|
||||
try:
|
||||
outp.connect(inp2)
|
||||
assert False, "Expected OutPort max connections error"
|
||||
except CompatibilityException:
|
||||
assert True
|
||||
@@ -0,0 +1,43 @@
|
||||
from radiuma_api.scheduler import Scheduler
|
||||
from radiuma_api.workflow import Workflow
|
||||
from radiuma_api.module import Module
|
||||
from radiuma_api.io_port import InPort, OutPort, ImageType
|
||||
from radiuma_api.execution_context import ExecutionContext
|
||||
from radiuma_api.task import Status
|
||||
# Codes Go below:
|
||||
|
||||
|
||||
class Reader(Module):
|
||||
def __init__(self):
|
||||
super().__init__("Reader")
|
||||
self._add_output_port(OutPort("images", ImageType()))
|
||||
|
||||
def run(self, context):
|
||||
context.put(self.outputs[0], "DATA")
|
||||
self._set_status(Status.COMPLETED)
|
||||
|
||||
class Extractor(Module):
|
||||
def __init__(self):
|
||||
super().__init__("Extractor")
|
||||
self._add_input_port(InPort("images", ImageType()))
|
||||
|
||||
def run(self, context):
|
||||
assert context.get(self.inputs[0]) == "DATA"
|
||||
self._set_status(Status.COMPLETED)
|
||||
|
||||
def test_scheduler_runs_in_order():
|
||||
ctx = ExecutionContext()
|
||||
wf = Workflow("WF")
|
||||
|
||||
r = Reader()
|
||||
e = Extractor()
|
||||
|
||||
r.outputs[0].connect(e.inputs[0])
|
||||
|
||||
wf.add_child(r)
|
||||
wf.add_child(e)
|
||||
|
||||
wf.run(ctx)
|
||||
|
||||
assert r.status == Status.COMPLETED
|
||||
assert e.status == Status.COMPLETED
|
||||
@@ -0,0 +1,24 @@
|
||||
from radiuma_api.task import Status
|
||||
from radiuma_api.module import Module
|
||||
from radiuma_api.execution_context import ExecutionContext
|
||||
from radiuma_api.io_port import OutPort, ImageType
|
||||
# Codes Go below:
|
||||
|
||||
|
||||
class Dummy(Module):
|
||||
def __init__(self):
|
||||
super().__init__("Dummy")
|
||||
self._add_output_port(OutPort("o", ImageType()))
|
||||
|
||||
def run(self, context):
|
||||
self._set_status(Status.RUNNING)
|
||||
context.put(self.outputs[0], None)
|
||||
self._set_status(Status.COMPLETED)
|
||||
|
||||
def test_task_lifecycle():
|
||||
ctx = ExecutionContext()
|
||||
t = Dummy()
|
||||
|
||||
assert t.status == Status.PENDING
|
||||
t.run(ctx)
|
||||
assert t.status == Status.COMPLETED
|
||||
@@ -0,0 +1,57 @@
|
||||
from radiuma_api.workflow import Workflow
|
||||
from radiuma_api.execution_context import ExecutionContext
|
||||
from radiuma_api.io_port import InPort, OutPort, ImageType, TextType
|
||||
from radiuma_api.module import Module
|
||||
from radiuma_api.task import Status
|
||||
from radiuma_api.asset import Asset
|
||||
# Codes Go below:
|
||||
|
||||
|
||||
class Reader(Module):
|
||||
def __init__(self):
|
||||
super().__init__("Reader")
|
||||
self._add_output_port(OutPort("images", ImageType()))
|
||||
|
||||
def run(self, context):
|
||||
context.put(self.outputs[0], Asset(["img1"], "images"))
|
||||
self._set_status(Status.COMPLETED)
|
||||
|
||||
class Extractor(Module):
|
||||
def __init__(self):
|
||||
super().__init__("Extractor")
|
||||
self._add_input_port(InPort("images", ImageType()))
|
||||
self._add_output_port(OutPort("features", TextType()))
|
||||
|
||||
def run(self, context):
|
||||
imgs = context.get(self.inputs[0]).data
|
||||
context.put(self.outputs[0], Asset([{"img": imgs[0]}], "features"))
|
||||
self._set_status(Status.COMPLETED)
|
||||
|
||||
class Writer(Module):
|
||||
def __init__(self):
|
||||
super().__init__("Writer")
|
||||
self._add_input_port(InPort("features", TextType()))
|
||||
|
||||
def run(self, context):
|
||||
feats = context.get(self.inputs[0]).data
|
||||
assert feats[0]["img"] == "img1"
|
||||
self._set_status(Status.COMPLETED)
|
||||
|
||||
def test_end_to_end():
|
||||
ctx = ExecutionContext()
|
||||
wf = Workflow("WF")
|
||||
|
||||
r = Reader()
|
||||
e = Extractor()
|
||||
w = Writer()
|
||||
|
||||
r.outputs[0].connect(e.inputs[0])
|
||||
e.outputs[0].connect(w.inputs[0])
|
||||
|
||||
wf.add_child(r)
|
||||
wf.add_child(e)
|
||||
wf.add_child(w)
|
||||
|
||||
wf.run(ctx)
|
||||
|
||||
assert w.status == Status.COMPLETED
|
||||
Reference in New Issue
Block a user