Copilot commented on code in PR #10670:
URL: https://github.com/apache/gravitino/pull/10670#discussion_r3044985592


##########
clients/client-python/gravitino/client/metadata_object_tag_operations.py:
##########
@@ -0,0 +1,124 @@
+# 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 gravitino.api.metadata_object import MetadataObject
+from gravitino.api.tag.supports_tags import SupportsTags
+from gravitino.api.tag.tag import Tag
+from gravitino.client.generic_tag import GenericTag
+from gravitino.dto.requests.tag_associate_request import TagsAssociateRequest
+from gravitino.dto.responses.tag_response import (
+    TagListResponse,
+    TagNamesListResponse,
+    TagResponse,
+)
+from gravitino.exceptions.handlers.tag_error_handler import TAG_ERROR_HANDLER
+from gravitino.rest.rest_utils import encode_string
+from gravitino.utils.http_client import HTTPClient
+
+
+class MetadataObjectTagOperations(SupportsTags):
+    """
+    The implementation of SupportsTags. This interface will be composited into 
catalog, schema,
+    table, fileset and topic to provide tag operations for these metadata 
objects
+    """
+
+    TAG_REQUEST_PATH = "api/metalakes/{}/objects/{}/{}/tags"
+
+    def __init__(
+        self,
+        metalake_name: str,
+        metadata_object: MetadataObject,
+        rest_client: HTTPClient,
+    ):
+        super().__init__()
+        self.metalake_name = metalake_name
+        self.metadata_object = metadata_object
+        self.rest_client = rest_client
+        self.tag_request_path = 
MetadataObjectTagOperations.TAG_REQUEST_PATH.format(
+            encode_string(metalake_name),
+            metadata_object.type().name.lower(),
+            encode_string(metadata_object.full_name()),
+        )
+
+    def list_tags(self) -> list[str]:
+        response = self.rest_client.get(
+            self.tag_request_path,
+            params={},
+            error_handler=TAG_ERROR_HANDLER,
+        )
+        list_tags_resp = TagNamesListResponse.from_json(
+            response.body, infer_missing=True
+        )
+        list_tags_resp.validate()
+
+        return list_tags_resp.tag_names()
+
+    def list_tags_info(self) -> list[Tag]:
+        response = self.rest_client.get(
+            self.tag_request_path,
+            params={"details": "true"},
+            error_handler=TAG_ERROR_HANDLER,
+        )
+
+        list_info_resp = TagListResponse.from_json(response.body, 
infer_missing=True)
+        list_info_resp.validate()
+
+        return [
+            GenericTag(
+                self.metalake_name,
+                tag_dto,
+                self.rest_client,
+            )
+            for tag_dto in list_info_resp.tags()
+        ]
+
+    def get_tag(self, name) -> Tag:
+        response = self.rest_client.get(
+            f"{self.tag_request_path}/{encode_string(name)}",
+            params={},
+            error_handler=TAG_ERROR_HANDLER,
+        )

Review Comment:
   `get_tag` is missing a `name: str` type annotation and does not validate 
that `name` is non-empty before URL-encoding. This is inconsistent with other 
tag APIs (e.g., metalake `get_tag`) and can lead to confusing errors (e.g., 
encoding `None`/blank). Add a precondition check (non-null/non-blank) and type 
the parameter as `str`.



##########
clients/client-python/gravitino/client/relational_table.py:
##########
@@ -213,3 +227,20 @@ def add_partition(self, partition: Partition) -> Partition:
         )
         partition_list_resp.validate()
         return partition_list_resp.get_partitions()[0]
+
+    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 table_full_name(self, table_ns: Namespace, table_name: str) -> str:

Review Comment:
   `table_full_name` looks like an internal helper (it doesn’t use any instance 
state beyond inputs) but is currently a public instance method on 
`RelationalTable`. Consider making it a private `@staticmethod` (or inlining) 
to avoid expanding the public API surface unnecessarily.
   ```suggestion
       @staticmethod
       def _table_full_name(table_ns: Namespace, table_name: str) -> str:
   ```



