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 70abed164f [#9241] feat(python-client): Add tag-related interface
(#9242)
70abed164f is described below
commit 70abed164f2f77d6c69e62c794ee0b03bc840214
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))