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 9e80cbcf9 [#4012] improvement(client-python): Refactor Error Handling 
in client-python (#4093)
9e80cbcf9 is described below

commit 9e80cbcf9ea8de21eabec9fdb3651d5b376c7b26
Author: noidname01 <[email protected]>
AuthorDate: Tue Jul 9 18:01:33 2024 +0800

    [#4012] improvement(client-python): Refactor Error Handling in 
client-python (#4093)
    
    ### What changes were proposed in this pull request?
    
    * Refactor the error handling structure, each API can implement their
    own error handler to raise custom exceptions
    * Add unit test for error handler, but unit tests and integration tests
    for each API(ex. metalake, catalog, schema) have not been added, I will
    create issues for them.
            - [ ] Add Metalake Error Handler and related exceptions, test cases
            - [ ] Add Catalog Error Handler and related exceptions, test cases
            - [ ] Add Schema Error Handler and related exceptions, test cases
            - [ ] Add OAuth Error Handler and related exceptions, test cases
    * Create `gravitino/exceptions/base.py` to define all the exceptions.
    * Remove some unused files and exceptions
    
    ### Why are the changes needed?
    
    Fix: #4012
    
    ### Does this PR introduce _any_ user-facing change?
    
    No
    
    ### How was this patch tested?
    
    UT added and one IT added, test with `./gradlew
    clients:client-python:test`
    
    ---------
    
    Co-authored-by: TimWang <[email protected]>
---
 .../gravitino/catalog/fileset_catalog.py           |  18 +++-
 .../gravitino/client/gravitino_client.py           |  12 ---
 .../gravitino/client/gravitino_client_base.py      |   2 +-
 .../gravitino/client/gravitino_metalake.py         |  12 ---
 .../gravitino/client/gravitino_version.py          |   2 +-
 clients/client-python/gravitino/constants/error.py |  71 ++++++++++++
 .../gravitino/dto/responses/base_response.py       |   5 +-
 .../gravitino/dto/responses/error_response.py      |  79 ++++++++++++++
 .../gravitino/dto/responses/version_response.py    |   4 +-
 clients/client-python/gravitino/exceptions/base.py |  83 ++++++++++++++
 .../exceptions/gravitino_runtime_exception.py      |  25 -----
 .../__init__.py}                                   |   6 --
 .../error_handler.py}                              |  26 +++--
 .../exceptions/handlers/fileset_error_handler.py   |  43 ++++++++
 .../rest_error_handler.py}                         |  26 +++--
 .../gravitino/exceptions/not_found_exception.py    |  24 -----
 clients/client-python/gravitino/filesystem/gvfs.py |   2 +-
 clients/client-python/gravitino/name_identifier.py |   4 +-
 clients/client-python/gravitino/namespace.py       |   5 +-
 .../client-python/gravitino/rest/rest_message.py   |   6 --
 .../client-python/gravitino/utils/exceptions.py    | 114 --------------------
 .../client-python/gravitino/utils/http_client.py   |  75 ++++++++++---
 clients/client-python/scripts/generate_version.py  |   2 +-
 .../tests/integration/base_hadoop_env.py           |   2 +-
 .../tests/integration/hdfs_container.py            |   2 +-
 .../tests/integration/integration_test_env.py      |   2 +-
 .../tests/integration/test_fileset_catalog.py      |   6 ++
 .../tests/integration/test_gvfs_with_hdfs.py       |   2 +-
 .../tests/unittests/test_error_handler.py          | 120 +++++++++++++++++++++
 .../tests/unittests/test_gravitino_version.py      |   2 +-
 .../tests/unittests/test_gvfs_with_local.py        |   2 +-
 31 files changed, 531 insertions(+), 253 deletions(-)

diff --git a/clients/client-python/gravitino/catalog/fileset_catalog.py 
b/clients/client-python/gravitino/catalog/fileset_catalog.py
index 82c345411..5ab2e00e6 100644
--- a/clients/client-python/gravitino/catalog/fileset_catalog.py
+++ b/clients/client-python/gravitino/catalog/fileset_catalog.py
@@ -35,6 +35,7 @@ from gravitino.name_identifier import NameIdentifier
 from gravitino.namespace import Namespace
 from gravitino.utils import HTTPClient
 from gravitino.rest.rest_utils import encode_string
+from gravitino.exceptions.handlers.fileset_error_handler import 
FILESET_ERROR_HANDLER
 
 logger = logging.getLogger(__name__)
 
@@ -88,7 +89,10 @@ class FilesetCatalog(BaseSchemaCatalog):
 
         full_namespace = self._get_fileset_full_namespace(namespace)
 
-        resp = 
self.rest_client.get(self.format_fileset_request_path(full_namespace))
+        resp = self.rest_client.get(
+            self.format_fileset_request_path(full_namespace),
+            error_handler=FILESET_ERROR_HANDLER,
+        )
         entity_list_resp = EntityListResponse.from_json(resp.body, 
infer_missing=True)
         entity_list_resp.validate()
 
@@ -114,7 +118,8 @@ class FilesetCatalog(BaseSchemaCatalog):
         full_namespace = self._get_fileset_full_namespace(ident.namespace())
 
         resp = self.rest_client.get(
-            
f"{self.format_fileset_request_path(full_namespace)}/{encode_string(ident.name())}"
+            
f"{self.format_fileset_request_path(full_namespace)}/{encode_string(ident.name())}",
+            error_handler=FILESET_ERROR_HANDLER,
         )
         fileset_resp = FilesetResponse.from_json(resp.body, infer_missing=True)
         fileset_resp.validate()
@@ -163,7 +168,9 @@ class FilesetCatalog(BaseSchemaCatalog):
         )
 
         resp = self.rest_client.post(
-            self.format_fileset_request_path(full_namespace), req
+            self.format_fileset_request_path(full_namespace),
+            req,
+            error_handler=FILESET_ERROR_HANDLER,
         )
         fileset_resp = FilesetResponse.from_json(resp.body, infer_missing=True)
         fileset_resp.validate()