##########
clients/client-python/gravitino/client/generic_column.py:
##########
@@ -36,6 +41,15 @@ def __init__(
         table: str,
     ):
         self._internal_column = column
+        column_object = MetadataObjects.of(
+            [catalog, schema, table, column.name()],
+            MetadataObject.Type.COLUMN,
+        )
+        self.object_tag_operations = MetadataObjectTagOperations(
+            metalake,
+            column_object,
+            rest_client,
+        )

Review Comment:
   `object_tag_operations` is introduced as a public attribute, but the rest of 
`GenericColumn`’s internal state uses leading-underscore names (e.g., 
`_internal_column`). Consider renaming this to `_object_tag_operations` (and 
updating references) to avoid exposing a mutable internal helper as part of the 
public surface area.



##########
clients/client-python/gravitino/client/metadata_object_tag_operations.py:
##########
@@ -0,0 +1,124 @@
+# 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 gravitino.api.metadata_object import MetadataObject
+from gravitino.api.tag.supports_tags import SupportsTags
+from gravitino.api.tag.tag import Tag
+from gravitino.client.generic_tag import GenericTag
+from gravitino.dto.requests.tag_associate_request import TagsAssociateRequest
+from gravitino.dto.responses.tag_response import (
+    TagListResponse,
+    TagNamesListResponse,
+    TagResponse,
+)
+from gravitino.exceptions.handlers.tag_error_handler import TAG_ERROR_HANDLER
+from gravitino.rest.rest_utils import encode_string
+from gravitino.utils.http_client import HTTPClient
+
+
+class MetadataObjectTagOperations(SupportsTags):
+    """
+    The implementation of SupportsTags. This interface will be composited into 
catalog, schema,
+    table, fileset and topic to provide tag operations for these metadata 
objects
+    """
+
+    TAG_REQUEST_PATH = "api/metalakes/{}/objects/{}/{}/tags"
+
+    def __init__(
+        self,
+        metalake_name: str,
+        metadata_object: MetadataObject,
+        rest_client: HTTPClient,
+    ):
+        super().__init__()
+        self.metalake_name = metalake_name
+        self.metadata_object = metadata_object
+        self.rest_client = rest_client
+        self.tag_request_path = 
MetadataObjectTagOperations.TAG_REQUEST_PATH.format(
+            encode_string(metalake_name),
+            metadata_object.type().name.lower(),
+            encode_string(metadata_object.full_name()),
+        )
+
+    def list_tags(self) -> list[str]:
+        response = self.rest_client.get(
+            self.tag_request_path,
+            params={},
+            error_handler=TAG_ERROR_HANDLER,
+        )
+        list_tags_resp = TagNamesListResponse.from_json(
+            response.body, infer_missing=True
+        )
+        list_tags_resp.validate()
+
+        return list_tags_resp.tag_names()
+
+    def list_tags_info(self) -> list[Tag]:
+        response = self.rest_client.get(
+            self.tag_request_path,
+            params={"details": "true"},
+            error_handler=TAG_ERROR_HANDLER,
+        )
+
+        list_info_resp = TagListResponse.from_json(response.body, 
infer_missing=True)
+        list_info_resp.validate()
+
+        return [
+            GenericTag(
+                self.metalake_name,
+                tag_dto,
+                self.rest_client,
+            )
+            for tag_dto in list_info_resp.tags()
+        ]
+
+    def get_tag(self, name) -> Tag:
+        response = self.rest_client.get(
+            f"{self.tag_request_path}/{encode_string(name)}",
+            params={},
+            error_handler=TAG_ERROR_HANDLER,
+        )
+
+        get_resp = TagResponse.from_json(response.body, infer_missing=True)
+        get_resp.validate()
+
+        return GenericTag(
+            self.metalake_name,
+            get_resp.tag(),
+            self.rest_client,
+        )
+
+    def associate_tags(
+        self, tags_to_add: list[str], tags_to_remove: list[str]
+    ) -> list[str]:
+        associate_resp = TagsAssociateRequest(tags_to_add, tags_to_remove)
+        associate_resp.validate()
+
+        response = self.rest_client.post(
+            self.tag_request_path,
+            json=associate_resp,
+            error_handler=TAG_ERROR_HANDLER,
+        )
+
+        associate_resp = TagNamesListResponse.from_json(
+            response.body, infer_missing=True
+        )
+        associate_resp.validate()
+
+        return associate_resp.tag_names()

