This is an automated email from the ASF dual-hosted git repository. diqiu50 pushed a commit to branch cherry-pick/10753-to-branch-1.2 in repository https://gitbox.apache.org/repos/asf/gravitino.git
commit 2e1b949d2fb32cdc84b84699f9e8ff2623103c47 Author: Sun Yuhan <[email protected]> AuthorDate: Mon Apr 13 15:29:13 2026 +0800 [Cherry-pick to branch-1.2] [#10753] fix(client-python): use PUT method for alter_tag API call (#10754) --- .../gravitino/client/gravitino_metalake.py | 30 +- .../client-python/tests/unittests/test_tag_api.py | 482 +++++++++++++++++++++ 2 files changed, 510 insertions(+), 2 deletions(-) diff --git a/clients/client-python/gravitino/client/gravitino_metalake.py b/clients/client-python/gravitino/client/gravitino_metalake.py index 00b7ae3694..bd171d51c5 100644 --- a/clients/client-python/gravitino/client/gravitino_metalake.py +++ b/clients/client-python/gravitino/client/gravitino_metalake.py @@ -623,8 +623,34 @@ class GravitinoMetalake( NoSuchTagException: If the tag does not exist. NoSuchMetalakeException: If the metalake does not exist. """ - # TODO implement alter_tag - raise NotImplementedError() + Precondition.check_argument( + StringUtils.is_not_blank(tag_name), + "tag name must not be null or empty", + ) + Precondition.check_argument( + changes is not None and len(changes) > 0, + "at least one change is required", + ) + updates = [DTOConverters.to_tag_update_request(change) for change in changes] + update_req = TagUpdatesRequest(updates) + update_req.validate() + + url = self.API_METALAKES_TAG_PATH.format( + encode_string(self.name()), encode_string(tag_name) + ) + response = self.rest_client.put( + url, + json=update_req, + error_handler=TAG_ERROR_HANDLER, + ) + tag_resp: TagResponse = TagResponse.from_json(response.body, infer_missing=True) + + tag_resp.validate() + return GenericTag( + self.name(), + tag_resp.tag(), + self.rest_client, + ) def delete_tag(self, tag_name) -> bool: """ diff --git a/clients/client-python/tests/unittests/test_tag_api.py b/clients/client-python/tests/unittests/test_tag_api.py new file mode 100644 index 0000000000..6ced9e0464 --- /dev/null +++ b/clients/client-python/tests/unittests/test_tag_api.py @@ -0,0 +1,482 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from __future__ import annotations + +import unittest +from unittest.mock import patch + +from gravitino import GravitinoClient +from gravitino.api.tag import Tag +from gravitino.api.tag.tag_change import TagChange +from gravitino.dto.responses.drop_response import DropResponse +from gravitino.dto.responses.tag_response import ( + TagListResponse, + TagNamesListResponse, + TagResponse, +) +from tests.unittests import mock_base + + +@mock_base.mock_data +class TestTagAPI(unittest.TestCase): + _metalake_name: str = "metalake_demo" + + def test_client_get_tag(self, *mock_method) -> None: + with mock_base.mock_tag_methods(): + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + retrieved_tag = client.get_tag("tagA") + self._check_default_tag_a(retrieved_tag) + + def test_client_list_tag(self, *mock_method) -> None: + with mock_base.mock_tag_methods(): + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + retrieved_tags = client.list_tags() + self.assertEqual(2, len(retrieved_tags)) + self.assertTrue("tagA" in retrieved_tags) + self.assertTrue("tagB" in retrieved_tags) + + client.create_tag("tagC", "mock tag C", None) + + retrieved_tags = client.list_tags() + self.assertEqual(3, len(retrieved_tags)) + self.assertTrue("tagA" in retrieved_tags) + self.assertTrue("tagB" in retrieved_tags) + self.assertTrue("tagC" in retrieved_tags) + + def test_client_list_tag_info(self, *mock_method) -> None: + with mock_base.mock_tag_methods(): + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + retrieved_tags = client.list_tags_info() + self.assertEqual(2, len(retrieved_tags)) + tag_names = [tag.name() for tag in retrieved_tags] + + self.assertTrue("tagA" in tag_names) + self.assertTrue("tagB" in tag_names) + + tag_comments = [tag.comment() for tag in retrieved_tags] + self.assertTrue("mock tag A" in tag_comments) + self.assertTrue("mock tag B" in tag_comments) + + def test_client_remove_tag(self, *mock_method) -> None: + with mock_base.mock_tag_methods(): + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + retrieved_tags = client.list_tags() + self.assertEqual(2, len(retrieved_tags)) + self.assertTrue("tagA" in retrieved_tags) + self.assertTrue("tagB" in retrieved_tags) + + client.delete_tag("tagA") + retrieved_tags = client.list_tags() + self.assertEqual(1, len(retrieved_tags)) + self.assertTrue("tagA" not in retrieved_tags) + self.assertTrue("tagB" in retrieved_tags) + + def test_client_create_tag(self, *mock_method) -> None: + with mock_base.mock_tag_methods(): + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + retrieved_tags = client.list_tags() + self.assertEqual(2, len(retrieved_tags)) + self.assertTrue("tagA" in retrieved_tags) + self.assertTrue("tagB" in retrieved_tags) + + client.create_tag("tagC", "mock tag C", None) + retrieved_tags = client.list_tags() + self.assertEqual(3, len(retrieved_tags)) + self.assertTrue("tagA" in retrieved_tags) + self.assertTrue("tagB" in retrieved_tags) + self.assertTrue("tagC" in retrieved_tags) + + def test_client_alter_tag_with_name(self, *mock_method) -> None: + with mock_base.mock_tag_methods(): + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + retrieved_tag = client.get_tag("tagA") + self._check_default_tag_a(retrieved_tag) + + change = TagChange.rename("tagA-new") + client.alter_tag("tagA", change) + + with self.assertRaises(ValueError): + client.get_tag("tagA") + + retrieved_tag = client.get_tag("tagA-new") + self.assertEqual("tagA-new", retrieved_tag.name()) + self.assertEqual("mock tag A", retrieved_tag.comment()) + self.assertEqual( + { + "key1": "value1", + "key2": "value2", + }, + retrieved_tag.properties(), + ) + + def test_client_alter_tag_with_comment(self, *mock_method) -> None: + with mock_base.mock_tag_methods(): + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + retrieved_tag = client.get_tag("tagA") + self._check_default_tag_a(retrieved_tag) + + change = TagChange.update_comment("new comment") + client.alter_tag("tagA", change) + + retrieved_tag = client.get_tag("tagA") + self.assertEqual("tagA", retrieved_tag.name()) + self.assertEqual("new comment", retrieved_tag.comment()) + self.assertEqual( + { + "key1": "value1", + "key2": "value2", + }, + retrieved_tag.properties(), + ) + + def test_client_alter_tag_with_remove_property(self, *mock_method) -> None: + with mock_base.mock_tag_methods(): + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + retrieved_tag = client.get_tag("tagA") + self._check_default_tag_a(retrieved_tag) + + change = TagChange.remove_property("key1") + client.alter_tag("tagA", change) + + retrieved_tag = client.get_tag("tagA") + self.assertEqual("tagA", retrieved_tag.name()) + self.assertEqual("mock tag A", retrieved_tag.comment()) + self.assertEqual( + { + "key2": "value2", + }, + retrieved_tag.properties(), + ) + + def test_client_alter_tag_with_add_property(self, *mock_method) -> None: + with mock_base.mock_tag_methods(): + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + retrieved_tag = client.get_tag("tagA") + self._check_default_tag_a(retrieved_tag) + + change = TagChange.set_property("key3", "value3") + client.alter_tag("tagA", change) + + retrieved_tag = client.get_tag("tagA") + self.assertEqual("tagA", retrieved_tag.name()) + self.assertEqual("mock tag A", retrieved_tag.comment()) + self.assertEqual( + { + "key1": "value1", + "key2": "value2", + "key3": "value3", + }, + retrieved_tag.properties(), + ) + + def test_client_alter_tag_with_replace_property(self, *mock_method) -> None: + with mock_base.mock_tag_methods(): + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + retrieved_tag = client.get_tag("tagA") + self._check_default_tag_a(retrieved_tag) + + change = TagChange.set_property("key1", "value3") + client.alter_tag("tagA", change) + + retrieved_tag = client.get_tag("tagA") + self.assertEqual("tagA", retrieved_tag.name()) + self.assertEqual("mock tag A", retrieved_tag.comment()) + self.assertEqual( + { + "key1": "value3", + "key2": "value2", + }, + retrieved_tag.properties(), + ) + + def test_client_alter_tag_with_all_operations(self, *mock_method) -> None: + with mock_base.mock_tag_methods(): + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + retrieved_tag = client.get_tag("tagB") + self._check_default_tag_b(retrieved_tag) + + changes = [ + TagChange.set_property("key1", "value3"), + TagChange.remove_property("key2"), + TagChange.update_comment("mock tag B updated"), + TagChange.rename("new_tag_B"), + ] + + client.alter_tag("tagB", *changes) + retrieved_tag = client.get_tag("new_tag_B") + + self.assertEqual("new_tag_B", retrieved_tag.name()) + self.assertEqual("mock tag B updated", retrieved_tag.comment()) + self.assertEqual( + { + "key1": "value3", + }, + retrieved_tag.properties(), + ) + + def test_gravitino_list_tag_api(self, *mock_method) -> None: + resp = TagNamesListResponse(0, ["tagA", "tagB", "tagC"]) + json_str = resp.to_json() + mock_resp = mock_base.mock_http_response(json_str) + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + with patch( + "gravitino.utils.http_client.HTTPClient.get", + return_value=mock_resp, + ): + tags = client.list_tags() + self.assertEqual(3, len(tags)) + self.assertTrue("tagA" in tags) + self.assertTrue("tagB" in tags) + self.assertTrue("tagC" in tags) + + def test_gravitino_list_tag_info_api(self, *mock_method) -> None: + tag = mock_base.build_tag_dto() + resp = TagListResponse(0, [tag]) + json_str = resp.to_json() + mock_resp = mock_base.mock_http_response(json_str) + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + with patch( + "gravitino.utils.http_client.HTTPClient.get", + return_value=mock_resp, + ): + tags = client.list_tags_info() + self.assertEqual(1, len(tags)) + self.check_tag_equal(tag, tags[0]) + + def test_gravitino_metalake_delete_tag_api(self, *mock_method) -> None: + resp = DropResponse(0, True) + json_str = resp.to_json() + mock_resp = mock_base.mock_http_response(json_str) + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + with patch( + "gravitino.utils.http_client.HTTPClient.delete", + return_value=mock_resp, + ): + is_dropped = client.delete_tag("tag1") + self.assertTrue(is_dropped) + + def test_gravitino_metalake_delete_tag_api_with_empty_tag_name( + self, *mock_method + ) -> None: + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + with self.assertRaises(ValueError): + client.delete_tag(" ") + + def test_gravitino_metalake_create_tag_api(self, *mock_method) -> None: + tag = mock_base.build_tag_dto() + resp = TagResponse(0, tag) + json_str = resp.to_json() + mock_resp = mock_base.mock_http_response(json_str) + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + with patch( + "gravitino.utils.http_client.HTTPClient.post", + return_value=mock_resp, + ): + created_tag = client.create_tag(tag.name(), tag.comment(), tag.properties()) + self.check_tag_equal(tag, created_tag) + + def test_gravitino_metalake_get_tag_api(self, *mock_method) -> None: + tag = mock_base.build_tag_dto() + resp = TagResponse(0, tag) + json_str = resp.to_json() + mock_resp = mock_base.mock_http_response(json_str) + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + with patch( + "gravitino.utils.http_client.HTTPClient.get", + return_value=mock_resp, + ): + retrieved_tag = client.get_tag(tag.name()) + self.check_tag_equal(tag, retrieved_tag) + + def test_gravitino_metalake_get_tag_api_with_empty_tag_name( + self, *mock_method + ) -> None: + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + with self.assertRaises(ValueError): + client.get_tag(" ") + + def test_gravitino_metalake_alter_tag_api(self, *mock_method) -> None: + tag = mock_base.build_tag_dto( + "tagB", + "mock tag B", + { + "key2": "value2", + }, + ) + resp = TagResponse(0, tag) + json_str = resp.to_json() + mock_resp = mock_base.mock_http_response(json_str) + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + with patch( + "gravitino.utils.http_client.HTTPClient.put", + return_value=mock_resp, + ) as mock_put: + rename_change = TagChange.rename("tagB") + update_comment_change = TagChange.update_comment("mock tag B") + update_properties_change = TagChange.set_property("key2", "value2") + + updated_tag = client.alter_tag( + tag.name(), + rename_change, + update_comment_change, + update_properties_change, + ) + + self.assertEqual("tagB", updated_tag.name()) + self.assertEqual("mock tag B", updated_tag.comment()) + self.assertEqual( + { + "key2": "value2", + }, + updated_tag.properties(), + ) + mock_put.assert_called_once() + + def test_gravitino_metalake_alter_tag_api_with_empty_tag_name( + self, *mock_args + ) -> None: + client = GravitinoClient( + uri="http://localhost:8090", + metalake_name=self._metalake_name, + check_version=False, + ) + + with self.assertRaises(ValueError): + client.alter_tag(" ", TagChange.rename("tagB")) + + def _check_default_tag_a(self, tag: Tag) -> None: + self.assertEqual("tagA", tag.name()) + self.assertEqual("mock tag A", tag.comment()) + self.assertEqual( + { + "key1": "value1", + "key2": "value2", + }, + tag.properties(), + ) + + def _check_default_tag_b(self, tag: Tag) -> None: + self.assertEqual("tagB", tag.name()) + self.assertEqual("mock tag B", tag.comment()) + self.assertEqual( + { + "key1": "value1", + "key2": "value2", + }, + tag.properties(), + ) + + def check_tag_equal(self, left: Tag, right: Tag) -> None: + self.assertEqual(left.name(), right.name()) + self.assertEqual(left.comment(), right.comment()) + self.assertEqual(left.properties(), right.properties())