@@ -195,7 +202,9 @@ class FilesetCatalog(BaseSchemaCatalog):
         req.validate()
 
         resp = self.rest_client.put(
-            
f"{self.format_fileset_request_path(full_namespace)}/{ident.name()}", req
+            
f"{self.format_fileset_request_path(full_namespace)}/{ident.name()}",
+            req,
+            error_handler=FILESET_ERROR_HANDLER,
         )
         fileset_resp = FilesetResponse.from_json(resp.body, infer_missing=True)
         fileset_resp.validate()
@@ -221,6 +230,7 @@ class FilesetCatalog(BaseSchemaCatalog):
 
             resp = self.rest_client.delete(
                 
f"{self.format_fileset_request_path(full_namespace)}/{ident.name()}",
+                error_handler=FILESET_ERROR_HANDLER,
             )
             drop_resp = DropResponse.from_json(resp.body, infer_missing=True)
             drop_resp.validate()
diff --git a/clients/client-python/gravitino/client/gravitino_client.py 
b/clients/client-python/gravitino/client/gravitino_client.py
index bd2337a16..21ba63c25 100644
--- a/clients/client-python/gravitino/client/gravitino_client.py
+++ b/clients/client-python/gravitino/client/gravitino_client.py
@@ -26,18 +26,6 @@ from gravitino.client.gravitino_client_base import 
GravitinoClientBase
 from gravitino.client.gravitino_metalake import GravitinoMetalake
 
 
-class NoSuchMetalakeException(Exception):
-    pass
-
-
-class NoSuchCatalogException(Exception):
-    pass
-
-
-class CatalogAlreadyExistsException(Exception):
-    pass
-
-
 class GravitinoClient(GravitinoClientBase):
     """Gravitino Client for a user to interact with the Gravitino API, 
allowing the client to list,
     load, create, and alter Catalog.
diff --git a/clients/client-python/gravitino/client/gravitino_client_base.py 
b/clients/client-python/gravitino/client/gravitino_client_base.py
index 0bf77071a..442b77dee 100644
--- a/clients/client-python/gravitino/client/gravitino_client_base.py
+++ b/clients/client-python/gravitino/client/gravitino_client_base.py
@@ -28,7 +28,7 @@ from gravitino.dto.version_dto import VersionDTO
 from gravitino.dto.responses.metalake_response import MetalakeResponse
 from gravitino.dto.responses.version_response import VersionResponse
 from gravitino.utils import HTTPClient
-from gravitino.exceptions.gravitino_runtime_exception import 
GravitinoRuntimeException
+from gravitino.exceptions.base import GravitinoRuntimeException
 from gravitino.constants.version import VERSION_INI, Version
 from gravitino.name_identifier import NameIdentifier
 
diff --git a/clients/client-python/gravitino/client/gravitino_metalake.py 
b/clients/client-python/gravitino/client/gravitino_metalake.py
index e4009fc17..9ef787a07 100644
--- a/clients/client-python/gravitino/client/gravitino_metalake.py
+++ b/clients/client-python/gravitino/client/gravitino_metalake.py
@@ -36,18 +36,6 @@ from gravitino.utils import HTTPClient
 logger = logging.getLogger(__name__)
 
 
-class NoSuchMetalakeException(Exception):
-    pass
-
-
-class NoSuchCatalogException(Exception):
-    pass
-
-
-class CatalogAlreadyExistsException(Exception):
-    pass
-
-
 class GravitinoMetalake(MetalakeDTO):
     """
     Gravitino Metalake is the top-level metadata repository for users. It 
contains a list of catalogs
diff --git a/clients/client-python/gravitino/client/gravitino_version.py 
b/clients/client-python/gravitino/client/gravitino_version.py
index f59e78a86..c69376f1b 100644
--- a/clients/client-python/gravitino/client/gravitino_version.py
+++ b/clients/client-python/gravitino/client/gravitino_version.py
@@ -22,7 +22,7 @@ from dataclasses import dataclass
 from enum import Enum
 
 from gravitino.dto.version_dto import VersionDTO
-from gravitino.exceptions.gravitino_runtime_exception import 
GravitinoRuntimeException
+from gravitino.exceptions.base import GravitinoRuntimeException
 
 
 class Version(Enum):
