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

jerryshao 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 1be561eb6b [#10849] feat(core): Add IT for SupportsTags. (#10861)
1be561eb6b is described below

commit 1be561eb6b299e5723b4886c8d563b5ca2cbb468
Author: Lord of Abyss <[email protected]>
AuthorDate: Fri May 8 16:08:32 2026 +0800

    [#10849] feat(core): Add IT for SupportsTags. (#10861)
    
    ### What changes were proposed in this pull request?
    
    Add IT for SupportsTags.
    
    ### Why are the changes needed?
    
    Fix: #10849
    
    ### Does this PR introduce _any_ user-facing change?
    
    no
    
    ### How was this patch tested?
    
    it test.
    
    ---------
    
    Co-authored-by: Copilot <[email protected]>
---
 clients/client-python/gravitino/api/catalog.py     |  19 +-
 .../client-python/gravitino/api/file/fileset.py    |   7 +-
 clients/client-python/gravitino/api/model/model.py |   7 +-
 clients/client-python/gravitino/api/rel/table.py   |   5 +
 clients/client-python/gravitino/api/schema.py      |   7 +-
 .../gravitino/api/supports_schemas.py              |   9 +-
 .../gravitino/client/base_schema_catalog.py        |   3 +
 .../gravitino/client/generic_fileset.py            |   3 +
 .../gravitino/client/generic_model.py              |   3 +
 .../gravitino/client/generic_schema.py             |   3 +
 .../gravitino/client/relational_table.py           |   3 +
 .../tests/integration/containers/base_container.py |  23 +-
 .../tests/integration/test_supports_tags.py        | 423 +++++++++++++++++++++
 13 files changed, 497 insertions(+), 18 deletions(-)

diff --git a/clients/client-python/gravitino/api/catalog.py 
b/clients/client-python/gravitino/api/catalog.py
index 34c916f7eb..5e97fe8455 100644
--- a/clients/client-python/gravitino/api/catalog.py
+++ b/clients/client-python/gravitino/api/catalog.py
@@ -21,6 +21,7 @@ from typing import Dict, Optional
 
 from gravitino.api.auditable import Auditable
 from gravitino.api.supports_schemas import SupportsSchemas
+from gravitino.api.tag.supports_tags import SupportsTags
 
 
 class Catalog(Auditable):
@@ -179,17 +180,17 @@ class Catalog(Auditable):
         """
         raise UnsupportedOperationException("Catalog does not support topic 
operations")
 
-    def as_model_catalog(self) -> "ModelCatalog":  # noqa: F821
+    def as_model_catalog(self) -> "GenericModelCatalog":
         """
         Returns:
-            the {@link ModelCatalog} if the catalog supports model operations.
+            the {@link GenericModelCatalog} if the catalog supports model 
operations.
 
         Raises:
             UnsupportedOperationException if the catalog does not support 
model operations.
         """
         raise UnsupportedOperationException("Catalog does not support model 
operations")
 
-    def as_function_catalog(self) -> "FunctionCatalog":  # noqa: F821
+    def as_function_catalog(self) -> "FunctionCatalog":
         """
         Returns:
             the {@link FunctionCatalog} if the catalog supports function 
operations.
@@ -201,6 +202,18 @@ class Catalog(Auditable):
             "Catalog does not support function operations"
         )
 
+    def supports_tags(self) -> SupportsTags:
+        """
+        Return the {@link SupportsTags} if the catalog supports tag operations.
+
+        Raises:
+            UnsupportedOperationException: if the catalog does not support tag 
operations.
+
+        Returns:
+            SupportsTags: the {@link SupportsTags} instance
+        """
+        raise UnsupportedOperationException("Catalog does not support tag 
operations")
+
 
 class UnsupportedOperationException(Exception):
     pass
diff --git a/clients/client-python/gravitino/api/file/fileset.py 
b/clients/client-python/gravitino/api/file/fileset.py
index 71a7250664..7639fe49a4 100644
--- a/clients/client-python/gravitino/api/file/fileset.py
+++ b/clients/client-python/gravitino/api/file/fileset.py
@@ -17,9 +17,11 @@
 
 from abc import abstractmethod
 from enum import Enum
-from typing import Optional, Dict
+from typing import Dict, Optional
 
 from gravitino.api.auditable import Auditable
+from gravitino.api.tag.supports_tags import SupportsTags
+from gravitino.exceptions.base import UnsupportedOperationException
 
 
 class Fileset(Auditable):
@@ -209,3 +211,6 @@ class Fileset(Auditable):
             The properties of the fileset object. Empty map is returned if no 
properties are set.
         """
         pass
+
+    def supports_tags(self) -> SupportsTags:
+        raise UnsupportedOperationException("Fileset does not support tag 
operations.")
diff --git a/clients/client-python/gravitino/api/model/model.py 
b/clients/client-python/gravitino/api/model/model.py
index 650bb4cbed..8e21982478 100644
--- a/clients/client-python/gravitino/api/model/model.py
+++ b/clients/client-python/gravitino/api/model/model.py
@@ -15,10 +15,12 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from typing import Dict, Optional
 from abc import abstractmethod
+from typing import Dict, Optional
 
 from gravitino.api.auditable import Auditable
+from gravitino.api.tag.supports_tags import SupportsTags
+from gravitino.exceptions.base import UnsupportedOperationException
 
 
 class Model(Auditable):
@@ -72,3 +74,6 @@ class Model(Auditable):
             The latest version of the model object.
         """
         pass
+
+    def supports_tags(self) -> SupportsTags:
+        raise UnsupportedOperationException("Model does not support tag 
operations.")
diff --git a/clients/client-python/gravitino/api/rel/table.py 
b/clients/client-python/gravitino/api/rel/table.py
index f0f5af9147..141291d36a 100644
--- a/clients/client-python/gravitino/api/rel/table.py
+++ b/clients/client-python/gravitino/api/rel/table.py
@@ -27,6 +27,8 @@ from gravitino.api.rel.expressions.transforms.transform 
import Transform
 from gravitino.api.rel.expressions.transforms.transforms import Transforms
 from gravitino.api.rel.indexes.index import Index
 from gravitino.api.rel.indexes.indexes import Indexes
+from gravitino.api.tag.supports_tags import SupportsTags
+from gravitino.exceptions.base import UnsupportedOperationException
 
 
 class Table(Auditable):
@@ -110,3 +112,6 @@ class Table(Auditable):
         """
 
         return {}
+
+    def supports_tags(self) -> SupportsTags:
+        raise UnsupportedOperationException("Table does not support tag 
operations.")
diff --git a/clients/client-python/gravitino/api/schema.py 
b/clients/client-python/gravitino/api/schema.py
index 216e15031b..5a7ec0a088 100644
--- a/clients/client-python/gravitino/api/schema.py
+++ b/clients/client-python/gravitino/api/schema.py
@@ -16,9 +16,11 @@
 # under the License.
 
 from abc import abstractmethod
-from typing import Optional, Dict
+from typing import Dict, Optional
 
 from gravitino.api.auditable import Auditable
+from gravitino.api.tag.supports_tags import SupportsTags
+from gravitino.exceptions.base import UnsupportedOperationException
 
 
 class Schema(Auditable):
@@ -43,3 +45,6 @@ class Schema(Auditable):
     def properties(self) -> Dict[str, str]:
         """Returns the properties of the Schema. An empty dictionary is 
returned if no properties are set."""
         return {}
+
+    def supports_tags(self) -> SupportsTags:
+        raise UnsupportedOperationException("Schema does not support tag 
operations.")
diff --git a/clients/client-python/gravitino/api/supports_schemas.py 
b/clients/client-python/gravitino/api/supports_schemas.py
index 8eea57f08d..36f0ac6a98 100644
--- a/clients/client-python/gravitino/api/supports_schemas.py
+++ b/clients/client-python/gravitino/api/supports_schemas.py
@@ -16,16 +16,11 @@
 # under the License.
 
 from abc import ABC, abstractmethod
-from typing import List, Dict
+from typing import Dict, List
 
 from gravitino.api.schema import Schema
 from gravitino.api.schema_change import SchemaChange
-
-
-class NoSuchSchemaException(Exception):
-    """Exception raised if the schema does not exist."""
-
-    pass
+from gravitino.exceptions.base import NoSuchSchemaException
 
 
 class SupportsSchemas(ABC):
diff --git a/clients/client-python/gravitino/client/base_schema_catalog.py 
b/clients/client-python/gravitino/client/base_schema_catalog.py
index 52d3d171f0..89872f893a 100644
--- a/clients/client-python/gravitino/client/base_schema_catalog.py
+++ b/clients/client-python/gravitino/client/base_schema_catalog.py
@@ -352,3 +352,6 @@ class BaseSchemaCatalog(
         self, tags_to_add: List[str], tags_to_remove: List[str]
     ) -> List[str]:
         return self._object_tag_operations.associate_tags(tags_to_add, 
tags_to_remove)
+
+    def supports_tags(self) -> SupportsTags:
+        return self
diff --git a/clients/client-python/gravitino/client/generic_fileset.py 
b/clients/client-python/gravitino/client/generic_fileset.py
index f002f76620..1aa0be9685 100644
--- a/clients/client-python/gravitino/client/generic_fileset.py
+++ b/clients/client-python/gravitino/client/generic_fileset.py
@@ -80,6 +80,9 @@ class GenericFileset(
     def support_credentials(self) -> SupportsCredentials:
         return self
 
+    def supports_tags(self) -> SupportsTags:
+        return self
+
     def get_credentials(self) -> List[Credential]:
         return self._object_credential_operations.get_credentials()
 
diff --git a/clients/client-python/gravitino/client/generic_model.py 
b/clients/client-python/gravitino/client/generic_model.py
index e4ed97dc98..2cfbc87369 100644
--- a/clients/client-python/gravitino/client/generic_model.py
+++ b/clients/client-python/gravitino/client/generic_model.py
@@ -84,3 +84,6 @@ class GenericModel(Model, SupportsTags):
         self, tags_to_add: list[str], tags_to_remove: list[str]
     ) -> list[str]:
         return self._model_tag_operations.associate_tags(tags_to_add, 
tags_to_remove)
+
+    def supports_tags(self) -> SupportsTags:
+        return self
diff --git a/clients/client-python/gravitino/client/generic_schema.py 
b/clients/client-python/gravitino/client/generic_schema.py
index eaf375dcf1..8301e6ab87 100644
--- a/clients/client-python/gravitino/client/generic_schema.py
+++ b/clients/client-python/gravitino/client/generic_schema.py
@@ -91,3 +91,6 @@ class GenericSchema(
         return self._metadata_object_tag_operations.associate_tags(
             tags_to_add, tags_to_remove
         )
+
+    def supports_tags(self) -> SupportsTags:
+        return self
diff --git a/clients/client-python/gravitino/client/relational_table.py 
b/clients/client-python/gravitino/client/relational_table.py
index aa1feafdb1..5295b1fb84 100644
--- a/clients/client-python/gravitino/client/relational_table.py
+++ b/clients/client-python/gravitino/client/relational_table.py
@@ -246,3 +246,6 @@ class RelationalTable(
         self, tags_to_add: list[str], tags_to_remove: list[str]
     ) -> list[str]:
         return self._object_tag_operations.associate_tags(tags_to_add, 
tags_to_remove)
+
+    def supports_tags(self) -> SupportsTags:
+        return self
diff --git 
a/clients/client-python/tests/integration/containers/base_container.py 
b/clients/client-python/tests/integration/containers/base_container.py
index ebc9eee018..35adec621f 100644
--- a/clients/client-python/tests/integration/containers/base_container.py
+++ b/clients/client-python/tests/integration/containers/base_container.py
@@ -21,7 +21,7 @@ from typing import Dict
 
 import docker
 from docker import types as tp
-from docker.errors import NotFound
+from docker.errors import DockerException, NotFound
 
 from gravitino.exceptions.base import GravitinoRuntimeException
 
@@ -130,25 +130,38 @@ class BaseContainer:
             ) from e
 
     def close(self):
+        if self._container is None:
+            return
+
         try:
             self._container.kill()
-        except RuntimeError as e:
+        except DockerException as e:
             logger.warning(
                 "Exception occurred while killing container %s : %s",
                 self._container_name,
                 e,
             )
         try:
-            self._container.remove()
-        except RuntimeError as e:
+            self._network.disconnect(self._container, force=True)
+        except DockerException as e:
+            logger.warning(
+                "Exception occurred while disconnecting container %s from 
network %s : %s",
+                self._container_name,
+                self._network_name,
+                e,
+            )
+        try:
+            self._container.remove(force=True)
+        except DockerException as e:
             logger.warning(
                 "Exception occurred while removing container %s : %s",
                 self._container_name,
                 e,
             )
         try:
+            self._network.reload()
             self._network.remove()
-        except RuntimeError as e:
+        except DockerException as e:
             logger.warning(
                 "Exception occurred while removing network %s : %s",
                 self._network_name,
diff --git a/clients/client-python/tests/integration/test_supports_tags.py 
b/clients/client-python/tests/integration/test_supports_tags.py
new file mode 100644
index 0000000000..2e13aad2db
--- /dev/null
+++ b/clients/client-python/tests/integration/test_supports_tags.py
@@ -0,0 +1,423 @@
+# 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 typing as tp
+from random import randint
+
+from gravitino import Catalog, GravitinoAdminClient, GravitinoClient, 
GravitinoMetalake
+from gravitino.api.file.fileset import Fileset
+from gravitino.api.model.model import Model
+from gravitino.api.rel.table import Table
+from gravitino.api.rel.table_catalog import TableCatalog
+from gravitino.api.rel.types.types import Types
+from gravitino.api.tag import Tag
+from gravitino.api.tag.supports_tags import SupportsTags
+from gravitino.dto.rel.column_dto import ColumnDTO
+from gravitino.dto.rel.partitioning.identity_partitioning_dto import (
+    IdentityPartitioningDTO,
+)
+from gravitino.exceptions.base import NoSuchTagException
+from gravitino.name_identifier import NameIdentifier
+from tests.integration.containers.hdfs_container import HDFSContainer
+from tests.integration.integration_test_env import IntegrationTestEnv
+
+# pylint: disable=too-many-instance-attributes
+
+
+class TestSupportsTags(IntegrationTestEnv):
+    relational_catalog_provider: str = "hive"
+    fileset_comment: str = "fileset_comment"
+    catalog_location_prop: str = "location"
+
+    fileset_location: str = "/tmp/TestFilesetCatalog"
+    fileset_properties_key1: str = "fileset_properties_key1"
+    fileset_properties_value1: str = "fileset_properties_value1"
+    fileset_properties_key2: str = "fileset_properties_key2"
+    fileset_properties_value2: str = "fileset_properties_value2"
+    fileset_properties: tp.Dict[str, str] = {
+        fileset_properties_key1: fileset_properties_value1,
+        fileset_properties_key2: fileset_properties_value2,
+    }
+
+    _metalake_name: str = "tag_it_metalake" + str(randint(0, 1000))
+    _relational_catalog_name: str = "relational_catalog" + str(randint(0, 
1000))
+    _model_catalog_name: str = "model_catalog" + str(randint(0, 1000))
+    _fileset_catalog_name: str = "fileset_catalog" + str(randint(0, 1000))
+    # SCHEMA
+    _schema_name = "tag_it_schema" + str(randint(0, 1000))
+    # OTHER
+    _table_name: str = "tag_it_table" + str(randint(0, 1000))
+    _fileset_name: str = "tag_it_fileset" + str(randint(0, 1000))
+    _model_name: str = "tag_it_model" + str(randint(0, 1000))
+
+    _tag_name1: str = "tag_it_tag1" + str(randint(0, 1000))
+    _tag_name2: str = "tag_it_tag2" + str(randint(0, 1000))
+    _tag_name3: str = "tag_it_tag3" + str(randint(0, 1000))
+    _tag_name4: str = "tag_it_tag4" + str(randint(0, 1000))
+
+    _gravitino_admin_client: GravitinoAdminClient
+    _gravitino_client: GravitinoClient
+    _metalake: GravitinoMetalake
+    _model_catalog: Catalog
+    _table_catalog: TableCatalog
+    _relational_catalog: Catalog
+    _fileset_catalog: Catalog
+    _tag1: Tag
+    _tag2: Tag
+    _tag3: Tag
+    _tag4: Tag
+
+    _table_ident: NameIdentifier
+    _fileset_ident: NameIdentifier
+    _model_ident: NameIdentifier
+    _hdfs_container: HDFSContainer
+
+    @classmethod
+    def setUpClass(cls) -> None:
+        super().setUpClass()
+        cls._hdfs_container = HDFSContainer()
+        cls._gravitino_admin_client = 
GravitinoAdminClient(uri="http://localhost:8090";)
+
+        cls._metalake = cls._gravitino_admin_client.create_metalake(
+            cls._metalake_name, comment="test metalake", properties={}
+        )
+        cls._gravitino_client = GravitinoClient(
+            uri="http://localhost:8090";, metalake_name=cls._metalake_name
+        )
+
+        cls._gravitino_client.create_tag(cls._tag_name1, "test tag1", {})
+        cls._gravitino_client.create_tag(cls._tag_name2, "test tag2", {})
+        cls._gravitino_client.create_tag(cls._tag_name3, "test tag3", {})
+        cls._gravitino_client.create_tag(cls._tag_name4, "test tag4", {})
+
+        cls._tag1 = cls._gravitino_client.get_tag(cls._tag_name1)
+        cls._tag2 = cls._gravitino_client.get_tag(cls._tag_name2)
+        cls._tag3 = cls._gravitino_client.get_tag(cls._tag_name3)
+        cls._tag4 = cls._gravitino_client.get_tag(cls._tag_name4)
+
+        hive_metastore_uri = f"thrift://{cls._hdfs_container.get_ip()}:9083"
+
+        cls._model_catalog = cls._gravitino_client.create_catalog(
+            name=cls._model_catalog_name,
+            catalog_type=Catalog.Type.MODEL,
+            provider=None,
+            comment="comment",
+            properties={},
+        )
+        cls._fileset_catalog = cls._gravitino_client.create_catalog(
+            name=cls._fileset_catalog_name,
+            catalog_type=Catalog.Type.FILESET,
+            provider=None,
+            comment="",
+            properties={cls.catalog_location_prop: "/tmp/test1"},
+        )
+        cls._relational_catalog = cls._gravitino_client.create_catalog(
+            name=cls._relational_catalog_name,
+            catalog_type=Catalog.Type.RELATIONAL,
+            provider=cls.relational_catalog_provider,
+            comment="Test relational catalog",
+            properties={"metastore.uris": hive_metastore_uri},
+        )
+        cls._table_catalog = cls._relational_catalog.as_table_catalog()
+
+    @classmethod
+    def tearDownClass(cls) -> None:
+        try:
+            cls._gravitino_client.drop_catalog(name=cls._model_catalog_name, 
force=True)
+            cls._gravitino_client.drop_catalog(
+                name=cls._relational_catalog_name, force=True
+            )
+            cls._gravitino_client.drop_catalog(
+                name=cls._fileset_catalog_name, force=True
+            )
+
+            cls._gravitino_client.delete_tag(cls._tag_name1)
+            cls._gravitino_client.delete_tag(cls._tag_name2)
+            cls._gravitino_client.delete_tag(cls._tag_name3)
+            cls._gravitino_client.delete_tag(cls._tag_name4)
+
+            cls._gravitino_admin_client.drop_metalake(
+                name=cls._metalake_name, force=True
+            )
+        finally:
+            cls._hdfs_container.close()
+            super().tearDownClass()
+
+    def setUp(self) -> None:
+        self._table_ident: NameIdentifier = NameIdentifier.of(
+            self._schema_name,
+            self._table_name,
+        )
+        self._fileset_ident: NameIdentifier = NameIdentifier.of(
+            self._schema_name,
+            self._fileset_name,
+        )
+        self._model_ident = NameIdentifier.of(
+            self._schema_name,
+            self._model_name,
+        )
+
+    def tearDown(self) -> None:
+        if self._model_catalog.as_schemas().schema_exists(self._schema_name):
+            self._model_catalog.as_schemas().drop_schema(self._schema_name, 
True)
+        if 
self._relational_catalog.as_schemas().schema_exists(self._schema_name):
+            
self._relational_catalog.as_schemas().drop_schema(self._schema_name, True)
+        if self._fileset_catalog.as_schemas().schema_exists(self._schema_name):
+            self._fileset_catalog.as_schemas().drop_schema(self._schema_name, 
True)
+
+    def test_catalog_tag_operations(self) -> None:
+        """
+        Test tag operations (associate, list, get, dissociate) on catalog.
+        """
+        model_catalog = self._model_catalog.supports_tags()
+
+        model_catalog.associate_tags(
+            tags_to_add=[self._tag_name3, self._tag_name4],
+            tags_to_remove=[],
+        )
+        model_catalog.associate_tags(
+            tags_to_add=[self._tag_name1, self._tag_name2],
+            tags_to_remove=[self._tag_name3, self._tag_name4],
+        )
+        self._test_list_tags(model_catalog)
+        self._test_list_tags_info(model_catalog)
+        self._test_get_tag(model_catalog)
+        model_catalog.associate_tags(
+            tags_to_add=[],
+            tags_to_remove=[self._tag_name1, self._tag_name2],
+        )
+        self._check_no_tag_associated(model_catalog)
+
+    def test_schema_tag_operations(self) -> None:
+        """
+        Test tag operations (associate, list, get, dissociate) on schema.
+        """
+        self._model_catalog.as_schemas().create_schema(
+            schema_name=self._schema_name,
+            comment="model it schema",
+            properties={},
+        )
+
+        schema = 
self._model_catalog.as_schemas().load_schema(self._schema_name)
+
+        schema_supports_tags = schema.supports_tags()
+        schema_supports_tags.associate_tags(
+            tags_to_add=[self._tag_name3, self._tag_name4],
+            tags_to_remove=[],
+        )
+        schema_supports_tags.associate_tags(
+            tags_to_add=[self._tag_name1, self._tag_name2],
+            tags_to_remove=[self._tag_name3, self._tag_name4],
+        )
+        self._test_list_tags(schema_supports_tags)
+        self._test_list_tags_info(schema_supports_tags)
+        self._test_get_tag(schema_supports_tags)
+
+        schema.supports_tags().associate_tags(
+            tags_to_add=[],
+            tags_to_remove=[self._tag_name1, self._tag_name2],
+        )
+        self._check_no_tag_associated(schema.supports_tags())
+
+    def test_fileset_tag_operations(self) -> None:
+        """
+        Test tag operations (associate, list, get, dissociate) on fileset.
+        """
+        self._fileset_catalog.as_schemas().create_schema(
+            schema_name=self._schema_name,
+            comment="fileset it schema",
+            properties={},
+        )
+
+        fileset: Fileset = 
self._fileset_catalog.as_fileset_catalog().create_fileset(
+            ident=self._fileset_ident,
+            fileset_type=Fileset.Type.MANAGED,
+            comment=self.fileset_comment,
+            storage_location=self.fileset_location,
+            properties=self.fileset_properties,
+        )
+        fileset.supports_tags().associate_tags(
+            tags_to_add=[self._tag_name3, self._tag_name4],
+            tags_to_remove=[],
+        )
+        fileset.supports_tags().associate_tags(
+            tags_to_add=[self._tag_name1, self._tag_name2],
+            tags_to_remove=[self._tag_name3, self._tag_name4],
+        )
+        self._test_list_tags(fileset.supports_tags())
+        self._test_list_tags_info(fileset.supports_tags())
+        self._test_get_tag(fileset.supports_tags())
+
+        fileset.supports_tags().associate_tags(
+            tags_to_add=[],
+            tags_to_remove=[self._tag_name1, self._tag_name2],
+        )
+        self._check_no_tag_associated(fileset.supports_tags())
+
+    def test_model_tag_operations(self) -> None:
+        """
+        Test tag operations (associate, list, get, dissociate) on model.
+        """
+        self._model_catalog.as_schemas().create_schema(
+            schema_name=self._schema_name,
+            comment="model it schema",
+            properties={},
+        )
+        model: Model = self._model_catalog.as_model_catalog().register_model(
+            self._model_ident, "test_model", {}
+        )
+
+        model.supports_tags().associate_tags(
+            tags_to_add=[self._tag_name3, self._tag_name4],
+            tags_to_remove=[],
+        )
+        model.supports_tags().associate_tags(
+            tags_to_add=[self._tag_name1, self._tag_name2],
+            tags_to_remove=[self._tag_name3, self._tag_name4],
+        )
+        self._test_list_tags(model.supports_tags())
+        self._test_list_tags_info(model.supports_tags())
+        self._test_get_tag(model.supports_tags())
+        model.supports_tags().associate_tags(
+            tags_to_add=[],
+            tags_to_remove=[self._tag_name1, self._tag_name2],
+        )
+        self._check_no_tag_associated(model.supports_tags())
+
+    def test_table_tag_operations(self) -> None:
+        """Test tag operations (associate, list, get, dissociate) on table."""
+
+        self._relational_catalog.as_schemas().create_schema(
+            schema_name=self._schema_name,
+            comment="table it schema",
+            properties={},
+        )
+        relational_table = self.create_test_table()
+
+        relational_table.supports_tags().associate_tags(
+            tags_to_add=[self._tag_name3, self._tag_name4],
+            tags_to_remove=[],
+        )
+        relational_table.supports_tags().associate_tags(
+            tags_to_add=[self._tag_name1, self._tag_name2],
+            tags_to_remove=[self._tag_name3, self._tag_name4],
+        )
+        self._test_list_tags(relational_table.supports_tags())
+        self._test_list_tags_info(relational_table.supports_tags())
+        self._test_get_tag(relational_table.supports_tags())
+
+        relational_table.supports_tags().associate_tags(
+            tags_to_add=[],
+            tags_to_remove=[self._tag_name1, self._tag_name2],
+        )
+        self._check_no_tag_associated(relational_table.supports_tags())
+
+    def test_column_tag_operations(self) -> None:
+        """Test tag operations (associate, list, get, dissociate) on column."""
+        self._relational_catalog.as_schemas().create_schema(
+            schema_name=self._schema_name,
+            comment="table it schema",
+            properties={},
+        )
+        relational_table = self.create_test_table()
+        for column in relational_table.columns():
+            column.supports_tags().associate_tags(
+                tags_to_add=[self._tag_name3, self._tag_name4],
+                tags_to_remove=[],
+            )
+            column.supports_tags().associate_tags(
+                tags_to_add=[self._tag_name1, self._tag_name2],
+                tags_to_remove=[self._tag_name3, self._tag_name4],
+            )
+            self._test_list_tags(column.supports_tags())
+            self._test_list_tags_info(column.supports_tags())
+            self._test_get_tag(column.supports_tags())
+
+            column.supports_tags().associate_tags(
+                tags_to_add=[],
+                tags_to_remove=[self._tag_name1, self._tag_name2],
+            )
+            self._check_no_tag_associated(column.supports_tags())
+
+    def test_get_non_existent_tag_raises_exception(self) -> None:
+        """
+        Test get tag on non-existent tag.
+        """
+        model_catalog = self._model_catalog.supports_tags()
+        model_catalog.associate_tags(
+            tags_to_add=[self._tag_name3, self._tag_name4],
+            tags_to_remove=[],
+        )
+        try:
+            with self.assertRaises(NoSuchTagException):
+                model_catalog.get_tag(self._tag_name1)
+        finally:
+            model_catalog.associate_tags(
+                tags_to_add=[],
+                tags_to_remove=[self._tag_name3, self._tag_name4],
+            )
+
+    def _test_get_tag(self, supports_tags: SupportsTags) -> None:
+        tag1: Tag = supports_tags.get_tag(self._tag_name1)
+        self._check_tag(self._tag1, tag1)
+
+        tag2: Tag = supports_tags.get_tag(self._tag_name2)
+        self._check_tag(self._tag2, tag2)
+
+    def _test_list_tags(self, supports_tags: SupportsTags) -> None:
+        tags = supports_tags.list_tags()
+        self.assertEqual(2, len(tags))
+        self.assertIn(self._tag_name1, tags)
+        self.assertIn(self._tag_name2, tags)
+
+    def _test_list_tags_info(self, supports_tags: SupportsTags) -> None:
+        tags: list[Tag] = supports_tags.list_tags_info()
+        self.assertEqual(2, len(tags))
+        for tag in tags:
+            if tag.name() == self._tag_name1:
+                self._check_tag(self._tag1, tag)
+            elif tag.name() == self._tag_name2:
+                self._check_tag(self._tag2, tag)
+            else:
+                self.fail("Unknown tag name")
+
+    def create_test_table(self) -> Table:
+        return self._table_catalog.create_table(
+            identifier=self._table_ident,
+            columns=[
+                ColumnDTO.builder()
+                .with_name("dt")
+                .with_data_type(Types.DateType.get())
+                .build(),
+                ColumnDTO.builder()
+                .with_name("country")
+                .with_data_type(Types.StringType.get())
+                .build(),
+            ],
+            partitioning=[
+                IdentityPartitioningDTO("dt"),
+                IdentityPartitioningDTO("country"),
+            ],
+        )
+
+    def _check_no_tag_associated(self, supports_tags: SupportsTags) -> None:
+        self.assertEqual(0, len(supports_tags.list_tags()))
+        self.assertListEqual([], supports_tags.list_tags_info())
+
+    def _check_tag(self, expected_tag: Tag, real_tag: Tag) -> None:
+        self.assertEqual(expected_tag.name(), real_tag.name())
+        self.assertEqual(expected_tag.comment(), real_tag.comment())
+        self.assertEqual(expected_tag.properties(), real_tag.properties())

Reply via email to