Review Comment:
   In `associate_tags`, the variable name `associate_resp` is reused for both 
the request DTO and the response DTO, which makes the flow harder to follow and 
easier to misuse during future edits. Use distinct names (e.g., `request` / 
`response` or `resp_dto`) for clarity.
   ```suggestion
           associate_request = TagsAssociateRequest(tags_to_add, tags_to_remove)
           associate_request.validate()
   
           response = self.rest_client.post(
               self.tag_request_path,
               json=associate_request,
               error_handler=TAG_ERROR_HANDLER,
           )
   
           associate_response = TagNamesListResponse.from_json(
               response.body, infer_missing=True
           )
           associate_response.validate()
   
           return associate_response.tag_names()
   ```



##########
clients/client-python/gravitino/client/metadata_object_tag_operations.py:
##########
@@ -0,0 +1,124 @@
+# 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 gravitino.api.metadata_object import MetadataObject
+from gravitino.api.tag.supports_tags import SupportsTags
+from gravitino.api.tag.tag import Tag
+from gravitino.client.generic_tag import GenericTag
+from gravitino.dto.requests.tag_associate_request import TagsAssociateRequest
+from gravitino.dto.responses.tag_response import (
+    TagListResponse,
+    TagNamesListResponse,
+    TagResponse,
+)
+from gravitino.exceptions.handlers.tag_error_handler import TAG_ERROR_HANDLER
+from gravitino.rest.rest_utils import encode_string
+from gravitino.utils.http_client import HTTPClient
+
+
+class MetadataObjectTagOperations(SupportsTags):
+    """
+    The implementation of SupportsTags. This interface will be composited into 
catalog, schema,
+    table, fileset and topic to provide tag operations for these metadata 
objects

Review Comment:
   The class docstring lists the objects this helper is composed into as 
"catalog, schema, table, fileset and topic", but this implementation is also 
used for columns (and likely models via catalogs). Update the docstring to 
reflect current usage to avoid misleading API readers.
   ```suggestion
       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.
   ```



##########
clients/client-python/tests/unittests/dto/requests/test_tags_associate_request.py:
##########
@@ -0,0 +1,61 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+import json as _json
+import unittest
+
+from gravitino.dto.requests.tag_associate_request import TagsAssociateRequest
+from gravitino.exceptions.base import IllegalArgumentException
+
+
+class TestTagsAssociateRequest(unittest.TestCase):
+    def test_create_request(self) -> None:
+        request = TagsAssociateRequest(
+            ["tag_to_add_1", "tag_to_add_2"], ["tag_to_remove_1", 
"tag_to_remove_2"]
+        )
+        json_str = _json.dumps(
+            {
+                "tagsToAdd": ["tag_to_add_1", "tag_to_add_2"],
+                "tagsToRemove": ["tag_to_remove_1", "tag_to_remove_2"],
+            }
+        )
+
+        self.assertEqual(json_str, request.to_json())
+        deserialized_request = TagsAssociateRequest.from_json(json_str)
+
+        self.assertTrue(isinstance(deserialized_request, TagsAssociateRequest))
+        self.assertEqual(
+            ["tag_to_add_1", "tag_to_add_2"], deserialized_request.tags_to_add
+        )
+        self.assertEqual(
+            ["tag_to_remove_1", "tag_to_remove_2"], 
deserialized_request.tags_to_remove
+        )
+
+    def test_associaate_request_validate(self) -> None:
+        invalid_request1 = TagsAssociateRequest(
+            None, None
+        )  # pyright: ignore[reportArgumentType]
+        invalid_request2 = TagsAssociateRequest(
+            ["tag_to_add_1", " "], ["tag_to_remove_1", "tag_to_remove_2"]
+        )

Review Comment:
   Typo in test method name: `test_associaate_request_validate` should be 
`test_associate_request_validate`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to