From 53e7e4acf3ebcf912883ab3c8f0e5f3443956d0c Mon Sep 17 00:00:00 2001 From: thiagobomfin-galileo Date: Fri, 13 Mar 2026 13:01:22 -0300 Subject: [PATCH 1/7] feat(datasets): add update method to Datasets service --- src/galileo/datasets.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/galileo/datasets.py b/src/galileo/datasets.py index f058e7e1..3a2af999 100644 --- a/src/galileo/datasets.py +++ b/src/galileo/datasets.py @@ -17,6 +17,7 @@ query_dataset_versions_datasets_dataset_id_versions_query_post, query_datasets_datasets_query_post, update_dataset_content_datasets_dataset_id_content_patch, + update_dataset_datasets_dataset_id_patch, ) from galileo.resources.models import DatasetRow, ListDatasetVersionParams, ListDatasetVersionResponse from galileo.resources.models.body_create_dataset_datasets_post import BodyCreateDatasetDatasetsPost @@ -38,6 +39,7 @@ from galileo.resources.models.synthetic_dataset_extension_request import SyntheticDatasetExtensionRequest from galileo.resources.models.synthetic_dataset_extension_response import SyntheticDatasetExtensionResponse from galileo.resources.models.update_dataset_content_request import UpdateDatasetContentRequest +from galileo.resources.models.update_dataset_request import UpdateDatasetRequest from galileo.resources.types import UNSET, File, Unset from galileo.schema.datasets import DatasetRecord from galileo.utils.datasets import validate_dataset_in_project @@ -420,6 +422,43 @@ def delete( return delete_dataset_datasets_dataset_id_delete.sync(client=self.config.api_client, dataset_id=dataset.id) + def update(self, dataset_id: str, *, name: Optional[str] = None) -> Dataset: + """ + Updates a dataset's properties. + + Parameters + ---------- + dataset_id : str + The ID of the dataset to update. + name : str, optional + The new name for the dataset. + + Returns + ------- + Dataset + The updated dataset. + + Raises + ------ + DatasetAPIException + If the API request fails. + ValueError + If the server returns no response. + """ + body = UpdateDatasetRequest(name=name) + + response = update_dataset_datasets_dataset_id_patch.sync( + dataset_id=dataset_id, client=self.config.api_client, body=body + ) + + if isinstance(response, HTTPValidationError): + raise DatasetAPIException(response.detail) + + if not response: + raise ValueError(f"Unable to update dataset: {dataset_id}") + + return Dataset(dataset_db=response) + def create( self, name: str, content: DatasetType, *, project_id: Optional[str] = None, project_name: Optional[str] = None ) -> Dataset: From 483a64bd61daa44377f1939a30e8f34ced628cb6 Mon Sep 17 00:00:00 2001 From: thiagobomfin-galileo Date: Fri, 13 Mar 2026 13:02:11 -0300 Subject: [PATCH 2/7] feat(projects): add update() method to Projects service --- src/galileo/projects.py | 46 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/galileo/projects.py b/src/galileo/projects.py index 7a33688f..25815451 100644 --- a/src/galileo/projects.py +++ b/src/galileo/projects.py @@ -14,6 +14,7 @@ get_project_projects_project_id_get, get_projects_projects_get, list_user_project_collaborators_projects_project_id_users_get, + update_project_projects_project_id_put, update_user_project_collaborator_projects_project_id_users_user_id_patch, ) from galileo.resources.models.collaborator_role import CollaboratorRole @@ -25,6 +26,8 @@ from galileo.resources.models.project_db import ProjectDB from galileo.resources.models.project_db_thin import ProjectDBThin from galileo.resources.models.project_type import ProjectType +from galileo.resources.models.project_update import ProjectUpdate +from galileo.resources.models.project_update_response import ProjectUpdateResponse from galileo.resources.models.user_collaborator import UserCollaborator from galileo.resources.models.user_collaborator_create import UserCollaboratorCreate from galileo.resources.types import UNSET, Unset @@ -297,6 +300,49 @@ def create(self, name: str) -> Project: return Project(project=response) + def update(self, project_id: str, *, name: Optional[str] = None) -> ProjectUpdateResponse: + """ + Updates a project's properties. + + Parameters + ---------- + project_id : str + The ID of the project to update. + name : str, optional + The new name for the project. + + Returns + ------- + ProjectUpdateResponse + The updated project data returned by the API. + + Raises + ------ + ProjectsAPIException + If the server returns an error response. + ValueError + If the server returns no response. + """ + body = ProjectUpdate(name=name) + + detailed_response = update_project_projects_project_id_put.sync_detailed( + project_id=project_id, client=self.config.api_client, body=body + ) + + if detailed_response.status_code != httpx.codes.OK: + raise ProjectsAPIException(detailed_response.content) + + response = detailed_response.parsed + + if isinstance(response, HTTPValidationError): + _logger.error(response) + raise ProjectsAPIException(f"Failed to update project: {response.detail}") + + if not response: + raise ValueError(f"Unable to update project: {project_id}") + + return response + def share_project_with_user( self, project_id: str, user_id: str, role: CollaboratorRole = CollaboratorRole.VIEWER ) -> UserCollaborator: From 714bbb9d932651f7aa87005a2fa269fb90763ecc Mon Sep 17 00:00:00 2001 From: thiagobomfin-galileo Date: Fri, 13 Mar 2026 13:03:12 -0300 Subject: [PATCH 3/7] feat(future): implement Project.save() and Dataset.save() --- src/galileo/__future__/dataset.py | 48 +++++++++++++++++++++++++----- src/galileo/__future__/project.py | 49 +++++++++++++++++++++++++++---- 2 files changed, 84 insertions(+), 13 deletions(-) diff --git a/src/galileo/__future__/dataset.py b/src/galileo/__future__/dataset.py index 8398f398..02db9553 100644 --- a/src/galileo/__future__/dataset.py +++ b/src/galileo/__future__/dataset.py @@ -526,8 +526,9 @@ def save(self) -> Dataset: """ Save changes to this dataset. - This method is a placeholder for future functionality to update - dataset properties. + Persists any local modifications to the API. If the dataset has not been + created yet (LOCAL_ONLY state), this calls create() instead. If already + synced with no pending changes, this is a no-op. Returns ------- @@ -535,9 +536,42 @@ def save(self) -> Dataset: Raises ------ - NotImplementedError: This functionality is not yet implemented. + ValueError: If the dataset has been deleted or has no ID set. + Exception: If the API call fails. + + Examples + -------- + dataset = Dataset.get(name="my-dataset") + dataset.name = "renamed-dataset" + dataset._set_state(SyncState.DIRTY) + dataset.save() + assert dataset.is_synced() """ - raise NotImplementedError( - "Dataset updates are not yet implemented. " - "Use add_rows() to add content or other specific methods to modify dataset state." - ) + if self.sync_state == SyncState.LOCAL_ONLY: + return self.create() + if self.sync_state == SyncState.SYNCED: + logger.debug(f"Dataset.save: id='{self.id}' - already synced, no action needed") + return self + if self.sync_state == SyncState.DELETED: + raise ValueError("Cannot save a deleted dataset.") + + # DIRTY or FAILED_SYNC + if self.id is None: + raise ValueError("Dataset ID is not set. Cannot update a dataset without an ID.") + + try: + logger.info(f"Dataset.save: name='{self.name}' id='{self.id}' - started") + datasets_service = Datasets() + updated_dataset = datasets_service.update(self.id, name=self.name) + + # Update attributes from response + self.name = updated_dataset.name + self.updated_at = updated_dataset.updated_at + + self._set_state(SyncState.SYNCED) + logger.info(f"Dataset.save: id='{self.id}' - completed") + return self + except Exception as e: + self._set_state(SyncState.FAILED_SYNC, error=e) + logger.error(f"Dataset.save: id='{self.id}' - failed: {e}") + raise diff --git a/src/galileo/__future__/project.py b/src/galileo/__future__/project.py index 2f919b5f..215b34ec 100644 --- a/src/galileo/__future__/project.py +++ b/src/galileo/__future__/project.py @@ -9,6 +9,7 @@ from galileo.__future__.shared.base import StateManagementMixin, SyncState from galileo.__future__.shared.exceptions import APIError, ValidationError from galileo.projects import Projects +from galileo.resources.types import Unset if TYPE_CHECKING: from galileo.__future__.dataset import Dataset @@ -760,8 +761,9 @@ def save(self) -> Project: """ Save changes to this project. - This method is a placeholder for future functionality to update - project properties. + Persists any local modifications to the API. If the project has not been + created yet (LOCAL_ONLY state), this calls create() instead. If already + synced with no pending changes, this is a no-op. Returns ------- @@ -769,11 +771,46 @@ def save(self) -> Project: Raises ------ - NotImplementedError: This functionality is not yet implemented. + ValueError: If the project has been deleted or has no ID set. + Exception: If the API call fails. + + Examples + -------- + project = Project.get(name="My Project") + project.name = "Renamed Project" + project._set_state(SyncState.DIRTY) + project.save() + assert project.is_synced() """ - raise NotImplementedError( - "Project updates are not yet implemented. Use specific methods to modify project state." - ) + if self.sync_state == SyncState.LOCAL_ONLY: + return self.create() + if self.sync_state == SyncState.SYNCED: + logger.debug(f"Project.save: id='{self.id}' - already synced, no action needed") + return self + if self.sync_state == SyncState.DELETED: + raise ValueError("Cannot save a deleted project.") + + # DIRTY or FAILED_SYNC + if self.id is None: + raise ValueError("Project ID is not set. Cannot update a project without an ID.") + + try: + logger.info(f"Project.save: name='{self.name}' id='{self.id}' - started") + projects_service = Projects() + updated_project = projects_service.update(self.id, name=self.name) + + # Update attributes from response + if not isinstance(updated_project.name, Unset): + self.name = updated_project.name + self.updated_at = updated_project.updated_at + + self._set_state(SyncState.SYNCED) + logger.info(f"Project.save: id='{self.id}' - completed") + return self + except Exception as e: + self._set_state(SyncState.FAILED_SYNC, error=e) + logger.error(f"Project.save: id='{self.id}' - failed: {e}") + raise # Import at end to avoid circular import (log_stream.py imports Project) From ed15a4af2681775c6b8ba943ce77b65c6a457aa6 Mon Sep 17 00:00:00 2001 From: thiagobomfin-galileo Date: Fri, 13 Mar 2026 13:03:48 -0300 Subject: [PATCH 4/7] test(future): add test coverage for Project.save() and Dataset.save() --- tests/future/test_dataset.py | 109 +++++++++++++++++++++++++++++++++-- tests/future/test_project.py | 109 +++++++++++++++++++++++++++++++++-- 2 files changed, 208 insertions(+), 10 deletions(-) diff --git a/tests/future/test_dataset.py b/tests/future/test_dataset.py index 20dd0dec..22829a95 100644 --- a/tests/future/test_dataset.py +++ b/tests/future/test_dataset.py @@ -261,16 +261,115 @@ def test_refresh_raises_error_for_local_only(self, reset_configuration: None) -> dataset.refresh() -class TestDatasetMethods: - """Test suite for other Dataset methods.""" +class TestDatasetSave: + """Test suite for Dataset.save() method.""" - def test_save_raises_not_implemented_error(self, reset_configuration: None) -> None: - """Test save() raises NotImplementedError.""" + @patch("galileo.__future__.dataset.Datasets") + def test_save_local_only_delegates_to_create( + self, mock_datasets_class: MagicMock, reset_configuration: None, mock_dataset: MagicMock + ) -> None: + # Given: a local-only dataset and a mocked create response + mock_service = MagicMock() + mock_datasets_class.return_value = mock_service + mock_service.create.return_value = mock_dataset + + # When: save() is called on a LOCAL_ONLY dataset dataset = Dataset(name="Test Dataset") + result = dataset.save() + + # Then: create() is called and the dataset is synced + mock_service.create.assert_called_once_with(name="Test Dataset", content=[]) + assert result.id == mock_dataset.id + assert result.is_synced() + + @patch("galileo.__future__.dataset.Datasets") + def test_save_synced_is_noop( + self, mock_datasets_class: MagicMock, reset_configuration: None, mock_dataset: MagicMock + ) -> None: + # Given: a synced dataset + mock_service = MagicMock() + mock_datasets_class.return_value = mock_service + mock_service.get.return_value = mock_dataset + + dataset = Dataset.get(id=mock_dataset.id) + assert dataset.is_synced() + + # When: save() is called + result = dataset.save() + + # Then: no API update call is made and dataset remains synced + mock_service.update.assert_not_called() + assert result is dataset + assert result.is_synced() + + def test_save_deleted_raises_value_error(self, reset_configuration: None) -> None: + # Given: a dataset in DELETED state + dataset = Dataset(name="Test Dataset") + dataset.id = str(uuid4()) + dataset._set_state(SyncState.DELETED) + + # When/Then: save() raises ValueError + with pytest.raises(ValueError, match="Cannot save a deleted dataset"): + dataset.save() + + def test_save_without_id_raises_value_error(self, reset_configuration: None) -> None: + # Given: a dataset in DIRTY state with no ID + dataset = Dataset(name="Test Dataset") + dataset._set_state(SyncState.DIRTY) + + # When/Then: save() raises ValueError because there is no ID to update + with pytest.raises(ValueError, match="Dataset ID is not set"): + dataset.save() - with pytest.raises(NotImplementedError, match="not yet implemented"): + @patch("galileo.__future__.dataset.Datasets") + def test_save_dirty_calls_update_and_syncs_attributes( + self, mock_datasets_class: MagicMock, reset_configuration: None, mock_dataset: MagicMock + ) -> None: + # Given: a synced dataset that has been renamed and marked dirty + mock_service = MagicMock() + mock_datasets_class.return_value = mock_service + mock_service.get.return_value = mock_dataset + + updated_response = MagicMock() + updated_response.configure_mock(name="Renamed Dataset") + updated_response.updated_at = MagicMock() + mock_service.update.return_value = updated_response + + dataset = Dataset.get(id=mock_dataset.id) + dataset.name = "Renamed Dataset" + dataset._set_state(SyncState.DIRTY) + + # When: save() is called + result = dataset.save() + + # Then: update is called with the new name and state is synced + mock_service.update.assert_called_once_with(mock_dataset.id, name="Renamed Dataset") + assert result.name == "Renamed Dataset" + assert result.is_synced() + + @patch("galileo.__future__.dataset.Datasets") + def test_save_handles_api_failure( + self, mock_datasets_class: MagicMock, reset_configuration: None, mock_dataset: MagicMock + ) -> None: + # Given: a dirty dataset and an API that raises an error + mock_service = MagicMock() + mock_datasets_class.return_value = mock_service + mock_service.get.return_value = mock_dataset + mock_service.update.side_effect = RuntimeError("API error") + + dataset = Dataset.get(id=mock_dataset.id) + dataset._set_state(SyncState.DIRTY) + + # When/Then: the exception propagates and state is FAILED_SYNC + with pytest.raises(RuntimeError, match="API error"): dataset.save() + assert dataset.sync_state == SyncState.FAILED_SYNC + + +class TestDatasetMethods: + """Test suite for other Dataset methods.""" + def test_str_and_repr(self, reset_configuration: None) -> None: """Test __str__ and __repr__ return expected formats.""" dataset = Dataset(name="Test Dataset") diff --git a/tests/future/test_project.py b/tests/future/test_project.py index 7295a88d..3e8cb09c 100644 --- a/tests/future/test_project.py +++ b/tests/future/test_project.py @@ -285,16 +285,115 @@ def test_refresh_raises_error_if_project_no_longer_exists( assert project.sync_state == SyncState.FAILED_SYNC -class TestProjectMethods: - """Test suite for other Project methods.""" +class TestProjectSave: + """Test suite for Project.save() method.""" + + @patch("galileo.__future__.project.Projects") + def test_save_local_only_delegates_to_create( + self, mock_projects_class: MagicMock, reset_configuration: None, mock_project: MagicMock + ) -> None: + # Given: a local-only project and a mocked create response + mock_service = MagicMock() + mock_projects_class.return_value = mock_service + mock_service.create.return_value = mock_project + + # When: save() is called on a LOCAL_ONLY project + project = Project(name="Test Project") + result = project.save() + + # Then: create() is called and the project is synced + mock_service.create.assert_called_once_with(name="Test Project") + assert result.id == mock_project.id + assert result.is_synced() + + @patch("galileo.__future__.project.Projects") + def test_save_synced_is_noop( + self, mock_projects_class: MagicMock, reset_configuration: None, mock_project: MagicMock + ) -> None: + # Given: a synced project + mock_service = MagicMock() + mock_projects_class.return_value = mock_service + mock_service.get.return_value = mock_project + + project = Project.get(id=mock_project.id) + assert project.is_synced() - def test_save_raises_not_implemented_error(self, reset_configuration: None) -> None: - """Test save() raises NotImplementedError.""" + # When: save() is called + result = project.save() + + # Then: no API update call is made and project remains synced + mock_service.update.assert_not_called() + assert result is project + assert result.is_synced() + + def test_save_deleted_raises_value_error(self, reset_configuration: None) -> None: + # Given: a project in DELETED state project = Project(name="Test Project") + project.id = str(uuid4()) + project._set_state(SyncState.DELETED) + + # When/Then: save() raises ValueError + with pytest.raises(ValueError, match="Cannot save a deleted project"): + project.save() - with pytest.raises(NotImplementedError, match="not yet implemented"): + def test_save_without_id_raises_value_error(self, reset_configuration: None) -> None: + # Given: a project in DIRTY state but with no ID + project = Project(name="Test Project") + project._set_state(SyncState.DIRTY) + + # When/Then: save() raises ValueError because there is no ID to update + with pytest.raises(ValueError, match="Project ID is not set"): project.save() + @patch("galileo.__future__.project.Projects") + def test_save_dirty_calls_update_and_syncs_attributes( + self, mock_projects_class: MagicMock, reset_configuration: None, mock_project: MagicMock + ) -> None: + # Given: a synced project that has been renamed and marked dirty + mock_service = MagicMock() + mock_projects_class.return_value = mock_service + mock_service.get.return_value = mock_project + + updated_response = MagicMock() + updated_response.configure_mock(name="Renamed Project") + updated_response.updated_at = MagicMock() + mock_service.update.return_value = updated_response + + project = Project.get(id=mock_project.id) + project.name = "Renamed Project" + project._set_state(SyncState.DIRTY) + + # When: save() is called + result = project.save() + + # Then: update is called with the new name and state is synced + mock_service.update.assert_called_once_with(mock_project.id, name="Renamed Project") + assert result.name == "Renamed Project" + assert result.is_synced() + + @patch("galileo.__future__.project.Projects") + def test_save_handles_api_failure( + self, mock_projects_class: MagicMock, reset_configuration: None, mock_project: MagicMock + ) -> None: + # Given: a dirty project and an API that raises an error + mock_service = MagicMock() + mock_projects_class.return_value = mock_service + mock_service.get.return_value = mock_project + mock_service.update.side_effect = RuntimeError("API error") + + project = Project.get(id=mock_project.id) + project._set_state(SyncState.DIRTY) + + # When/Then: the exception propagates and state is FAILED_SYNC + with pytest.raises(RuntimeError, match="API error"): + project.save() + + assert project.sync_state == SyncState.FAILED_SYNC + + +class TestProjectMethods: + """Test suite for other Project methods.""" + def test_str_representation(self, reset_configuration: None) -> None: """Test __str__ returns expected format.""" project = Project(name="Test Project") From 609bcb7b408ee2a87d53db48aff2315979657df6 Mon Sep 17 00:00:00 2001 From: thiagobomfin-galileo Date: Tue, 17 Mar 2026 15:02:02 -0300 Subject: [PATCH 5/7] fix(services): add lifecycle logging to Projects.update and Datasets.update --- src/galileo/datasets.py | 17 ++++++++++++++--- src/galileo/projects.py | 14 +++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/galileo/datasets.py b/src/galileo/datasets.py index 3a2af999..67ede5e9 100644 --- a/src/galileo/datasets.py +++ b/src/galileo/datasets.py @@ -445,18 +445,29 @@ def update(self, dataset_id: str, *, name: Optional[str] = None) -> Dataset: ValueError If the server returns no response. """ + logger.info("action='dataset.update' phase='start' dataset_id=%s name=%s", dataset_id, name) + body = UpdateDatasetRequest(name=name) - response = update_dataset_datasets_dataset_id_patch.sync( - dataset_id=dataset_id, client=self.config.api_client, body=body - ) + try: + response = update_dataset_datasets_dataset_id_patch.sync( + dataset_id=dataset_id, client=self.config.api_client, body=body + ) + except Exception as e: + logger.error("action='dataset.update' phase='error' dataset_id=%s error=%s", dataset_id, e) + raise if isinstance(response, HTTPValidationError): + logger.error( + "action='dataset.update' phase='error' dataset_id=%s validation_error=%s", dataset_id, response.detail + ) raise DatasetAPIException(response.detail) if not response: + logger.error("action='dataset.update' phase='error' dataset_id=%s reason='empty response'", dataset_id) raise ValueError(f"Unable to update dataset: {dataset_id}") + logger.info("action='dataset.update' phase='complete' dataset_id=%s name=%s", response.id, name) return Dataset(dataset_db=response) def create( diff --git a/src/galileo/projects.py b/src/galileo/projects.py index 25815451..2c36354f 100644 --- a/src/galileo/projects.py +++ b/src/galileo/projects.py @@ -323,6 +323,8 @@ def update(self, project_id: str, *, name: Optional[str] = None) -> ProjectUpdat ValueError If the server returns no response. """ + _logger.info("action='project.update' phase='start' project_id=%s name=%s", project_id, name) + body = ProjectUpdate(name=name) detailed_response = update_project_projects_project_id_put.sync_detailed( @@ -330,17 +332,27 @@ def update(self, project_id: str, *, name: Optional[str] = None) -> ProjectUpdat ) if detailed_response.status_code != httpx.codes.OK: + _logger.error( + "action='project.update' phase='error' project_id=%s status=%s content=%s", + project_id, + detailed_response.status_code, + detailed_response.content, + ) raise ProjectsAPIException(detailed_response.content) response = detailed_response.parsed if isinstance(response, HTTPValidationError): - _logger.error(response) + _logger.error( + "action='project.update' phase='error' project_id=%s validation_error=%s", project_id, response.detail + ) raise ProjectsAPIException(f"Failed to update project: {response.detail}") if not response: + _logger.error("action='project.update' phase='error' project_id=%s reason='empty response'", project_id) raise ValueError(f"Unable to update project: {project_id}") + _logger.info("action='project.update' phase='complete' project_id=%s name=%s", project_id, name) return response def share_project_with_user( From 972b15b6f651b3e96b0030b8a4151886cb11af6d Mon Sep 17 00:00:00 2001 From: thiagobomfin-galileo Date: Wed, 18 Mar 2026 19:44:34 -0300 Subject: [PATCH 6/7] fix ci --- src/galileo/__future__/dataset.py | 7 +++++-- src/galileo/__future__/project.py | 12 ++++++------ src/galileo/datasets.py | 26 +++++++++++++------------- src/galileo/projects.py | 30 ++++++++++++++---------------- 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/src/galileo/__future__/dataset.py b/src/galileo/__future__/dataset.py index 02db9553..9943e9cd 100644 --- a/src/galileo/__future__/dataset.py +++ b/src/galileo/__future__/dataset.py @@ -541,11 +541,14 @@ def save(self) -> Dataset: Examples -------- + # Create a new dataset + dataset = Dataset(name="my-dataset") + dataset.save() + + # Update an existing dataset's name dataset = Dataset.get(name="my-dataset") dataset.name = "renamed-dataset" - dataset._set_state(SyncState.DIRTY) dataset.save() - assert dataset.is_synced() """ if self.sync_state == SyncState.LOCAL_ONLY: return self.create() diff --git a/src/galileo/__future__/project.py b/src/galileo/__future__/project.py index 215b34ec..23106955 100644 --- a/src/galileo/__future__/project.py +++ b/src/galileo/__future__/project.py @@ -776,11 +776,14 @@ def save(self) -> Project: Examples -------- + # Create a new project + project = Project(name="My Project") + project.save() + + # Update an existing project's name project = Project.get(name="My Project") project.name = "Renamed Project" - project._set_state(SyncState.DIRTY) project.save() - assert project.is_synced() """ if self.sync_state == SyncState.LOCAL_ONLY: return self.create() @@ -790,7 +793,6 @@ def save(self) -> Project: if self.sync_state == SyncState.DELETED: raise ValueError("Cannot save a deleted project.") - # DIRTY or FAILED_SYNC if self.id is None: raise ValueError("Project ID is not set. Cannot update a project without an ID.") @@ -799,9 +801,7 @@ def save(self) -> Project: projects_service = Projects() updated_project = projects_service.update(self.id, name=self.name) - # Update attributes from response - if not isinstance(updated_project.name, Unset): - self.name = updated_project.name + self.name = updated_project.name self.updated_at = updated_project.updated_at self._set_state(SyncState.SYNCED) diff --git a/src/galileo/datasets.py b/src/galileo/datasets.py index 67ede5e9..d205add3 100644 --- a/src/galileo/datasets.py +++ b/src/galileo/datasets.py @@ -445,29 +445,29 @@ def update(self, dataset_id: str, *, name: Optional[str] = None) -> Dataset: ValueError If the server returns no response. """ - logger.info("action='dataset.update' phase='start' dataset_id=%s name=%s", dataset_id, name) + logger.info("Datasets.update: dataset_id=%s name=%s - started", dataset_id, name) body = UpdateDatasetRequest(name=name) - try: - response = update_dataset_datasets_dataset_id_patch.sync( - dataset_id=dataset_id, client=self.config.api_client, body=body - ) - except Exception as e: - logger.error("action='dataset.update' phase='error' dataset_id=%s error=%s", dataset_id, e) - raise + detailed_response = update_dataset_datasets_dataset_id_patch.sync_detailed( + dataset_id=dataset_id, client=self.config.api_client, body=body + ) + + if detailed_response.status_code != 200: + logger.error("Datasets.update: dataset_id=%s status=%s - failed", dataset_id, detailed_response.status_code) + raise DatasetAPIException(detailed_response.content) + + response = detailed_response.parsed if isinstance(response, HTTPValidationError): - logger.error( - "action='dataset.update' phase='error' dataset_id=%s validation_error=%s", dataset_id, response.detail - ) + logger.error("Datasets.update: dataset_id=%s validation_error=%s - failed", dataset_id, response.detail) raise DatasetAPIException(response.detail) if not response: - logger.error("action='dataset.update' phase='error' dataset_id=%s reason='empty response'", dataset_id) + logger.error("Datasets.update: dataset_id=%s - failed: empty response", dataset_id) raise ValueError(f"Unable to update dataset: {dataset_id}") - logger.info("action='dataset.update' phase='complete' dataset_id=%s name=%s", response.id, name) + logger.info("Datasets.update: dataset_id=%s - completed", dataset_id) return Dataset(dataset_db=response) def create( diff --git a/src/galileo/projects.py b/src/galileo/projects.py index 2c36354f..77efd4b7 100644 --- a/src/galileo/projects.py +++ b/src/galileo/projects.py @@ -104,13 +104,16 @@ class Project: permissions: Union[Unset, list["Permission"]] = UNSET type: Union[None, ProjectType, Unset] = UNSET - def __init__(self, project: Union[None, ProjectDBThin, ProjectDB, ProjectCreateResponse] = None) -> None: + def __init__( + self, + project: Union[None, ProjectDBThin, ProjectDB, ProjectCreateResponse, ProjectUpdateResponse] = None, + ) -> None: """ Initialize a Project instance. Parameters ---------- - project (Union[None, ProjectDBThin, ProjectDB, ProjectCreateResponse], optional): + project : Union[None, ProjectDBThin, ProjectDB, ProjectCreateResponse, ProjectUpdateResponse], optional The project data to initialize from. If None, creates an empty project instance. Defaults to None. """ @@ -300,7 +303,7 @@ def create(self, name: str) -> Project: return Project(project=response) - def update(self, project_id: str, *, name: Optional[str] = None) -> ProjectUpdateResponse: + def update(self, project_id: str, *, name: Optional[str] = None) -> Project: """ Updates a project's properties. @@ -313,8 +316,8 @@ def update(self, project_id: str, *, name: Optional[str] = None) -> ProjectUpdat Returns ------- - ProjectUpdateResponse - The updated project data returned by the API. + Project + The updated project. Raises ------ @@ -323,7 +326,7 @@ def update(self, project_id: str, *, name: Optional[str] = None) -> ProjectUpdat ValueError If the server returns no response. """ - _logger.info("action='project.update' phase='start' project_id=%s name=%s", project_id, name) + _logger.info("Projects.update: project_id=%s name=%s - started", project_id, name) body = ProjectUpdate(name=name) @@ -333,27 +336,22 @@ def update(self, project_id: str, *, name: Optional[str] = None) -> ProjectUpdat if detailed_response.status_code != httpx.codes.OK: _logger.error( - "action='project.update' phase='error' project_id=%s status=%s content=%s", - project_id, - detailed_response.status_code, - detailed_response.content, + "Projects.update: project_id=%s status=%s - failed", project_id, detailed_response.status_code ) raise ProjectsAPIException(detailed_response.content) response = detailed_response.parsed if isinstance(response, HTTPValidationError): - _logger.error( - "action='project.update' phase='error' project_id=%s validation_error=%s", project_id, response.detail - ) + _logger.error("Projects.update: project_id=%s validation_error=%s - failed", project_id, response.detail) raise ProjectsAPIException(f"Failed to update project: {response.detail}") if not response: - _logger.error("action='project.update' phase='error' project_id=%s reason='empty response'", project_id) + _logger.error("Projects.update: project_id=%s - failed: empty response", project_id) raise ValueError(f"Unable to update project: {project_id}") - _logger.info("action='project.update' phase='complete' project_id=%s name=%s", project_id, name) - return response + _logger.info("Projects.update: project_id=%s - completed", project_id) + return Project(project=response) def share_project_with_user( self, project_id: str, user_id: str, role: CollaboratorRole = CollaboratorRole.VIEWER From 897be4fa90bd5d39b31c688bc908c15d1819f116 Mon Sep 17 00:00:00 2001 From: thiagobomfin-galileo Date: Wed, 18 Mar 2026 19:57:03 -0300 Subject: [PATCH 7/7] pre-commit fix --- src/galileo/__future__/project.py | 1 - src/galileo/projects.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/galileo/__future__/project.py b/src/galileo/__future__/project.py index 23106955..e82fdd06 100644 --- a/src/galileo/__future__/project.py +++ b/src/galileo/__future__/project.py @@ -9,7 +9,6 @@ from galileo.__future__.shared.base import StateManagementMixin, SyncState from galileo.__future__.shared.exceptions import APIError, ValidationError from galileo.projects import Projects -from galileo.resources.types import Unset if TYPE_CHECKING: from galileo.__future__.dataset import Dataset diff --git a/src/galileo/projects.py b/src/galileo/projects.py index 77efd4b7..65628b19 100644 --- a/src/galileo/projects.py +++ b/src/galileo/projects.py @@ -105,8 +105,7 @@ class Project: type: Union[None, ProjectType, Unset] = UNSET def __init__( - self, - project: Union[None, ProjectDBThin, ProjectDB, ProjectCreateResponse, ProjectUpdateResponse] = None, + self, project: Union[None, ProjectDBThin, ProjectDB, ProjectCreateResponse, ProjectUpdateResponse] = None ) -> None: """ Initialize a Project instance.