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 a7d221cd0a [#12181] feat(client-python): Support tags for views and 
functions (#12182)
a7d221cd0a is described below

commit a7d221cd0a33fbce27931bb7889bee7ba32385c2
Author: Zhiguo Wu <[email protected]>
AuthorDate: Mon Jul 27 14:36:40 2026 +0800

    [#12181] feat(client-python): Support tags for views and functions (#12182)
    
    ### What changes were proposed in this pull request?
    
    This PR adds Python client support for tag operations on views and
    functions.
    
    Changes include:
    
    - Add default `supports_tags()` methods to the `View` and `Function` API
    classes.
    - Extend `GenericView` and `GenericFunction` with tag operations backed
    by `MetadataObjectTagOperations`.
    - Return tag-capable wrappers from Python client view and function
    operations.
    - Add Python client unit and integration tests for view/function tag
    support.
    
    ### Why are the changes needed?
    
    #11844 enables server-side tag association for `VIEW` and `FUNCTION`
    metadata objects. The Python client should expose the same capability so
    users can manage tags for views and functions through the typed client
    API, consistent with other tag-capable metadata objects.
    
    Fix: #12181
    Related: #11844
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes.
    
    Python client users can now call `supports_tags()` on `View` and
    `Function` objects returned by the client, for example:
    
    ```python
    view.supports_tags().associate_tags(...)
    function.supports_tags().list_tags()
    ```
    
    ### How was this patch tested?
    
    - `./gradlew :clients:client-python:black :clients:client-python:pylint
    :clients:client-python:unitTests`
    - `./gradlew :clients:client-python:integrationTest`
---
 .../gravitino/api/function/function.py             |  13 +++
 clients/client-python/gravitino/api/rel/view.py    |  13 +++
 .../client/function_catalog_operations.py          |  17 +++-
 .../gravitino/client/generic_function.py           |  56 ++++++++++-
 .../client-python/gravitino/client/generic_view.py |  46 ++++++++-
 .../client/metadata_object_tag_operations.py       |   5 +-
 .../gravitino/client/relational_catalog.py         |   6 +-
 .../tests/integration/test_supports_tags.py        | 107 +++++++++++++++++++++
 .../tests/unittests/api/function/__init__.py       |  16 +++
 .../tests/unittests/api/function/test_function.py  |  35 +++++++
 .../tests/unittests/api/rel/test_view.py           |   7 ++
 .../tests/unittests/test_generic_function.py       |  79 +++++++++++++++
 .../tests/unittests/test_generic_view.py           |  24 +++++
 13 files changed, 411 insertions(+), 13 deletions(-)

diff --git a/clients/client-python/gravitino/api/function/function.py 
b/clients/client-python/gravitino/api/function/function.py
index 4aec6550f4..8eff5dc546 100644
--- a/clients/client-python/gravitino/api/function/function.py
+++ b/clients/client-python/gravitino/api/function/function.py
@@ -21,6 +21,8 @@ from typing import List, Optional
 from gravitino.api.auditable import Auditable
 from gravitino.api.function.function_definition import FunctionDefinition
 from gravitino.api.function.function_type import FunctionType
+from gravitino.api.tag.supports_tags import SupportsTags
+from gravitino.exceptions.base import UnsupportedOperationException
 
 
 class Function(Auditable):
@@ -61,3 +63,14 @@ class Function(Auditable):
     def definitions(self) -> List[FunctionDefinition]:
         """Returns the definitions of the function."""
         pass
+
+    def supports_tags(self) -> SupportsTags:
+        """Return tag operations if the function supports tags.
+
+        Raises:
+            UnsupportedOperationException: If the function does not support 
tag operations.
+
+        Returns:
+            SupportsTags: The tag operations supported by the function.
+        """
+        raise UnsupportedOperationException("Function does not support tag 
operations.")
diff --git a/clients/client-python/gravitino/api/rel/view.py 
b/clients/client-python/gravitino/api/rel/view.py
index 7db72a6750..fd495f0342 100644
--- a/clients/client-python/gravitino/api/rel/view.py
+++ b/clients/client-python/gravitino/api/rel/view.py
@@ -22,6 +22,8 @@ from gravitino.api.auditable import Auditable
 from gravitino.api.rel.column import Column
 from gravitino.api.rel.representation import Representation
 from gravitino.api.rel.sql_representation import SQLRepresentation
+from gravitino.api.tag.supports_tags import SupportsTags
+from gravitino.exceptions.base import UnsupportedOperationException
 
 
 class View(Auditable):
@@ -66,3 +68,14 @@ class View(Auditable):
     def properties(self) -> dict[str, str]:
         """Returns the view properties."""
         return {}
+
+    def supports_tags(self) -> SupportsTags:
+        """Return tag operations if the view supports tags.
+
+        Raises:
+            UnsupportedOperationException: If the view does not support tag 
operations.
+
+        Returns:
+            SupportsTags: The tag operations supported by the view.
+        """
+        raise UnsupportedOperationException("View does not support tag 
operations.")
diff --git 
a/clients/client-python/gravitino/client/function_catalog_operations.py 
b/clients/client-python/gravitino/client/function_catalog_operations.py
index 46050c7812..1d2e533ff5 100644
--- a/clients/client-python/gravitino/client/function_catalog_operations.py
+++ b/clients/client-python/gravitino/client/function_catalog_operations.py
@@ -132,7 +132,10 @@ class FunctionCatalogOperations(FunctionCatalog):
         function_list_response = FunctionListResponse.from_json(resp.body)
         function_list_response.validate()
 
-        return [GenericFunction(func) for func in 
function_list_response.functions()]
+        return [
+            GenericFunction(func, self._rest_client, full_namespace)
+            for func in function_list_response.functions()
+        ]
 
     def get_function(self, ident: NameIdentifier) -> Function:
         """Get a function by NameIdentifier from the catalog.
@@ -158,7 +161,9 @@ class FunctionCatalogOperations(FunctionCatalog):
         function_response = FunctionResponse.from_json(resp.body)
         function_response.validate()
 
-        return GenericFunction(function_response.function())
+        return GenericFunction(
+            function_response.function(), self._rest_client, full_namespace
+        )
 
     def register_function(
         self,
@@ -211,7 +216,9 @@ class FunctionCatalogOperations(FunctionCatalog):
         function_response = FunctionResponse.from_json(resp.body)
         function_response.validate()
 
-        return GenericFunction(function_response.function())
+        return GenericFunction(
+            function_response.function(), self._rest_client, full_namespace
+        )
 
     def alter_function(
         self, ident: NameIdentifier, *changes: FunctionChange
@@ -250,7 +257,9 @@ class FunctionCatalogOperations(FunctionCatalog):
         function_response = FunctionResponse.from_json(resp.body)
         function_response.validate()
 
-        return GenericFunction(function_response.function())
+        return GenericFunction(
+            function_response.function(), self._rest_client, full_namespace
+        )
 
     def drop_function(self, ident: NameIdentifier) -> bool:
         """Drop a function from the catalog.
diff --git a/clients/client-python/gravitino/client/generic_function.py 
b/clients/client-python/gravitino/client/generic_function.py
index 634084b291..2ef6bdfd72 100644
--- a/clients/client-python/gravitino/client/generic_function.py
+++ b/clients/client-python/gravitino/client/generic_function.py
@@ -20,20 +20,47 @@ from typing import List, Optional
 from gravitino.api.function.function import Function
 from gravitino.api.function.function_definition import FunctionDefinition
 from gravitino.api.function.function_type import FunctionType
+from gravitino.api.metadata_object import MetadataObject
+from gravitino.api.metadata_objects import MetadataObjects
+from gravitino.api.tag.supports_tags import SupportsTags
+from gravitino.api.tag.tag import Tag
+from gravitino.client.metadata_object_tag_operations import 
MetadataObjectTagOperations
 from gravitino.dto.audit_dto import AuditDTO
 from gravitino.dto.function.function_dto import FunctionDTO
+from gravitino.namespace import Namespace
+from gravitino.utils.http_client import HTTPClient
 
 
-class GenericFunction(Function):
+class GenericFunction(Function, SupportsTags):
     """A generic implementation of the Function interface."""
 
-    def __init__(self, function_dto: FunctionDTO):
+    def __init__(
+        self,
+        function_dto: FunctionDTO,
+        rest_client: HTTPClient,
+        function_namespace: Namespace,
+    ):
         """Create a GenericFunction from a FunctionDTO.
 
         Args:
             function_dto: The function DTO.
+            rest_client: The REST client for tag operations.
+            function_namespace: The full function namespace in 
metalake.catalog.schema format.
         """
         self._function_dto = function_dto
+        function_object: MetadataObject = MetadataObjects.of(
+            [
+                function_namespace.level(1),
+                function_namespace.level(2),
+                function_dto.name(),
+            ],
+            MetadataObject.Type.FUNCTION,
+        )
+        self._object_tag_operations = MetadataObjectTagOperations(
+            function_namespace.level(0),
+            function_object,
+            rest_client,
+        )
 
     def name(self) -> str:
         """Returns the function name."""
@@ -58,3 +85,28 @@ class GenericFunction(Function):
     def audit_info(self) -> Optional[AuditDTO]:
         """Returns the audit information."""
         return self._function_dto.audit_info()
+
+    def list_tags(self) -> list[str]:
+        """List the tag names associated with the function."""
+        return self._object_tag_operations.list_tags()
+
+    def list_tags_info(self) -> list[Tag]:
+        """List the tags associated with the function."""
+        return self._object_tag_operations.list_tags_info()
+
+    def get_tag(self, name: str) -> Tag:
+        """Get an associated tag by name."""
+        return self._object_tag_operations.get_tag(name)
+
+    def associate_tags(
+        self, tags_to_add: list[str], tags_to_remove: list[str]
+    ) -> list[str]:
+        """Associate or disassociate tags with the function."""
+        return self._object_tag_operations.associate_tags(
+            tags_to_add,
+            tags_to_remove,
+        )
+
+    def supports_tags(self) -> SupportsTags:
+        """Return the function's tag operations."""
+        return self
diff --git a/clients/client-python/gravitino/client/generic_view.py 
b/clients/client-python/gravitino/client/generic_view.py
index ce4e2a765c..991a2e4ce0 100644
--- a/clients/client-python/gravitino/client/generic_view.py
+++ b/clients/client-python/gravitino/client/generic_view.py
@@ -18,25 +18,49 @@
 from typing import Optional
 
 from gravitino.api.audit import Audit
+from gravitino.api.metadata_object import MetadataObject
+from gravitino.api.metadata_objects import MetadataObjects
 from gravitino.api.rel.column import Column
 from gravitino.api.rel.representation import Representation
 from gravitino.api.rel.view import View
+from gravitino.api.tag.supports_tags import SupportsTags
+from gravitino.api.tag.tag import Tag
+from gravitino.client.metadata_object_tag_operations import 
MetadataObjectTagOperations
 from gravitino.dto.rel.view_dto import ViewDTO
+from gravitino.namespace import Namespace
+from gravitino.utils.http_client import HTTPClient
 
 
-class GenericView(View):
+class GenericView(View, SupportsTags):
     """A generic implementation of the View interface."""
 
     def __init__(
         self,
         view_dto: ViewDTO,
+        rest_client: HTTPClient,
+        view_namespace: Namespace,
     ):
         """Create a GenericView from a ViewDTO.
 
         Args:
             view_dto: The view DTO.
+            rest_client: The REST client for tag operations.
+            view_namespace: The full view namespace in metalake.catalog.schema 
format.
         """
         self._view_dto = view_dto
+        view_object: MetadataObject = MetadataObjects.of(
+            [
+                view_namespace.level(1),
+                view_namespace.level(2),
+                view_dto.name(),
+            ],
+            MetadataObject.Type.VIEW,
+        )
+        self._object_tag_operations = MetadataObjectTagOperations(
+            view_namespace.level(0),
+            view_object,
+            rest_client,
+        )
 
     def name(self) -> str:
         return self._view_dto.name()
@@ -61,3 +85,23 @@ class GenericView(View):
 
     def audit_info(self) -> Audit:
         return self._view_dto.audit_info()
+
+    def list_tags(self) -> list[str]:
+        return self._object_tag_operations.list_tags()
+
+    def list_tags_info(self) -> list[Tag]:
+        return self._object_tag_operations.list_tags_info()
+
+    def get_tag(self, name: str) -> Tag:
+        return self._object_tag_operations.get_tag(name)
+
+    def associate_tags(
+        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/metadata_object_tag_operations.py 
b/clients/client-python/gravitino/client/metadata_object_tag_operations.py
index 499486019c..8f46ed0c6d 100644
--- a/clients/client-python/gravitino/client/metadata_object_tag_operations.py
+++ b/clients/client-python/gravitino/client/metadata_object_tag_operations.py
@@ -36,9 +36,8 @@ from gravitino.utils.string_utils import StringUtils
 
 class MetadataObjectTagOperations(SupportsTags):
     """
-    The implementation of SupportsTags. This helper is composed into metadata 
objects,
-    including catalog, schema, table, column, fileset, and topic, to provide 
tag
-    operations for these objects.
+    The implementation of SupportsTags. This helper is composed into supported 
metadata
+    objects to provide tag operations.
     """
 
     TAG_REQUEST_PATH = "api/metalakes/{}/objects/{}/{}/tags"
diff --git a/clients/client-python/gravitino/client/relational_catalog.py 
b/clients/client-python/gravitino/client/relational_catalog.py
index d42af35804..c8cffbe662 100644
--- a/clients/client-python/gravitino/client/relational_catalog.py
+++ b/clients/client-python/gravitino/client/relational_catalog.py
@@ -381,7 +381,7 @@ class RelationalCatalog(
         )
         view_resp = ViewResponse.from_json(resp.body, infer_missing=True)
         view_resp.validate()
-        return GenericView(view_resp.view())
+        return GenericView(view_resp.view(), self.rest_client, full_namespace)
 
     def create_view(
         self,
@@ -412,7 +412,7 @@ class RelationalCatalog(
         )
         view_resp = ViewResponse.from_json(resp.body, infer_missing=True)
         view_resp.validate()
-        return GenericView(view_resp.view())
+        return GenericView(view_resp.view(), self.rest_client, full_namespace)
 
     def alter_view(self, identifier: NameIdentifier, *changes: ViewChange) -> 
View:
         self._check_view_name_identifier(identifier)
@@ -429,7 +429,7 @@ class RelationalCatalog(
         )
         view_resp = ViewResponse.from_json(resp.body, infer_missing=True)
         view_resp.validate()
-        return GenericView(view_resp.view())
+        return GenericView(view_resp.view(), self.rest_client, full_namespace)
 
     def drop_view(self, identifier: NameIdentifier) -> bool:
         self._check_view_name_identifier(identifier)
diff --git a/clients/client-python/tests/integration/test_supports_tags.py 
b/clients/client-python/tests/integration/test_supports_tags.py
index 2e13aad2db..39bbe4e75d 100644
--- a/clients/client-python/tests/integration/test_supports_tags.py
+++ b/clients/client-python/tests/integration/test_supports_tags.py
@@ -19,10 +19,18 @@ from random import randint
 
 from gravitino import Catalog, GravitinoAdminClient, GravitinoClient, 
GravitinoMetalake
 from gravitino.api.file.fileset import Fileset
+from gravitino.api.function.function import Function
+from gravitino.api.function.function_definition import FunctionDefinitions
+from gravitino.api.function.function_type import FunctionType
+from gravitino.api.function.sql_impl import SQLImpl
 from gravitino.api.model.model import Model
+from gravitino.api.rel.column import Column
+from gravitino.api.rel.dialects import Dialects
+from gravitino.api.rel.sql_representation import SQLRepresentation
 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.rel.view import View
 from gravitino.api.tag import Tag
 from gravitino.api.tag.supports_tags import SupportsTags
 from gravitino.dto.rel.column_dto import ColumnDTO
@@ -60,6 +68,8 @@ class TestSupportsTags(IntegrationTestEnv):
     _schema_name = "tag_it_schema" + str(randint(0, 1000))
     # OTHER
     _table_name: str = "tag_it_table" + str(randint(0, 1000))
+    _view_name: str = "tag_it_view" + str(randint(0, 1000))
+    _function_name: str = "tag_it_function" + str(randint(0, 1000))
     _fileset_name: str = "tag_it_fileset" + str(randint(0, 1000))
     _model_name: str = "tag_it_model" + str(randint(0, 1000))
 
@@ -81,6 +91,8 @@ class TestSupportsTags(IntegrationTestEnv):
     _tag4: Tag
 
     _table_ident: NameIdentifier
+    _view_ident: NameIdentifier
+    _function_ident: NameIdentifier
     _fileset_ident: NameIdentifier
     _model_ident: NameIdentifier
     _hdfs_container: HDFSContainer
@@ -161,6 +173,14 @@ class TestSupportsTags(IntegrationTestEnv):
             self._schema_name,
             self._table_name,
         )
+        self._view_ident: NameIdentifier = NameIdentifier.of(
+            self._schema_name,
+            self._view_name,
+        )
+        self._function_ident: NameIdentifier = NameIdentifier.of(
+            self._schema_name,
+            self._function_name,
+        )
         self._fileset_ident: NameIdentifier = NameIdentifier.of(
             self._schema_name,
             self._fileset_name,
@@ -325,6 +345,93 @@ class TestSupportsTags(IntegrationTestEnv):
         )
         self._check_no_tag_associated(relational_table.supports_tags())
 
+    def test_view_tag_operations(self) -> None:
+        """Test tag operations (associate, list, get, dissociate) on view."""
+        self._relational_catalog.as_schemas().create_schema(
+            schema_name=self._schema_name,
+            comment="view it schema",
+            properties={},
+        )
+        table = self.create_test_table()
+        view: View = self._relational_catalog.as_view_catalog().create_view(
+            identifier=self._view_ident,
+            columns=[
+                Column.of("dt", Types.DateType.get()),
+                Column.of("country", Types.StringType.get()),
+            ],
+            representations=[
+                SQLRepresentation(
+                    Dialects.HIVE,
+                    f"SELECT dt, country FROM {table.name()}",
+                )
+            ],
+            comment="view for tag operations",
+            properties={},
+        )
+
+        view.supports_tags().associate_tags(
+            tags_to_add=[self._tag_name3, self._tag_name4],
+            tags_to_remove=[],
+        )
+        view.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(view.supports_tags())
+        self._test_list_tags_info(view.supports_tags())
+        self._test_get_tag(view.supports_tags())
+
+        view.supports_tags().associate_tags(
+            tags_to_add=[],
+            tags_to_remove=[self._tag_name1, self._tag_name2],
+        )
+        self._check_no_tag_associated(view.supports_tags())
+
+    def test_function_tag_operations(self) -> None:
+        """Test tag operations (associate, list, get, dissociate) on 
function."""
+        self._relational_catalog.as_schemas().create_schema(
+            schema_name=self._schema_name,
+            comment="function it schema",
+            properties={},
+        )
+        implementation = (
+            SQLImpl.builder()
+            .with_runtime_type(SQLImpl.RuntimeType.SPARK)
+            .with_sql("SELECT 1")
+            .build()
+        )
+        definition = FunctionDefinitions.of(
+            [],
+            Types.IntegerType.get(),
+            [implementation],
+        )
+        function_catalog = self._relational_catalog.as_function_catalog()
+        function: Function = function_catalog.register_function(
+            ident=self._function_ident,
+            comment="function for tag operations",
+            function_type=FunctionType.SCALAR,
+            deterministic=True,
+            definitions=[definition],
+        )
+
+        function.supports_tags().associate_tags(
+            tags_to_add=[self._tag_name3, self._tag_name4],
+            tags_to_remove=[],
+        )
+        function.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(function.supports_tags())
+        self._test_list_tags_info(function.supports_tags())
+        self._test_get_tag(function.supports_tags())
+
+        function.supports_tags().associate_tags(
+            tags_to_add=[],
+            tags_to_remove=[self._tag_name1, self._tag_name2],
+        )
+        self._check_no_tag_associated(function.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(
diff --git a/clients/client-python/tests/unittests/api/function/__init__.py 
b/clients/client-python/tests/unittests/api/function/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/clients/client-python/tests/unittests/api/function/__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/api/function/test_function.py 
b/clients/client-python/tests/unittests/api/function/test_function.py
new file mode 100644
index 0000000000..202d4a8dc0
--- /dev/null
+++ b/clients/client-python/tests/unittests/api/function/test_function.py
@@ -0,0 +1,35 @@
+# 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 unittest.mock import Mock
+
+from gravitino.api.function.function import Function
+from gravitino.exceptions.base import UnsupportedOperationException
+
+
+class TestFunction(unittest.TestCase):
+    def test_default_comment(self):
+        function = Mock(spec=Function)
+
+        self.assertIsNone(Function.comment(function))
+
+    def test_supports_tags_raises_exception(self):
+        function = Mock(spec=Function)
+
+        with self.assertRaises(UnsupportedOperationException):
+            Function.supports_tags(function)
diff --git a/clients/client-python/tests/unittests/api/rel/test_view.py 
b/clients/client-python/tests/unittests/api/rel/test_view.py
index 19fa1769d7..54f30d3c9b 100644
--- a/clients/client-python/tests/unittests/api/rel/test_view.py
+++ b/clients/client-python/tests/unittests/api/rel/test_view.py
@@ -22,6 +22,7 @@ from gravitino.api.rel.dialects import Dialects
 from gravitino.api.rel.representation import Representation
 from gravitino.api.rel.sql_representation import SQLRepresentation
 from gravitino.api.rel.view import View
+from gravitino.exceptions.base import UnsupportedOperationException
 
 
 class TestView(unittest.TestCase):
@@ -67,3 +68,9 @@ class TestView(unittest.TestCase):
         self.assertIsNone(View.default_catalog(view))
         self.assertIsNone(View.default_schema(view))
         self.assertEqual({}, View.properties(view))
+
+    def test_supports_tags_raises_exception(self):
+        view = Mock(spec=View)
+
+        with self.assertRaises(UnsupportedOperationException):
+            View.supports_tags(view)
diff --git a/clients/client-python/tests/unittests/test_generic_function.py 
b/clients/client-python/tests/unittests/test_generic_function.py
new file mode 100644
index 0000000000..85ad00af1e
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_generic_function.py
@@ -0,0 +1,79 @@
+# 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.function.function_type import FunctionType
+from gravitino.api.rel.types.types import Types
+from gravitino.api.tag.supports_tags import SupportsTags
+from gravitino.client.generic_function import GenericFunction
+from gravitino.dto.audit_dto import AuditDTO
+from gravitino.dto.function.function_definition_dto import 
FunctionDefinitionDTO
+from gravitino.dto.function.function_dto import FunctionDTO
+from gravitino.namespace import Namespace
+from gravitino.utils.http_client import HTTPClient
+
+
+class TestGenericFunction(unittest.TestCase):
+    _rest_client = HTTPClient("http://localhost:8080";)
+    _function_namespace = Namespace.of(
+        "demo_metalake",
+        "demo_catalog",
+        "demo_schema",
+    )
+
+    def _generic_function(self) -> GenericFunction:
+        return GenericFunction(
+            FunctionDTO(
+                _name="demo_function",
+                _function_type=FunctionType.SCALAR,
+                _deterministic=True,
+                _definitions=[
+                    FunctionDefinitionDTO(
+                        _parameters=[],
+                        _return_type=Types.IntegerType.get(),
+                        _impls=[],
+                    )
+                ],
+                _comment="comment",
+                _audit=AuditDTO("creator"),
+            ),
+            self._rest_client,
+            self._function_namespace,
+        )
+
+    def test_generic_function(self) -> None:
+        generic_function = self._generic_function()
+
+        self.assertEqual("demo_function", generic_function.name())
+        self.assertEqual(FunctionType.SCALAR, generic_function.function_type())
+        self.assertTrue(generic_function.deterministic())
+        self.assertEqual("comment", generic_function.comment())
+        self.assertEqual(1, len(generic_function.definitions()))
+        self.assertEqual("creator", generic_function.audit_info().creator())
+
+    def test_extends_supports_tags_class(self) -> None:
+        generic_function = self._generic_function()
+
+        self.assertTrue(issubclass(GenericFunction, SupportsTags))
+        expected_methods = ["list_tags", "list_tags_info", "get_tag", 
"associate_tags"]
+        self.assertTrue(
+            all(
+                callable(getattr(generic_function, method, None))
+                for method in expected_methods
+            )
+        )
diff --git a/clients/client-python/tests/unittests/test_generic_view.py 
b/clients/client-python/tests/unittests/test_generic_view.py
index a07c15be05..6eac593f5e 100644
--- a/clients/client-python/tests/unittests/test_generic_view.py
+++ b/clients/client-python/tests/unittests/test_generic_view.py
@@ -18,13 +18,23 @@
 import unittest
 
 from gravitino.api.rel.dialects import Dialects
+from gravitino.api.tag.supports_tags import SupportsTags
 from gravitino.client.generic_view import GenericView
 from gravitino.dto.audit_dto import AuditDTO
 from gravitino.dto.rel.sql_representation_dto import SQLRepresentationDTO
 from gravitino.dto.rel.view_dto import ViewDTO
+from gravitino.namespace import Namespace
+from gravitino.utils.http_client import HTTPClient
 
 
 class TestGenericView(unittest.TestCase):
+    _rest_client = HTTPClient("http://localhost:8080";)
+    _view_namespace = Namespace.of(
+        "demo_metalake",
+        "demo_catalog",
+        "demo_schema",
+    )
+
     def _generic_view(
         self,
         name: str = "demo_view",
@@ -45,6 +55,8 @@ class TestGenericView(unittest.TestCase):
                 _properties={"key": "value"},
                 _audit=AuditDTO("creator"),
             ),
+            self._rest_client,
+            self._view_namespace,
         )
 
     def test_generic_view(self) -> None:
@@ -59,3 +71,15 @@ class TestGenericView(unittest.TestCase):
         self.assertEqual(1, len(generic_view.representations()))
         self.assertEqual("SELECT 1", 
generic_view.sql_for(Dialects.TRINO).sql())
         self.assertEqual("creator", generic_view.audit_info().creator())
+
+    def test_extends_supports_tags_class(self) -> None:
+        generic_view = self._generic_view()
+
+        self.assertTrue(issubclass(GenericView, SupportsTags))
+        expected_methods = ["list_tags", "list_tags_info", "get_tag", 
"associate_tags"]
+        self.assertTrue(
+            all(
+                callable(getattr(generic_view, method, None))
+                for method in expected_methods
+            )
+        )

Reply via email to