diff --git a/clients/client-python/gravitino/constants/error.py 
b/clients/client-python/gravitino/constants/error.py
new file mode 100644
index 000000000..8f4bdbffb
--- /dev/null
+++ b/clients/client-python/gravitino/constants/error.py
@@ -0,0 +1,71 @@
+"""
+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 enum import IntEnum
+
+from gravitino.exceptions.base import (
+    RESTException,
+    IllegalArugmentException,
+    NotFoundException,
+    InternalError,
+    AlreadyExistException,
+    NotEmptyException,
+    UnsupportedOperationException,
+)
+
+
+class ErrorConstants(IntEnum):
+    """Constants representing error codes for responses."""
+
+    # Error codes for REST responses error.
+    REST_ERROR_CODE = 1000
+
+    # Error codes for illegal arguments.
+    ILLEGAL_ARGUMENTS_CODE = 1001
+
+    # Error codes for internal errors.
+    INTERNAL_ERROR_CODE = 1002
+
+    # Error codes for not found.
+    NOT_FOUND_CODE = 1003
+
+    # Error codes for already exists.
+    ALREADY_EXISTS_CODE = 1004
+
+    # Error codes for non empty.
+    NON_EMPTY_CODE = 1005
+
+    # Error codes for unsupported operation.
+    UNSUPPORTED_OPERATION_CODE = 1006
+
+    # Error codes for invalid state.
+    UNKNOWN_ERROR_CODE = 1100
+
+
+EXCEPTION_MAPPING = {
+    RESTException: ErrorConstants.REST_ERROR_CODE,
+    IllegalArugmentException: ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
+    InternalError: ErrorConstants.INTERNAL_ERROR_CODE,
+    NotFoundException: ErrorConstants.NOT_FOUND_CODE,
+    AlreadyExistException: ErrorConstants.ALREADY_EXISTS_CODE,
+    NotEmptyException: ErrorConstants.NON_EMPTY_CODE,
+    UnsupportedOperationException: ErrorConstants.UNSUPPORTED_OPERATION_CODE,
+}
+
+ERROR_CODE_MAPPING = {v: k for k, v in EXCEPTION_MAPPING.items()}
diff --git a/clients/client-python/gravitino/dto/responses/base_response.py 
b/clients/client-python/gravitino/dto/responses/base_response.py
index 71efece86..8ce91bd9b 100644
--- a/clients/client-python/gravitino/dto/responses/base_response.py
+++ b/clients/client-python/gravitino/dto/responses/base_response.py
@@ -35,7 +35,6 @@ class BaseResponse(RESTResponse):
 
     def validate(self):
         """Validates the response code.
-        TODO: @throws IllegalArgumentException if code value is negative.
+        @throws IllegalArgumentException if code value is negative.
         """
-        if self._code < 0:
-            raise ValueError("code must be >= 0")
+        assert self._code >= 0, "code must be >= 0"
diff --git a/clients/client-python/gravitino/dto/responses/error_response.py 
b/clients/client-python/gravitino/dto/responses/error_response.py
new file mode 100644
index 000000000..ad1fd273e
--- /dev/null
+++ b/clients/client-python/gravitino/dto/responses/error_response.py
@@ -0,0 +1,79 @@
+"""
+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 List, Optional
+from dataclasses import dataclass, field
+from dataclasses_json import config
+
+from gravitino.dto.responses.base_response import BaseResponse
+from gravitino.constants.error import ErrorConstants, EXCEPTION_MAPPING
+from gravitino.exceptions.base import UnknownError
+
+
+@dataclass
+class ErrorResponse(BaseResponse):
+    """Represents an error response."""
+
+    _type: str = field(metadata=config(field_name="type"))
+    _message: str = field(metadata=config(field_name="message"))
+    _stack: Optional[List[str]] = field(metadata=config(field_name="stack"))
+
+    def type(self):
+        return self._type
+
+    def message(self):
+        return self._message
+
+    def stack(self):
+        return self._stack
+
+    def validate(self):
+        super().validate()
+
+        assert (
+            self._type is not None and len(self._type) != 0
+        ), "type cannot be None or empty"
+        assert (
+            self._message is not None and len(self._message) != 0
+        ), "message cannot be None or empty"
+
+    def __repr__(self) -> str:
+        return (
+            f"ErrorResponse(code={self._code}, type={self._type}, 
message={self._message})"
+            + ("\n\t" + "\n\t".join(self._stack) if self._stack is not None 
else "")
+        )
+
+    def format_error_message(self) -> str:
+        return (
+            f"{self._message}\n" + "\n".join(self._stack)
+            if self._stack is not None
+            else ""
+        )
+
+    @classmethod
+    def generate_error_response(
+        cls, exception: Exception, message: str
+    ) -> "ErrorResponse":
+        for exception_type, error_code in EXCEPTION_MAPPING.items():
+            if issubclass(exception, exception_type):
+                return cls(error_code, exception.__name__, message, None)
+
+        return cls(
+            ErrorConstants.UNKNOWN_ERROR_CODE, UnknownError.__name__, message, 
None
+        )
diff --git a/clients/client-python/gravitino/dto/responses/version_response.py 
b/clients/client-python/gravitino/dto/responses/version_response.py
index 6994dbca1..abeebcabf 100644
--- a/clients/client-python/gravitino/dto/responses/version_response.py
+++ b/clients/client-python/gravitino/dto/responses/version_response.py
@@ -20,8 +20,8 @@ under the License.
 from dataclasses import dataclass, field
 from dataclasses_json import config
 
-from .base_response import BaseResponse
-from ..version_dto import VersionDTO
+from gravitino.dto.responses.base_response import BaseResponse
+from gravitino.dto.version_dto import VersionDTO
 
 
 @dataclass
