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

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

commit 5b6a5f67accdfb32698d1b92e5d38a1c4deaccee
Author: Lord of Abyss <[email protected]>
AuthorDate: Fri Dec 12 18:47:31 2025 +0800

    [#9241] feat(python-client): Add tag-related interface (#9242)
    
    ### What changes were proposed in this pull request?
    
    Add tag-related interface:
    1. Add SupportsTags interface
    2. Add TagOperations interface
    3. Add TagChange and related dataclass
    
    
    ### Why are the changes needed?
    
    Fix: #9241
    
    ### Does this PR introduce _any_ user-facing change?
    
    no
    
    ### How was this patch tested?
    
    local test.
---
 .../client-python/gravitino/api/tag/__init__.py    |  10 ++
 .../gravitino/api/tag/supports_tags.py             |  20 +--
 clients/client-python/gravitino/api/tag/tag.py     |   9 +-
 .../client-python/gravitino/api/tag/tag_change.py  | 144 +++++++++++++++++++++
 .../gravitino/api/tag/tag_operations.py            | 135 +++++++++++++++++++
 .../gravitino/client/gravitino_client.py           | 103 ++++++++++++++-
 .../gravitino/client/gravitino_metalake.py         | 113 +++++++++++++++-
 .../tests/unittests/api/tag/test_tag_change.py     |  50 +++++++
 8 files changed, 563 insertions(+), 21 deletions(-)

diff --git a/clients/client-python/gravitino/api/tag/__init__.py 
b/clients/client-python/gravitino/api/tag/__init__.py
index 13a83393a9..28b1469c2f 100644
--- a/clients/client-python/gravitino/api/tag/__init__.py
+++ b/clients/client-python/gravitino/api/tag/__init__.py
@@ -14,3 +14,13 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+
+from __future__ import annotations
+
+from gravitino.api.tag.tag import Tag
+from gravitino.api.tag.tag_change import TagChange
+
+__all__ = [
+    "Tag",
+    "TagChange",
+]
diff --git a/clients/client-python/gravitino/api/tag/supports_tags.py 
b/clients/client-python/gravitino/api/tag/supports_tags.py
index 89b405f890..f1e9a20510 100644
--- a/clients/client-python/gravitino/api/tag/supports_tags.py
+++ b/clients/client-python/gravitino/api/tag/supports_tags.py
@@ -15,9 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
+from __future__ import annotations
 
 from abc import ABC, abstractmethod
-from typing import List
 
 from gravitino.api.tag.tag import Tag
 
@@ -29,20 +29,20 @@ class SupportsTags(ABC):
     """
 
     @abstractmethod
-    def list_tags(self) -> List[str]:
+    def list_tags(self) -> list[str]:
         """List all the tag names for the specific object.
 
         Returns:
-            List[str]: The list of tag names.
+            list[str]: The list of tag names.
         """
         pass
 
     @abstractmethod
-    def list_tags_info(self) -> List[Tag]:
+    def list_tags_info(self) -> list[Tag]:
         """List all the tags with details for the specific object.
 
         Returns:
-            List[Tag]: The list of tags.
+            list[Tag]: The list of tags.
         """
         pass
 
@@ -63,8 +63,8 @@ class SupportsTags(ABC):
 
     @abstractmethod
     def associate_tags(
-        self, tags_to_add: List[str], tags_to_remove: List[str]
-    ) -> List[str]:
+        self, tags_to_add: list[str], tags_to_remove: list[str]
+    ) -> list[str]:
         """Associate tags to the specific object.
 
         The `tags_to_add` will be added to the object, and the 
`tags_to_remove` will be removed from the object.
@@ -75,13 +75,13 @@ class SupportsTags(ABC):
         3. If the tag is already associated with the object, it will raise 
`TagAlreadyAssociatedException`.
 
         Args:
-            tags_to_add (List[str]): The arrays of tag name to be added to the 
object.
-            tags_to_remove (List[str]): The array of tag name to be removed 
from the object.
+            tags_to_add (list[str]): The arrays of tag name to be added to the 
object.
+            tags_to_remove (list[str]): The array of tag name to be removed 
from the object.
 
         Raises:
             TagAlreadyAssociatedException: If the tag is already associated 
with the object.
 
         Returns:
-            List[str]: The array of tag names that are associated with the 
object.
+            list[str]: The array of tag names that are associated with the 
object.
         """
         pass
diff --git a/clients/client-python/gravitino/api/tag/tag.py 
b/clients/client-python/gravitino/api/tag/tag.py
index 6e2a510461..db8ee0e06c 100644
--- a/clients/client-python/gravitino/api/tag/tag.py
+++ b/clients/client-python/gravitino/api/tag/tag.py
@@ -15,9 +15,10 @@
 # specific language governing permissions and limitations
 # under the License.
 
+from __future__ import annotations
 
 from abc import ABC, abstractmethod
-from typing import ClassVar, Dict, List, Optional
+from typing import ClassVar, Optional
 
 from gravitino.api.auditable import Auditable
 from gravitino.api.metadata_object import MetadataObject
@@ -38,11 +39,11 @@ class AssociatedObjects(ABC):
         return 0 if objects is None else len(objects)
 
     @abstractmethod
-    def objects(self) -> Optional[List[MetadataObject]]:
+    def objects(self) -> Optional[list[MetadataObject]]:
         """Get the associated objects.
 
         Returns:
-            Optional[List[MetadataObject]]: The list of objects that are 
associated with this tag..
+            Optional[list[MetadataObject]]: The list of objects that are 
associated with this tag..
         """
         pass
 
@@ -80,7 +81,7 @@ class Tag(Auditable):
         pass
 
     @abstractmethod
-    def properties(self) -> Dict[str, str]:
+    def properties(self) -> dict[str, str]:
         """Get the properties of the tag.
 
         Returns:
diff --git a/clients/client-python/gravitino/api/tag/tag_change.py 
b/clients/client-python/gravitino/api/tag/tag_change.py
new file mode 100644
index 0000000000..1bb84e991f
--- /dev/null
+++ b/clients/client-python/gravitino/api/tag/tag_change.py
@@ -0,0 +1,144 @@
+# 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
+
+from abc import ABC
+from dataclasses import dataclass, field
+
+from dataclasses_json import config
+
+
+class TagChange(ABC):
+    """Interface for supporting tag changes.
+    This interface will be used to provide tag modification operations for 
each tag."""
+
+    @staticmethod
+    def rename(new_name: str) -> RenameTag:
+        """
+        Create a tag change instance to rename the tag.
+
+        Args:
+            new_name (str): The new name of the tag.
+
+        Returns:
+            RenameTag: A tag change instance to rename the tag.
+        """
+        return TagChange.RenameTag(new_name)
+
+    @staticmethod
+    def update_comment(new_comment: str) -> UpdateTagComment:
+        """
+        Create a tag change instance to update the tag comment.
+
+        Args:
+            new_comment (str): The new comment of the tag.
+
+        Returns:
+            UpdateTagComment: A tag change instance to update the tag comment.
+        """
+        return TagChange.UpdateTagComment(new_comment)
+
+    @staticmethod
+    def set_property(tag_property: str, tag_value: str) -> SetProperty:
+        """
+        Creates a new tag change instance to set the property and value for 
the tag.
+
+        Args:
+            tag_property (str): The property to set.
+            tag_value (str): The value to set.
+
+        Returns:
+            SetProperty: The tag change instance to set the property and value 
for the tag.
+        """
+        return TagChange.SetProperty(tag_property, tag_value)
+
+    @staticmethod
+    def remove_property(tag_property: str) -> RemoveProperty:
+        """
+        Creates a new tag change instance to remove a property from the tag.
+
+        Args:
+            tag_property (str): The property to remove.
+
+        Returns:
+            RemoveProperty: The tag change instance to remove a property from 
the tag.
+        """
+        return TagChange.RemoveProperty(tag_property)
+
+    @dataclass(frozen=True, eq=True)
+    class RenameTag:
+        """A tag change to rename the tag."""
+
+        _new_name: str = field(metadata=config(field_name="newName"))
+
+        @property
+        def new_name(self) -> str:
+            """The new name of the tag."""
+            return self._new_name
+
+        def __str__(self) -> str:
+            return f"RENAMETAG {self._new_name}"
+
+    @dataclass(frozen=True, eq=True)
+    class UpdateTagComment:
+        """A tag change to update the tag comment."""
+
+        _new_comment: str = field(metadata=config(field_name="newComment"))
+
+        @property
+        def new_comment(self) -> str:
+            """The new comment of the tag."""
+            return self._new_comment
+
+        def __str__(self) -> str:
+            return f"UPDATETAGCOMMENT {self._new_comment}"
+
+    @dataclass(frozen=True, eq=True)
+    class SetProperty:
+        """A tag change to set a property-value pair for the tag."""
+
+        _property: str = field(metadata=config(field_name="property"))
+        _value: str = field(metadata=config(field_name="value"))
+
+        @property
+        def name(self) -> str:
+            """The property to set."""
+            return self._property
+
+        @property
+        def value(self) -> str:
+            """The value to set."""
+            return self._value
+
+        def __str__(self) -> str:
+            return f"SETTAGPROPERTY {self._property} = {self._value}"
+
+    @dataclass(frozen=True, eq=True)
+    class RemoveProperty:
+        """A tag change to remove a property-value pair for the tag."""
+
+        _property: str = field(metadata=config(field_name="property"))
+
+        @property
+        def removed_property(self) -> str:
+            """The property to remove."""
+            return self._property
+
+        def __str__(self) -> str:
+            return f"REMOVETAGPROPERTY {self._property}"
diff --git a/clients/client-python/gravitino/api/tag/tag_operations.py 
b/clients/client-python/gravitino/api/tag/tag_operations.py
new file mode 100644
index 0000000000..58bbce2657
--- /dev/null
+++ b/clients/client-python/gravitino/api/tag/tag_operations.py
@@ -0,0 +1,135 @@
+# 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
+
+from abc import ABC, abstractmethod
+
+from gravitino.api.tag.tag import Tag
+from gravitino.api.tag.tag_change import TagChange
+
+
+class TagOperations(ABC):
+    """
+    Interface for supporting global tag operations. This interface will 
provide tag listing, getting,
+    creating, and other tag operations under a metalake. This interface will 
be mixed with
+    GravitinoMetalake or GravitinoClient to provide tag operations.
+    """
+
+    @abstractmethod
+    def list_tags(self) -> list[str]:
+        """List all the tag names under a metalake.
+
+        Returns:
+            list[str]: The list of tag names.
+
+        Raises:
+            NoSuchMetalakeException: If the metalake does not exist.
+        """
+        pass
+
+    @abstractmethod
+    def list_tags_info(self) -> list[Tag]:
+        """
+        List tags information under a metalake.
+
+        Returns:
+            list[Tag]: The list of tag information.
+
+        Raises:
+            NoSuchMetalakeException: If the metalake does not exist.
+        """
+        pass
+
+    @abstractmethod
+    def get_tag(self, tag_name: str) -> Tag:
+        """
+        Get a tag by its name under a metalake.
+
+        Args:
+            tag_name (str): The name of the tag.
+
+        Returns:
+            Tag: The tag information.
+
+        Raises:
+            NoSuchTagException: If the tag does not exist.
+        """
+        pass
+
+    @abstractmethod
+    def create_tag(
+        self,
+        tag_name: str,
+        comment: str,
+        properties: dict[str, str],
+    ) -> Tag:
+        """
+        Create a new tag under a metalake.
+
+        Raises:
+            NoSuchMetalakeException: If the metalake does not exist.
+            TagAlreadyExistsException: If the tag already exists.
+
+        Args:
+            tag_name (str): The name of the tag.
+            comment (str): The comment of the tag.
+            properties (dict[str, str]): The properties of the tag.
+
+        Returns:
+            Tag: The tag information.
+        """
+        pass
+
+    @abstractmethod
+    def alter_tag(
+        self,
+        tag_name: str,
+        *changes: TagChange,
+    ) -> Tag:
+        """
+        Alter a tag under a metalake.
+
+        Args:
+            tag_name (str): The name of the tag.
+            changes (TagChange): The changes to apply to the tag.
+
+        Returns:
+            Tag: The altered tag.
+
+        Raises:
+            NoSuchTagException: If the tag does not exist.
+            NoSuchMetalakeException: If the metalake does not exist.
+        """
+        pass
+
+    @abstractmethod
+    def delete_tag(self, tag_name: str) -> bool:
+        """
+        Delete a tag under a metalake.
+
+        Args:
+            tag_name (str): The name of the tag.
+
+        Returns:
+            bool: True if the tag was deleted, False otherwise.
+
+        Raises:
+            NoSuchTagException: If the tag does not exist.
+            NoSuchMetalakeException: If the metalake does not exist.
+        """
+        pass
diff --git a/clients/client-python/gravitino/client/gravitino_client.py 
b/clients/client-python/gravitino/client/gravitino_client.py
index 02b61cb774..862b4bce33 100644
--- a/clients/client-python/gravitino/client/gravitino_client.py
+++ b/clients/client-python/gravitino/client/gravitino_client.py
@@ -15,7 +15,9 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from typing import List, Dict
+from __future__ import annotations
+
+from typing import Dict, List, Optional
 
 from gravitino.api.catalog import Catalog
 from gravitino.api.catalog_change import CatalogChange
@@ -23,12 +25,15 @@ from gravitino.api.job.job_handle import JobHandle
 from gravitino.api.job.job_template import JobTemplate
 from gravitino.api.job.job_template_change import JobTemplateChange
 from gravitino.api.job.supports_jobs import SupportsJobs
+from gravitino.api.tag.tag_operations import TagOperations
 from gravitino.auth.auth_data_provider import AuthDataProvider
 from gravitino.client.gravitino_client_base import GravitinoClientBase
 from gravitino.client.gravitino_metalake import GravitinoMetalake
 
+from ..api.tag.tag import Tag
+
 
-class GravitinoClient(GravitinoClientBase, SupportsJobs):
+class GravitinoClient(GravitinoClientBase, SupportsJobs, TagOperations):
     """Gravitino Client for a user to interact with the Gravitino API, 
allowing the client to list,
     load, create, and alter Catalog.
 
@@ -43,8 +48,8 @@ class GravitinoClient(GravitinoClientBase, SupportsJobs):
         metalake_name: str,
         check_version: bool = True,
         auth_data_provider: AuthDataProvider = None,
-        request_headers: dict = None,
-        client_config: dict = None,
+        request_headers: Optional[dict] = None,
+        client_config: Optional[dict] = None,
     ):
         """Constructs a new GravitinoClient with the given URI, authenticator 
and AuthDataProvider.
 
@@ -235,3 +240,93 @@ class GravitinoClient(GravitinoClientBase, SupportsJobs):
             NoSuchJobException: If no job with the specified ID exists.
         """
         return self.get_metalake().cancel_job(job_id)
+
+    # Tag operations
+    def list_tags(self) -> list[str]:
+        """List all the tag names under a metalake.
+
+        Returns:
+            list[str]: The list of tag names.
+
+        Raises:
+            NoSuchMetalakeException: If the metalake does not exist.
+        """
+        return self.get_metalake().list_tags()
+
+    def list_tags_info(self) -> list[Tag]:
+        """
+        List tags information under a metalake.
+
+        Returns:
+            list[Tag]: The list of tag information.
+
+        Raises:
+            NoSuchMetalakeException: If the metalake does not exist.
+        """
+        return self.get_metalake().list_tags_info()
+
+    def get_tag(self, tag_name) -> Tag:
+        """
+        Get a tag by its name under a metalake.
+
+        Args:
+            tag_name (str): The name of the tag.
+
+        Returns:
+            Tag: The tag information.
+
+        Raises:
+            NoSuchTagException: If the tag does not exist.
+        """
+        return self.get_metalake().get_tag(tag_name)
+
+    def create_tag(self, tag_name, comment, properties) -> Tag:
+        """
+        Create a new tag under a metalake.
+
+        Raises:
+            NoSuchMetalakeException: If the metalake does not exist.
+            TagAlreadyExistsException: If the tag already exists.
+
+        Args:
+            tag_name (str): The name of the tag.
+            comment (str): The comment of the tag.
+            properties (dict[str, str]): The properties of the tag.
+
+        Returns:
+            Tag: The tag information.
+        """
+        return self.get_metalake().create_tag(tag_name, comment, properties)
+
+    def alter_tag(self, tag_name, *changes) -> Tag:
+        """
+        Alter a tag under a metalake.
+
+        Args:
+            tag_name (str): The name of the tag.
+            changes (TagChange): The changes to apply to the tag.
+
+        Returns:
+            Tag: The altered tag.
+
+        Raises:
+            NoSuchTagException: If the tag does not exist.
+            NoSuchMetalakeException: If the metalake does not exist.
+        """
+        return self.get_metalake().alter_tag(tag_name, *changes)
+
+    def delete_tag(self, tag_name) -> bool:
+        """
+        Delete a tag under a metalake.
+
+        Args:
+            tag_name (str): The name of the tag.
+
+        Returns:
+            bool: True if the tag was deleted, False otherwise.
+
+        Raises:
+            NoSuchTagException: If the tag does not exist.
+            NoSuchMetalakeException: If the metalake does not exist.
+        """
+        return self.get_metalake().delete_tag(tag_name)
diff --git a/clients/client-python/gravitino/client/gravitino_metalake.py 
b/clients/client-python/gravitino/client/gravitino_metalake.py
index 86ae92b812..a10437ce04 100644
--- a/clients/client-python/gravitino/client/gravitino_metalake.py
+++ b/clients/client-python/gravitino/client/gravitino_metalake.py
@@ -16,7 +16,7 @@
 # under the License.
 
 import logging
-from typing import List, Dict
+from typing import Dict, List
 
 from gravitino.api.catalog import Catalog
 from gravitino.api.catalog_change import CatalogChange
@@ -24,6 +24,8 @@ from gravitino.api.job.job_handle import JobHandle
 from gravitino.api.job.job_template import JobTemplate
 from gravitino.api.job.job_template_change import JobTemplateChange
 from gravitino.api.job.supports_jobs import SupportsJobs
+from gravitino.api.tag.tag import Tag
+from gravitino.api.tag.tag_operations import TagOperations
 from gravitino.client.dto_converters import DTOConverters
 from gravitino.client.generic_job_handle import GenericJobHandle
 from gravitino.dto.metalake_dto import MetalakeDTO
@@ -50,11 +52,14 @@ from gravitino.exceptions.handlers.job_error_handler import 
JOB_ERROR_HANDLER
 from gravitino.rest.rest_utils import encode_string
 from gravitino.utils import HTTPClient
 
-
 logger = logging.getLogger(__name__)
 
 
-class GravitinoMetalake(MetalakeDTO, SupportsJobs):
+class GravitinoMetalake(
+    MetalakeDTO,
+    SupportsJobs,
+    TagOperations,
+):
     """
     Gravitino Metalake is the top-level metadata repository for users. It 
contains a list of catalogs
     as sub-level metadata collections. With GravitinoMetalake, users can list, 
create, load,
@@ -275,6 +280,10 @@ class GravitinoMetalake(MetalakeDTO, SupportsJobs):
             url, json=catalog_disable_request, 
error_handler=CATALOG_ERROR_HANDLER
         )
 
