From 0dd454117441f83ceb5e8bbffd2b7aabf34d38f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 23:00:05 +0000 Subject: [PATCH 1/3] Initial plan From 19567354f62966dbdf3cdd60ee1d6f4488deb208 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 23:17:22 +0000 Subject: [PATCH 2/3] Implement Python codebase for workflow definitions Co-authored-by: timurbazhirov <721112+timurbazhirov@users.noreply.github.com> --- pyproject.toml | 6 +- src/py/mat3ra/__init__.py | 1 - src/py/mat3ra/wode/__init__.py | 55 +++++- src/py/mat3ra/wode/enums.py | 51 ++++++ src/py/mat3ra/wode/subworkflows/__init__.py | 7 + .../mat3ra/wode/subworkflows/subworkflow.py | 38 ++++ src/py/mat3ra/wode/units/__init__.py | 25 +++ src/py/mat3ra/wode/units/assertion.py | 19 ++ src/py/mat3ra/wode/units/assignment.py | 20 +++ src/py/mat3ra/wode/units/base.py | 50 ++++++ src/py/mat3ra/wode/units/condition.py | 19 ++ src/py/mat3ra/wode/units/execution.py | 25 +++ src/py/mat3ra/wode/units/io.py | 21 +++ src/py/mat3ra/wode/units/map.py | 19 ++ src/py/mat3ra/wode/units/processing.py | 20 +++ src/py/mat3ra/wode/units/reduce.py | 19 ++ src/py/mat3ra/wode/units/subworkflow.py | 21 +++ src/py/mat3ra/wode/workflows/__init__.py | 7 + src/py/mat3ra/wode/workflows/workflow.py | 40 +++++ tests/py/test_sample.py | 11 -- tests/py/test_wode.py | 169 ++++++++++++++++++ 21 files changed, 627 insertions(+), 16 deletions(-) create mode 100644 src/py/mat3ra/wode/enums.py create mode 100644 src/py/mat3ra/wode/subworkflows/__init__.py create mode 100644 src/py/mat3ra/wode/subworkflows/subworkflow.py create mode 100644 src/py/mat3ra/wode/units/__init__.py create mode 100644 src/py/mat3ra/wode/units/assertion.py create mode 100644 src/py/mat3ra/wode/units/assignment.py create mode 100644 src/py/mat3ra/wode/units/base.py create mode 100644 src/py/mat3ra/wode/units/condition.py create mode 100644 src/py/mat3ra/wode/units/execution.py create mode 100644 src/py/mat3ra/wode/units/io.py create mode 100644 src/py/mat3ra/wode/units/map.py create mode 100644 src/py/mat3ra/wode/units/processing.py create mode 100644 src/py/mat3ra/wode/units/reduce.py create mode 100644 src/py/mat3ra/wode/units/subworkflow.py create mode 100644 src/py/mat3ra/wode/workflows/__init__.py create mode 100644 src/py/mat3ra/wode/workflows/workflow.py delete mode 100644 tests/py/test_sample.py create mode 100644 tests/py/test_wode.py diff --git a/pyproject.toml b/pyproject.toml index d46be7d3..b705c911 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,11 @@ classifiers = [ "Topic :: Software Development", ] dependencies = [ - "numpy", + "mat3ra-esse>=2025.10.8", + "mat3ra-mode>=2025.11.6", + "mat3ra-ade>=2025.10.2", + "mat3ra-utils>=2025.9.20", + "pydantic>=2.0.0", ] [project.optional-dependencies] diff --git a/src/py/mat3ra/__init__.py b/src/py/mat3ra/__init__.py index 98cad5d6..ea89e843 100644 --- a/src/py/mat3ra/__init__.py +++ b/src/py/mat3ra/__init__.py @@ -1,2 +1 @@ """mat3ra namespace package.""" - diff --git a/src/py/mat3ra/wode/__init__.py b/src/py/mat3ra/wode/__init__.py index 5e1f4cb0..62ec9a19 100644 --- a/src/py/mat3ra/wode/__init__.py +++ b/src/py/mat3ra/wode/__init__.py @@ -1,6 +1,55 @@ -import numpy as np +""" +WOrkflow DEfinitions - Python implementation. +This package provides Python classes for workflow definitions, including: +- Workflow: A complete computational workflow +- Subworkflow: A logical collection of units +- Units: Individual workflow steps (Execution, Assignment, I/O, etc.) +""" -def get_length(vec: np.ndarray) -> float: - return float(np.linalg.norm(vec)) +from .enums import ( + IO_ID_COLUMN, + UNIT_NAME_INVALID_CHARS, + UnitStatus, + UnitTag, + UnitType, + WorkflowStatus, +) +from .subworkflows import Subworkflow +from .units import ( + AssertionUnit, + AssignmentUnit, + BaseUnit, + ConditionUnit, + ExecutionUnit, + IOUnit, + MapUnit, + ProcessingUnit, + ReduceUnit, + SubworkflowUnit, +) +from .workflows import Workflow +__all__ = [ + # Enums and constants + "UnitType", + "UnitStatus", + "UnitTag", + "WorkflowStatus", + "IO_ID_COLUMN", + "UNIT_NAME_INVALID_CHARS", + # Main classes + "Workflow", + "Subworkflow", + # Unit classes + "BaseUnit", + "ExecutionUnit", + "AssignmentUnit", + "ConditionUnit", + "IOUnit", + "MapUnit", + "ReduceUnit", + "AssertionUnit", + "ProcessingUnit", + "SubworkflowUnit", +] diff --git a/src/py/mat3ra/wode/enums.py b/src/py/mat3ra/wode/enums.py new file mode 100644 index 00000000..3e637f55 --- /dev/null +++ b/src/py/mat3ra/wode/enums.py @@ -0,0 +1,51 @@ +""" +Enums and constants for workflow definitions. +Shared across the codebase and tests. +""" + +from enum import Enum + + +class UnitType(str, Enum): + """Types of workflow units.""" + + CONVERGENCE = "convergence" + EXIT = "exit" + EXECUTION = "execution" + MAP = "map" + REDUCE = "reduce" + ASSIGNMENT = "assignment" + CONDITION = "condition" + SUBWORKFLOW = "subworkflow" + PROCESSING = "processing" + IO = "io" + ASSERTION = "assertion" + + +class UnitStatus(str, Enum): + """Status of workflow units.""" + + IDLE = "idle" + ACTIVE = "active" + FINISHED = "finished" + ERROR = "error" + WARNING = "warning" + + +class UnitTag(str, Enum): + """Tags for workflow units.""" + + HAS_CONVERGENCE_PARAM = "hasConvergenceParam" + HAS_CONVERGENCE_RESULT = "hasConvergenceResult" + + +class WorkflowStatus(str, Enum): + """Status of workflows.""" + + UP_TO_DATE = "up-to-date" + OUTDATED = "outdated" + + +# Constants +IO_ID_COLUMN = "exabyteId" +UNIT_NAME_INVALID_CHARS = "/" diff --git a/src/py/mat3ra/wode/subworkflows/__init__.py b/src/py/mat3ra/wode/subworkflows/__init__.py new file mode 100644 index 00000000..ba0740a2 --- /dev/null +++ b/src/py/mat3ra/wode/subworkflows/__init__.py @@ -0,0 +1,7 @@ +"""Subworkflows module.""" + +from .subworkflow import Subworkflow + +__all__ = [ + "Subworkflow", +] diff --git a/src/py/mat3ra/wode/subworkflows/subworkflow.py b/src/py/mat3ra/wode/subworkflows/subworkflow.py new file mode 100644 index 00000000..7178d3d5 --- /dev/null +++ b/src/py/mat3ra/wode/subworkflows/subworkflow.py @@ -0,0 +1,38 @@ +"""Subworkflow class.""" + +from typing import Any, Dict, List, Optional +from uuid import uuid4 + +from pydantic import BaseModel, ConfigDict, Field + + +class Subworkflow(BaseModel): + """ + Subworkflow class representing a logical collection of units. + + A subworkflow groups units together with a specific application and model. + """ + + model_config = ConfigDict(extra="allow", populate_by_name=True) + + id: str = Field(default_factory=lambda: str(uuid4()), alias="_id", description="Unique ID for the subworkflow") + name: str = Field(..., description="Name of the subworkflow") + application: Dict[str, Any] = Field(..., description="Application configuration") + model: Dict[str, Any] = Field(..., description="Model configuration") + method: Optional[Dict[str, Any]] = Field(default=None, description="Method configuration") + units: List[Dict[str, Any]] = Field(default_factory=list, description="List of units in the subworkflow") + properties: List[str] = Field(default_factory=list, description="Properties of the subworkflow") + repetition: int = Field(default=0, description="Repetition number") + + def model_dump(self, **kwargs) -> Dict[str, Any]: + """Export subworkflow to dictionary format.""" + # Use by_alias to maintain _id field name in output + if "by_alias" not in kwargs: + kwargs["by_alias"] = True + return super().model_dump(**kwargs) + + def model_dump_json(self, **kwargs) -> str: + """Export subworkflow to JSON format.""" + if "by_alias" not in kwargs: + kwargs["by_alias"] = True + return super().model_dump_json(**kwargs) diff --git a/src/py/mat3ra/wode/units/__init__.py b/src/py/mat3ra/wode/units/__init__.py new file mode 100644 index 00000000..46b65e50 --- /dev/null +++ b/src/py/mat3ra/wode/units/__init__.py @@ -0,0 +1,25 @@ +"""Workflow units module.""" + +from .assertion import AssertionUnit +from .assignment import AssignmentUnit +from .base import BaseUnit +from .condition import ConditionUnit +from .execution import ExecutionUnit +from .io import IOUnit +from .map import MapUnit +from .processing import ProcessingUnit +from .reduce import ReduceUnit +from .subworkflow import SubworkflowUnit + +__all__ = [ + "BaseUnit", + "ExecutionUnit", + "AssignmentUnit", + "ConditionUnit", + "IOUnit", + "MapUnit", + "ReduceUnit", + "AssertionUnit", + "ProcessingUnit", + "SubworkflowUnit", +] diff --git a/src/py/mat3ra/wode/units/assertion.py b/src/py/mat3ra/wode/units/assertion.py new file mode 100644 index 00000000..cfe35fd5 --- /dev/null +++ b/src/py/mat3ra/wode/units/assertion.py @@ -0,0 +1,19 @@ +"""Assertion unit class.""" + +from typing import Literal, Optional + +from pydantic import Field + +from ..enums import UnitType +from .base import BaseUnit + + +class AssertionUnit(BaseUnit): + """ + Assertion unit for validating expressions. + + This unit type asserts that an expression evaluates to true. + """ + + type: Literal[UnitType.ASSERTION] = Field(default=UnitType.ASSERTION) + expression: Optional[str] = Field(default=None, description="Expression to assert") diff --git a/src/py/mat3ra/wode/units/assignment.py b/src/py/mat3ra/wode/units/assignment.py new file mode 100644 index 00000000..39a03d4f --- /dev/null +++ b/src/py/mat3ra/wode/units/assignment.py @@ -0,0 +1,20 @@ +"""Assignment unit class.""" + +from typing import Any, Literal, Optional + +from pydantic import Field + +from ..enums import UnitType +from .base import BaseUnit + + +class AssignmentUnit(BaseUnit): + """ + Assignment unit for setting values. + + This unit type assigns values to variables in the workflow context. + """ + + type: Literal[UnitType.ASSIGNMENT] = Field(default=UnitType.ASSIGNMENT) + value: Optional[Any] = Field(default=None, description="Value to assign") + operand: Optional[str] = Field(default=None, description="Target operand") diff --git a/src/py/mat3ra/wode/units/base.py b/src/py/mat3ra/wode/units/base.py new file mode 100644 index 00000000..670e4bcc --- /dev/null +++ b/src/py/mat3ra/wode/units/base.py @@ -0,0 +1,50 @@ +"""Base unit class for workflow units.""" + +from typing import Any, Dict, List, Optional +from uuid import uuid4 + +from pydantic import BaseModel, ConfigDict, Field + +from ..enums import UnitStatus + + +class BaseUnit(BaseModel): + """ + Base class for workflow units. + + A unit is the fundamental building block of a workflow, representing + a single step or operation in a computational workflow. + """ + + model_config = ConfigDict(use_enum_values=True, extra="allow") + + name: str = Field(..., description="Human-readable name of the unit") + type: str = Field(..., description="Type of the unit") + flowchartId: str = Field(default_factory=lambda: str(uuid4()), description="Unique ID for flowchart visualization") + head: bool = Field(default=False, description="Whether this is the head unit") + next: Optional[str] = Field(default=None, description="ID of the next unit in the workflow") + status: UnitStatus = Field(default=UnitStatus.IDLE, description="Current status of the unit") + statusTrack: List[Dict[str, Any]] = Field(default_factory=list, description="History of status changes") + tags: List[str] = Field(default_factory=list, description="Tags associated with the unit") + isDraft: bool = Field(default=False, description="Whether this is a draft unit") + repetition: int = Field(default=0, description="Repetition number for the unit") + + def is_in_status(self, status: UnitStatus) -> bool: + """ + Check whether a unit is currently in a given status. + + Args: + status: Status to check + + Returns: + True if unit is in the given status + """ + return self.status == status + + def model_dump(self, **kwargs) -> Dict[str, Any]: + """Export unit to dictionary format.""" + return super().model_dump(**kwargs) + + def model_dump_json(self, **kwargs) -> str: + """Export unit to JSON format.""" + return super().model_dump_json(**kwargs) diff --git a/src/py/mat3ra/wode/units/condition.py b/src/py/mat3ra/wode/units/condition.py new file mode 100644 index 00000000..a9f0f038 --- /dev/null +++ b/src/py/mat3ra/wode/units/condition.py @@ -0,0 +1,19 @@ +"""Condition unit class.""" + +from typing import Literal, Optional + +from pydantic import Field + +from ..enums import UnitType +from .base import BaseUnit + + +class ConditionUnit(BaseUnit): + """ + Condition unit for evaluating conditions. + + This unit type evaluates conditions to control workflow flow. + """ + + type: Literal[UnitType.CONDITION] = Field(default=UnitType.CONDITION) + condition: Optional[str] = Field(default=None, description="Condition to evaluate") diff --git a/src/py/mat3ra/wode/units/execution.py b/src/py/mat3ra/wode/units/execution.py new file mode 100644 index 00000000..35361b45 --- /dev/null +++ b/src/py/mat3ra/wode/units/execution.py @@ -0,0 +1,25 @@ +"""Execution unit class.""" + +from typing import Any, Dict, Literal, Optional + +from pydantic import Field + +from ..enums import UnitType +from .base import BaseUnit + + +class ExecutionUnit(BaseUnit): + """ + Execution unit for running applications. + + This unit type executes an application with specific parameters. + """ + + type: Literal[UnitType.EXECUTION] = Field(default=UnitType.EXECUTION) + application: Optional[Dict[str, Any]] = Field(default=None, description="Application to execute") + executable: Optional[Dict[str, Any]] = Field(default=None, description="Executable configuration") + flavor: Optional[Dict[str, Any]] = Field(default=None, description="Flavor configuration") + preProcessors: list = Field(default_factory=list, description="Pre-processors to run") + postProcessors: list = Field(default_factory=list, description="Post-processors to run") + monitors: list = Field(default_factory=list, description="Monitors to use") + results: list = Field(default_factory=list, description="Results produced by the unit") diff --git a/src/py/mat3ra/wode/units/io.py b/src/py/mat3ra/wode/units/io.py new file mode 100644 index 00000000..8bf4d54a --- /dev/null +++ b/src/py/mat3ra/wode/units/io.py @@ -0,0 +1,21 @@ +"""I/O unit class.""" + +from typing import Any, Dict, Literal, Optional + +from pydantic import Field + +from ..enums import UnitType +from .base import BaseUnit + + +class IOUnit(BaseUnit): + """ + I/O unit for reading and writing data. + + This unit type handles input/output operations. + """ + + type: Literal[UnitType.IO] = Field(default=UnitType.IO) + operation: Optional[str] = Field(default=None, description="I/O operation (read/write)") + source: Optional[Dict[str, Any]] = Field(default=None, description="Data source") + destination: Optional[Dict[str, Any]] = Field(default=None, description="Data destination") diff --git a/src/py/mat3ra/wode/units/map.py b/src/py/mat3ra/wode/units/map.py new file mode 100644 index 00000000..1027bc55 --- /dev/null +++ b/src/py/mat3ra/wode/units/map.py @@ -0,0 +1,19 @@ +"""Map unit class.""" + +from typing import Any, Literal, Optional + +from pydantic import Field + +from ..enums import UnitType +from .base import BaseUnit + + +class MapUnit(BaseUnit): + """ + Map unit for dynamic parallel operations. + + This unit type creates a dynamic number of units based on input data. + """ + + type: Literal[UnitType.MAP] = Field(default=UnitType.MAP) + input: Optional[Any] = Field(default=None, description="Input data to map over") diff --git a/src/py/mat3ra/wode/units/processing.py b/src/py/mat3ra/wode/units/processing.py new file mode 100644 index 00000000..04172288 --- /dev/null +++ b/src/py/mat3ra/wode/units/processing.py @@ -0,0 +1,20 @@ +"""Processing unit class.""" + +from typing import Any, Dict, Literal, Optional + +from pydantic import Field + +from ..enums import UnitType +from .base import BaseUnit + + +class ProcessingUnit(BaseUnit): + """ + Processing unit for data processing operations. + + This unit type performs processing operations on data. + """ + + type: Literal[UnitType.PROCESSING] = Field(default=UnitType.PROCESSING) + operation: Optional[str] = Field(default=None, description="Processing operation") + operationData: Optional[Dict[str, Any]] = Field(default=None, description="Data for the operation") diff --git a/src/py/mat3ra/wode/units/reduce.py b/src/py/mat3ra/wode/units/reduce.py new file mode 100644 index 00000000..6c4f19ec --- /dev/null +++ b/src/py/mat3ra/wode/units/reduce.py @@ -0,0 +1,19 @@ +"""Reduce unit class.""" + +from typing import Literal, Optional + +from pydantic import Field + +from ..enums import UnitType +from .base import BaseUnit + + +class ReduceUnit(BaseUnit): + """ + Reduce unit for collecting results. + + This unit type collects and combines results from fanned out operations. + """ + + type: Literal[UnitType.REDUCE] = Field(default=UnitType.REDUCE) + operation: Optional[str] = Field(default=None, description="Reduce operation") diff --git a/src/py/mat3ra/wode/units/subworkflow.py b/src/py/mat3ra/wode/units/subworkflow.py new file mode 100644 index 00000000..5370dbeb --- /dev/null +++ b/src/py/mat3ra/wode/units/subworkflow.py @@ -0,0 +1,21 @@ +"""Subworkflow unit class.""" + +from typing import Literal, Optional + +from pydantic import ConfigDict, Field + +from ..enums import UnitType +from .base import BaseUnit + + +class SubworkflowUnit(BaseUnit): + """ + Subworkflow unit for referencing subworkflows. + + This unit type references a subworkflow to be executed. + """ + + model_config = ConfigDict(populate_by_name=True) + + type: Literal[UnitType.SUBWORKFLOW] = Field(default=UnitType.SUBWORKFLOW) + id: Optional[str] = Field(default=None, alias="_id", description="ID of the subworkflow to execute") diff --git a/src/py/mat3ra/wode/workflows/__init__.py b/src/py/mat3ra/wode/workflows/__init__.py new file mode 100644 index 00000000..907aae24 --- /dev/null +++ b/src/py/mat3ra/wode/workflows/__init__.py @@ -0,0 +1,7 @@ +"""Workflows module.""" + +from .workflow import Workflow + +__all__ = [ + "Workflow", +] diff --git a/src/py/mat3ra/wode/workflows/workflow.py b/src/py/mat3ra/wode/workflows/workflow.py new file mode 100644 index 00000000..3c3712f4 --- /dev/null +++ b/src/py/mat3ra/wode/workflows/workflow.py @@ -0,0 +1,40 @@ +"""Workflow class.""" + +from typing import Any, Dict, List, Optional +from uuid import uuid4 + +from pydantic import BaseModel, ConfigDict, Field + + +class Workflow(BaseModel): + """ + Workflow class representing a complete computational workflow. + + A workflow consists of subworkflows and units that define a complete + computational procedure. + """ + + model_config = ConfigDict(extra="allow", populate_by_name=True) + + id: str = Field(default_factory=lambda: str(uuid4()), alias="_id", description="Unique ID for the workflow") + name: str = Field(..., description="Name of the workflow") + subworkflows: List[Dict[str, Any]] = Field(default_factory=list, description="List of subworkflows") + units: List[Dict[str, Any]] = Field(default_factory=list, description="List of units") + workflows: List[Dict[str, Any]] = Field(default_factory=list, description="Nested workflows") + properties: List[str] = Field(default_factory=list, description="Properties of the workflow") + applicationName: Optional[str] = Field(default=None, description="Name of the application") + compute: Optional[Dict[str, Any]] = Field(default=None, description="Compute configuration") + schemaVersion: str = Field(default="2022.5.10", description="Schema version") + + def model_dump(self, **kwargs) -> Dict[str, Any]: + """Export workflow to dictionary format.""" + # Use by_alias to maintain _id field name in output + if "by_alias" not in kwargs: + kwargs["by_alias"] = True + return super().model_dump(**kwargs) + + def model_dump_json(self, **kwargs) -> str: + """Export workflow to JSON format.""" + if "by_alias" not in kwargs: + kwargs["by_alias"] = True + return super().model_dump_json(**kwargs) diff --git a/tests/py/test_sample.py b/tests/py/test_sample.py deleted file mode 100644 index 6f31cc17..00000000 --- a/tests/py/test_sample.py +++ /dev/null @@ -1,11 +0,0 @@ -import numpy as np -from mat3ra.wode import get_length - - -def test_get_length(): - """Test that get_length returns correct type and value.""" - vec = np.array([1, 2]) - result = get_length(vec) - assert isinstance(result, float) - assert np.isclose(result, np.sqrt(5)) - diff --git a/tests/py/test_wode.py b/tests/py/test_wode.py new file mode 100644 index 00000000..24710e75 --- /dev/null +++ b/tests/py/test_wode.py @@ -0,0 +1,169 @@ +"""Tests for workflow definitions.""" + + +from mat3ra.wode import ( + AssertionUnit, + AssignmentUnit, + ConditionUnit, + ExecutionUnit, + IOUnit, + MapUnit, + ProcessingUnit, + ReduceUnit, + Subworkflow, + SubworkflowUnit, + UnitStatus, + UnitType, + Workflow, +) + + +def test_execution_unit_creation(): + """Test that ExecutionUnit can be created and has correct type.""" + unit = ExecutionUnit(name="test_execution", type=UnitType.EXECUTION) + assert unit.name == "test_execution" + assert unit.type == UnitType.EXECUTION + assert unit.status == UnitStatus.IDLE + assert isinstance(unit.flowchartId, str) + + +def test_assignment_unit_creation(): + """Test that AssignmentUnit can be created.""" + unit = AssignmentUnit(name="test_assignment", value=42) + assert unit.name == "test_assignment" + assert unit.type == UnitType.ASSIGNMENT + assert unit.value == 42 + + +def test_io_unit_creation(): + """Test that IOUnit can be created.""" + unit = IOUnit(name="test_io", operation="read") + assert unit.name == "test_io" + assert unit.type == UnitType.IO + assert unit.operation == "read" + + +def test_map_unit_creation(): + """Test that MapUnit can be created.""" + unit = MapUnit(name="test_map", input=[1, 2, 3]) + assert unit.name == "test_map" + assert unit.type == UnitType.MAP + assert unit.input == [1, 2, 3] + + +def test_reduce_unit_creation(): + """Test that ReduceUnit can be created.""" + unit = ReduceUnit(name="test_reduce", operation="sum") + assert unit.name == "test_reduce" + assert unit.type == UnitType.REDUCE + assert unit.operation == "sum" + + +def test_condition_unit_creation(): + """Test that ConditionUnit can be created.""" + unit = ConditionUnit(name="test_condition", condition="x > 0") + assert unit.name == "test_condition" + assert unit.type == UnitType.CONDITION + assert unit.condition == "x > 0" + + +def test_assertion_unit_creation(): + """Test that AssertionUnit can be created.""" + unit = AssertionUnit(name="test_assertion", expression="result == expected") + assert unit.name == "test_assertion" + assert unit.type == UnitType.ASSERTION + assert unit.expression == "result == expected" + + +def test_processing_unit_creation(): + """Test that ProcessingUnit can be created.""" + unit = ProcessingUnit(name="test_processing", operation="transform") + assert unit.name == "test_processing" + assert unit.type == UnitType.PROCESSING + assert unit.operation == "transform" + + +def test_subworkflow_unit_creation(): + """Test that SubworkflowUnit can be created.""" + unit = SubworkflowUnit(name="test_subworkflow", _id="sw-123") + assert unit.name == "test_subworkflow" + assert unit.type == UnitType.SUBWORKFLOW + assert unit.id == "sw-123" + + +def test_unit_status_check(): + """Test unit status checking.""" + unit = ExecutionUnit(name="test", status=UnitStatus.ACTIVE) + assert unit.is_in_status(UnitStatus.ACTIVE) + assert not unit.is_in_status(UnitStatus.IDLE) + + +def test_unit_serialization(): + """Test unit serialization to dict.""" + unit = ExecutionUnit(name="test_unit", application={"name": "espresso"}) + data = unit.model_dump() + assert data["name"] == "test_unit" + assert data["type"] == "execution" + assert data["application"]["name"] == "espresso" + + +def test_subworkflow_creation(): + """Test Subworkflow creation.""" + subworkflow = Subworkflow( + name="Test Subworkflow", + application={"name": "espresso", "version": "6.3"}, + model={"type": "dft", "subtype": "gga"}, + ) + assert subworkflow.name == "Test Subworkflow" + assert subworkflow.application["name"] == "espresso" + assert subworkflow.model["type"] == "dft" + assert isinstance(subworkflow.id, str) + + +def test_subworkflow_with_units(): + """Test Subworkflow with units.""" + unit_data = {"name": "scf", "type": "execution"} + subworkflow = Subworkflow( + name="SCF Calculation", + application={"name": "espresso"}, + model={"type": "dft"}, + units=[unit_data], + ) + assert len(subworkflow.units) == 1 + assert subworkflow.units[0]["name"] == "scf" + + +def test_workflow_creation(): + """Test Workflow creation.""" + workflow = Workflow(name="Test Workflow") + assert workflow.name == "Test Workflow" + assert isinstance(workflow.id, str) + assert workflow.schemaVersion == "2022.5.10" + + +def test_workflow_with_subworkflows(): + """Test Workflow with subworkflows.""" + subworkflow_data = { + "name": "SCF", + "application": {"name": "espresso"}, + "model": {"type": "dft"}, + } + unit_data = {"name": "scf_unit", "type": "subworkflow"} + workflow = Workflow(name="Complete Workflow", subworkflows=[subworkflow_data], units=[unit_data]) + assert workflow.name == "Complete Workflow" + assert len(workflow.subworkflows) == 1 + assert len(workflow.units) == 1 + assert workflow.subworkflows[0]["name"] == "SCF" + + +def test_workflow_serialization(): + """Test Workflow serialization.""" + workflow = Workflow( + name="Test Workflow", + properties=["band_structure", "total_energy"], + applicationName="espresso", + ) + data = workflow.model_dump() + assert data["name"] == "Test Workflow" + assert "band_structure" in data["properties"] + assert data["applicationName"] == "espresso" From 4d81abbeaebdf42e3a4e629b010713608010f9c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 23:19:22 +0000 Subject: [PATCH 3/3] Update README with Python installation and dev instructions Co-authored-by: timurbazhirov <721112+timurbazhirov@users.noreply.github.com> --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 8fc6c649..591e5f89 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,12 @@ For usage within a JavaScript project: npm install @mat3ra/wode ``` +For usage within a Python project: + +```bash +pip install mat3ra-wode +``` + For development: ```bash @@ -49,6 +55,8 @@ See [ESSE](https://github.com/Exabyte-io/esse) for additional context regarding Useful commands for development: +**JavaScript:** + ```bash # run linter without persistence npm run lint @@ -63,6 +71,18 @@ npm run transpile npm run test ``` +**Python:** + +```bash +# run tests +pytest tests/py + +# run linter +black src/py tests/py +ruff check src/py tests/py +isort src/py tests/py +``` + ### Using Linter Linter setup will prevent committing files that don't adhere to the code standard. It will