This is an automated email from the ASF dual-hosted git repository.
jshao 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 dfac744213 [#9882] Add implementation for TagOperations in python
client(part2) (#10554)
dfac744213 is described below
commit dfac744213ce2ae81452a96207cf00fee1e8f5d4
Author: Lord of Abyss <[email protected]>
AuthorDate: Thu Apr 2 18:03:04 2026 +0800
[#9882] Add implementation for TagOperations in python client(part2)
(#10554)
### What changes were proposed in this pull request?
Add rest implementation for `TagOperations` in python client
### Why are the changes needed?
Fix: #9882
### Does this PR introduce _any_ user-facing change?
Users can now manage tags using the `GravitinoClient`.
### How was this patch tested?
local unittest
---
.../gravitino/client/dto_converters.py | 40 ++
.../gravitino/client/gravitino_metalake.py | 76 +++-
.../tests/unittests/client/__init__.py | 16 +
.../tests/unittests/client/test_dto_converters.py | 100 +++++
clients/client-python/tests/unittests/mock_base.py | 153 ++++++-
.../client-python/tests/unittests/test_tag_api.py | 481 +++++++++++++++++++++
6 files changed, 856 insertions(+), 10 deletions(-)
diff --git a/clients/client-python/gravitino/client/dto_converters.py
b/clients/client-python/gravitino/client/dto_converters.py
index bc24cde268..925063a41b 100644
--- a/clients/client-python/gravitino/client/dto_converters.py
+++ b/clients/client-python/gravitino/client/dto_converters.py
@@ -15,6 +15,8 @@
# specific language governing permissions and limitations
# under the License.
+from __future__ import annotations
+
from gravitino.api.catalog import Catalog
from gravitino.api.catalog_change import CatalogChange
from gravitino.api.job.job_template import JobTemplate, JobType
@@ -30,6 +32,7 @@ from gravitino.api.job.job_template_change import (
from gravitino.api.job.shell_job_template import ShellJobTemplate
from gravitino.api.job.spark_job_template import SparkJobTemplate
from gravitino.api.metalake_change import MetalakeChange
+from gravitino.api.tag.tag_change import TagChange
from gravitino.client.fileset_catalog import FilesetCatalog
from gravitino.client.generic_model_catalog import GenericModelCatalog
from gravitino.client.relational_catalog import RelationalCatalog
@@ -48,6 +51,11 @@ from gravitino.dto.requests.job_template_update_request
import (
UpdateJobTemplateContentRequest,
)
from gravitino.dto.requests.metalake_update_request import
MetalakeUpdateRequest
+from gravitino.dto.requests.tag_update_request import (
+ TagUpdateRequest,
+ TagUpdateRequestBase,
+)
+from gravitino.exceptions.base import IllegalArgumentException
from gravitino.namespace import Namespace
from gravitino.utils import HTTPClient
@@ -285,3 +293,35 @@ class DTOConverters:
return UpdateJobTemplateContentRequest(template_update_dto)
raise ValueError(f"Unknown change type: {type(change).__name__}")
+
+ @staticmethod
+ def to_tag_update_request(
+ change: TagChange,
+ ) -> TagUpdateRequestBase:
+ """
+ Converts a TagChange to a TagUpdateRequestBase.
+
+ Args:
+ change (TagChange): the TagChange to convert.
+
+ Raises:
+ IllegalArgumentException: if the change is not a valid TagChange.
+
+ Returns:
+ TagUpdateRequestBase: the TagUpdateRequestBase corresponding to
the change.
+ """
+ if isinstance(change, TagChange.RenameTag):
+ return TagUpdateRequest.RenameTagRequest(change.new_name)
+
+ if isinstance(change, TagChange.UpdateTagComment):
+ return TagUpdateRequest.UpdateTagCommentRequest(change.new_comment)
+
+ if isinstance(change, TagChange.SetProperty):
+ return TagUpdateRequest.SetTagPropertyRequest(
+ change.name,
+ change.value,
+ )
+ if isinstance(change, TagChange.RemoveProperty):
+ return
TagUpdateRequest.RemoveTagPropertyRequest(change.removed_property)
+
+ raise IllegalArgumentException(f"Unknown change type: {type(change)}")
diff --git a/clients/client-python/gravitino/client/gravitino_metalake.py
b/clients/client-python/gravitino/client/gravitino_metalake.py
index 00b7ae3694..a88dc52711 100644
--- a/clients/client-python/gravitino/client/gravitino_metalake.py
+++ b/clients/client-python/gravitino/client/gravitino_metalake.py
@@ -41,6 +41,7 @@ from gravitino.dto.requests.job_template_updates_request
import (
JobTemplateUpdatesRequest,
)
from gravitino.dto.requests.tag_create_request import TagCreateRequest
+from gravitino.dto.requests.tag_updates_request import TagUpdatesRequest
from gravitino.dto.responses.catalog_list_response import CatalogListResponse
from gravitino.dto.responses.catalog_response import CatalogResponse
from gravitino.dto.responses.drop_response import DropResponse
@@ -49,13 +50,18 @@ from gravitino.dto.responses.job_list_response import
JobListResponse
from gravitino.dto.responses.job_response import JobResponse
from gravitino.dto.responses.job_template_list_response import
JobTemplateListResponse
from gravitino.dto.responses.job_template_response import JobTemplateResponse
-from gravitino.dto.responses.tag_response import TagNamesListResponse,
TagResponse
+from gravitino.dto.responses.tag_response import (
+ TagListResponse,
+ TagNamesListResponse,
+ TagResponse,
+)
from gravitino.exceptions.handlers.catalog_error_handler import
CATALOG_ERROR_HANDLER
from gravitino.exceptions.handlers.job_error_handler import JOB_ERROR_HANDLER
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
from gravitino.utils.precondition import Precondition
+from gravitino.utils.string_utils import StringUtils
logger = logging.getLogger(__name__)
@@ -544,8 +550,24 @@ class GravitinoMetalake(
Raises:
NoSuchMetalakeException: If the metalake does not exist.
"""
- # TODO implement list_tags_info
- raise NotImplementedError()
+ url = self.API_METALAKES_TAGS_PATH.format(encode_string(self.name()))
+
+ response = self.rest_client.get(
+ url,
+ 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.name(),
+ tag_dto,
+ self.rest_client,
+ )
+ for tag_dto in list_info_resp.tags()
+ ]
def get_tag(self, tag_name) -> Tag:
"""
@@ -623,8 +645,34 @@ class GravitinoMetalake(
NoSuchTagException: If the tag does not exist.
NoSuchMetalakeException: If the metalake does not exist.
"""
- # TODO implement alter_tag
- raise NotImplementedError()
+ Precondition.check_argument(
+ StringUtils.is_not_blank(tag_name),
+ "tag name must not be null or empty",
+ )
+ Precondition.check_argument(
+ changes is not None and len(changes) > 0,
+ "at least one change is required",
+ )
+ updates = [DTOConverters.to_tag_update_request(change) for change in
changes]
+ update_req = TagUpdatesRequest(updates)
+ update_req.validate()
+
+ url = self.API_METALAKES_TAG_PATH.format(
+ encode_string(self.name()), encode_string(tag_name)
+ )
+ response = self.rest_client.post(
+ url,
+ json=update_req,
+ error_handler=TAG_ERROR_HANDLER,
+ )
+ tag_resp: TagResponse = TagResponse.from_json(response.body,
infer_missing=True)
+
+ tag_resp.validate()
+ return GenericTag(
+ self.name(),
+ tag_resp.tag(),
+ self.rest_client,
+ )
def delete_tag(self, tag_name) -> bool:
"""
@@ -640,5 +688,19 @@ class GravitinoMetalake(
NoSuchTagException: If the tag does not exist.
NoSuchMetalakeException: If the metalake does not exist.
"""
- # TODO implement delete_tag
- raise NotImplementedError()
+ Precondition.check_argument(
+ StringUtils.is_not_blank(tag_name),
+ "tag name must not be null or empty",
+ )
+
+ url = self.API_METALAKES_TAG_PATH.format(
+ encode_string(self.name()), encode_string(tag_name)
+ )
+ response = self.rest_client.delete(
+ url,
+ error_handler=TAG_ERROR_HANDLER,
+ )
+ drop_response = DropResponse.from_json(response.body,
infer_missing=True)
+ drop_response.validate()
+
+ return drop_response.dropped()
diff --git a/clients/client-python/tests/unittests/client/__init__.py
b/clients/client-python/tests/unittests/client/__init__.py
new file mode 100644
index 0000000000..13a83393a9
--- /dev/null
+++ b/clients/client-python/tests/unittests/client/__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/client/test_dto_converters.py
b/clients/client-python/tests/unittests/client/test_dto_converters.py
new file mode 100644
index 0000000000..d99cde1124
--- /dev/null
+++ b/clients/client-python/tests/unittests/client/test_dto_converters.py
@@ -0,0 +1,100 @@
+# 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 json as _json
+import unittest
+
+from gravitino.api.tag.tag_change import TagChange
+from gravitino.client.dto_converters import DTOConverters
+from gravitino.dto.requests.tag_update_request import TagUpdateRequest
+from gravitino.exceptions.base import IllegalArgumentException
+
+
+class TestDtoConverters(unittest.TestCase):
+ def test_to_tag_update_request_with_name(self) -> None:
+ new_tag_name = "new_tag_name"
+ change = TagChange.rename(new_tag_name)
+ rename_request = DTOConverters.to_tag_update_request(change)
+
+ self.assertTrue(isinstance(rename_request,
TagUpdateRequest.RenameTagRequest))
+ self.assertEqual(new_tag_name, rename_request.new_name)
+
+ json_str = _json.dumps(
+ {
+ "@type": "rename",
+ "newName": f"{new_tag_name}",
+ }
+ )
+ self.assertEqual(json_str, rename_request.to_json())
+
+ def test_to_tag_update_request_with_comment(self) -> None:
+ new_comment = "new_comment"
+ change = TagChange.update_comment(new_comment)
+ update_comment_request = DTOConverters.to_tag_update_request(change)
+
+ self.assertTrue(
+ isinstance(update_comment_request,
TagUpdateRequest.UpdateTagCommentRequest)
+ )
+
+ json_str = _json.dumps(
+ {
+ "@type": "updateComment",
+ "newComment": f"{new_comment}",
+ }
+ )
+ self.assertEqual(json_str, update_comment_request.to_json())
+
+ def test_to_tag_update_request_with_new_property(self) -> None:
+ new_prop = "key"
+ new_value = "value"
+ change = TagChange.set_property(new_prop, new_value)
+ set_property_request = DTOConverters.to_tag_update_request(change)
+
+ self.assertTrue(
+ isinstance(set_property_request,
TagUpdateRequest.SetTagPropertyRequest)
+ )
+ json_str = _json.dumps(
+ {
+ "@type": "setProperty",
+ "property": f"{new_prop}",
+ "value": f"{new_value}",
+ }
+ )
+ self.assertEqual(json_str, set_property_request.to_json())
+
+ def test_to_tag_update_request_with_remove_property(self) -> None:
+ removed_prop = "key"
+ change = TagChange.remove_property(removed_prop)
+ remove_property_request = DTOConverters.to_tag_update_request(change)
+
+ self.assertTrue(
+ isinstance(
+ remove_property_request,
TagUpdateRequest.RemoveTagPropertyRequest
+ )
+ )
+ json_str = _json.dumps(
+ {
+ "@type": "removeProperty",
+ "property": f"{removed_prop}",
+ }
+ )
+ self.assertEqual(json_str, remove_property_request.to_json())
+
+ def test_to_tag_update_request_with_unsupport_type(self) -> None:
+ with self.assertRaises(IllegalArgumentException):
+ DTOConverters.to_tag_update_request(None)
diff --git a/clients/client-python/tests/unittests/mock_base.py
b/clients/client-python/tests/unittests/mock_base.py
index c4e9c2910a..8ce25fad67 100644
--- a/clients/client-python/tests/unittests/mock_base.py
+++ b/clients/client-python/tests/unittests/mock_base.py
@@ -16,20 +16,57 @@
# under the License.
import json
-from unittest.mock import patch
+import typing as tp
+from contextlib import contextmanager
+from http.client import HTTPResponse
+from unittest.mock import MagicMock, Mock, patch
-from gravitino import GravitinoMetalake, Catalog, Fileset
+from gravitino import Catalog, Fileset, GravitinoMetalake
+from gravitino.api.tag.tag_change import TagChange
from gravitino.client.fileset_catalog import FilesetCatalog
from gravitino.client.generic_fileset import GenericFileset
from gravitino.client.generic_model_catalog import GenericModelCatalog
-from gravitino.dto.fileset_dto import FilesetDTO
from gravitino.dto.audit_dto import AuditDTO
+from gravitino.dto.fileset_dto import FilesetDTO
from gravitino.dto.metalake_dto import MetalakeDTO
from gravitino.dto.schema_dto import SchemaDTO
+from gravitino.dto.tag_dto import TagDTO
from gravitino.namespace import Namespace
+from gravitino.utils import Response
from gravitino.utils.http_client import HTTPClient
+def build_tag_dto(
+ name: str = "tagA",
+ comment: str = "commentA",
+ properties: tp.Optional[dict[str, str]] = None,
+) -> TagDTO:
+ if properties is None:
+ properties = {
+ "key1": "value1",
+ "key2": "value2",
+ }
+
+ return (
+ TagDTO.builder()
+ .name(name)
+ .comment(comment)
+ .properties(properties)
+ .audit_info(build_audit_info())
+ .inherited(False)
+ .build()
+ )
+
+
+def build_audit_info() -> AuditDTO:
+ return AuditDTO(
+ _creator="test",
+ _create_time="2022-01-01T00:00:00Z",
+ _last_modifier="test",
+ _last_modified_time="2024-04-05T10:10:35.218Z",
+ )
+
+
def mock_load_metalake():
audit_dto = AuditDTO(
_creator="test",
@@ -154,3 +191,113 @@ def mock_data(cls):
def mock_name_identifier_json(name, namespace):
return json.dumps({"name": name, "namespace": namespace}).encode("utf-8")
+
+
+class MockTagRepo:
+ def __init__(self) -> None:
+ self.tag_store = {
+ "tagA": build_tag_dto("tagA", "mock tag A"),
+ "tagB": build_tag_dto("tagB", "mock tag B"),
+ }
+
+ def mock_list_tags(self) -> list[str]:
+ return list(self.tag_store.keys())
+
+ def mock_list_tags_info(self) -> list[TagDTO]:
+ return list(self.tag_store.values())
+
+ def mock_get_tag(self, tag_name: str) -> TagDTO:
+ if tag_name not in self.tag_store:
+ raise ValueError(f"Tag {tag_name} does not exist")
+ return self.tag_store[tag_name]
+
+ def mock_create_tag(
+ self,
+ tag_name: str,
+ comment: str = "",
+ properties=None,
+ ) -> TagDTO:
+ if tag_name in self.tag_store:
+ raise ValueError(f"Tag {tag_name} already exists")
+ self.tag_store[tag_name] = build_tag_dto(tag_name, comment, properties)
+ return self.tag_store[tag_name]
+
+ def mock_alter_tag(self, tag_name: str, *changes) -> TagDTO:
+ if tag_name not in self.tag_store:
+ raise ValueError(f"Tag {tag_name} does not exist")
+
+ for change in changes:
+ current_tag_obj = self.tag_store[tag_name]
+
+ if isinstance(change, TagChange.RenameTag):
+ self.tag_store[change.new_name] = build_tag_dto(
+ change.new_name,
+ current_tag_obj.comment(),
+ dict(current_tag_obj.properties()),
+ )
+ del self.tag_store[tag_name]
+ tag_name = change.new_name
+
+ elif isinstance(change, TagChange.UpdateTagComment):
+ self.tag_store[tag_name] = build_tag_dto(
+ current_tag_obj.name(),
+ change.new_comment,
+ dict(current_tag_obj.properties()),
+ )
+
+ elif isinstance(change, TagChange.RemoveProperty):
+ new_properties = dict(current_tag_obj.properties())
+ new_properties.pop(change.removed_property, None)
+
+ self.tag_store[tag_name] = build_tag_dto(
+ current_tag_obj.name(),
+ current_tag_obj.comment(),
+ new_properties,
+ )
+
+ elif isinstance(change, TagChange.SetProperty):
+ new_properties = dict(current_tag_obj.properties())
+ new_properties[change.name] = change.value
+
+ self.tag_store[tag_name] = build_tag_dto(
+ current_tag_obj.name(),
+ current_tag_obj.comment(),
+ new_properties,
+ )
+
+ else:
+ raise ValueError(f"Unknown tag change type: {change}")
+
+ return self.tag_store[tag_name]
+
+ def mock_delete_tag(self, tag_name: str) -> bool:
+ if tag_name not in self.tag_store:
+ return False
+ del self.tag_store[tag_name]
+ return True
+
+
+@contextmanager
+def mock_tag_methods():
+ repo = MockTagRepo()
+
+ with patch.multiple(
+ GravitinoMetalake,
+ list_tags=MagicMock(side_effect=repo.mock_list_tags),
+ list_tags_info=MagicMock(side_effect=repo.mock_list_tags_info),
+ get_tag=MagicMock(side_effect=repo.mock_get_tag),
+ create_tag=MagicMock(side_effect=repo.mock_create_tag),
+ alter_tag=MagicMock(side_effect=repo.mock_alter_tag),
+ delete_tag=MagicMock(side_effect=repo.mock_delete_tag),
+ ) as mocks:
+ yield mocks, repo
+
+
+def mock_http_response(json_str: str) -> Response:
+ mock_http_resp = Mock(HTTPResponse)
+ mock_http_resp.getcode.return_value = 200
+ mock_http_resp.read.return_value = json_str.encode("utf-8")
+ mock_http_resp.info.return_value = None
+ mock_http_resp.url = None
+ mock_resp = Response(mock_http_resp)
+ return mock_resp
diff --git a/clients/client-python/tests/unittests/test_tag_api.py
b/clients/client-python/tests/unittests/test_tag_api.py
new file mode 100644
index 0000000000..cb5e40d281
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_tag_api.py
@@ -0,0 +1,481 @@
+# 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 unittest
+from unittest.mock import patch
+
+from gravitino import GravitinoClient
+from gravitino.api.tag import Tag
+from gravitino.api.tag.tag_change import TagChange
+from gravitino.dto.responses.drop_response import DropResponse
+from gravitino.dto.responses.tag_response import (
+ TagListResponse,
+ TagNamesListResponse,
+ TagResponse,
+)
+from tests.unittests import mock_base
+
+
+@mock_base.mock_data
+class TestTagAPI(unittest.TestCase):
+ _metalake_name: str = "metalake_demo"
+
+ def test_client_get_tag(self, *mock_method) -> None:
+ with mock_base.mock_tag_methods():
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ retrieved_tag = client.get_tag("tagA")
+ self._check_default_tag_a(retrieved_tag)
+
+ def test_client_list_tag(self, *mock_method) -> None:
+ with mock_base.mock_tag_methods():
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ retrieved_tags = client.list_tags()
+ self.assertEqual(2, len(retrieved_tags))
+ self.assertTrue("tagA" in retrieved_tags)
+ self.assertTrue("tagB" in retrieved_tags)
+
+ client.create_tag("tagC", "mock tag C", None)
+
+ retrieved_tags = client.list_tags()
+ self.assertEqual(3, len(retrieved_tags))
+ self.assertTrue("tagA" in retrieved_tags)
+ self.assertTrue("tagB" in retrieved_tags)
+ self.assertTrue("tagC" in retrieved_tags)
+
+ def test_client_list_tag_info(self, *mock_method) -> None:
+ with mock_base.mock_tag_methods():
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ retrieved_tags = client.list_tags_info()
+ self.assertEqual(2, len(retrieved_tags))
+ tag_names = [tag.name() for tag in retrieved_tags]
+
+ self.assertTrue("tagA" in tag_names)
+ self.assertTrue("tagB" in tag_names)
+
+ tag_comments = [tag.comment() for tag in retrieved_tags]
+ self.assertTrue("mock tag A" in tag_comments)
+ self.assertTrue("mock tag B" in tag_comments)
+
+ def test_client_remove_tag(self, *mock_method) -> None:
+ with mock_base.mock_tag_methods():
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ retrieved_tags = client.list_tags()
+ self.assertEqual(2, len(retrieved_tags))
+ self.assertTrue("tagA" in retrieved_tags)
+ self.assertTrue("tagB" in retrieved_tags)
+
+ client.delete_tag("tagA")
+ retrieved_tags = client.list_tags()
+ self.assertEqual(1, len(retrieved_tags))
+ self.assertTrue("tagA" not in retrieved_tags)
+ self.assertTrue("tagB" in retrieved_tags)
+
+ def test_client_create_tag(self, *mock_method) -> None:
+ with mock_base.mock_tag_methods():
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ retrieved_tags = client.list_tags()
+ self.assertEqual(2, len(retrieved_tags))
+ self.assertTrue("tagA" in retrieved_tags)
+ self.assertTrue("tagB" in retrieved_tags)
+
+ client.create_tag("tagC", "mock tag C", None)
+ retrieved_tags = client.list_tags()
+ self.assertEqual(3, len(retrieved_tags))
+ self.assertTrue("tagA" in retrieved_tags)
+ self.assertTrue("tagB" in retrieved_tags)
+ self.assertTrue("tagC" in retrieved_tags)
+
+ def test_client_alter_tag_with_name(self, *mock_method) -> None:
+ with mock_base.mock_tag_methods():
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ retrieved_tag = client.get_tag("tagA")
+ self._check_default_tag_a(retrieved_tag)
+
+ change = TagChange.rename("tagA-new")
+ client.alter_tag("tagA", change)
+
+ with self.assertRaises(ValueError):
+ client.get_tag("tagA")
+
+ retrieved_tag = client.get_tag("tagA-new")
+ self.assertEqual("tagA-new", retrieved_tag.name())
+ self.assertEqual("mock tag A", retrieved_tag.comment())
+ self.assertEqual(
+ {
+ "key1": "value1",
+ "key2": "value2",
+ },
+ retrieved_tag.properties(),
+ )
+
+ def test_client_alter_tag_with_comment(self, *mock_method) -> None:
+ with mock_base.mock_tag_methods():
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ retrieved_tag = client.get_tag("tagA")
+ self._check_default_tag_a(retrieved_tag)
+
+ change = TagChange.update_comment("new comment")
+ client.alter_tag("tagA", change)
+
+ retrieved_tag = client.get_tag("tagA")
+ self.assertEqual("tagA", retrieved_tag.name())
+ self.assertEqual("new comment", retrieved_tag.comment())
+ self.assertEqual(
+ {
+ "key1": "value1",
+ "key2": "value2",
+ },
+ retrieved_tag.properties(),
+ )
+
+ def test_client_alter_tag_with_remove_property(self, *mock_method) -> None:
+ with mock_base.mock_tag_methods():
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ retrieved_tag = client.get_tag("tagA")
+ self._check_default_tag_a(retrieved_tag)
+
+ change = TagChange.remove_property("key1")
+ client.alter_tag("tagA", change)
+
+ retrieved_tag = client.get_tag("tagA")
+ self.assertEqual("tagA", retrieved_tag.name())
+ self.assertEqual("mock tag A", retrieved_tag.comment())
+ self.assertEqual(
+ {
+ "key2": "value2",
+ },
+ retrieved_tag.properties(),
+ )
+
+ def test_client_alter_tag_with_add_property(self, *mock_method) -> None:
+ with mock_base.mock_tag_methods():
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ retrieved_tag = client.get_tag("tagA")
+ self._check_default_tag_a(retrieved_tag)
+
+ change = TagChange.set_property("key3", "value3")
+ client.alter_tag("tagA", change)
+
+ retrieved_tag = client.get_tag("tagA")
+ self.assertEqual("tagA", retrieved_tag.name())
+ self.assertEqual("mock tag A", retrieved_tag.comment())
+ self.assertEqual(
+ {
+ "key1": "value1",
+ "key2": "value2",
+ "key3": "value3",
+ },
+ retrieved_tag.properties(),
+ )
+
+ def test_client_alter_tag_with_replace_property(self, *mock_method) ->
None:
+ with mock_base.mock_tag_methods():
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ retrieved_tag = client.get_tag("tagA")
+ self._check_default_tag_a(retrieved_tag)
+
+ change = TagChange.set_property("key1", "value3")
+ client.alter_tag("tagA", change)
+
+ retrieved_tag = client.get_tag("tagA")
+ self.assertEqual("tagA", retrieved_tag.name())
+ self.assertEqual("mock tag A", retrieved_tag.comment())
+ self.assertEqual(
+ {
+ "key1": "value3",
+ "key2": "value2",
+ },
+ retrieved_tag.properties(),
+ )
+
+ def test_client_alter_tag_with_all_operations(self, *mock_method) -> None:
+ with mock_base.mock_tag_methods():
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ retrieved_tag = client.get_tag("tagB")
+ self._check_default_tag_b(retrieved_tag)
+
+ changes = [
+ TagChange.set_property("key1", "value3"),
+ TagChange.remove_property("key2"),
+ TagChange.update_comment("mock tag B updated"),
+ TagChange.rename("new_tag_B"),
+ ]
+
+ client.alter_tag("tagB", *changes)
+ retrieved_tag = client.get_tag("new_tag_B")
+
+ self.assertEqual("new_tag_B", retrieved_tag.name())
+ self.assertEqual("mock tag B updated", retrieved_tag.comment())
+ self.assertEqual(
+ {
+ "key1": "value3",
+ },
+ retrieved_tag.properties(),
+ )
+
+ def test_gravitino_list_tag_api(self, *mock_method) -> None:
+ resp = TagNamesListResponse(0, ["tagA", "tagB", "tagC"])
+ json_str = resp.to_json()
+ mock_resp = mock_base.mock_http_response(json_str)
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.get",
+ return_value=mock_resp,
+ ):
+ tags = client.list_tags()
+ self.assertEqual(3, len(tags))
+ self.assertTrue("tagA" in tags)
+ self.assertTrue("tagB" in tags)
+ self.assertTrue("tagC" in tags)
+
+ def test_gravitino_list_tag_info_api(self, *mock_method) -> None:
+ tag = mock_base.build_tag_dto()
+ resp = TagListResponse(0, [tag])
+ json_str = resp.to_json()
+ mock_resp = mock_base.mock_http_response(json_str)
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.get",
+ return_value=mock_resp,
+ ):
+ tags = client.list_tags_info()
+ self.assertEqual(1, len(tags))
+ self.check_tag_equal(tag, tags[0])
+
+ def test_gravitino_metalake_delete_tag_api(self, *mock_method) -> None:
+ resp = DropResponse(0, True)
+ json_str = resp.to_json()
+ mock_resp = mock_base.mock_http_response(json_str)
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.delete",
+ return_value=mock_resp,
+ ):
+ is_dropped = client.delete_tag("tag1")
+ self.assertTrue(is_dropped)
+
+ def test_gravitino_metalake_delete_tag_api_with_empty_tag_name(
+ self, *mock_method
+ ) -> None:
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ with self.assertRaises(ValueError):
+ client.delete_tag(" ")
+
+ def test_gravitino_metalake_create_tag_api(self, *mock_method) -> None:
+ tag = mock_base.build_tag_dto()
+ resp = TagResponse(0, tag)
+ json_str = resp.to_json()
+ mock_resp = mock_base.mock_http_response(json_str)
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.post",
+ return_value=mock_resp,
+ ):
+ created_tag = client.create_tag(tag.name(), tag.comment(),
tag.properties())
+ self.check_tag_equal(tag, created_tag)
+
+ def test_gravitino_metalake_get_tag_api(self, *mock_method) -> None:
+ tag = mock_base.build_tag_dto()
+ resp = TagResponse(0, tag)
+ json_str = resp.to_json()
+ mock_resp = mock_base.mock_http_response(json_str)
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.get",
+ return_value=mock_resp,
+ ):
+ retrieved_tag = client.get_tag(tag.name())
+ self.check_tag_equal(tag, retrieved_tag)
+
+ def test_gravitino_metalake_get_tag_api_with_empty_tag_name(
+ self, *mock_method
+ ) -> None:
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ with self.assertRaises(ValueError):
+ client.get_tag(" ")
+
+ def test_gravitino_metalake_alter_tag_api(self, *mock_method) -> None:
+ tag = mock_base.build_tag_dto(
+ "tagB",
+ "mock tag B",
+ {
+ "key2": "value2",
+ },
+ )
+ resp = TagResponse(0, tag)
+ json_str = resp.to_json()
+ mock_resp = mock_base.mock_http_response(json_str)
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ with patch(
+ "gravitino.utils.http_client.HTTPClient.post",
+ return_value=mock_resp,
+ ):
+ rename_change = TagChange.rename("tagB")
+ update_comment_change = TagChange.update_comment("mock tag B")
+ update_properties_change = TagChange.set_property("key2", "value2")
+
+ updated_tag = client.alter_tag(
+ tag.name(),
+ rename_change,
+ update_comment_change,
+ update_properties_change,
+ )
+
+ self.assertEqual("tagB", updated_tag.name())
+ self.assertEqual("mock tag B", updated_tag.comment())
+ self.assertEqual(
+ {
+ "key2": "value2",
+ },
+ updated_tag.properties(),
+ )
+
+ def test_gravitino_metalake_alter_tag_api_with_empty_tag_name(
+ self, *mock_args
+ ) -> None:
+ client = GravitinoClient(
+ uri="http://localhost:8090",
+ metalake_name=self._metalake_name,
+ check_version=False,
+ )
+
+ with self.assertRaises(ValueError):
+ client.alter_tag(" ", TagChange.rename("tagB"))
+
+ def _check_default_tag_a(self, tag: Tag) -> None:
+ self.assertEqual("tagA", tag.name())
+ self.assertEqual("mock tag A", tag.comment())
+ self.assertEqual(
+ {
+ "key1": "value1",
+ "key2": "value2",
+ },
+ tag.properties(),
+ )
+
+ def _check_default_tag_b(self, tag: Tag) -> None:
+ self.assertEqual("tagB", tag.name())
+ self.assertEqual("mock tag B", tag.comment())
+ self.assertEqual(
+ {
+ "key1": "value1",
+ "key2": "value2",
+ },
+ tag.properties(),
+ )
+
+ def check_tag_equal(self, left: Tag, right: Tag) -> None:
+ self.assertEqual(left.name(), right.name())
+ self.assertEqual(left.comment(), right.comment())
+ self.assertEqual(left.properties(), right.properties())