This is an automated email from the ASF dual-hosted git repository.

jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new dfac744213 [#9882] Add implementation for TagOperations in python 
client(part2) (#10554)
dfac744213 is described below

commit dfac744213ce2ae81452a96207cf00fee1e8f5d4
Author: Lord of Abyss <[email protected]>
AuthorDate: Thu Apr 2 18:03:04 2026 +0800

    [#9882] Add implementation for TagOperations in python client(part2) 
(#10554)
    
    ### What changes were proposed in this pull request?
    
    Add rest implementation for `TagOperations` in python client
    
    ### Why are the changes needed?
    
    Fix: #9882
    
    ### Does this PR introduce _any_ user-facing change?
    
    Users can now manage tags using the `GravitinoClient`.
    
    ### How was this patch tested?
    
    local unittest
---
 .../gravitino/client/dto_converters.py             |  40 ++
 .../gravitino/client/gravitino_metalake.py         |  76 +++-
 .../tests/unittests/client/__init__.py             |  16 +
 .../tests/unittests/client/test_dto_converters.py  | 100 +++++
 clients/client-python/tests/unittests/mock_base.py | 153 ++++++-
 .../client-python/tests/unittests/test_tag_api.py  | 481 +++++++++++++++++++++
 6 files changed, 856 insertions(+), 10 deletions(-)

diff --git a/clients/client-python/gravitino/client/dto_converters.py 
b/clients/client-python/gravitino/client/dto_converters.py
index bc24cde268..925063a41b 100644
--- a/clients/client-python/gravitino/client/dto_converters.py
+++ b/clients/client-python/gravitino/client/dto_converters.py
@@ -15,6 +15,8 @@
 # specific language governing permissions and limitations
 # under the License.
 
+from __future__ import annotations
+
 from gravitino.api.catalog import Catalog
 from gravitino.api.catalog_change import CatalogChange
 from gravitino.api.job.job_template import JobTemplate, JobType
@@ -30,6 +32,7 @@ from gravitino.api.job.job_template_change import (
 from gravitino.api.job.shell_job_template import ShellJobTemplate
 from gravitino.api.job.spark_job_template import SparkJobTemplate
 from gravitino.api.metalake_change import MetalakeChange
+from gravitino.api.tag.tag_change import TagChange
 from gravitino.client.fileset_catalog import FilesetCatalog
 from gravitino.client.generic_model_catalog import GenericModelCatalog
 from gravitino.client.relational_catalog import RelationalCatalog
@@ -48,6 +51,11 @@ from gravitino.dto.requests.job_template_update_request 
import (
     UpdateJobTemplateContentRequest,
 )
 from gravitino.dto.requests.metalake_update_request import 
MetalakeUpdateRequest
+from gravitino.dto.requests.tag_update_request import (
+    TagUpdateRequest,
+    TagUpdateRequestBase,
+)
+from gravitino.exceptions.base import IllegalArgumentException
 from gravitino.namespace import Namespace
 from gravitino.utils import HTTPClient
 
@@ -285,3 +293,35 @@ class DTOConverters:
             return UpdateJobTemplateContentRequest(template_update_dto)
 
         raise ValueError(f"Unknown change type: {type(change).__name__}")
+
+    @staticmethod
+    def to_tag_update_request(
+        change: TagChange,
+    ) -> TagUpdateRequestBase:
+        """
+        Converts a TagChange to a TagUpdateRequestBase.
+
+        Args:
+            change (TagChange): the TagChange to convert.
+
+        Raises:
+            IllegalArgumentException: if the change is not a valid TagChange.
+
+        Returns:
+            TagUpdateRequestBase: the TagUpdateRequestBase corresponding to 
the change.
+        """
+        if isinstance(change, TagChange.RenameTag):
+            return TagUpdateRequest.RenameTagRequest(change.new_name)
+
+        if isinstance(change, TagChange.UpdateTagComment):
+            return TagUpdateRequest.UpdateTagCommentRequest(change.new_comment)
+
+        if isinstance(change, TagChange.SetProperty):
+            return TagUpdateRequest.SetTagPropertyRequest(
+                change.name,
+                change.value,
+            )
+        if isinstance(change, TagChange.RemoveProperty):
+            return 
TagUpdateRequest.RemoveTagPropertyRequest(change.removed_property)
+
+        raise IllegalArgumentException(f"Unknown change type: {type(change)}")
diff --git a/clients/client-python/gravitino/client/gravitino_metalake.py 
b/clients/client-python/gravitino/client/gravitino_metalake.py
index 00b7ae3694..a88dc52711 100644
--- a/clients/client-python/gravitino/client/gravitino_metalake.py
+++ b/clients/client-python/gravitino/client/gravitino_metalake.py
@@ -41,6 +41,7 @@ from gravitino.dto.requests.job_template_updates_request 
import (
     JobTemplateUpdatesRequest,
 )
 from gravitino.dto.requests.tag_create_request import TagCreateRequest
+from gravitino.dto.requests.tag_updates_request import TagUpdatesRequest
 from gravitino.dto.responses.catalog_list_response import CatalogListResponse
 from gravitino.dto.responses.catalog_response import CatalogResponse
 from gravitino.dto.responses.drop_response import DropResponse
@@ -49,13 +50,18 @@ from gravitino.dto.responses.job_list_response import 
JobListResponse
 from gravitino.dto.responses.job_response import JobResponse
 from gravitino.dto.responses.job_template_list_response import 
JobTemplateListResponse
 from gravitino.dto.responses.job_template_response import JobTemplateResponse
-from gravitino.dto.responses.tag_response import TagNamesListResponse, 
TagResponse
+from gravitino.dto.responses.tag_response import (
+    TagListResponse,
+    TagNamesListResponse,
+    TagResponse,
+)
 from gravitino.exceptions.handlers.catalog_error_handler import 
CATALOG_ERROR_HANDLER
 from gravitino.exceptions.handlers.job_error_handler import JOB_ERROR_HANDLER
 from gravitino.exceptions.handlers.tag_error_handler import TAG_ERROR_HANDLER
 from gravitino.rest.rest_utils import encode_string
 from gravitino.utils.http_client import HTTPClient
 from gravitino.utils.precondition import Precondition
+from gravitino.utils.string_utils import StringUtils
 
 logger = logging.getLogger(__name__)
 
@@ -544,8 +550,24 @@ class GravitinoMetalake(
         Raises:
             NoSuchMetalakeException: If the metalake does not exist.
         """
-        # TODO implement list_tags_info
-        raise NotImplementedError()
+        url = self.API_METALAKES_TAGS_PATH.format(encode_string(self.name()))
+
+        response = self.rest_client.get(
+            url,
+            params={"details": "true"},
+            error_handler=TAG_ERROR_HANDLER,
+        )
+        list_info_resp = TagListResponse.from_json(response.body, 
infer_missing=True)
+        list_info_resp.validate()
+
+        return [
+            GenericTag(
+                self.name(),
+                tag_dto,
+                self.rest_client,
+            )
+            for tag_dto in list_info_resp.tags()
+        ]
 
     def get_tag(self, tag_name) -> Tag:
         """
@@ -623,8 +645,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.post(
+            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:
         """
@@ -640,5 +688,19 @@ class GravitinoMetalake(
             NoSuchTagException: If the tag does not exist.
             NoSuchMetalakeException: If the metalake does not exist.
         """
-        # TODO implement delete_tag
-        raise NotImplementedError()
+        Precondition.check_argument(
+            StringUtils.is_not_blank(tag_name),
+            "tag name must not be null or empty",
+        )
+
+        url = self.API_METALAKES_TAG_PATH.format(
+            encode_string(self.name()), encode_string(tag_name)
+        )
+        response = self.rest_client.delete(
+            url,
+            error_handler=TAG_ERROR_HANDLER,
+        )
+        drop_response = DropResponse.from_json(response.body, 
infer_missing=True)
+        drop_response.validate()
+
+        return drop_response.dropped()
diff --git a/clients/client-python/tests/unittests/client/__init__.py 
b/clients/client-python/tests/unittests/client/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/clients/client-python/tests/unittests/client/__init__.py
@@ -0,0 +1,16 @@
+# 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.
diff --git 
a/clients/client-python/tests/unittests/client/test_dto_converters.py 
b/clients/client-python/tests/unittests/client/test_dto_converters.py
new file mode 100644
index 0000000000..d99cde1124
--- /dev/null
+++ b/clients/client-python/tests/unittests/client/test_dto_converters.py
@@ -0,0 +1,100 @@
+# 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.
+
+
+import json as _json
+import unittest
+
+from gravitino.api.tag.tag_change import TagChange
+from gravitino.client.dto_converters import DTOConverters
+from gravitino.dto.requests.tag_update_request import TagUpdateRequest
+from gravitino.exceptions.base import IllegalArgumentException
+
+
+class TestDtoConverters(unittest.TestCase):
+    def test_to_tag_update_request_with_name(self) -> None:
+        new_tag_name = "new_tag_name"
+        change = TagChange.rename(new_tag_name)
+        rename_request = DTOConverters.to_tag_update_request(change)
+
+        self.assertTrue(isinstance(rename_request, 
TagUpdateRequest.RenameTagRequest))
+        self.assertEqual(new_tag_name, rename_request.new_name)
+
+        json_str = _json.dumps(
+            {
+                "@type": "rename",
+                "newName": f"{new_tag_name}",
+            }
+        )
+        self.assertEqual(json_str, rename_request.to_json())
+
+    def test_to_tag_update_request_with_comment(self) -> None:
+        new_comment = "new_comment"
+        change = TagChange.update_comment(new_comment)
+        update_comment_request = DTOConverters.to_tag_update_request(change)
+
+        self.assertTrue(
+            isinstance(update_comment_request, 
TagUpdateRequest.UpdateTagCommentRequest)
+        )
+
+        json_str = _json.dumps(
+            {
+                "@type": "updateComment",
+                "newComment": f"{new_comment}",
+            }
+        )
+        self.assertEqual(json_str, update_comment_request.to_json())
+
+    def test_to_tag_update_request_with_new_property(self) -> None:
+        new_prop = "key"
+        new_value = "value"
+        change = TagChange.set_property(new_prop, new_value)
+        set_property_request = DTOConverters.to_tag_update_request(change)
+
+        self.assertTrue(
+            isinstance(set_property_request, 
TagUpdateRequest.SetTagPropertyRequest)
+        )
+        json_str = _json.dumps(
+            {
+                "@type": "setProperty",
+                "property": f"{new_prop}",
+                "value": f"{new_value}",
+            }
+        )
+        self.assertEqual(json_str, set_property_request.to_json())
+
+    def test_to_tag_update_request_with_remove_property(self) -> None:
+        removed_prop = "key"
+        change = TagChange.remove_property(removed_prop)
+        remove_property_request = DTOConverters.to_tag_update_request(change)
+
+        self.assertTrue(
+            isinstance(
+                remove_property_request, 
TagUpdateRequest.RemoveTagPropertyRequest
+            )
+        )
+        json_str = _json.dumps(
+            {
+                "@type": "removeProperty",
+                "property": f"{removed_prop}",
+            }
+        )
+        self.assertEqual(json_str, remove_property_request.to_json())
+
+    def test_to_tag_update_request_with_unsupport_type(self) -> None:
+        with self.assertRaises(IllegalArgumentException):
+            DTOConverters.to_tag_update_request(None)
diff --git a/clients/client-python/tests/unittests/mock_base.py 
b/clients/client-python/tests/unittests/mock_base.py
index c4e9c2910a..8ce25fad67 100644
--- a/clients/client-python/tests/unittests/mock_base.py
+++ b/clients/client-python/tests/unittests/mock_base.py
@@ -16,20 +16,57 @@
 # under the License.
 
 import json
-from unittest.mock import patch
+import typing as tp
+from contextlib import contextmanager
+from http.client import HTTPResponse
+from unittest.mock import MagicMock, Mock, patch
 
-from gravitino import GravitinoMetalake, Catalog, Fileset
+from gravitino import Catalog, Fileset, GravitinoMetalake
+from gravitino.api.tag.tag_change import TagChange
 from gravitino.client.fileset_catalog import FilesetCatalog
 from gravitino.client.generic_fileset import GenericFileset
 from gravitino.client.generic_model_catalog import GenericModelCatalog
-from gravitino.dto.fileset_dto import FilesetDTO
 from gravitino.dto.audit_dto import AuditDTO
+from gravitino.dto.fileset_dto import FilesetDTO
 from gravitino.dto.metalake_dto import MetalakeDTO
 from gravitino.dto.schema_dto import SchemaDTO
+from gravitino.dto.tag_dto import TagDTO
 from gravitino.namespace import Namespace
+from gravitino.utils import Response
 from gravitino.utils.http_client import HTTPClient
 
 
+def build_tag_dto(
+    name: str = "tagA",
+    comment: str = "commentA",
+    properties: tp.Optional[dict[str, str]] = None,
+) -> TagDTO:
+    if properties is None:
+        properties = {
+            "key1": "value1",
+            "key2": "value2",
+        }
+
+    return (
+        TagDTO.builder()
+        .name(name)
+        .comment(comment)
+        .properties(properties)
+        .audit_info(build_audit_info())
+        .inherited(False)
+        .build()
+    )
+
+
+def build_audit_info() -> AuditDTO:
+    return AuditDTO(
+        _creator="test",
+        _create_time="2022-01-01T00:00:00Z",
+        _last_modifier="test",
+        _last_modified_time="2024-04-05T10:10:35.218Z",
+    )
+
+
 def mock_load_metalake():
     audit_dto = AuditDTO(
         _creator="test",
@@ -154,3 +191,113 @@ def mock_data(cls):
 
 def mock_name_identifier_json(name, namespace):
     return json.dumps({"name": name, "namespace": namespace}).encode("utf-8")
+
+
+class MockTagRepo:
+    def __init__(self) -> None:
+        self.tag_store = {
+            "tagA": build_tag_dto("tagA", "mock tag A"),
+            "tagB": build_tag_dto("tagB", "mock tag B"),
+        }
+
+    def mock_list_tags(self) -> list[str]:
+        return list(self.tag_store.keys())
+
+    def mock_list_tags_info(self) -> list[TagDTO]:
+        return list(self.tag_store.values())
+
+    def mock_get_tag(self, tag_name: str) -> TagDTO:
+        if tag_name not in self.tag_store:
+            raise ValueError(f"Tag {tag_name} does not exist")
+        return self.tag_store[tag_name]
+
+    def mock_create_tag(
+        self,
+        tag_name: str,
+        comment: str = "",
+        properties=None,
+    ) -> TagDTO:
+        if tag_name in self.tag_store:
+            raise ValueError(f"Tag {tag_name} already exists")
+        self.tag_store[tag_name] = build_tag_dto(tag_name, comment, properties)
+        return self.tag_store[tag_name]
+
+    def mock_alter_tag(self, tag_name: str, *changes) -> TagDTO:
+        if tag_name not in self.tag_store:
+            raise ValueError(f"Tag {tag_name} does not exist")
+
+        for change in changes:
+            current_tag_obj = self.tag_store[tag_name]
+
+            if isinstance(change, TagChange.RenameTag):
+                self.tag_store[change.new_name] = build_tag_dto(
+                    change.new_name,
+                    current_tag_obj.comment(),
+                    dict(current_tag_obj.properties()),
+                )
+                del self.tag_store[tag_name]
+                tag_name = change.new_name
+
+            elif isinstance(change, TagChange.UpdateTagComment):
+                self.tag_store[tag_name] = build_tag_dto(
+                    current_tag_obj.name(),
+                    change.new_comment,
+                    dict(current_tag_obj.properties()),
+                )
+
+            elif isinstance(change, TagChange.RemoveProperty):
+                new_properties = dict(current_tag_obj.properties())
+                new_properties.pop(change.removed_property, None)
+
+                self.tag_store[tag_name] = build_tag_dto(
+                    current_tag_obj.name(),
+                    current_tag_obj.comment(),
+                    new_properties,
+                )
+
+            elif isinstance(change, TagChange.SetProperty):
+                new_properties = dict(current_tag_obj.properties())
+                new_properties[change.name] = change.value
+
+                self.tag_store[tag_name] = build_tag_dto(
+                    current_tag_obj.name(),
+                    current_tag_obj.comment(),
+                    new_properties,
+                )
+
+            else:
+                raise ValueError(f"Unknown tag change type: {change}")
+
+        return self.tag_store[tag_name]
+
+    def mock_delete_tag(self, tag_name: str) -> bool:
+        if tag_name not in self.tag_store:
+            return False
+        del self.tag_store[tag_name]
+        return True
+
+
+@contextmanager
+def mock_tag_methods():
+    repo = MockTagRepo()
+
+    with patch.multiple(
+        GravitinoMetalake,
+        list_tags=MagicMock(side_effect=repo.mock_list_tags),
+        list_tags_info=MagicMock(side_effect=repo.mock_list_tags_info),
+        get_tag=MagicMock(side_effect=repo.mock_get_tag),
+        create_tag=MagicMock(side_effect=repo.mock_create_tag),
+        alter_tag=MagicMock(side_effect=repo.mock_alter_tag),
+        delete_tag=MagicMock(side_effect=repo.mock_delete_tag),
+    ) as mocks:
+        yield mocks, repo
+
+
+def mock_http_response(json_str: str) -> Response:
+    mock_http_resp = Mock(HTTPResponse)
+    mock_http_resp.getcode.return_value = 200
+    mock_http_resp.read.return_value = json_str.encode("utf-8")
+    mock_http_resp.info.return_value = None
+    mock_http_resp.url = None
+    mock_resp = Response(mock_http_resp)
+    return mock_resp
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..cb5e40d281
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_tag_api.py
@@ -0,0 +1,481 @@
+# 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.post",
+            return_value=mock_resp,
+        ):
+            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(),
+            )
+
+    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())


Reply via email to