Copilot commented on code in PR #9483: URL: https://github.com/apache/gravitino/pull/9483#discussion_r2621828574
########## clients/client-python/gravitino/client/generic_tag.py: ########## @@ -0,0 +1,150 @@ +# 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 typing import Optional + +from gravitino.api.metadata_object import MetadataObject +from gravitino.api.tag.tag import Tag +from gravitino.dto.audit_dto import AuditDTO +from gravitino.dto.responses.metadata_object_list_response import ( + MetadataObjectListResponse, +) +from gravitino.dto.tag_dto import TagDTO +from gravitino.exceptions.handlers.error_handler import ErrorHandler +from gravitino.exceptions.handlers.error_handlers import ErrorHandlers +from gravitino.rest.rest_utils import encode_string +from gravitino.utils import HTTPClient +from gravitino.utils.http_client import Response + + +class GenericTag(Tag, Tag.AssociatedObjects): + """Represents a generic tag.""" + + API_LIST_OBJECTS_ENDPOINT = "api/metalakes/{}/tags/{}/objects" + + def __init__( + self, + metalake: str, + tag_dto: TagDTO, + client: HTTPClient, + ) -> None: + self._metalake = metalake + self._tag_dto = tag_dto + self._client = client + + def __eq__(self, other: object) -> bool: + if not isinstance(other, GenericTag): + return False + + return self._tag_dto == other._tag_dto + + def __hash__(self) -> int: + return hash(self._tag_dto) + + def name(self) -> str: + """Get the name of the tag. + + Returns: + str: The name of the tag. + """ + return self._tag_dto.name() + + def comment(self) -> str: + """Get the comment of the tag. + + Returns: + str: The comment of the tag. + """ + return self._tag_dto.comment() + + def properties(self) -> dict[str, str]: + """Get the properties of the tag. + + Returns: + Dict[str, str]: The properties of the tag. + """ + return self._tag_dto.properties() + + def inherited(self) -> Optional[bool]: + """Check if the tag is inherited from a parent object or not. + + If the tag is inherited, it will return `True`, if it is owned by the object itself, it will return `False`. + + **Note**. The return value is optional, only when the tag is associated with an object, and called from the + object, the return value will be present. Otherwise, it will be empty. + + Returns: + Optional[bool]: + True if the tag is inherited, false if it is owned by the object itself. Empty if the + tag is not associated with any object. + """ + return self._tag_dto.inherited() + + def audit_info(self) -> AuditDTO: + """ + Retrieve The audit information of the entity. + + Returns: + AuditDTO: The audit information of the entity. + """ + return self._tag_dto.audit_info() + + def associated_objects(self) -> Tag.AssociatedObjects: + """ + The associated objects of the tag. + + Returns: + AssociatedObjects: The associated objects of the tag. + """ + return self + + def objects(self) -> list[MetadataObject]: + """ + Retrieve The list of objects that are associated with this tag. + + Returns: + list[MetadataObject]: The list of objects that are associated with this tag. + """ + url = self.API_LIST_OBJECTS_ENDPOINT.format( + self._metalake, + encode_string(self.name()), + ) + + response = self.get_response(url, ErrorHandlers.tag_error_handler()) + objects_resp = MetadataObjectListResponse.from_json( + response.body, infer_missing=True + ) + objects_resp.validate() + + return objects_resp.metadata_objects() + + def get_response(self, url: str, error_handler: ErrorHandler) -> Response: + """ + Get the response from the server. for test convenience. Review Comment: Grammatical error in documentation. The phrase 'for test convenience' should be 'for testing convenience' or 'for test convenience purposes' to be grammatically correct. ```suggestion Get the response from the server, for testing convenience. ``` ########## clients/client-python/gravitino/client/gravitino_metalake.py: ########## @@ -548,8 +570,18 @@ def get_tag(self, tag_name) -> Tag: Raises: NoSuchTagException: If the tag does not exist. """ - # TODO implement get_tag - raise NotImplementedError() + Precondition.check_string_not_empty( + tag_name, "tag name must not be null or empty" + ) + url = self.API_METALAKES_TAGS_PATH.format(encode_string(self.name())) Review Comment: The URL construction for getting a specific tag is incorrect. The get_tag method should append the tag_name to the base URL. Currently, it's using the same URL as list_tags, which would return all tags instead of a specific one. The correct pattern should be: url = f"{self.API_METALAKES_TAGS_PATH.format(encode_string(self.name()))}/{encode_string(tag_name)}" ```suggestion url = f"{self.API_METALAKES_TAGS_PATH.format(encode_string(self.name()))}/{encode_string(tag_name)}" ``` ########## clients/client-python/gravitino/dto/requests/tag_create_request.py: ########## @@ -0,0 +1,46 @@ +# 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 dataclasses import dataclass, field +from typing import Optional + +from dataclasses_json import config, dataclass_json + +from gravitino.rest.rest_message import RESTRequest +from gravitino.utils.precondition import Precondition + + +@dataclass_json +@dataclass +class TagCreateRequest(RESTRequest): + """Represents a request to create a tag.""" + + _name: str = field(metadata=config(field_name="name")) + _comment: Optional[str] = field(metadata=config(field_name="comment")) Review Comment: Missing default value for optional field. The _comment field is marked as Optional but doesn't have a default value. This will cause a dataclass error since it comes before _properties which has a default. Add 'default=None' to the field definition. ```suggestion _comment: Optional[str] = field(default=None, metadata=config(field_name="comment")) ``` ########## clients/client-python/gravitino/exceptions/handlers/error_handlers.py: ########## @@ -0,0 +1,51 @@ +# 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 gravitino.exceptions.handlers.job_error_handler import ( + JOB_ERROR_HANDLER, +) +from gravitino.exceptions.handlers.tag_error_handler import ( + TAG_ERROR_HANDLER, + RestErrorHandler, +) Review Comment: Incorrect import statement. RestErrorHandler is being imported from tag_error_handler module, but it should be imported from rest_error_handler module. The correct import should be: from gravitino.exceptions.handlers.rest_error_handler import RestErrorHandler ```suggestion ) from gravitino.exceptions.handlers.rest_error_handler import RestErrorHandler ``` ########## clients/client-python/gravitino/dto/responses/metadata_object_list_response.py: ########## @@ -0,0 +1,59 @@ +# 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 dataclasses import dataclass, field + +from dataclasses_json import config + +from gravitino.dto.metadata_object_dto import MetadataObjectDTO +from gravitino.dto.responses.base_response import BaseResponse +from gravitino.utils.precondition import Precondition + + +@dataclass +class MetadataObjectListResponse(BaseResponse): + """Represents a response containing a list of metadata objects.""" + + _metadata_objects: list[MetadataObjectDTO] = field( Review Comment: Missing default value for list field. The _metadata_objects field should have a default_factory to avoid potential initialization issues. Add default_factory=list to the field definition. ```suggestion _metadata_objects: list[MetadataObjectDTO] = field( default_factory=list, ``` ########## clients/client-python/gravitino/client/generic_tag.py: ########## @@ -0,0 +1,150 @@ +# 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 typing import Optional + +from gravitino.api.metadata_object import MetadataObject +from gravitino.api.tag.tag import Tag +from gravitino.dto.audit_dto import AuditDTO +from gravitino.dto.responses.metadata_object_list_response import ( + MetadataObjectListResponse, +) +from gravitino.dto.tag_dto import TagDTO +from gravitino.exceptions.handlers.error_handler import ErrorHandler +from gravitino.exceptions.handlers.error_handlers import ErrorHandlers +from gravitino.rest.rest_utils import encode_string +from gravitino.utils import HTTPClient +from gravitino.utils.http_client import Response + + +class GenericTag(Tag, Tag.AssociatedObjects): + """Represents a generic tag.""" + + API_LIST_OBJECTS_ENDPOINT = "api/metalakes/{}/tags/{}/objects" + + def __init__( + self, + metalake: str, + tag_dto: TagDTO, + client: HTTPClient, + ) -> None: + self._metalake = metalake + self._tag_dto = tag_dto + self._client = client + + def __eq__(self, other: object) -> bool: + if not isinstance(other, GenericTag): + return False + + return self._tag_dto == other._tag_dto + + def __hash__(self) -> int: + return hash(self._tag_dto) + + def name(self) -> str: + """Get the name of the tag. + + Returns: + str: The name of the tag. + """ + return self._tag_dto.name() + + def comment(self) -> str: + """Get the comment of the tag. + + Returns: + str: The comment of the tag. + """ + return self._tag_dto.comment() + + def properties(self) -> dict[str, str]: + """Get the properties of the tag. + + Returns: + Dict[str, str]: The properties of the tag. + """ + return self._tag_dto.properties() + + def inherited(self) -> Optional[bool]: + """Check if the tag is inherited from a parent object or not. + + If the tag is inherited, it will return `True`, if it is owned by the object itself, it will return `False`. + + **Note**. The return value is optional, only when the tag is associated with an object, and called from the + object, the return value will be present. Otherwise, it will be empty. + + Returns: + Optional[bool]: + True if the tag is inherited, false if it is owned by the object itself. Empty if the + tag is not associated with any object. + """ + return self._tag_dto.inherited() + + def audit_info(self) -> AuditDTO: + """ + Retrieve The audit information of the entity. + + Returns: + AuditDTO: The audit information of the entity. + """ + return self._tag_dto.audit_info() + + def associated_objects(self) -> Tag.AssociatedObjects: + """ + The associated objects of the tag. + + Returns: + AssociatedObjects: The associated objects of the tag. + """ + return self + + def objects(self) -> list[MetadataObject]: + """ + Retrieve The list of objects that are associated with this tag. Review Comment: Inconsistent capitalization in documentation. The word 'The' at the beginning of 'Retrieve The list' should be lowercase to be consistent with standard documentation style. Change to 'Retrieve the list'. ```suggestion Retrieve the list of objects that are associated with this tag. ``` ########## clients/client-python/gravitino/exceptions/base.py: ########## @@ -177,6 +177,10 @@ class TagAlreadyExistsException(AlreadyExistsException): """An exception thrown when a tag with specified name already associated to a metadata object.""" Review Comment: Incorrect exception docstring. TagAlreadyExistsException should describe when a tag already exists in the system (during creation), not when it's associated with an object. The docstring should be: "An exception thrown when a tag with specified name already exists." ```suggestion """An exception thrown when a tag with specified name already exists.""" ``` ########## clients/client-python/gravitino/client/generic_tag.py: ########## @@ -0,0 +1,150 @@ +# 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 typing import Optional + +from gravitino.api.metadata_object import MetadataObject +from gravitino.api.tag.tag import Tag +from gravitino.dto.audit_dto import AuditDTO +from gravitino.dto.responses.metadata_object_list_response import ( + MetadataObjectListResponse, +) +from gravitino.dto.tag_dto import TagDTO +from gravitino.exceptions.handlers.error_handler import ErrorHandler +from gravitino.exceptions.handlers.error_handlers import ErrorHandlers +from gravitino.rest.rest_utils import encode_string +from gravitino.utils import HTTPClient +from gravitino.utils.http_client import Response + + +class GenericTag(Tag, Tag.AssociatedObjects): + """Represents a generic tag.""" + + API_LIST_OBJECTS_ENDPOINT = "api/metalakes/{}/tags/{}/objects" + + def __init__( + self, + metalake: str, + tag_dto: TagDTO, + client: HTTPClient, + ) -> None: + self._metalake = metalake + self._tag_dto = tag_dto + self._client = client + + def __eq__(self, other: object) -> bool: + if not isinstance(other, GenericTag): + return False + + return self._tag_dto == other._tag_dto + + def __hash__(self) -> int: + return hash(self._tag_dto) + + def name(self) -> str: + """Get the name of the tag. + + Returns: + str: The name of the tag. + """ + return self._tag_dto.name() + + def comment(self) -> str: + """Get the comment of the tag. + + Returns: + str: The comment of the tag. + """ + return self._tag_dto.comment() + + def properties(self) -> dict[str, str]: + """Get the properties of the tag. + + Returns: + Dict[str, str]: The properties of the tag. + """ + return self._tag_dto.properties() + + def inherited(self) -> Optional[bool]: + """Check if the tag is inherited from a parent object or not. + + If the tag is inherited, it will return `True`, if it is owned by the object itself, it will return `False`. + + **Note**. The return value is optional, only when the tag is associated with an object, and called from the + object, the return value will be present. Otherwise, it will be empty. + + Returns: + Optional[bool]: + True if the tag is inherited, false if it is owned by the object itself. Empty if the + tag is not associated with any object. + """ + return self._tag_dto.inherited() + + def audit_info(self) -> AuditDTO: + """ + Retrieve The audit information of the entity. Review Comment: Inconsistent capitalization in documentation. The word 'The' at the beginning of 'Retrieve The audit' should be lowercase to be consistent with standard documentation style. Change to 'Retrieve the audit'. ```suggestion Retrieve the audit information of the entity. ``` ########## clients/client-python/gravitino/dto/responses/metadata_object_list_response.py: ########## @@ -0,0 +1,59 @@ +# 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 dataclasses import dataclass, field + +from dataclasses_json import config + +from gravitino.dto.metadata_object_dto import MetadataObjectDTO +from gravitino.dto.responses.base_response import BaseResponse +from gravitino.utils.precondition import Precondition + + +@dataclass +class MetadataObjectListResponse(BaseResponse): + """Represents a response containing a list of metadata objects.""" + + _metadata_objects: list[MetadataObjectDTO] = field( + metadata=config(field_name="metadataObjects") + ) + + def metadata_objects(self) -> list[MetadataObjectDTO]: + """ + Retrieve the list of metadata objects. + + Returns: + list[MetadataObjectDTO]: The list of metadata objects. + """ + return self._metadata_objects + + def validate(self) -> None: + """ + Validates the response data. + """ + super().validate() + + for metadata_object in self._metadata_objects: + Precondition.check_argument( + metadata_object is not None + and metadata_object.type() is not None + and (name := metadata_object.name()) is not None + and name.strip() != "", + "metadataObject must not be null and it's field cannot null or empty", Review Comment: Grammar error: "it's" should be "its" (possessive form, not contraction). The error message should read: "metadataObject must not be null and its field cannot be null or empty" ```suggestion "metadataObject must not be null and its field cannot null or empty", ``` ########## clients/client-python/gravitino/api/tag/tag.py: ########## @@ -117,3 +117,28 @@ def associated_objects(self) -> AssociatedObjects: raise UnsupportedOperationException( "The associated_objects method is not supported." ) + + class AssociatedObjects(ABC): + """The interface of the associated objects of the tag.""" + + def count(self) -> int: + """ + Retrive the number of objects that are associated with this Tag + + Returns: + int: The number of objects that are associated with this Tag + """ + return 0 if (s := self.objects()) is None else len(s) + + @abstractmethod + def objects(self) -> list[MetadataObject]: + """ + Retrieve The list of objects that are associated with this tag. Review Comment: Inconsistent capitalization in documentation. The word 'The' at the beginning of 'Retrieve The list' should be lowercase to be consistent with standard documentation style. Change to 'Retrieve the list'. ```suggestion Retrieve the list of objects that are associated with this tag. ``` ########## clients/client-python/gravitino/api/tag/tag.py: ########## @@ -117,3 +117,28 @@ def associated_objects(self) -> AssociatedObjects: raise UnsupportedOperationException( "The associated_objects method is not supported." ) + + class AssociatedObjects(ABC): + """The interface of the associated objects of the tag.""" + + def count(self) -> int: + """ + Retrive the number of objects that are associated with this Tag Review Comment: Spelling error: 'Retrive' should be 'Retrieve'. ```suggestion Retrieve the number of objects that are associated with this Tag ``` ########## clients/client-python/gravitino/client/gravitino_metalake.py: ########## @@ -71,6 +75,7 @@ class GravitinoMetalake( API_METALAKES_CATALOGS_PATH = "api/metalakes/{}/catalogs/{}" API_METALAKES_JOB_TEMPLATES_PATH = "api/metalakes/{}/jobs/templates" API_METALAKES_JOB_RUNS_PATH = "api/metalakes/{}/jobs/runs" + API_METALAKES_TAGS_PATH = "api/metalakes/{}/tags/{}" Review Comment: The API_METALAKES_TAGS_PATH constant has two placeholders but should only have one. According to the Java implementation, the path should be "api/metalakes/{}/tags" with only the metalake name placeholder. The tag name should be appended when needed (e.g., in get_tag method). ```suggestion API_METALAKES_TAGS_PATH = "api/metalakes/{}/tags" ``` ########## clients/client-python/tests/unittests/dto/requests/test_tag_create_request.py: ########## @@ -0,0 +1,44 @@ +# 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 import TagCreateRequest + + +class TestTagCreateRequest(unittest.TestCase): + def tag_create_request_serde(self) -> None: Review Comment: Test method name is missing the 'test_' prefix. The method should be named 'test_tag_create_request_serde' to be discovered and executed by the test runner. ```suggestion def test_tag_create_request_serde(self) -> None: ``` ########## clients/client-python/gravitino/dto/tag_dto.py: ########## @@ -0,0 +1,152 @@ +# 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 dataclasses import dataclass, field +from typing import Optional + +from dataclasses_json import config, dataclass_json + +from gravitino.api.tag.tag import Tag +from gravitino.dto.audit_dto import AuditDTO + + +@dataclass_json +@dataclass +class TagDTO(Tag): + """Represents a Tag Data Transfer Object (DTO).""" + + _name: str = field(metadata=config(field_name="name")) + _comment: str = field(metadata=config(field_name="comment")) + _properties: dict[str, str] = field(metadata=config(field_name="properties")) + + _audit: AuditDTO = field(default=None, metadata=config(field_name="audit")) + _inherited: Optional[bool] = field( + default=None, metadata=config(field_name="inherited") + ) + + def __eq__(self, other: object): + if not isinstance(other, TagDTO): + return False + return ( + self._name == other._name + and self._comment == other._comment + and self._properties == other._properties + and self._audit == other._audit + ) + + def __hash__(self) -> int: + return hash( + ( + self._name, + self._comment, + frozenset(self._properties.items()) if self._properties else None, + self._audit, + ) + ) + + @staticmethod + def builder() -> TagDTO.Builder: + return TagDTO.Builder() + + def name(self) -> str: + """Get the name of the tag. + + Returns: + str: The name of the tag. + """ + return self._name + + def comment(self): Review Comment: Missing return type hint. The comment method should have a return type annotation of '-> str' to be consistent with the other methods in the class and improve type safety. ```suggestion def comment(self) -> str: ``` ########## clients/client-python/gravitino/dto/responses/tag_response.py: ########## @@ -0,0 +1,70 @@ +# 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 dataclasses import dataclass, field + +from dataclasses_json import config + +from gravitino.dto.responses.base_response import BaseResponse +from gravitino.dto.tag_dto import TagDTO +from gravitino.utils.precondition import Precondition + + +@dataclass +class TagNamesListResponse(BaseResponse): + """Represents a response for a Tag Names List request.""" + + _tags: list[str] = field(default_factory=list, metadata=config(field_name="names")) + + def tag_names(self) -> list[str]: + return self._tags + + def validate(self) -> None: + Precondition.check_argument( + self._tags is not None, "Tag Names List response should have tags" + ) + + for tag_name in self._tags: + Precondition.check_string_not_empty( + tag_name, "Tag Names List response should have non-empty tag names" + ) + + +@dataclass +class TagListResponse(BaseResponse): + """Represents a response for a Tag List request.""" + + _tags: list[TagDTO] = field( + default_factory=list, metadata=config(field_name="tags") + ) + + def tags(self) -> list[TagDTO]: + return self._tags + + +@dataclass +class TagResponse(BaseResponse): + """Represents a response for a tag.""" + + _tag: TagDTO = field(default=None, metadata=config(field_name="tag")) + + def validate(self) -> None: + Precondition.check_argument( + self._tag is not None, "Tag response should have a tag" + ) Review Comment: Missing getter method for the tag field. The TagResponse class is missing a tag() method to access the _tag field. This is needed at line 581 and 617 in gravitino_metalake.py where tag_resp.tag() is called. Add a method: def tag(self) -> TagDTO: return self._tag ########## clients/client-python/tests/unittests/test_generic_tag.py: ########## @@ -0,0 +1,230 @@ +# 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 unittest.mock import MagicMock + +from gravitino.api.metadata_object import MetadataObject +from gravitino.client.generic_tag import GenericTag +from gravitino.dto.audit_dto import AuditDTO +from gravitino.dto.metadata_object_dto import MetadataObjectDTO +from gravitino.dto.tag_dto import TagDTO +from gravitino.exceptions.base import InternalError, NoSuchMetalakeException +from gravitino.utils import HTTPClient +from gravitino.utils.http_client import Response + + +class TestGenericTag(unittest.TestCase): + METALAKE = "metalake1" + + TAG_DTO = ( + TagDTO.Builder() + .name("tag1") + .comment("comment1") + .properties({"key1": "value1"}) + .inherited(True) + .audit_info(AuditDTO(_creator="test", _create_time="2022-01-01T00:00:00Z")) + .build() + ) + + @classmethod + def setUpClass(cls): + cls._rest_client = HTTPClient("http://localhost:8090") + cls._metalake_name = "metalake_demo" + + def test_create_generic_tag(self): + generic_tag = GenericTag(self.METALAKE, self.TAG_DTO, self._rest_client) + self.assertEqual("tag1", generic_tag.name()) + self.assertEqual("comment1", generic_tag.comment()) + self.assertEqual({"key1": "value1"}, generic_tag.properties()) + self.assertEqual(True, generic_tag.inherited()) + self.assertEqual( + AuditDTO(_creator="test", _create_time="2022-01-01T00:00:00Z"), + generic_tag.audit_info(), + ) + + def test_generic_tag_associated_objects(self): + # Test normal situation + response_body = { + "code": 0, + "metadataObjects": [ + { + "fullName": "catalog1", + "type": "catalog", + }, + { + "fullName": "catalog1.schema1", + "type": "schema", + }, + { + "fullName": "catalog1.schema1.table1", + "type": "table", + }, + { + "fullName": "catalog1.schema1.table1.column1", + "type": "column", + }, + ], + } + + expected_associated_objects = [ + MetadataObjectDTO.builder() + .full_name("catalog1") + .type(MetadataObject.Type.CATALOG) + .build(), + MetadataObjectDTO.builder() + .full_name("catalog1.schema1") + .type(MetadataObject.Type.SCHEMA) + .build(), + MetadataObjectDTO.builder() + .full_name("catalog1.schema1.table1") + .type(MetadataObject.Type.TABLE) + .build(), + MetadataObjectDTO.builder() + .full_name("catalog1.schema1.table1.column1") + .type(MetadataObject.Type.COLUMN) + .build(), + ] + generic_tag = TestGenericTagEntity( + self.METALAKE, self.TAG_DTO, self._rest_client, response_body + ) + objects = generic_tag.associated_objects().objects() + self.assertEqual(4, len(objects)) + self.assertEqual(4, generic_tag.count()) + + for i, v in enumerate(objects): + self.assertEqual(v, expected_associated_objects[i]) + self.assertEqual(v.full_name(), expected_associated_objects[i].full_name()) + self.assertEqual(v.type(), expected_associated_objects[i].type()) + self.assertEqual(v.parent(), expected_associated_objects[i].parent()) + self.assertEqual(v.name(), expected_associated_objects[i].name()) + + # Test return empty array + generic_tag = TestGenericTagEntity( + self.METALAKE, + self.TAG_DTO, + self._rest_client, + { + "code": 0, + "metadataObjects": [], + }, + ) + objects = generic_tag.associated_objects().objects() + self.assertEqual(0, len(objects)) + self.assertEqual(0, generic_tag.count()) + + # Test throw NoSuchMetalakeException + with self.assertRaises(NoSuchMetalakeException): + generic_tag = TestGenericTagEntity( + self.METALAKE, + self.TAG_DTO, + self._rest_client, + { + "code": 0, + "metadataObjects": [], + }, + NoSuchMetalakeException, + ) + generic_tag.associated_objects().objects() + + # Test throw internal error + with self.assertRaises(InternalError): + generic_tag = TestGenericTagEntity( + self.METALAKE, + self.TAG_DTO, + self._rest_client, + { + "code": 0, + "metadataObjects": [], + }, + InternalError, + ) + generic_tag.associated_objects().objects() + + def test_hash_and_equal(self) -> None: + tag_dto1 = ( + TagDTO.Builder() + .name("tag1") + .comment("comment1") + .properties({"key1": "value1"}) + .inherited(True) + .audit_info(AuditDTO(_creator="test", _create_time="2022-01-01T00:00:00Z")) + .build() + ) + generic_tag1 = GenericTag(self.METALAKE, tag_dto1, self._rest_client) + + tag_dto2 = ( + TagDTO.Builder() + .name("tag1") + .comment("comment1") + .properties({"key1": "value1"}) + .inherited(True) + .audit_info(AuditDTO(_creator="test", _create_time="2022-01-01T00:00:00Z")) + .build() + ) + generic_tag2 = GenericTag(self.METALAKE, tag_dto2, self._rest_client) + + tag_dto3 = ( + TagDTO.Builder() + .name("tag1") + .comment("comment1") + .properties({"key1": "value1"}) + .inherited(True) + .audit_info(AuditDTO(_creator="test", _create_time="2022-01-02T00:00:00Z")) + .build() + ) + generic_tag3 = GenericTag(self.METALAKE, tag_dto3, self._rest_client) + + self.assertEqual(generic_tag1, generic_tag2) + self.assertEqual(hash(generic_tag1), hash(generic_tag2)) + + self.assertNotEqual(generic_tag1, generic_tag3) + self.assertNotEqual(hash(generic_tag1), hash(generic_tag3)) + + +class TestGenericTagEntity(GenericTag): Review Comment: The class 'TestGenericTagEntity' does not override ['__eq__'](1), but adds the new attribute [dump_object](2). The class 'TestGenericTagEntity' does not override ['__eq__'](1), but adds the new attribute [throw_error](3). -- 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]
