This is an automated email from the ASF dual-hosted git repository. yuqi4733 pushed a commit to branch internal-main in repository https://gitbox.apache.org/repos/asf/gravitino.git
commit 5b6a5f67accdfb32698d1b92e5d38a1c4deaccee Author: Lord of Abyss <[email protected]> AuthorDate: Fri Dec 12 18:47:31 2025 +0800 [#9241] feat(python-client): Add tag-related interface (#9242) ### What changes were proposed in this pull request? Add tag-related interface: 1. Add SupportsTags interface 2. Add TagOperations interface 3. Add TagChange and related dataclass ### Why are the changes needed? Fix: #9241 ### Does this PR introduce _any_ user-facing change? no ### How was this patch tested? local test. --- .../client-python/gravitino/api/tag/__init__.py | 10 ++ .../gravitino/api/tag/supports_tags.py | 20 +-- clients/client-python/gravitino/api/tag/tag.py | 9 +- .../client-python/gravitino/api/tag/tag_change.py | 144 +++++++++++++++++++++ .../gravitino/api/tag/tag_operations.py | 135 +++++++++++++++++++ .../gravitino/client/gravitino_client.py | 103 ++++++++++++++- .../gravitino/client/gravitino_metalake.py | 113 +++++++++++++++- .../tests/unittests/api/tag/test_tag_change.py | 50 +++++++ 8 files changed, 563 insertions(+), 21 deletions(-) diff --git a/clients/client-python/gravitino/api/tag/__init__.py b/clients/client-python/gravitino/api/tag/__init__.py index 13a83393a9..28b1469c2f 100644 --- a/clients/client-python/gravitino/api/tag/__init__.py +++ b/clients/client-python/gravitino/api/tag/__init__.py @@ -14,3 +14,13 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. + +from __future__ import annotations + +from gravitino.api.tag.tag import Tag +from gravitino.api.tag.tag_change import TagChange + +__all__ = [ + "Tag", + "TagChange", +] diff --git a/clients/client-python/gravitino/api/tag/supports_tags.py b/clients/client-python/gravitino/api/tag/supports_tags.py index 89b405f890..f1e9a20510 100644 --- a/clients/client-python/gravitino/api/tag/supports_tags.py +++ b/clients/client-python/gravitino/api/tag/supports_tags.py @@ -15,9 +15,9 @@ # specific language governing permissions and limitations # under the License. +from __future__ import annotations from abc import ABC, abstractmethod -from typing import List from gravitino.api.tag.tag import Tag @@ -29,20 +29,20 @@ class SupportsTags(ABC): """ @abstractmethod - def list_tags(self) -> List[str]: + def list_tags(self) -> list[str]: """List all the tag names for the specific object. Returns: - List[str]: The list of tag names. + list[str]: The list of tag names. """ pass @abstractmethod - def list_tags_info(self) -> List[Tag]: + def list_tags_info(self) -> list[Tag]: """List all the tags with details for the specific object. Returns: - List[Tag]: The list of tags. + list[Tag]: The list of tags. """ pass @@ -63,8 +63,8 @@ class SupportsTags(ABC): @abstractmethod def associate_tags( - self, tags_to_add: List[str], tags_to_remove: List[str] - ) -> List[str]: + self, tags_to_add: list[str], tags_to_remove: list[str] + ) -> list[str]: """Associate tags to the specific object. The `tags_to_add` will be added to the object, and the `tags_to_remove` will be removed from the object. @@ -75,13 +75,13 @@ class SupportsTags(ABC): 3. If the tag is already associated with the object, it will raise `TagAlreadyAssociatedException`. Args: - tags_to_add (List[str]): The arrays of tag name to be added to the object. - tags_to_remove (List[str]): The array of tag name to be removed from the object. + tags_to_add (list[str]): The arrays of tag name to be added to the object. + tags_to_remove (list[str]): The array of tag name to be removed from the object. Raises: TagAlreadyAssociatedException: If the tag is already associated with the object. Returns: - List[str]: The array of tag names that are associated with the object. + list[str]: The array of tag names that are associated with the object. """ pass diff --git a/clients/client-python/gravitino/api/tag/tag.py b/clients/client-python/gravitino/api/tag/tag.py index 6e2a510461..db8ee0e06c 100644 --- a/clients/client-python/gravitino/api/tag/tag.py +++ b/clients/client-python/gravitino/api/tag/tag.py @@ -15,9 +15,10 @@ # specific language governing permissions and limitations # under the License. +from __future__ import annotations from abc import ABC, abstractmethod -from typing import ClassVar, Dict, List, Optional +from typing import ClassVar, Optional from gravitino.api.auditable import Auditable from gravitino.api.metadata_object import MetadataObject @@ -38,11 +39,11 @@ class AssociatedObjects(ABC): return 0 if objects is None else len(objects) @abstractmethod - def objects(self) -> Optional[List[MetadataObject]]: + def objects(self) -> Optional[list[MetadataObject]]: """Get the associated objects. Returns: - Optional[List[MetadataObject]]: The list of objects that are associated with this tag.. + Optional[list[MetadataObject]]: The list of objects that are associated with this tag.. """ pass @@ -80,7 +81,7 @@ class Tag(Auditable): pass @abstractmethod - def properties(self) -> Dict[str, str]: + def properties(self) -> dict[str, str]: """Get the properties of the tag. Returns: diff --git a/clients/client-python/gravitino/api/tag/tag_change.py b/clients/client-python/gravitino/api/tag/tag_change.py new file mode 100644 index 0000000000..1bb84e991f --- /dev/null +++ b/clients/client-python/gravitino/api/tag/tag_change.py @@ -0,0 +1,144 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + + +from __future__ import annotations + +from abc import ABC +from dataclasses import dataclass, field + +from dataclasses_json import config + + +class TagChange(ABC): + """Interface for supporting tag changes. + This interface will be used to provide tag modification operations for each tag.""" + + @staticmethod + def rename(new_name: str) -> RenameTag: + """ + Create a tag change instance to rename the tag. + + Args: + new_name (str): The new name of the tag. + + Returns: + RenameTag: A tag change instance to rename the tag. + """ + return TagChange.RenameTag(new_name) + + @staticmethod + def update_comment(new_comment: str) -> UpdateTagComment: + """ + Create a tag change instance to update the tag comment. + + Args: + new_comment (str): The new comment of the tag. + + Returns: + UpdateTagComment: A tag change instance to update the tag comment. + """ + return TagChange.UpdateTagComment(new_comment) + + @staticmethod + def set_property(tag_property: str, tag_value: str) -> SetProperty: + """ + Creates a new tag change instance to set the property and value for the tag. + + Args: + tag_property (str): The property to set. + tag_value (str): The value to set. + + Returns: + SetProperty: The tag change instance to set the property and value for the tag. + """ + return TagChange.SetProperty(tag_property, tag_value) + + @staticmethod + def remove_property(tag_property: str) -> RemoveProperty: + """ + Creates a new tag change instance to remove a property from the tag. + + Args: + tag_property (str): The property to remove. + + Returns: + RemoveProperty: The tag change instance to remove a property from the tag. + """ + return TagChange.RemoveProperty(tag_property) + + @dataclass(frozen=True, eq=True) + class RenameTag: + """A tag change to rename the tag.""" + + _new_name: str = field(metadata=config(field_name="newName")) + + @property + def new_name(self) -> str: + """The new name of the tag.""" + return self._new_name + + def __str__(self) -> str: + return f"RENAMETAG {self._new_name}" + + @dataclass(frozen=True, eq=True) + class UpdateTagComment: + """A tag change to update the tag comment.""" + + _new_comment: str = field(metadata=config(field_name="newComment")) + + @property + def new_comment(self) -> str: + """The new comment of the tag.""" + return self._new_comment + + def __str__(self) -> str: + return f"UPDATETAGCOMMENT {self._new_comment}" + + @dataclass(frozen=True, eq=True) + class SetProperty: + """A tag change to set a property-value pair for the tag.""" + + _property: str = field(metadata=config(field_name="property")) + _value: str = field(metadata=config(field_name="value")) + + @property + def name(self) -> str: + """The property to set.""" + return self._property + + @property + def value(self) -> str: + """The value to set.""" + return self._value + + def __str__(self) -> str: + return f"SETTAGPROPERTY {self._property} = {self._value}" + + @dataclass(frozen=True, eq=True) + class RemoveProperty: + """A tag change to remove a property-value pair for the tag.""" + + _property: str = field(metadata=config(field_name="property")) + + @property + def removed_property(self) -> str: + """The property to remove.""" + return self._property + + def __str__(self) -> str: + return f"REMOVETAGPROPERTY {self._property}" diff --git a/clients/client-python/gravitino/api/tag/tag_operations.py b/clients/client-python/gravitino/api/tag/tag_operations.py new file mode 100644 index 0000000000..58bbce2657 --- /dev/null +++ b/clients/client-python/gravitino/api/tag/tag_operations.py @@ -0,0 +1,135 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +from __future__ import annotations + +from abc import ABC, abstractmethod + +from gravitino.api.tag.tag import Tag +from gravitino.api.tag.tag_change import TagChange + + +class TagOperations(ABC): + """ + Interface for supporting global tag operations. This interface will provide tag listing, getting, + creating, and other tag operations under a metalake. This interface will be mixed with + GravitinoMetalake or GravitinoClient to provide tag operations. + """ + + @abstractmethod + def list_tags(self) -> list[str]: + """List all the tag names under a metalake. + + Returns: + list[str]: The list of tag names. + + Raises: + NoSuchMetalakeException: If the metalake does not exist. + """ + pass + + @abstractmethod + def list_tags_info(self) -> list[Tag]: + """ + List tags information under a metalake. + + Returns: + list[Tag]: The list of tag information. + + Raises: + NoSuchMetalakeException: If the metalake does not exist. + """ + pass + + @abstractmethod + def get_tag(self, tag_name: str) -> Tag: + """ + Get a tag by its name under a metalake. + + Args: + tag_name (str): The name of the tag. + + Returns: + Tag: The tag information. + + Raises: + NoSuchTagException: If the tag does not exist. + """ + pass + + @abstractmethod + def create_tag( + self, + tag_name: str, + comment: str, + properties: dict[str, str], + ) -> Tag: + """ + Create a new tag under a metalake. + + Raises: + NoSuchMetalakeException: If the metalake does not exist. + TagAlreadyExistsException: If the tag already exists. + + Args: + tag_name (str): The name of the tag. + comment (str): The comment of the tag. + properties (dict[str, str]): The properties of the tag. + + Returns: + Tag: The tag information. + """ + pass + + @abstractmethod + def alter_tag( + self, + tag_name: str, + *changes: TagChange, + ) -> Tag: + """ + Alter a tag under a metalake. + + Args: + tag_name (str): The name of the tag. + changes (TagChange): The changes to apply to the tag. + + Returns: + Tag: The altered tag. + + Raises: + NoSuchTagException: If the tag does not exist. + NoSuchMetalakeException: If the metalake does not exist. + """ + pass + + @abstractmethod + def delete_tag(self, tag_name: str) -> bool: + """ + Delete a tag under a metalake. + + Args: + tag_name (str): The name of the tag. + + Returns: + bool: True if the tag was deleted, False otherwise. + + Raises: + NoSuchTagException: If the tag does not exist. + NoSuchMetalakeException: If the metalake does not exist. + """ + pass diff --git a/clients/client-python/gravitino/client/gravitino_client.py b/clients/client-python/gravitino/client/gravitino_client.py index 02b61cb774..862b4bce33 100644 --- a/clients/client-python/gravitino/client/gravitino_client.py +++ b/clients/client-python/gravitino/client/gravitino_client.py @@ -15,7 +15,9 @@ # specific language governing permissions and limitations # under the License. -from typing import List, Dict +from __future__ import annotations + +from typing import Dict, List, Optional from gravitino.api.catalog import Catalog from gravitino.api.catalog_change import CatalogChange @@ -23,12 +25,15 @@ from gravitino.api.job.job_handle import JobHandle from gravitino.api.job.job_template import JobTemplate from gravitino.api.job.job_template_change import JobTemplateChange from gravitino.api.job.supports_jobs import SupportsJobs +from gravitino.api.tag.tag_operations import TagOperations from gravitino.auth.auth_data_provider import AuthDataProvider from gravitino.client.gravitino_client_base import GravitinoClientBase from gravitino.client.gravitino_metalake import GravitinoMetalake +from ..api.tag.tag import Tag + -class GravitinoClient(GravitinoClientBase, SupportsJobs): +class GravitinoClient(GravitinoClientBase, SupportsJobs, TagOperations): """Gravitino Client for a user to interact with the Gravitino API, allowing the client to list, load, create, and alter Catalog. @@ -43,8 +48,8 @@ class GravitinoClient(GravitinoClientBase, SupportsJobs): metalake_name: str, check_version: bool = True, auth_data_provider: AuthDataProvider = None, - request_headers: dict = None, - client_config: dict = None, + request_headers: Optional[dict] = None, + client_config: Optional[dict] = None, ): """Constructs a new GravitinoClient with the given URI, authenticator and AuthDataProvider. @@ -235,3 +240,93 @@ class GravitinoClient(GravitinoClientBase, SupportsJobs): NoSuchJobException: If no job with the specified ID exists. """ return self.get_metalake().cancel_job(job_id) + + # Tag operations + def list_tags(self) -> list[str]: + """List all the tag names under a metalake. + + Returns: + list[str]: The list of tag names. + + Raises: + NoSuchMetalakeException: If the metalake does not exist. + """ + return self.get_metalake().list_tags() + + def list_tags_info(self) -> list[Tag]: + """ + List tags information under a metalake. + + Returns: + list[Tag]: The list of tag information. + + Raises: + NoSuchMetalakeException: If the metalake does not exist. + """ + return self.get_metalake().list_tags_info() + + def get_tag(self, tag_name) -> Tag: + """ + Get a tag by its name under a metalake. + + Args: + tag_name (str): The name of the tag. + + Returns: + Tag: The tag information. + + Raises: + NoSuchTagException: If the tag does not exist. + """ + return self.get_metalake().get_tag(tag_name) + + def create_tag(self, tag_name, comment, properties) -> Tag: + """ + Create a new tag under a metalake. + + Raises: + NoSuchMetalakeException: If the metalake does not exist. + TagAlreadyExistsException: If the tag already exists. + + Args: + tag_name (str): The name of the tag. + comment (str): The comment of the tag. + properties (dict[str, str]): The properties of the tag. + + Returns: + Tag: The tag information. + """ + return self.get_metalake().create_tag(tag_name, comment, properties) + + def alter_tag(self, tag_name, *changes) -> Tag: + """ + Alter a tag under a metalake. + + Args: + tag_name (str): The name of the tag. + changes (TagChange): The changes to apply to the tag. + + Returns: + Tag: The altered tag. + + Raises: + NoSuchTagException: If the tag does not exist. + NoSuchMetalakeException: If the metalake does not exist. + """ + return self.get_metalake().alter_tag(tag_name, *changes) + + def delete_tag(self, tag_name) -> bool: + """ + Delete a tag under a metalake. + + Args: + tag_name (str): The name of the tag. + + Returns: + bool: True if the tag was deleted, False otherwise. + + Raises: + NoSuchTagException: If the tag does not exist. + NoSuchMetalakeException: If the metalake does not exist. + """ + return self.get_metalake().delete_tag(tag_name) diff --git a/clients/client-python/gravitino/client/gravitino_metalake.py b/clients/client-python/gravitino/client/gravitino_metalake.py index 86ae92b812..a10437ce04 100644 --- a/clients/client-python/gravitino/client/gravitino_metalake.py +++ b/clients/client-python/gravitino/client/gravitino_metalake.py @@ -16,7 +16,7 @@ # under the License. import logging -from typing import List, Dict +from typing import Dict, List from gravitino.api.catalog import Catalog from gravitino.api.catalog_change import CatalogChange @@ -24,6 +24,8 @@ from gravitino.api.job.job_handle import JobHandle from gravitino.api.job.job_template import JobTemplate from gravitino.api.job.job_template_change import JobTemplateChange from gravitino.api.job.supports_jobs import SupportsJobs +from gravitino.api.tag.tag import Tag +from gravitino.api.tag.tag_operations import TagOperations from gravitino.client.dto_converters import DTOConverters from gravitino.client.generic_job_handle import GenericJobHandle from gravitino.dto.metalake_dto import MetalakeDTO @@ -50,11 +52,14 @@ from gravitino.exceptions.handlers.job_error_handler import JOB_ERROR_HANDLER from gravitino.rest.rest_utils import encode_string from gravitino.utils import HTTPClient - logger = logging.getLogger(__name__) -class GravitinoMetalake(MetalakeDTO, SupportsJobs): +class GravitinoMetalake( + MetalakeDTO, + SupportsJobs, + TagOperations, +): """ Gravitino Metalake is the top-level metadata repository for users. It contains a list of catalogs as sub-level metadata collections. With GravitinoMetalake, users can list, create, load, @@ -275,6 +280,10 @@ class GravitinoMetalake(MetalakeDTO, SupportsJobs): url, json=catalog_disable_request, error_handler=CATALOG_ERROR_HANDLER ) + ########## + # Job operations + ########## + def list_job_templates(self) -> List[JobTemplate]: """List all the registered job templates in Gravitino. @@ -497,3 +506,101 @@ class GravitinoMetalake(MetalakeDTO, SupportsJobs): resp.validate() return GenericJobHandle(resp.job()) + + ######### + # Tag operations + ######### + def list_tags(self) -> list[str]: + """List all the tag names under a metalake. + + Returns: + list[str]: The list of tag names. + + Raises: + NoSuchMetalakeException: If the metalake does not exist. + """ + # TODO implement list_tags + raise NotImplementedError() + + def list_tags_info(self) -> List[Tag]: + """ + List tags information under a metalake. + + Returns: + list[Tag]: The list of tag information. + + Raises: + NoSuchMetalakeException: If the metalake does not exist. + """ + # TODO implement list_tags_info + raise NotImplementedError() + + def get_tag(self, tag_name) -> Tag: + """ + Get a tag by its name under a metalake. + + Args: + tag_name (str): The name of the tag. + + Returns: + Tag: The tag information. + + Raises: + NoSuchTagException: If the tag does not exist. + """ + # TODO implement get_tag + raise NotImplementedError() + + def create_tag(self, tag_name, comment, properties) -> Tag: + """ + Create a new tag under a metalake. + + Raises: + NoSuchMetalakeException: If the metalake does not exist. + TagAlreadyExistsException: If the tag already exists. + + Args: + tag_name (str): The name of the tag. + comment (str): The comment of the tag. + properties (dict[str, str]): The properties of the tag. + + Returns: + Tag: The tag information. + """ + # TODO implement create_tag + raise NotImplementedError() + + def alter_tag(self, tag_name, *changes) -> Tag: + """ + Alter a tag under a metalake. + + Args: + tag_name (str): The name of the tag. + changes (TagChange): The changes to apply to the tag. + + Returns: + Tag: The altered tag. + + Raises: + NoSuchTagException: If the tag does not exist. + NoSuchMetalakeException: If the metalake does not exist. + """ + # TODO implement alter_tag + raise NotImplementedError() + + def delete_tag(self, tag_name) -> bool: + """ + Delete a tag under a metalake. + + Args: + tag_name (str): The name of the tag. + + Returns: + bool: True if the tag was deleted, False otherwise. + + Raises: + NoSuchTagException: If the tag does not exist. + NoSuchMetalakeException: If the metalake does not exist. + """ + # TODO implement delete_tag + raise NotImplementedError() diff --git a/clients/client-python/tests/unittests/api/tag/test_tag_change.py b/clients/client-python/tests/unittests/api/tag/test_tag_change.py new file mode 100644 index 0000000000..eb0a24c0dc --- /dev/null +++ b/clients/client-python/tests/unittests/api/tag/test_tag_change.py @@ -0,0 +1,50 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +import unittest + +from gravitino.api.tag.tag_change import TagChange + + +class TestTagChange(unittest.TestCase): + def test_rename(self) -> None: + expected_new_name = "new_name" + tag_change = TagChange.rename(expected_new_name) + self.assertEqual(expected_new_name, tag_change.new_name) + self.assertEqual(f"RENAMETAG {expected_new_name}", str(tag_change)) + + def test_update_comment(self) -> None: + expected_new_comment = "new_comment" + tag_change = TagChange.update_comment(expected_new_comment) + self.assertEqual(expected_new_comment, tag_change.new_comment) + self.assertEqual(f"UPDATETAGCOMMENT {expected_new_comment}", str(tag_change)) + + def test_set_property(self) -> None: + expected_property = "property" + expected_value = "value" + tag_change = TagChange.set_property(expected_property, expected_value) + self.assertEqual(expected_property, tag_change.name) + self.assertEqual(expected_value, tag_change.value) + self.assertEqual( + f"SETTAGPROPERTY {expected_property} = {expected_value}", str(tag_change) + ) + + def test_remove_property(self) -> None: + expected_property = "property" + tag_change = TagChange.remove_property(expected_property) + self.assertEqual(expected_property, tag_change.removed_property) + self.assertEqual(f"REMOVETAGPROPERTY {expected_property}", str(tag_change))
