From e172fae9e1343c46f0c72098c12a151e7c282ca0 Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Thu, 9 Apr 2026 11:31:32 +0200 Subject: [PATCH 1/4] Rename geometry workflow API to catalyst --- flow360/cloud/flow360_requests.py | 4 +- flow360/component/geometry.py | 17 ++-- flow360/component/project.py | 14 ++- tests/simulation/test_project.py | 136 ++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 8 deletions(-) diff --git a/flow360/cloud/flow360_requests.py b/flow360/cloud/flow360_requests.py index c3c0fc0a0..2e96ae969 100644 --- a/flow360/cloud/flow360_requests.py +++ b/flow360/cloud/flow360_requests.py @@ -118,10 +118,10 @@ class NewGeometryRequest(Flow360RequestsV2): alias="lengthUnit", description="project length unit" ) description: str = pd_v2.Field(default="", description="project description") - use_nextflow: bool = pd_v2.Field( + use_catalyst: bool = pd_v2.Field( default=False, alias="useNextflow", - description="Route geometry processing through Nextflow pipeline instead of legacy system", + description="Use the Catalyst workflow for geometry processing", ) diff --git a/flow360/component/geometry.py b/flow360/component/geometry.py index c19e3c66e..b68eccbeb 100644 --- a/flow360/component/geometry.py +++ b/flow360/component/geometry.py @@ -39,6 +39,8 @@ from flow360.exceptions import Flow360FileError, Flow360ValueError from flow360.log import log +GeometryWorkflow = Literal["standard", "catalyst"] + class GeometryStatus(Enum): """Status of geometry resource, the is_final method is overloaded""" @@ -107,7 +109,7 @@ def __init__( length_unit: LengthUnitType = "m", tags: List[str] = None, folder: Optional[Folder] = None, - use_nextflow_pipelines: bool = False, + workflow: GeometryWorkflow = "standard", ): """ Initialize a GeometryDraft with common attributes. @@ -139,7 +141,7 @@ def __init__( self.length_unit = length_unit self.solver_version = solver_version self.folder = folder - self.use_nextflow_pipelines = use_nextflow_pipelines + self.workflow = workflow # pylint: disable=fixme # TODO: create a DependableResourceDraft for GeometryDraft and SurfaceMeshDraft @@ -169,6 +171,11 @@ def _validate_geometry(self): f"specified length_unit : {self.length_unit} is invalid. " f"Valid options are: {list(LengthUnitType.__args__)}" ) + if self.workflow not in ("standard", "catalyst"): + raise Flow360ValueError( + f"specified workflow : {self.workflow} is invalid. " + "Valid options are: ['standard', 'catalyst']" + ) def _set_default_project_name(self): """Set default project name if not provided for project creation.""" @@ -243,7 +250,7 @@ def _create_project_root_resource( parent_folder_id=self.folder.id if self.folder else "ROOT.FLOW360", length_unit=self.length_unit, description=description, - use_nextflow=self.use_nextflow_pipelines, + use_catalyst=self.workflow == "catalyst", ) resp = RestApi(GeometryInterface.endpoint).post(req.dict()) @@ -476,7 +483,7 @@ def from_file( length_unit: LengthUnitType = "m", tags: List[str] = None, folder: Optional[Folder] = None, - use_nextflow_pipelines: bool = False, + workflow: GeometryWorkflow = "standard", ) -> GeometryDraft: return GeometryDraft( file_names=file_names, @@ -485,7 +492,7 @@ def from_file( length_unit=length_unit, tags=tags, folder=folder, - use_nextflow_pipelines=use_nextflow_pipelines, + workflow=workflow, ) @classmethod diff --git a/flow360/component/project.py b/flow360/component/project.py index b27011a5e..4c9516479 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -931,6 +931,7 @@ def _create_project_from_files( tags: List[str] = None, run_async: bool = False, folder: Optional[Folder] = None, + workflow: Literal["standard", "catalyst"] = "standard", ): """ Initializes a project from a file. @@ -969,7 +970,13 @@ def _create_project_from_files( if isinstance(files, GeometryFiles): draft = Geometry.from_file( - files.file_names, name, solver_version, length_unit, tags, folder=folder + files.file_names, + name, + solver_version, + length_unit, + tags, + folder=folder, + workflow=workflow, ) elif isinstance(files, SurfaceMeshFile): draft = SurfaceMeshV2.from_file( @@ -1150,6 +1157,7 @@ def from_geometry( tags: List[str] = None, run_async: bool = False, folder: Optional[Folder] = None, + workflow: Literal["standard", "catalyst"] = "standard", ): """ Initializes a project from local geometry files. @@ -1170,6 +1178,9 @@ def from_geometry( Whether to create project asynchronously (default is False). folder : Optional[Folder], optional Parent folder for the project. If None, creates in root. + workflow : {"standard", "catalyst"}, optional + Workflow used for project geometry preparation. Use `"catalyst"` + for Catalyst-backed geometry processing (default is `"standard"`). Returns ------- @@ -1205,6 +1216,7 @@ def from_geometry( tags=tags, run_async=run_async, folder=folder, + workflow=workflow, ) @classmethod diff --git a/tests/simulation/test_project.py b/tests/simulation/test_project.py index 7fa8a9c39..38ccbb21b 100644 --- a/tests/simulation/test_project.py +++ b/tests/simulation/test_project.py @@ -16,6 +16,7 @@ from flow360.component.simulation.services import ValidationCalledBy, validate_model from flow360.component.simulation.utils import model_attribute_unlock from flow360.component.volume_mesh import VolumeMeshV2 +from flow360.examples import Cylinder3D from flow360.exceptions import Flow360ConfigurationError, Flow360ValueError log.set_logging_level("DEBUG") @@ -57,6 +58,141 @@ def test_from_cloud(mock_id, mock_response): project.get_case(asset_id=current_case_id) +def test_from_geometry_passes_workflow(monkeypatch): + Cylinder3D.get_files() + captured = {} + + class _MockDraft: + def submit(self, run_async=False): + assert run_async is True + return MagicMock(project_id="prj-test-project-id") + + def _mock_from_file( + file_names, + project_name=None, + solver_version=None, + length_unit="m", + tags=None, + folder=None, + workflow="standard", + ): + captured["file_names"] = file_names + captured["project_name"] = project_name + captured["solver_version"] = solver_version + captured["length_unit"] = length_unit + captured["workflow"] = workflow + return _MockDraft() + + monkeypatch.setattr("flow360.component.project.Geometry.from_file", _mock_from_file) + + project_id = fl.Project.from_geometry( + Cylinder3D.geometry, + name="catalyst-project", + solver_version="release-test", + length_unit="cm", + run_async=True, + workflow="catalyst", + ) + + assert project_id == "prj-test-project-id" + assert captured["file_names"] == Cylinder3D.geometry + assert captured["project_name"] == "catalyst-project" + assert captured["solver_version"] == "release-test" + assert captured["length_unit"] == "cm" + assert captured["workflow"] == "catalyst" + + +def _fake_geometry_api_response(geo_id: str = "geo-test-0001", prj_id: str = "prj-test-payload"): + return { + "id": geo_id, + "name": "test-geo", + "userId": "user-test", + "status": "uploaded", + "projectId": prj_id, + "createdAt": "2026-01-01T00:00:00Z", + "updatedAt": "2026-01-01T00:00:00Z", + "deleted": False, + "tags": [], + } + + +def _mock_upload_files(self, *args, **kwargs): + geo = MagicMock() + geo.short_description.return_value = "test-geo (geo-test)" + geo.id = "geo-test-0001" + geo.project_id = "prj-test-payload" + return geo + + +def test_catalyst_workflow_reaches_api_payload(monkeypatch): + Cylinder3D.get_files() + captured_payload: dict = {} + + class _FakeRestApi: + def __init__(self, endpoint, **kwargs): + pass + + def post(self, json_body): + captured_payload.update(json_body) + return _fake_geometry_api_response() + + monkeypatch.setattr("flow360.component.geometry.RestApi", _FakeRestApi) + monkeypatch.setattr("os.path.exists", lambda _: True) + monkeypatch.setattr( + "flow360.component.geometry.GeometryDraft._upload_files", _mock_upload_files + ) + + draft = Geometry.from_file( + file_names=Cylinder3D.geometry, + project_name="payload-test", + solver_version="release-test", + length_unit="cm", + workflow="catalyst", + ) + + assert draft.workflow == "catalyst" + draft.submit(run_async=True) + + assert captured_payload.get("useNextflow") is True, ( + f"Expected Catalyst workflow to set the compatibility flag, got: {captured_payload}" + ) + + +def test_standard_workflow_is_default(monkeypatch): + Cylinder3D.get_files() + captured_payload: dict = {} + + class _FakeRestApi: + def __init__(self, endpoint, **kwargs): + pass + + def post(self, json_body): + captured_payload.update(json_body) + return _fake_geometry_api_response( + geo_id="geo-test-0002", prj_id="prj-test-default" + ) + + monkeypatch.setattr("flow360.component.geometry.RestApi", _FakeRestApi) + monkeypatch.setattr("os.path.exists", lambda _: True) + monkeypatch.setattr( + "flow360.component.geometry.GeometryDraft._upload_files", _mock_upload_files + ) + + draft = Geometry.from_file( + file_names=Cylinder3D.geometry, + project_name="default-test", + solver_version="release-test", + length_unit="cm", + ) + + assert draft.workflow == "standard" + draft.submit(run_async=True) + + assert captured_payload.get("useNextflow") is False, ( + f"Expected standard workflow to keep the compatibility flag disabled, got: {captured_payload}" + ) + + def test_root_asset_entity_change_reflection(mock_id, mock_response): project = fl.Project.from_cloud(project_id="prj-41d2333b-85fd-4bed-ae13-15dcb6da519e") geo = project.geometry From 8eb5f3a8f95d8468c9507b19f74ad4f72b814081 Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Thu, 9 Apr 2026 11:45:56 +0200 Subject: [PATCH 2/4] Format catalyst workflow tests --- tests/simulation/test_project.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/simulation/test_project.py b/tests/simulation/test_project.py index 38ccbb21b..dd427937c 100644 --- a/tests/simulation/test_project.py +++ b/tests/simulation/test_project.py @@ -153,9 +153,9 @@ def post(self, json_body): assert draft.workflow == "catalyst" draft.submit(run_async=True) - assert captured_payload.get("useNextflow") is True, ( - f"Expected Catalyst workflow to set the compatibility flag, got: {captured_payload}" - ) + assert ( + captured_payload.get("useNextflow") is True + ), f"Expected Catalyst workflow to set the compatibility flag, got: {captured_payload}" def test_standard_workflow_is_default(monkeypatch): @@ -168,9 +168,7 @@ def __init__(self, endpoint, **kwargs): def post(self, json_body): captured_payload.update(json_body) - return _fake_geometry_api_response( - geo_id="geo-test-0002", prj_id="prj-test-default" - ) + return _fake_geometry_api_response(geo_id="geo-test-0002", prj_id="prj-test-default") monkeypatch.setattr("flow360.component.geometry.RestApi", _FakeRestApi) monkeypatch.setattr("os.path.exists", lambda _: True) @@ -188,9 +186,9 @@ def post(self, json_body): assert draft.workflow == "standard" draft.submit(run_async=True) - assert captured_payload.get("useNextflow") is False, ( - f"Expected standard workflow to keep the compatibility flag disabled, got: {captured_payload}" - ) + assert ( + captured_payload.get("useNextflow") is False + ), f"Expected standard workflow to keep the compatibility flag disabled, got: {captured_payload}" def test_root_asset_entity_change_reflection(mock_id, mock_response): From d6415ab098116432ce517e4a508304da60a21f04 Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Thu, 9 Apr 2026 12:12:56 +0200 Subject: [PATCH 3/4] Reuse GeometryWorkflow type alias --- flow360/component/project.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flow360/component/project.py b/flow360/component/project.py index 4c9516479..c4ffccae9 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -27,7 +27,7 @@ fetch_examples, find_example_by_name, ) -from flow360.component.geometry import Geometry +from flow360.component.geometry import Geometry, GeometryWorkflow from flow360.component.interfaces import ( GeometryInterface, ProjectInterface, @@ -931,7 +931,7 @@ def _create_project_from_files( tags: List[str] = None, run_async: bool = False, folder: Optional[Folder] = None, - workflow: Literal["standard", "catalyst"] = "standard", + workflow: GeometryWorkflow = "standard", ): """ Initializes a project from a file. @@ -1157,7 +1157,7 @@ def from_geometry( tags: List[str] = None, run_async: bool = False, folder: Optional[Folder] = None, - workflow: Literal["standard", "catalyst"] = "standard", + workflow: GeometryWorkflow = "standard", ): """ Initializes a project from local geometry files. From 9bf84bc37033994df47c0b4661f8b0de5e6a7ba5 Mon Sep 17 00:00:00 2001 From: Maciej Skarysz Date: Thu, 9 Apr 2026 12:27:04 +0200 Subject: [PATCH 4/4] Send only catalyst workflow field --- flow360/cloud/flow360_requests.py | 1 - flow360/component/project.py | 3 ++- tests/simulation/test_project.py | 10 ++++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/flow360/cloud/flow360_requests.py b/flow360/cloud/flow360_requests.py index 2e96ae969..deb727aec 100644 --- a/flow360/cloud/flow360_requests.py +++ b/flow360/cloud/flow360_requests.py @@ -120,7 +120,6 @@ class NewGeometryRequest(Flow360RequestsV2): description: str = pd_v2.Field(default="", description="project description") use_catalyst: bool = pd_v2.Field( default=False, - alias="useNextflow", description="Use the Catalyst workflow for geometry processing", ) diff --git a/flow360/component/project.py b/flow360/component/project.py index c4ffccae9..3c2709eaf 100644 --- a/flow360/component/project.py +++ b/flow360/component/project.py @@ -1180,7 +1180,8 @@ def from_geometry( Parent folder for the project. If None, creates in root. workflow : {"standard", "catalyst"}, optional Workflow used for project geometry preparation. Use `"catalyst"` - for Catalyst-backed geometry processing (default is `"standard"`). + for geometry preparation recommended for GAI and snappy workflows + (default is `"standard"`). Returns ------- diff --git a/tests/simulation/test_project.py b/tests/simulation/test_project.py index dd427937c..cede19573 100644 --- a/tests/simulation/test_project.py +++ b/tests/simulation/test_project.py @@ -154,8 +154,9 @@ def post(self, json_body): draft.submit(run_async=True) assert ( - captured_payload.get("useNextflow") is True - ), f"Expected Catalyst workflow to set the compatibility flag, got: {captured_payload}" + captured_payload.get("useCatalyst") is True + ), f"Expected Catalyst workflow to set useCatalyst=true, got: {captured_payload}" + assert set(captured_payload) >= {"useCatalyst"} def test_standard_workflow_is_default(monkeypatch): @@ -187,8 +188,9 @@ def post(self, json_body): draft.submit(run_async=True) assert ( - captured_payload.get("useNextflow") is False - ), f"Expected standard workflow to keep the compatibility flag disabled, got: {captured_payload}" + captured_payload.get("useCatalyst") is False + ), f"Expected standard workflow to keep useCatalyst=false, got: {captured_payload}" + assert set(captured_payload) >= {"useCatalyst"} def test_root_asset_entity_change_reflection(mock_id, mock_response):