This is an automated email from the ASF dual-hosted git repository.

jshao pushed a commit to branch branch-0.6
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/branch-0.6 by this push:
     new 3591709ac [#4227] improvement(client-python): Add Catalog Error 
Handler and related exceptions, test cases in client-python (#4456)
3591709ac is described below

commit 3591709ac6a6cde9f1e0815089a9090d581550b3
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Aug 9 15:11:45 2024 +0800

    [#4227] improvement(client-python): Add Catalog Error Handler and related 
exceptions, test cases in client-python (#4456)
    
    ### What changes were proposed in this pull request?
    
    - Add Catalog Error Handler and related exceptions, test cases in
    `client-python` based on `client-java`
    - Add lacking ITs in `test_catalog.py`
    
    ### Why are the changes needed?
    
    Fix: #4227
    
    ### Does this PR introduce _any_ user-facing change?
    
    No
    
    ### How was this patch tested?
    
    UT Added and test with `./gradlew client:client-python:test`
    
    Co-authored-by: Jing-Jia Hung <[email protected]>
---
 .../gravitino/client/gravitino_metalake.py         | 19 ++++--
 clients/client-python/gravitino/constants/error.py |  5 ++
 .../client-python/gravitino/dto/dto_converters.py  |  4 +-
 .../dto/requests/catalog_update_request.py         | 23 +++----
 .../gravitino/dto/responses/catalog_response.py    | 10 +--
 clients/client-python/gravitino/exceptions/base.py |  8 +++
 .../exceptions/handlers/catalog_error_handler.py   | 52 +++++++++++++++
 .../tests/integration/test_catalog.py              | 76 +++++++++++++++++++++-
 .../tests/unittests/test_error_handler.py          | 43 ++++++++++++
 9 files changed, 213 insertions(+), 27 deletions(-)

diff --git a/clients/client-python/gravitino/client/gravitino_metalake.py 
b/clients/client-python/gravitino/client/gravitino_metalake.py
index 9ef787a07..1cf5c4f72 100644
--- a/clients/client-python/gravitino/client/gravitino_metalake.py
+++ b/clients/client-python/gravitino/client/gravitino_metalake.py
@@ -30,6 +30,7 @@ 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
 from gravitino.dto.responses.entity_list_response import EntityListResponse
+from gravitino.exceptions.handlers.catalog_error_handler import 
CATALOG_ERROR_HANDLER
 from gravitino.utils import HTTPClient
 
 
@@ -66,7 +67,7 @@ class GravitinoMetalake(MetalakeDTO):
             A list of the catalog names under this metalake.
         """
         url = f"api/metalakes/{self.name()}/catalogs"
-        response = self.rest_client.get(url)
+        response = self.rest_client.get(url, 
error_handler=CATALOG_ERROR_HANDLER)
         entity_list = EntityListResponse.from_json(response.body, 
infer_missing=True)
         entity_list.validate()
         return [identifier.name() for identifier in entity_list.identifiers()]
@@ -82,7 +83,9 @@ class GravitinoMetalake(MetalakeDTO):
         """
         params = {"details": "true"}
         url = f"api/metalakes/{self.name()}/catalogs"
-        response = self.rest_client.get(url, params=params)
+        response = self.rest_client.get(
+            url, params=params, error_handler=CATALOG_ERROR_HANDLER
+        )
         catalog_list = CatalogListResponse.from_json(response.body, 
infer_missing=True)
 
         return [
@@ -103,7 +106,7 @@ class GravitinoMetalake(MetalakeDTO):
             The Catalog with specified name.
         """
         url = self.API_METALAKES_CATALOGS_PATH.format(self.name(), name)
-        response = self.rest_client.get(url)
+        response = self.rest_client.get(url, 
error_handler=CATALOG_ERROR_HANDLER)
         catalog_resp = CatalogResponse.from_json(response.body, 
infer_missing=True)
 
         return DTOConverters.to_catalog(
@@ -145,7 +148,9 @@ class GravitinoMetalake(MetalakeDTO):
         catalog_create_request.validate()
 
         url = f"api/metalakes/{self.name()}/catalogs"
-        response = self.rest_client.post(url, json=catalog_create_request)
+        response = self.rest_client.post(
+            url, json=catalog_create_request, 
error_handler=CATALOG_ERROR_HANDLER
+        )
         catalog_resp = CatalogResponse.from_json(response.body, 
infer_missing=True)
 
         return DTOConverters.to_catalog(
@@ -172,7 +177,9 @@ class GravitinoMetalake(MetalakeDTO):
         updates_request.validate()
 
         url = self.API_METALAKES_CATALOGS_PATH.format(self.name(), name)
-        response = self.rest_client.put(url, json=updates_request)
+        response = self.rest_client.put(
+            url, json=updates_request, error_handler=CATALOG_ERROR_HANDLER
+        )
         catalog_response = CatalogResponse.from_json(response.body, 
infer_missing=True)
         catalog_response.validate()
 
@@ -191,7 +198,7 @@ class GravitinoMetalake(MetalakeDTO):
         """
         try:
             url = self.API_METALAKES_CATALOGS_PATH.format(self.name(), name)
-            response = self.rest_client.delete(url)
+            response = self.rest_client.delete(url, 
error_handler=CATALOG_ERROR_HANDLER)
 
             drop_response = DropResponse.from_json(response.body, 
infer_missing=True)
             drop_response.validate()
diff --git a/clients/client-python/gravitino/constants/error.py 
b/clients/client-python/gravitino/constants/error.py
index e4b0ab61e..877d523ad 100644
--- a/clients/client-python/gravitino/constants/error.py
+++ b/clients/client-python/gravitino/constants/error.py
@@ -20,6 +20,7 @@ under the License.
 from enum import IntEnum
 
 from gravitino.exceptions.base import (
+    ConnectionFailedException,
     RESTException,
     IllegalArgumentException,
     NotFoundException,
@@ -54,6 +55,9 @@ class ErrorConstants(IntEnum):
     # Error codes for unsupported operation.
     UNSUPPORTED_OPERATION_CODE = 1006
 
+    # Error codes for connect to catalog failed.
+    CONNECTION_FAILED_CODE = 1007
+
     # Error codes for invalid state.
     UNKNOWN_ERROR_CODE = 1100
 
@@ -66,6 +70,7 @@ EXCEPTION_MAPPING = {
     AlreadyExistsException: ErrorConstants.ALREADY_EXISTS_CODE,
     NotEmptyException: ErrorConstants.NON_EMPTY_CODE,
     UnsupportedOperationException: ErrorConstants.UNSUPPORTED_OPERATION_CODE,
+    ConnectionFailedException: ErrorConstants.CONNECTION_FAILED_CODE,
 }
 
 ERROR_CODE_MAPPING = {v: k for k, v in EXCEPTION_MAPPING.items()}
diff --git a/clients/client-python/gravitino/dto/dto_converters.py 
b/clients/client-python/gravitino/dto/dto_converters.py
index 86e614557..a55f053b3 100644
--- a/clients/client-python/gravitino/dto/dto_converters.py
+++ b/clients/client-python/gravitino/dto/dto_converters.py
@@ -83,6 +83,8 @@ class DTOConverters:
                 change.property(), change.value()
             )
         if isinstance(change, CatalogChange.RemoveProperty):
-            return 
CatalogUpdateRequest.RemoveCatalogPropertyRequest(change.property())
+            return CatalogUpdateRequest.RemoveCatalogPropertyRequest(
+                change.get_property()
+            )
 
         raise ValueError(f"Unknown change type: {type(change).__name__}")
diff --git 
a/clients/client-python/gravitino/dto/requests/catalog_update_request.py 
b/clients/client-python/gravitino/dto/requests/catalog_update_request.py
index d3ba6298a..6ae06b118 100644
--- a/clients/client-python/gravitino/dto/requests/catalog_update_request.py
+++ b/clients/client-python/gravitino/dto/requests/catalog_update_request.py
@@ -62,9 +62,8 @@ class CatalogUpdateRequest:
             Raises:
                 IllegalArgumentException if the new name is not set.
             """
-            assert (
-                self._new_name is None
-            ), '"newName" field is required and cannot be empty'
+            if not self._new_name:
+                raise ValueError('"newName" field is required and cannot be 
empty')
 
     @dataclass
     class UpdateCatalogCommentRequest(CatalogUpdateRequestBase):
@@ -81,9 +80,8 @@ class CatalogUpdateRequest:
             return CatalogChange.update_comment(self._new_comment)
 
         def validate(self):
-            assert (
-                self._new_comment is None
-            ), '"newComment" field is required and cannot be empty'
+            if not self._new_comment:
+                raise ValueError('"newComment" field is required and cannot be 
empty')
 
     @dataclass
     class SetCatalogPropertyRequest(CatalogUpdateRequestBase):
@@ -104,10 +102,10 @@ class CatalogUpdateRequest:
             return CatalogChange.set_property(self._property, self._value)
 
         def validate(self):
-            assert (
-                self._property is None
-            ), '"property" field is required and cannot be empty'
-            assert self._value is None, '"value" field is required and cannot 
be empty'
+            if not self._property:
+                raise ValueError('"property" field is required and cannot be 
empty')
+            if not self._value:
+                raise ValueError('"value" field is required and cannot be 
empty')
 
     class RemoveCatalogPropertyRequest(CatalogUpdateRequestBase):
         """Request to remove a property from a catalog."""
@@ -123,6 +121,5 @@ class CatalogUpdateRequest:
             return CatalogChange.remove_property(self._property)
 
         def validate(self):
-            assert (
-                self._property is None
-            ), '"property" field is required and cannot be empty'
+            if not self._property:
+                raise ValueError('"property" field is required and cannot be 
empty')
diff --git a/clients/client-python/gravitino/dto/responses/catalog_response.py 
b/clients/client-python/gravitino/dto/responses/catalog_response.py
index 711816563..d1cefa0ba 100644
--- a/clients/client-python/gravitino/dto/responses/catalog_response.py
+++ b/clients/client-python/gravitino/dto/responses/catalog_response.py
@@ -39,12 +39,14 @@ class CatalogResponse(BaseResponse):
         """
         super().validate()
 
-        assert self.catalog is not None, "catalog must not be null"
+        assert self._catalog is not None, "catalog must not be null"
         assert (
-            self.catalog.name() is not None
+            self._catalog.name() is not None
         ), "catalog 'name' must not be null and empty"
-        assert self.catalog.type() is not None, "catalog 'type' must not be 
null"
-        assert self.catalog.audit_info() is not None, "catalog 'audit' must 
not be null"
+        assert self._catalog.type() is not None, "catalog 'type' must not be 
null"
+        assert (
+            self._catalog.audit_info() is not None
+        ), "catalog 'audit' must not be null"
 
     def catalog(self) -> CatalogDTO:
         return self._catalog
diff --git a/clients/client-python/gravitino/exceptions/base.py 
b/clients/client-python/gravitino/exceptions/base.py
index a7d6eee18..674121a5d 100644
--- a/clients/client-python/gravitino/exceptions/base.py
+++ b/clients/client-python/gravitino/exceptions/base.py
@@ -83,6 +83,10 @@ class SchemaAlreadyExistsException(AlreadyExistsException):
     """An exception thrown when a schema already exists."""
 
 
+class CatalogAlreadyExistsException(AlreadyExistsException):
+    """An exception thrown when a resource already exists."""
+
+
 class NotEmptyException(GravitinoRuntimeException):
     """Base class for all exceptions thrown when a resource is not empty."""
 
@@ -95,6 +99,10 @@ class UnknownError(RuntimeError):
     """An exception thrown when other unknown exception is thrown"""
 
 
+class ConnectionFailedException(GravitinoRuntimeException):
+    """An exception thrown when connect to catalog failed."""
+
+
 class UnauthorizedException(GravitinoRuntimeException):
     """An exception thrown when a user is not authorized to perform an 
action."""
 
diff --git 
a/clients/client-python/gravitino/exceptions/handlers/catalog_error_handler.py 
b/clients/client-python/gravitino/exceptions/handlers/catalog_error_handler.py
new file mode 100644
index 000000000..e714edad3
--- /dev/null
+++ 
b/clients/client-python/gravitino/exceptions/handlers/catalog_error_handler.py
@@ -0,0 +1,52 @@
+"""
+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.constants.error import ErrorConstants
+from gravitino.dto.responses.error_response import ErrorResponse
+from gravitino.exceptions.handlers.rest_error_handler import RestErrorHandler
+from gravitino.exceptions.base import (
+    ConnectionFailedException,
+    NoSuchMetalakeException,
+    NoSuchCatalogException,
+    CatalogAlreadyExistsException,
+)
+
+
+class CatalogErrorHandler(RestErrorHandler):
+
+    def handle(self, error_response: ErrorResponse):
+
+        error_message = error_response.format_error_message()
+        code = error_response.code()
+        exception_type = error_response.type()
+
+        if code == ErrorConstants.CONNECTION_FAILED_CODE:
+            raise ConnectionFailedException(error_message)
+        if code == ErrorConstants.NOT_FOUND_CODE:
+            if exception_type == NoSuchMetalakeException.__name__:
+                raise NoSuchMetalakeException(error_message)
+            if exception_type == NoSuchCatalogException.__name__:
+                raise NoSuchCatalogException(error_message)
+        if code == ErrorConstants.ALREADY_EXISTS_CODE:
+            raise CatalogAlreadyExistsException(error_message)
+
+        super().handle(error_response)
+
+
+CATALOG_ERROR_HANDLER = CatalogErrorHandler()
diff --git a/clients/client-python/tests/integration/test_catalog.py 
b/clients/client-python/tests/integration/test_catalog.py
index 3f8d2ae4b..2cf13f2cd 100644
--- a/clients/client-python/tests/integration/test_catalog.py
+++ b/clients/client-python/tests/integration/test_catalog.py
@@ -26,6 +26,11 @@ from gravitino import (
     GravitinoClient,
     Catalog,
 )
+from gravitino.api.catalog_change import CatalogChange
+from gravitino.exceptions.base import (
+    CatalogAlreadyExistsException,
+    NoSuchCatalogException,
+)
 
 from tests.integration.integration_test_env import IntegrationTestEnv
 
@@ -36,6 +41,7 @@ class TestCatalog(IntegrationTestEnv):
     metalake_name: str = "TestSchema_metalake" + str(randint(1, 10000))
 
     catalog_name: str = "testCatalog"
+    catalog_comment: str = "catalogComment"
     catalog_location_prop: str = "location"  # Fileset Catalog must set 
`location`
     catalog_provider: str = "hadoop"
 
@@ -59,11 +65,13 @@ class TestCatalog(IntegrationTestEnv):
         self.gravitino_client = GravitinoClient(
             uri="http://localhost:8090";, metalake_name=self.metalake_name
         )
-        self.gravitino_client.create_catalog(
-            name=self.catalog_name,
+
+    def create_catalog(self, catalog_name) -> Catalog:
+        return self.gravitino_client.create_catalog(
+            name=catalog_name,
             catalog_type=Catalog.Type.FILESET,
             provider=self.catalog_provider,
-            comment="",
+            comment=self.catalog_comment,
             properties={self.catalog_location_prop: "/tmp/test_schema"},
         )
 
@@ -86,5 +94,67 @@ class TestCatalog(IntegrationTestEnv):
             logger.error("Clean test data failed: %s", e)
 
     def test_list_catalogs(self):
+        self.create_catalog(self.catalog_name)
         catalog_names = self.gravitino_client.list_catalogs()
         self.assertTrue(self.catalog_name in catalog_names)
+
+    def test_create_catalog(self):
+        catalog = self.create_catalog(self.catalog_name)
+        self.assertEqual(catalog.name(), self.catalog_name)
+        self.assertEqual(
+            catalog.properties(), {self.catalog_location_prop: 
"/tmp/test_schema"}
+        )
+
+    def test_failed_create_catalog(self):
+        self.create_catalog(self.catalog_name)
+        with self.assertRaises(CatalogAlreadyExistsException):
+            _ = self.create_catalog(self.catalog_name)
+
+    def test_alter_catalog(self):
+        catalog = self.create_catalog(self.catalog_name)
+
+        catalog_new_name = self.catalog_name + "_new"
+        catalog_new_comment = self.catalog_comment + "_new"
+        catalog_properties_new_value = self.catalog_location_prop + "_new"
+        catalog_properties_new_key: str = "catalog_properties_new_key"
+
+        changes = (
+            CatalogChange.rename(catalog_new_name),
+            CatalogChange.update_comment(catalog_new_comment),
+            CatalogChange.set_property(
+                catalog_properties_new_key, catalog_properties_new_value
+            ),
+        )
+
+        catalog = self.gravitino_client.alter_catalog(self.catalog_name, 
*changes)
+        self.assertEqual(catalog.name(), catalog_new_name)
+        self.assertEqual(catalog.comment(), catalog_new_comment)
+        self.assertEqual(
+            catalog.properties().get(catalog_properties_new_key),
+            catalog_properties_new_value,
+        )
+        self.catalog_name = self.catalog_name + "_new"
+
+    def test_drop_catalog(self):
+        self.create_catalog(self.catalog_name)
+        
self.assertTrue(self.gravitino_client.drop_catalog(name=self.catalog_name))
+
+    def test_list_catalogs_info(self):
+        self.create_catalog(self.catalog_name)
+        catalogs_info = self.gravitino_client.list_catalogs_info()
+        self.assertTrue(any(item.name() == self.catalog_name for item in 
catalogs_info))
+
+    def test_load_catalog(self):
+        self.create_catalog(self.catalog_name)
+        catalog = self.gravitino_client.load_catalog(self.catalog_name)
+        self.assertIsNotNone(catalog)
+        self.assertEqual(catalog.name(), self.catalog_name)
+        self.assertEqual(catalog.comment(), self.catalog_comment)
+        self.assertEqual(
+            catalog.properties(), {self.catalog_location_prop: 
"/tmp/test_schema"}
+        )
+        self.assertEqual(catalog.audit_info().creator(), "anonymous")
+
+    def test_failed_load_catalog(self):
+        with self.assertRaises(NoSuchCatalogException):
+            self.gravitino_client.load_catalog(self.catalog_name)
diff --git a/clients/client-python/tests/unittests/test_error_handler.py 
b/clients/client-python/tests/unittests/test_error_handler.py
index fd92af495..b2695471f 100644
--- a/clients/client-python/tests/unittests/test_error_handler.py
+++ b/clients/client-python/tests/unittests/test_error_handler.py
@@ -34,11 +34,14 @@ from gravitino.exceptions.base import (
     NotEmptyException,
     SchemaAlreadyExistsException,
     UnsupportedOperationException,
+    ConnectionFailedException,
+    CatalogAlreadyExistsException,
 )
 
 from gravitino.exceptions.handlers.rest_error_handler import REST_ERROR_HANDLER
 from gravitino.exceptions.handlers.fileset_error_handler import 
FILESET_ERROR_HANDLER
 from gravitino.exceptions.handlers.metalake_error_handler import 
METALAKE_ERROR_HANDLER
+from gravitino.exceptions.handlers.catalog_error_handler import 
CATALOG_ERROR_HANDLER
 from gravitino.exceptions.handlers.schema_error_handler import 
SCHEMA_ERROR_HANDLER
 
 
@@ -151,6 +154,46 @@ class TestErrorHandler(unittest.TestCase):
                 ErrorResponse.generate_error_response(Exception, "mock error")
             )
 
+    def test_catalog_error_handler(self):
+
+        with self.assertRaises(ConnectionFailedException):
+            CATALOG_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(
+                    ConnectionFailedException, "mock error"
+                )
+            )
+
+        with self.assertRaises(NoSuchMetalakeException):
+            CATALOG_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(
+                    NoSuchMetalakeException, "mock error"
+                )
+            )
+
+        with self.assertRaises(NoSuchCatalogException):
+            CATALOG_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(
+                    NoSuchCatalogException, "mock error"
+                )
+            )
+
+        with self.assertRaises(CatalogAlreadyExistsException):
+            CATALOG_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(
+                    CatalogAlreadyExistsException, "mock error"
+                )
+            )
+
+        with self.assertRaises(InternalError):
+            CATALOG_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(InternalError, "mock 
error")
+            )
+
+        with self.assertRaises(RESTException):
+            CATALOG_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(Exception, "mock error")
+            )
+
     def test_schema_error_handler(self):
 
         with self.assertRaises(NoSuchCatalogException):

Reply via email to