From 097563839cf39ce3829c816b354a2344bd96bfd9 Mon Sep 17 00:00:00 2001 From: Maid Sultanovic Date: Mon, 8 Jun 2026 11:35:03 +0200 Subject: [PATCH] feat: add description to projects and workspaces --- src/ansys/simai/core/api/geomai/projects.py | 12 ++-- src/ansys/simai/core/api/geomai/workspaces.py | 10 ++-- src/ansys/simai/core/api/project.py | 10 ++-- src/ansys/simai/core/api/workspace.py | 10 ++-- src/ansys/simai/core/data/geomai/projects.py | 27 ++++++++- .../simai/core/data/geomai/workspaces.py | 23 +++++++- src/ansys/simai/core/data/projects.py | 27 ++++++++- src/ansys/simai/core/data/workspaces.py | 21 ++++++- tests/geomai/test_geomai_projects.py | 59 +++++++++++++++++++ tests/geomai/test_geomai_workspaces.py | 59 +++++++++++++++++++ tests/test_projects.py | 55 +++++++++++++++++ tests/test_workspace.py | 59 +++++++++++++++++++ 12 files changed, 340 insertions(+), 32 deletions(-) diff --git a/src/ansys/simai/core/api/geomai/projects.py b/src/ansys/simai/core/api/geomai/projects.py index c54a2bea..d036c9e9 100644 --- a/src/ansys/simai/core/api/geomai/projects.py +++ b/src/ansys/simai/core/api/geomai/projects.py @@ -50,10 +50,14 @@ def get_geomai_project_by_name(self, name: str): def create_geomai_project(self, **kwargs): return self._post("geomai/projects", json=kwargs) - def update_geomai_project(self, project_id: str, name: str): - request_json = {} - request_json["name"] = name - self._patch(f"geomai/projects/{project_id}", json=request_json, return_json=False) + def update_geomai_project(self, project_id: str, **kwargs): + """Update a GeomAI project. + + Args: + project_id: ID of the project. + **kwargs: Project fields to pass as keyword arguments. + """ + self._patch(f"geomai/projects/{project_id}", json=kwargs, return_json=False) def iter_training_data_in_geomai_project(self, project_id: str) -> Iterator[Dict[str, Any]]: next_page = f"geomai/projects/{project_id}/training-data" diff --git a/src/ansys/simai/core/api/geomai/workspaces.py b/src/ansys/simai/core/api/geomai/workspaces.py index 445d3892..f1a3f3b5 100644 --- a/src/ansys/simai/core/api/geomai/workspaces.py +++ b/src/ansys/simai/core/api/geomai/workspaces.py @@ -106,16 +106,14 @@ def download_geomai_workspace_model_evaluation_report( ): return self.download_file(f"geomai/workspaces/{workspace_id}/model-evaluation-report", file) - def update_geomai_workspace(self, workspace_id: str, name: str): - """Update a GeomAI workspace name. + def update_geomai_workspace(self, workspace_id: str, **kwargs): + """Update a GeomAI workspace. Args: workspace_id: ID of the workspace. - name: New name to give to the workspace. + **kwargs: Workspace fields to pass as keyword arguments. """ - request_json = {} - request_json["name"] = name - self._patch(f"geomai/workspaces/{workspace_id}", json=request_json, return_json=False) + self._patch(f"geomai/workspaces/{workspace_id}", json=kwargs, return_json=False) def get_geomai_workspace_model_configuration(self, workspace_id: str): """Get the model configuration used for the given GeomAI workspace. diff --git a/src/ansys/simai/core/api/project.py b/src/ansys/simai/core/api/project.py index 47b515eb..24edea16 100644 --- a/src/ansys/simai/core/api/project.py +++ b/src/ansys/simai/core/api/project.py @@ -50,16 +50,14 @@ def get_project_by_name(self, name: str): def create_project(self, **kwargs): return self._post("projects", json=kwargs) - def update_project(self, project_id: str, name: str): - """Update a project name. + def update_project(self, project_id: str, **kwargs): + """Update a project. Args: project_id: ID of the project. - name: New name to give to the project. + **kwargs: Project fields to pass as keyword arguments. """ - request_json = {} - request_json["name"] = name - self._patch(f"projects/{project_id}", json=request_json, return_json=False) + self._patch(f"projects/{project_id}", json=kwargs, return_json=False) def iter_training_data_in_project(self, project_id: str) -> Iterator[Dict[str, Any]]: next_page = f"projects/{project_id}/data" diff --git a/src/ansys/simai/core/api/workspace.py b/src/ansys/simai/core/api/workspace.py index 23d08c10..41192826 100644 --- a/src/ansys/simai/core/api/workspace.py +++ b/src/ansys/simai/core/api/workspace.py @@ -112,16 +112,14 @@ def download_workspace_model_evaluation_report(self, workspace_id: str, file: Op def download_workspace_mer_data(self, workspace_id: str, file: Optional[File]): return self.download_file(f"workspaces/{workspace_id}/mer-data", file) - def update_workspace(self, workspace_id: str, name: str): - """Update a workspace name. + def update_workspace(self, workspace_id: str, **kwargs): + """Update a workspace. Args: workspace_id: ID of the workspace. - name: New name to give to the workspace. + **kwargs: Workspace fields to pass as keyword arguments. """ - request_json = {} - request_json["name"] = name - self._patch(f"workspaces/{workspace_id}", json=request_json, return_json=False) + self._patch(f"workspaces/{workspace_id}", json=kwargs, return_json=False) def get_workspace_model_configuration(self, workspace_id: str): """Get the model configuration used for the given workspace. diff --git a/src/ansys/simai/core/data/geomai/projects.py b/src/ansys/simai/core/data/geomai/projects.py index 854240b6..11d540f1 100644 --- a/src/ansys/simai/core/data/geomai/projects.py +++ b/src/ansys/simai/core/data/geomai/projects.py @@ -50,6 +50,20 @@ def name(self) -> str: """Name of project.""" return self.fields["name"] + @property + def description(self) -> Optional[str]: + """Description of the project.""" + return self.fields.get("description") + + def set_description(self, new_description: Optional[str]) -> None: + """Set the project description. + + Args: + new_description: New description for the project. + """ + self._client._api.update_geomai_project(self.id, description=new_description) + self.reload() + def rename(self, new_name: str) -> None: """Rename the project. @@ -202,9 +216,16 @@ def list( return list(self.iter(raw_filters)) - def create(self, name: str) -> GeomAIProject: - """Create a project.""" - return self._model_from(self._client._api.create_geomai_project(name=name)) + def create(self, name: str, description: Optional[str] = None) -> GeomAIProject: + """Create a project. + + Args: + name: Name to give to the project. + description: Optional description for the project. + """ + return self._model_from( + self._client._api.create_geomai_project(name=name, description=description) + ) def get(self, id: Optional[str] = None, name: Optional[str] = None) -> GeomAIProject: """Get a project by either ID or name. diff --git a/src/ansys/simai/core/data/geomai/workspaces.py b/src/ansys/simai/core/data/geomai/workspaces.py index d4c003f2..8a283f5d 100644 --- a/src/ansys/simai/core/data/geomai/workspaces.py +++ b/src/ansys/simai/core/data/geomai/workspaces.py @@ -56,6 +56,20 @@ def name(self) -> str: """Name of the workspace.""" return self.fields["name"] + @property + def description(self) -> Optional[str]: + """Description of the workspace.""" + return self.fields.get("description") + + def set_description(self, new_description: Optional[str]) -> None: + """Set the workspace description. + + Args: + new_description: New description for the workspace. + """ + self._client._api.update_geomai_workspace(self.id, description=new_description) + self.reload() + @property def model_configuration(self) -> GeomAIModelConfiguration: """Model configuration used in the workspace.""" @@ -191,15 +205,20 @@ def get(self, id: Optional[str] = None, name: Optional[str] = None) -> GeomAIWor return self._model_from(self._client._api.get_geomai_workspace(id)) raise ValueError("Either 'id' or 'name' must be specified.") - def create(self, name: str, project: Identifiable["GeomAIProject"]) -> GeomAIWorkspace: + def create( + self, name: str, project: Identifiable["GeomAIProject"], description: Optional[str] = None + ) -> GeomAIWorkspace: """Create a workspace. Args: name: Name to give the new workspace. project: ID or :class:`project <.projects.GeomAIProject>` of the workspace. + description: Optional description for the workspace. """ return self._model_from( - self._client._api.create_geomai_workspace(name, get_id_from_identifiable(project)) + self._client._api.create_geomai_workspace( + name, get_id_from_identifiable(project), description=description + ) ) def delete(self, workspace: Identifiable[GeomAIWorkspace]) -> None: diff --git a/src/ansys/simai/core/data/projects.py b/src/ansys/simai/core/data/projects.py index 4eb048bc..7108ebf8 100644 --- a/src/ansys/simai/core/data/projects.py +++ b/src/ansys/simai/core/data/projects.py @@ -122,6 +122,20 @@ def name(self) -> str: """Name of project.""" return self.fields["name"] + @property + def description(self) -> Optional[str]: + """Description of the project.""" + return self.fields.get("description") + + def set_description(self, new_description: Optional[str]) -> None: + """Set the project description. + + Args: + new_description: New description for the project. + """ + self._client._api.update_project(self.id, description=new_description) + self.reload() + def rename(self, new_name: str) -> None: """Rename the project. @@ -320,9 +334,16 @@ def list( return list(self.iter(raw_filters)) - def create(self, name: str) -> Project: - """Create a project.""" - return self._model_from(self._client._api.create_project(name=name)) + def create(self, name: str, description: Optional[str] = None) -> Project: + """Create a project. + + Args: + name: Name to give to the project. + description: Optional description for the project. + """ + return self._model_from( + self._client._api.create_project(name=name, description=description) + ) def get(self, id: Optional[str] = None, name: Optional[str] = None) -> Project: """Get a project by either ID or name. diff --git a/src/ansys/simai/core/data/workspaces.py b/src/ansys/simai/core/data/workspaces.py index d1c845e8..1d36491f 100644 --- a/src/ansys/simai/core/data/workspaces.py +++ b/src/ansys/simai/core/data/workspaces.py @@ -112,6 +112,20 @@ def name(self) -> str: """Name of the workspace.""" return self.fields["name"] + @property + def description(self) -> Optional[str]: + """Description of the workspace.""" + return self.fields.get("description") + + def set_description(self, new_description: Optional[str]) -> None: + """Set the workspace description. + + Args: + new_description: New description for the workspace. + """ + self._client._api.update_workspace(self.id, description=new_description) + self.reload() + @property def model(self) -> ModelManifest: """Deprecated alias to :py:attr:`~model_manifest`.""" @@ -269,14 +283,17 @@ def get(self, id: Optional[str] = None, name: Optional[str] = None) -> Workspace return self._model_from(self._client._api.get_workspace(id)) raise ValueError("Either 'id' or 'name' must be specified.") - def create(self, name: str, model_id: str) -> Workspace: + def create(self, name: str, model_id: str, description: Optional[str] = None) -> Workspace: """Create a workspace. Args: name: Name to give the new workspace. model_id: ID of the model for the workspace to use. + description: Optional description for the workspace. """ - return self._model_from(self._client._api.create_workspace(name, model_id)) + return self._model_from( + self._client._api.create_workspace(name, model_id, description=description) + ) def delete(self, workspace: Identifiable[Workspace]) -> None: """Delete a workspace. diff --git a/tests/geomai/test_geomai_projects.py b/tests/geomai/test_geomai_projects.py index ee55e0b8..fcc3e54f 100644 --- a/tests/geomai/test_geomai_projects.py +++ b/tests/geomai/test_geomai_projects.py @@ -367,3 +367,62 @@ def test_geomai_project_last_model_none(simai_client): model = project.last_model assert model is None + + +def test_geomai_project_description_get(simai_client): + """WHEN a GeomAI project has a description + THEN it can be retrieved via the description property.""" + project = simai_client.geomai.projects._model_from( + {"id": "0011", "name": "riri", "description": "A geometry optimization study"} + ) + + assert project.description == "A geometry optimization study" + + +def test_geomai_project_description_get_none(simai_client): + """WHEN a GeomAI project has no description + THEN the description property returns None.""" + project = simai_client.geomai.projects._model_from({"id": "0011", "name": "riri"}) + + assert project.description is None + + +def test_geomai_project_description_set(simai_client, httpx_mock): + """WHEN setting a GeomAI project description + THEN a PATCH request is made with the description.""" + project = simai_client.geomai.projects._model_from( + {"id": "0011", "name": "riri", "description": None} + ) + + httpx_mock.add_response( + method="PATCH", + url="https://test.test/geomai/projects/0011", + status_code=204, + ) + httpx_mock.add_response( + method="GET", + url="https://test.test/geomai/projects/0011", + json={"id": "0011", "name": "riri", "description": "New description"}, + status_code=200, + ) + + project.set_description("New description") + assert project.description == "New description" + + +def test_geomai_project_create_with_description(simai_client, httpx_mock): + """WHEN creating a GeomAI project with a description + THEN the description is sent in the request.""" + httpx_mock.add_response( + method="POST", + url="https://test.test/geomai/projects", + json={"id": "0011", "name": "my_project", "description": "My project description"}, + status_code=200, + ) + + project = simai_client.geomai.projects.create( + name="my_project", description="My project description" + ) + + assert project.name == "my_project" + assert project.description == "My project description" diff --git a/tests/geomai/test_geomai_workspaces.py b/tests/geomai/test_geomai_workspaces.py index a6215537..56e6c418 100644 --- a/tests/geomai/test_geomai_workspaces.py +++ b/tests/geomai/test_geomai_workspaces.py @@ -190,3 +190,62 @@ def test_geomai_workspace_list_created_by_me(simai_client, httpx_mock): workspaces = simai_client.geomai.workspaces.list(created_by_me=True) assert [workspace.id for workspace in workspaces] == ["ws-1", "ws-2"] + + +def test_geomai_workspace_description_get(simai_client): + """WHEN a GeomAI workspace has a description + THEN it can be retrieved via the description property.""" + workspace = simai_client.geomai.workspaces._model_from( + {"id": "ws01", "name": "riri", "description": "My description"} + ) + + assert workspace.description == "My description" + + +def test_geomai_workspace_description_get_none(simai_client): + """WHEN a GeomAI workspace has no description + THEN the description property returns None.""" + workspace = simai_client.geomai.workspaces._model_from({"id": "ws01", "name": "riri"}) + + assert workspace.description is None + + +def test_geomai_workspace_description_set(simai_client, httpx_mock): + """WHEN setting a GeomAI workspace description + THEN a PATCH request is made with the description.""" + workspace = simai_client.geomai.workspaces._model_from( + {"id": "ws01", "name": "riri", "description": None} + ) + + httpx_mock.add_response( + method="PATCH", + url="https://test.test/geomai/workspaces/ws01", + status_code=204, + ) + httpx_mock.add_response( + method="GET", + url="https://test.test/geomai/workspaces/ws01", + json={"id": "ws01", "name": "riri", "description": "New description"}, + status_code=200, + ) + + workspace.set_description("New description") + assert workspace.description == "New description" + + +def test_geomai_workspace_create_with_description(simai_client, httpx_mock): + """WHEN creating a GeomAI workspace with a description + THEN the description is sent in the request.""" + httpx_mock.add_response( + method="POST", + url="https://test.test/geomai/projects/proj01/workspaces", + json={"id": "ws01", "name": "my_workspace", "description": "My workspace description"}, + status_code=200, + ) + + workspace = simai_client.geomai.workspaces.create( + name="my_workspace", project="proj01", description="My workspace description" + ) + + assert workspace.name == "my_workspace" + assert workspace.description == "My workspace description" diff --git a/tests/test_projects.py b/tests/test_projects.py index 82f2d424..400e97d4 100644 --- a/tests/test_projects.py +++ b/tests/test_projects.py @@ -600,3 +600,58 @@ def test_project_last_model_none(simai_client): model = project.last_model assert model is None + + +def test_project_description_get(simai_client): + """WHEN a project has a description + THEN it can be retrieved via the description property.""" + project = simai_client.projects._model_from( + {"id": "0011", "name": "riri", "description": "My description"} + ) + + assert project.description == "My description" + + +def test_project_description_get_none(simai_client): + """WHEN a project has no description + THEN the description property returns None.""" + project = simai_client.projects._model_from({"id": "0011", "name": "riri"}) + + assert project.description is None + + +def test_project_description_set(simai_client, httpx_mock): + """WHEN setting a project description + THEN a PATCH request is made with the description.""" + project = simai_client.projects._model_from({"id": "0011", "name": "riri", "description": None}) + + httpx_mock.add_response( + method="PATCH", + url="https://test.test/projects/0011", + status_code=204, + ) + httpx_mock.add_response( + method="GET", + url="https://test.test/projects/0011", + json={"id": "0011", "name": "riri", "description": "New description"}, + status_code=200, + ) + + project.set_description("New description") + assert project.description == "New description" + + +def test_project_create_with_description(simai_client, httpx_mock): + """WHEN creating a project with a description + THEN the description is sent in the request.""" + httpx_mock.add_response( + method="POST", + url="https://test.test/projects", + json={"id": "0011", "name": "my_project", "description": "My project description"}, + status_code=200, + ) + + project = simai_client.projects.create(name="my_project", description="My project description") + + assert project.name == "my_project" + assert project.description == "My project description" diff --git a/tests/test_workspace.py b/tests/test_workspace.py index 86540ac0..43147ca2 100644 --- a/tests/test_workspace.py +++ b/tests/test_workspace.py @@ -339,3 +339,62 @@ def test_workspace_list_created_by_me(simai_client, httpx_mock): workspaces = simai_client.workspaces.list(created_by_me=True) assert [workspace.id for workspace in workspaces] == ["ws-1", "ws-2"] + + +def test_workspace_description_get(simai_client): + """WHEN a workspace has a description + THEN it can be retrieved via the description property.""" + workspace = simai_client.workspaces._model_from( + {"id": "ws01", "name": "riri", "description": "My description"} + ) + + assert workspace.description == "My description" + + +def test_workspace_description_get_none(simai_client): + """WHEN a workspace has no description + THEN the description property returns None.""" + workspace = simai_client.workspaces._model_from({"id": "ws01", "name": "riri"}) + + assert workspace.description is None + + +def test_workspace_description_set(simai_client, httpx_mock): + """WHEN setting a workspace description + THEN a PATCH request is made with the description.""" + workspace = simai_client.workspaces._model_from( + {"id": "ws01", "name": "riri", "description": None} + ) + + httpx_mock.add_response( + method="PATCH", + url="https://test.test/workspaces/ws01", + status_code=204, + ) + httpx_mock.add_response( + method="GET", + url="https://test.test/workspaces/ws01", + json={"id": "ws01", "name": "riri", "description": "New description"}, + status_code=200, + ) + + workspace.set_description("New description") + assert workspace.description == "New description" + + +def test_workspace_create_with_description(simai_client, httpx_mock): + """WHEN creating a workspace with a description + THEN the description is sent in the request.""" + httpx_mock.add_response( + method="POST", + url="https://test.test/workspaces/", + json={"id": "ws01", "name": "my_workspace", "description": "My workspace description"}, + status_code=200, + ) + + workspace = simai_client.workspaces.create( + name="my_workspace", model_id="mdl01", description="My workspace description" + ) + + assert workspace.name == "my_workspace" + assert workspace.description == "My workspace description"