diff --git a/clients/client-python/gravitino/exceptions/base.py 
b/clients/client-python/gravitino/exceptions/base.py
new file mode 100644
index 000000000..96081f1ae
--- /dev/null
+++ b/clients/client-python/gravitino/exceptions/base.py
@@ -0,0 +1,83 @@
+"""
+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.
+"""
+
+
+class GravitinoRuntimeException(RuntimeError):
+    """Base class for all Gravitino runtime exceptions."""
+
+    def __init__(self, message, *args):
+        super().__init__(message % args)
+
+
+class RESTException(RuntimeError):
+    """An exception thrown when a REST request fails."""
+
+    def __init__(self, message, *args):
+        super().__init__(message % args)
+
+
+class IllegalArugmentException(ValueError):
+    """Base class for all exceptions thrown when arguments are invalid."""
+
+    def __init__(self, message, *args):
+        super().__init__(message % args)
+
+
+class IllegalNameIdentifierException(IllegalArugmentException):
+    """An exception thrown when a name identifier is invalid."""
+
+
+class IllegalNamespaceException(IllegalArugmentException):
+    """An exception thrown when a namespace is invalid."""
+
+
+class InternalError(GravitinoRuntimeException):
+    """Base class for all exceptions thrown internally"""
+
+
+class NotFoundException(GravitinoRuntimeException):
+    """Base class for all exceptions thrown when a resource is not found."""
+
+
+class NoSuchSchemaException(NotFoundException):
+    """An exception thrown when a schema is not found."""
+
+
+class NoSuchFilesetException(NotFoundException):
+    """Exception thrown when a file with specified name is not existed."""
+
+
+class NoSuchMetalakeException(NotFoundException):
+    """An exception thrown when a metalake is not found."""
+
+
+class AlreadyExistException(GravitinoRuntimeException):
+    """Base exception thrown when an entity or resource already exists."""
+
+
+class NotEmptyException(GravitinoRuntimeException):
+    """Base class for all exceptions thrown when a resource is not empty."""
+
+
+class UnsupportedOperationException(GravitinoRuntimeException):
+    """Base class for all exceptions thrown when an operation is unsupported"""
+
+
+class UnknownError(RuntimeError):
+    """An exception thrown when other unknown exception is thrown"""
diff --git 
a/clients/client-python/gravitino/exceptions/gravitino_runtime_exception.py 
b/clients/client-python/gravitino/exceptions/gravitino_runtime_exception.py
deleted file mode 100644
index d98bc169c..000000000
--- a/clients/client-python/gravitino/exceptions/gravitino_runtime_exception.py
+++ /dev/null
@@ -1,25 +0,0 @@
-"""
-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.
-"""
-
-
-class GravitinoRuntimeException(RuntimeError):
-    """Base class for all Gravitino runtime exceptions."""
-
-    def __init__(self, message, *args):
-        super().__init__(message % args)
diff --git 
a/clients/client-python/gravitino/exceptions/no_such_metalake_exception.py 
b/clients/client-python/gravitino/exceptions/handlers/__init__.py
similarity index 80%
rename from 
clients/client-python/gravitino/exceptions/no_such_metalake_exception.py
rename to clients/client-python/gravitino/exceptions/handlers/__init__.py
index 6dc136ca9..c206137f1 100644
--- a/clients/client-python/gravitino/exceptions/no_such_metalake_exception.py
+++ b/clients/client-python/gravitino/exceptions/handlers/__init__.py
@@ -16,9 +16,3 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 """
-
-from gravitino.exceptions.not_found_exception import NotFoundException
-
-
-class NoSuchMetalakeException(NotFoundException):
-    """An exception thrown when a metalake is not found."""
diff --git 
a/clients/client-python/gravitino/exceptions/illegal_namespace_exception.py 
b/clients/client-python/gravitino/exceptions/handlers/error_handler.py
similarity index 50%
rename from 
clients/client-python/gravitino/exceptions/illegal_namespace_exception.py
rename to clients/client-python/gravitino/exceptions/handlers/error_handler.py
index f8ded7070..d1ce2a4fc 100644
--- a/clients/client-python/gravitino/exceptions/illegal_namespace_exception.py
+++ b/clients/client-python/gravitino/exceptions/handlers/error_handler.py
@@ -17,12 +17,24 @@ specific language governing permissions and limitations
 under the License.
 """
 
+from abc import ABC, abstractmethod
+from gravitino.dto.responses.error_response import ErrorResponse
 
-class IllegalNamespaceException(Exception):
-    """An exception thrown when a namespace is invalid."""
 