+    ##########
+    # Job operations
+    ##########
+
     def list_job_templates(self) -> List[JobTemplate]:
         """List all the registered job templates in Gravitino.
 
@@ -497,3 +506,101 @@ class GravitinoMetalake(MetalakeDTO, SupportsJobs):
         resp.validate()
 
         return GenericJobHandle(resp.job())
+
+    #########
+    # Tag operations
+    #########
+    def list_tags(self) -> list[str]:
+        """List all the tag names under a metalake.
+
+        Returns:
+            list[str]: The list of tag names.
+
+        Raises:
+            NoSuchMetalakeException: If the metalake does not exist.
+        """
+        # TODO implement list_tags
+        raise NotImplementedError()
+
+    def list_tags_info(self) -> List[Tag]:
+        """
+        List tags information under a metalake.
+
+        Returns:
+            list[Tag]: The list of tag information.
+
+        Raises:
+            NoSuchMetalakeException: If the metalake does not exist.
+        """
+        # TODO implement list_tags_info
+        raise NotImplementedError()
+
+    def get_tag(self, tag_name) -> Tag:
+        """
+        Get a tag by its name under a metalake.
+
+        Args:
+            tag_name (str): The name of the tag.
+
+        Returns:
+            Tag: The tag information.
+
+        Raises:
+            NoSuchTagException: If the tag does not exist.
+        """
+        # TODO implement get_tag
+        raise NotImplementedError()
+
+    def create_tag(self, tag_name, comment, properties) -> Tag:
+        """
+        Create a new tag under a metalake.
+
+        Raises:
+            NoSuchMetalakeException: If the metalake does not exist.
+            TagAlreadyExistsException: If the tag already exists.
+
+        Args:
+            tag_name (str): The name of the tag.
+            comment (str): The comment of the tag.
+            properties (dict[str, str]): The properties of the tag.
+
+        Returns:
+            Tag: The tag information.
+        """
+        # TODO implement create_tag
+        raise NotImplementedError()
+
+    def alter_tag(self, tag_name, *changes) -> Tag:
+        """
+        Alter a tag under a metalake.
+
+        Args:
+            tag_name (str): The name of the tag.
+            changes (TagChange): The changes to apply to the tag.
+
+        Returns:
+            Tag: The altered tag.
+
+        Raises:
+            NoSuchTagException: If the tag does not exist.
+            NoSuchMetalakeException: If the metalake does not exist.
+        """
+        # TODO implement alter_tag
+        raise NotImplementedError()
+
+    def delete_tag(self, tag_name) -> bool:
+        """
+        Delete a tag under a metalake.
+
+        Args:
+            tag_name (str): The name of the tag.
+
+        Returns:
+            bool: True if the tag was deleted, False otherwise.
+
+        Raises:
+            NoSuchTagException: If the tag does not exist.
+            NoSuchMetalakeException: If the metalake does not exist.
+        """
+        # TODO implement delete_tag
+        raise NotImplementedError()
diff --git a/clients/client-python/tests/unittests/api/tag/test_tag_change.py 
b/clients/client-python/tests/unittests/api/tag/test_tag_change.py
new file mode 100644
index 0000000000..eb0a24c0dc
--- /dev/null
+++ b/clients/client-python/tests/unittests/api/tag/test_tag_change.py
@@ -0,0 +1,50 @@
+# 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 unittest
+
+from gravitino.api.tag.tag_change import TagChange
+
+
+class TestTagChange(unittest.TestCase):
+    def test_rename(self) -> None:
+        expected_new_name = "new_name"
+        tag_change = TagChange.rename(expected_new_name)
+        self.assertEqual(expected_new_name, tag_change.new_name)
+        self.assertEqual(f"RENAMETAG {expected_new_name}", str(tag_change))
+
+    def test_update_comment(self) -> None:
+        expected_new_comment = "new_comment"
+        tag_change = TagChange.update_comment(expected_new_comment)
+        self.assertEqual(expected_new_comment, tag_change.new_comment)
+        self.assertEqual(f"UPDATETAGCOMMENT {expected_new_comment}", 
str(tag_change))
+
+    def test_set_property(self) -> None:
+        expected_property = "property"
+        expected_value = "value"
+        tag_change = TagChange.set_property(expected_property, expected_value)
+        self.assertEqual(expected_property, tag_change.name)
+        self.assertEqual(expected_value, tag_change.value)
+        self.assertEqual(
+            f"SETTAGPROPERTY {expected_property} = {expected_value}", 
str(tag_change)
+        )
+
+    def test_remove_property(self) -> None:
+        expected_property = "property"
+        tag_change = TagChange.remove_property(expected_property)
+        self.assertEqual(expected_property, tag_change.removed_property)
+        self.assertEqual(f"REMOVETAGPROPERTY {expected_property}", 
str(tag_change))

Reply via email to