-    def __init__(self, message=None):
-        if message:
-            super().__init__(message)
-        else:
-            super().__init__()
+class ErrorHandler(ABC):
+    """The ErrorHandler class is an abstract class specialized for handling 
ErrorResponse objects.
+    Subclasses of ErrorHandler must implement the parseResponse method to 
provide custom parsing
+    logic for different types of errors.
+    """
+
+    @abstractmethod
+    def handle(self, error_response: ErrorResponse):
+        """Handles the error response and raise the appropriate Exception. The 
implementation will use the
+        provided error response to determine which exception to raise.
+
+        Args:
+          error_response: the error response from the server
+
+        Raises:
+          appropriate Exception.
+        """
diff --git 
a/clients/client-python/gravitino/exceptions/handlers/fileset_error_handler.py 
b/clients/client-python/gravitino/exceptions/handlers/fileset_error_handler.py
new file mode 100644
index 000000000..bff39d02b
--- /dev/null
+++ 
b/clients/client-python/gravitino/exceptions/handlers/fileset_error_handler.py
@@ -0,0 +1,43 @@
+"""
+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 NoSuchFilesetException, 
NoSuchSchemaException
+
+
+class FilesetErrorHandler(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.NOT_FOUND_CODE:
+            if exception_type == NoSuchSchemaException.__name__:
+                raise NoSuchSchemaException(error_message)
+            if exception_type == NoSuchFilesetException.__name__:
+                raise NoSuchFilesetException(error_message)
+
+        super().handle(error_response)
+
+
+FILESET_ERROR_HANDLER = FilesetErrorHandler()
diff --git 
a/clients/client-python/gravitino/exceptions/illegal_name_identifier_exception.py
 b/clients/client-python/gravitino/exceptions/handlers/rest_error_handler.py
similarity index 53%
rename from 
clients/client-python/gravitino/exceptions/illegal_name_identifier_exception.py
rename to 
clients/client-python/gravitino/exceptions/handlers/rest_error_handler.py
index bb14af12c..8319e6254 100644
--- 
a/clients/client-python/gravitino/exceptions/illegal_name_identifier_exception.py
+++ b/clients/client-python/gravitino/exceptions/handlers/rest_error_handler.py
@@ -17,12 +17,24 @@ specific language governing permissions and limitations
 under the License.
 """
 
+from gravitino.constants.error import ERROR_CODE_MAPPING
+from gravitino.dto.responses.error_response import ErrorResponse
+from gravitino.exceptions.handlers.error_handler import ErrorHandler
+from gravitino.exceptions.base import RESTException
 
-class IllegalNameIdentifierException(Exception):
-    """An exception thrown when a name identifier is invalid."""
 
-    def __init__(self, message=None):
-        if message:
-            super().__init__(message)
-        else:
-            super().__init__()
+class RestErrorHandler(ErrorHandler):
+
+    def handle(self, error_response: ErrorResponse):
+        error_message = error_response.format_error_message()
+        code = error_response.code()
+
+        if code in ERROR_CODE_MAPPING:
+            raise ERROR_CODE_MAPPING[code](error_message)
+
+        raise RESTException(
+            f"Unable to process: {error_message}",
+        )
+
+
+REST_ERROR_HANDLER = RestErrorHandler()
diff --git a/clients/client-python/gravitino/exceptions/not_found_exception.py 
b/clients/client-python/gravitino/exceptions/not_found_exception.py
deleted file mode 100644
index aff8cb583..000000000
--- a/clients/client-python/gravitino/exceptions/not_found_exception.py
+++ /dev/null
@@ -1,24 +0,0 @@
-"""
-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.gravitino_runtime_exception import 
GravitinoRuntimeException
-
-
-class NotFoundException(GravitinoRuntimeException):
-    """Base class for all exceptions thrown when a resource is not found."""
diff --git a/clients/client-python/gravitino/filesystem/gvfs.py 
b/clients/client-python/gravitino/filesystem/gvfs.py
index edb150677..a50c97f4c 100644
--- a/clients/client-python/gravitino/filesystem/gvfs.py
+++ b/clients/client-python/gravitino/filesystem/gvfs.py
@@ -33,7 +33,7 @@ from readerwriterlock import rwlock
 from gravitino.api.catalog import Catalog
 from gravitino.api.fileset import Fileset
 from gravitino.client.gravitino_client import GravitinoClient
-from gravitino.exceptions.gravitino_runtime_exception import 
GravitinoRuntimeException
+from gravitino.exceptions.base import GravitinoRuntimeException
 from gravitino.name_identifier import NameIdentifier
 
 PROTOCOL_NAME = "gvfs"
diff --git a/clients/client-python/gravitino/name_identifier.py 
b/clients/client-python/gravitino/name_identifier.py
index 069e22500..8b854b0dc 100644
--- a/clients/client-python/gravitino/name_identifier.py
+++ b/clients/client-python/gravitino/name_identifier.py
@@ -23,13 +23,11 @@ from dataclasses import dataclass, field
 
 from dataclasses_json import DataClassJsonMixin, config
 
-from gravitino.exceptions.illegal_name_identifier_exception import (
+from gravitino.exceptions.base import (
     IllegalNameIdentifierException,
 )
 from gravitino.namespace import Namespace
 
-# TODO: delete redundant methods
-
 
 @dataclass
 class NameIdentifier(DataClassJsonMixin):
diff --git a/clients/client-python/gravitino/namespace.py 
b/clients/client-python/gravitino/namespace.py
index 33747ed25..221fd3ea8 100644
--- a/clients/client-python/gravitino/namespace.py
+++ b/clients/client-python/gravitino/namespace.py
@@ -19,8 +19,7 @@ under the License.
 
 import json
 from typing import List, ClassVar
-
-# TODO: delete redundant methods
+from gravitino.exceptions.base import IllegalNamespaceException
 
 
 class Namespace:
@@ -137,4 +136,4 @@ class Namespace:
             args: The arguments to the message.
         """
         if not expression:
-            raise ValueError(message.format(*args))
+            raise IllegalNamespaceException(message.format(*args))
diff --git a/clients/client-python/gravitino/rest/rest_message.py 
b/clients/client-python/gravitino/rest/rest_message.py
index 59e93325b..b85ac6a6c 100644
--- a/clients/client-python/gravitino/rest/rest_message.py
+++ b/clients/client-python/gravitino/rest/rest_message.py
@@ -44,12 +44,6 @@ class RESTMessage(DataClassJsonMixin, ABC):
         pass
 
 
-class IllegalArgumentException(Exception):
-    """Exception raised if a REST message is not valid according to the REST 
spec."""
-
-    pass
-
-
 class RESTRequest(RESTMessage, ABC):
     """Interface to mark a REST request."""
 
diff --git a/clients/client-python/gravitino/utils/exceptions.py 
b/clients/client-python/gravitino/utils/exceptions.py
deleted file mode 100644
index 09314a5e0..000000000
--- a/clients/client-python/gravitino/utils/exceptions.py
+++ /dev/null
@@ -1,114 +0,0 @@
-"""
-MIT License
-
-Copyright (c) 2016 Dhamu
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-"""
-
-import json
-
-
-class HTTPError(Exception):
-    """Base of all other errors"""
-
-    def __init__(self, error):
-        self.status_code = error.code
-        self.reason = error.reason
-        self.body = error.read()
-        self.headers = error.hdrs
-
-    def json(self):
-        """
-        :Returns: object of response error from the API
-        """
-        try:
-            return json.loads(self.body.decode("utf-8"))
-        except json.decoder.JSONDecodeError:
-            return {"exception": self.body.decode("utf-8")}
-
-    def __str__(self):
-        return self.json().get("exception")
-
-
-class BadRequestsError(HTTPError):
-    pass
-
-
-class UnauthorizedError(HTTPError):
-    pass
-
-
-class ForbiddenError(HTTPError):
-    pass
-
-
-class NotFoundError(HTTPError):
-    pass
-
-
-class MethodNotAllowedError(HTTPError):
-    pass
-
-
-class PayloadTooLargeError(HTTPError):
-    pass
-
-
-class UnsupportedMediaTypeError(HTTPError):
-    pass
-
-
-class TooManyRequestsError(HTTPError):
-    pass
-
-
-class InternalServerError(HTTPError):
-    pass
-
-
-class ServiceUnavailableError(HTTPError):
-    pass
-
-
-class GatewayTimeoutError(HTTPError):
-    pass
-
-
-err_dict = {
-    400: BadRequestsError,
-    401: UnauthorizedError,
-    403: ForbiddenError,
-    404: NotFoundError,
-    405: MethodNotAllowedError,
-    413: PayloadTooLargeError,
-    415: UnsupportedMediaTypeError,
-    429: TooManyRequestsError,
-    500: InternalServerError,
-    503: ServiceUnavailableError,
-    504: GatewayTimeoutError,
-}
-
-
-def handle_error(error):
-    try:
-        exc = err_dict[error.code](error)
-    except KeyError:
-        return HTTPError(error)
-    return exc
diff --git a/clients/client-python/gravitino/utils/http_client.py 
b/clients/client-python/gravitino/utils/http_client.py
index 150c00002..67504f12d 100644
--- a/clients/client-python/gravitino/utils/http_client.py
+++ b/clients/client-python/gravitino/utils/http_client.py
@@ -23,17 +23,23 @@ SOFTWARE.
 """
 
 import logging
+from typing import Tuple
 from urllib.request import Request, build_opener
 from urllib.parse import urlencode
+
 from urllib.error import HTTPError
 import json as _json
 
 from gravitino.auth.auth_constants import AuthConstants
 from gravitino.auth.auth_data_provider import AuthDataProvider
 from gravitino.typing import JSONType
-from gravitino.utils.exceptions import handle_error
+
 from gravitino.constants.timeout import TIMEOUT
 
+from gravitino.dto.responses.error_response import ErrorResponse
+from gravitino.exceptions.base import RESTException, UnknownError
+from gravitino.exceptions.handlers.error_handler import ErrorHandler
+
 logger = logging.getLogger(__name__)
 
 
@@ -115,16 +121,33 @@ class HTTPClient:
                 _headers[key] = value
         return _headers
 
-    def _make_request(self, opener, request, timeout=None):
+    def _make_request(self, opener, request, timeout=None) -> Tuple[bool, 
Response]:
         timeout = timeout or self.timeout
         try:
-            return opener.open(request, timeout=timeout)
+            return (True, Response(opener.open(request, timeout=timeout)))
         except HTTPError as err:
-            exc = handle_error(err)
-            raise exc from None
+            err_body = err.read()
+
+            if err_body is None:
+                return (
+                    False,
+                    ErrorResponse.generate_error_response(RESTException, 
err.reason),
+                )
+
+            err_resp = ErrorResponse.from_json(err_body, infer_missing=True)
+            err_resp.validate()
+
+            return (False, err_resp)
 
     def _request(
-        self, method, endpoint, params=None, json=None, headers=None, 
timeout=None
+        self,
+        method,
+        endpoint,
+        params=None,
+        json=None,
+        headers=None,
+        timeout=None,
+        error_handler: ErrorHandler = None,
     ):
         method = method.upper()
         request_data = None
@@ -154,19 +177,41 @@ class HTTPClient:
                 self.auth_data_provider.get_token_data().decode("utf-8"),
             )
         request.get_method = lambda: method
-        return Response(self._make_request(opener, request, timeout=timeout))
+        is_success, resp = self._make_request(opener, request, timeout=timeout)
+
+        if is_success:
+            return resp
+
+        if not isinstance(error_handler, ErrorHandler):
+            raise UnknownError(
+                f"Unknown error handler {type(error_handler).__name__}, error 
response body: {resp}"
+            ) from None
+
+        error_handler.handle(resp)
+
+        # This code generally will not be run because the error handler should 
define the default behavior,
+        # and raise appropriate
+        raise UnknownError(
+            f"Error handler {type(error_handler).__name__} can't handle this 
response, error response body: {resp}"
+        ) from None
 
-    def get(self, endpoint, params=None, **kwargs):
-        return self._request("get", endpoint, params=params, **kwargs)
+    def get(self, endpoint, params=None, error_handler=None, **kwargs):
+        return self._request(
+            "get", endpoint, params=params, error_handler=error_handler, 
**kwargs
+        )
 
-    def delete(self, endpoint, **kwargs):
-        return self._request("delete", endpoint, **kwargs)
+    def delete(self, endpoint, error_handler=None, **kwargs):
+        return self._request("delete", endpoint, error_handler=error_handler, 
**kwargs)
 
-    def post(self, endpoint, json=None, **kwargs):
-        return self._request("post", endpoint, json=json, **kwargs)
+    def post(self, endpoint, json=None, error_handler=None, **kwargs):
+        return self._request(
+            "post", endpoint, json=json, error_handler=error_handler, **kwargs
+        )
 
-    def put(self, endpoint, json=None, **kwargs):
-        return self._request("put", endpoint, json=json, **kwargs)
+    def put(self, endpoint, json=None, error_handler=None, **kwargs):
+        return self._request(
+            "put", endpoint, json=json, error_handler=error_handler, **kwargs
+        )
 
     def close(self):
         self._request("close", "/")
diff --git a/clients/client-python/scripts/generate_version.py 
b/clients/client-python/scripts/generate_version.py
index 262bac5d1..bf7e8c449 100644
--- a/clients/client-python/scripts/generate_version.py
+++ b/clients/client-python/scripts/generate_version.py
@@ -23,7 +23,7 @@ import subprocess
 from datetime import datetime
 
 from gravitino.constants.version import Version, VERSION_INI, SETUP_FILE
-from gravitino.exceptions.gravitino_runtime_exception import 
GravitinoRuntimeException
+from gravitino.exceptions.base import GravitinoRuntimeException
 
 VERSION_PATTERN = r"version\s*=\s*['\"]([^'\"]+)['\"]"
 
diff --git a/clients/client-python/tests/integration/base_hadoop_env.py 
b/clients/client-python/tests/integration/base_hadoop_env.py
index 694331078..9099ed13f 100644
--- a/clients/client-python/tests/integration/base_hadoop_env.py
+++ b/clients/client-python/tests/integration/base_hadoop_env.py
@@ -23,7 +23,7 @@ import shutil
 import subprocess
 import tarfile
 
-from gravitino.exceptions.gravitino_runtime_exception import 
GravitinoRuntimeException
+from gravitino.exceptions.base import GravitinoRuntimeException
 
 logger = logging.getLogger(__name__)
 
diff --git a/clients/client-python/tests/integration/hdfs_container.py 
b/clients/client-python/tests/integration/hdfs_container.py
index 34bc41d86..1c3f1c21d 100644
--- a/clients/client-python/tests/integration/hdfs_container.py
+++ b/clients/client-python/tests/integration/hdfs_container.py
@@ -26,7 +26,7 @@ import docker
 from docker import types as tp
 from docker.errors import NotFound
 
-from gravitino.exceptions.gravitino_runtime_exception import 
GravitinoRuntimeException
+from gravitino.exceptions.base import GravitinoRuntimeException
 
 logger = logging.getLogger(__name__)
 
diff --git a/clients/client-python/tests/integration/integration_test_env.py 
b/clients/client-python/tests/integration/integration_test_env.py
index dde2c2410..8f1a5a2c8 100644
--- a/clients/client-python/tests/integration/integration_test_env.py
+++ b/clients/client-python/tests/integration/integration_test_env.py
@@ -26,7 +26,7 @@ import sys
 
 import requests
 
-from gravitino.exceptions.gravitino_runtime_exception import 
GravitinoRuntimeException
+from gravitino.exceptions.base import GravitinoRuntimeException
 
 logger = logging.getLogger(__name__)
 
diff --git a/clients/client-python/tests/integration/test_fileset_catalog.py 
b/clients/client-python/tests/integration/test_fileset_catalog.py
index da79ef1ed..25641ab20 100644
--- a/clients/client-python/tests/integration/test_fileset_catalog.py
+++ b/clients/client-python/tests/integration/test_fileset_catalog.py
@@ -29,6 +29,7 @@ from gravitino import (
     Fileset,
     FilesetChange,
 )
+from gravitino.exceptions.base import NoSuchFilesetException
 from tests.integration.integration_test_env import IntegrationTestEnv
 
 logger = logging.getLogger(__name__)
@@ -174,6 +175,11 @@ class TestFilesetCatalog(IntegrationTestEnv):
         self.assertEqual(fileset.properties(), self.fileset_properties)
         self.assertEqual(fileset.audit_info().creator(), "anonymous")
 
+    def test_failed_load_fileset(self):
+        catalog = self.gravitino_client.load_catalog(name=self.catalog_name)
+        with self.assertRaises(NoSuchFilesetException):
+            _ = 
catalog.as_fileset_catalog().load_fileset(ident=self.fileset_ident)
+
     def test_alter_fileset(self):
         self.create_fileset()
         fileset_propertie_new_value = self.fileset_properties_value2 + "_new"
diff --git a/clients/client-python/tests/integration/test_gvfs_with_hdfs.py 
b/clients/client-python/tests/integration/test_gvfs_with_hdfs.py
index 442bbc87e..53fde1223 100644
--- a/clients/client-python/tests/integration/test_gvfs_with_hdfs.py
+++ b/clients/client-python/tests/integration/test_gvfs_with_hdfs.py
@@ -40,7 +40,7 @@ from gravitino import (
     Catalog,
     Fileset,
 )
-from gravitino.exceptions.gravitino_runtime_exception import 
GravitinoRuntimeException
+from gravitino.exceptions.base import GravitinoRuntimeException
 from tests.integration.integration_test_env import IntegrationTestEnv
 from tests.integration.hdfs_container import HDFSContainer
 from tests.integration.base_hadoop_env import BaseHadoopEnvironment
diff --git a/clients/client-python/tests/unittests/test_error_handler.py 
b/clients/client-python/tests/unittests/test_error_handler.py
new file mode 100644
index 000000000..565c08151
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_error_handler.py
@@ -0,0 +1,120 @@
+"""
+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.dto.responses.error_response import ErrorResponse
+from gravitino.exceptions.base import (
+    NoSuchSchemaException,
+    NoSuchFilesetException,
+    InternalError,
+    RESTException,
+    NotFoundException,
+    IllegalArugmentException,
+    AlreadyExistException,
+    NotEmptyException,
+    UnsupportedOperationException,
+)
+
+from gravitino.exceptions.handlers.rest_error_handler import REST_ERROR_HANDLER
+from gravitino.exceptions.handlers.fileset_error_handler import 
FILESET_ERROR_HANDLER
+
+
+class TestErrorHandler(unittest.TestCase):
+
+    def test_rest_error_handler(self):
+
+        with self.assertRaises(RESTException):
+            REST_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(RESTException, "mock 
error")
+            )
+
+        with self.assertRaises(IllegalArugmentException):
+            REST_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(
+                    IllegalArugmentException, "mock error"
+                )
+            )
+
+        with self.assertRaises(InternalError):
+            REST_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(InternalError, "mock 
error")
+            )
+
+        with self.assertRaises(NotFoundException):
+            REST_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(NotFoundException, "mock 
error")
+            )
+
+        with self.assertRaises(NotFoundException):
+            REST_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(
+                    NoSuchFilesetException, "mock error"
+                )
+            )
+
+        with self.assertRaises(AlreadyExistException):
+            REST_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(
+                    AlreadyExistException, "mock error"
+                )
+            )
+
+        with self.assertRaises(NotEmptyException):
+            REST_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(NotEmptyException, "mock 
error")
+            )
+
+        with self.assertRaises(UnsupportedOperationException):
+            REST_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(
+                    UnsupportedOperationException, "mock error"
+                )
+            )
+
+        with self.assertRaises(RESTException):
+            REST_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(Exception, "mock error")
+            )
+
+    def test_fileset_error_handler(self):
+
+        with self.assertRaises(NoSuchFilesetException):
+            FILESET_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(
+                    NoSuchFilesetException, "mock error"
+                )
+            )
+
+        with self.assertRaises(NoSuchSchemaException):
+            FILESET_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(
+                    NoSuchSchemaException, "mock error"
+                )
+            )
+
+        with self.assertRaises(InternalError):
+            FILESET_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(InternalError, "mock 
error")
+            )
+
+        with self.assertRaises(RESTException):
+            FILESET_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(Exception, "mock error")
+            )
diff --git a/clients/client-python/tests/unittests/test_gravitino_version.py 
b/clients/client-python/tests/unittests/test_gravitino_version.py
index 02196effe..6f18bcfd1 100644
--- a/clients/client-python/tests/unittests/test_gravitino_version.py
+++ b/clients/client-python/tests/unittests/test_gravitino_version.py
@@ -21,7 +21,7 @@ import unittest
 
 from gravitino.client.gravitino_version import GravitinoVersion
 from gravitino.dto.version_dto import VersionDTO
-from gravitino.exceptions.gravitino_runtime_exception import 
GravitinoRuntimeException
+from gravitino.exceptions.base import GravitinoRuntimeException
 
 
 class TestGravitinoVersion(unittest.TestCase):
diff --git a/clients/client-python/tests/unittests/test_gvfs_with_local.py 
b/clients/client-python/tests/unittests/test_gvfs_with_local.py
index c5cebc764..61ea004a1 100644
--- a/clients/client-python/tests/unittests/test_gvfs_with_local.py
+++ b/clients/client-python/tests/unittests/test_gvfs_with_local.py
@@ -37,7 +37,7 @@ from gravitino import NameIdentifier
 from gravitino.dto.audit_dto import AuditDTO
 from gravitino.dto.fileset_dto import FilesetDTO
 from gravitino.filesystem.gvfs import FilesetContext, StorageType
-from gravitino.exceptions.gravitino_runtime_exception import 
GravitinoRuntimeException
+from gravitino.exceptions.base import GravitinoRuntimeException
 
 from tests.unittests import mock_base
 


Reply via email to