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

liuxun 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 c98c15197 [#5623] feat(python): supports credential API in python 
client (#5777)
c98c15197 is described below

commit c98c151972790793d5f5411052b10f381d75e95d
Author: FANNG <xiaoj...@datastrato.com>
AuthorDate: Thu Dec 12 21:48:07 2024 +0800

    [#5623] feat(python): supports credential API in python client (#5777)
    
    ### What changes were proposed in this pull request?
    supports credential API in python client
    
    ```python
            catalog = gravitino_client.load_catalog(catalog_name)
            catalog.as_fileset_catalog().support_credentials().get_credentials()
    
            fileset = catalog.as_fileset_catalog().load_fileset(
                NameIdentifier.of("schema", "fileset")
            )
            credentials = fileset.support_credentials().get_credentials()
    ```
    
    ### Why are the changes needed?
    
    Fix: #5623
    
    ### Does this PR introduce _any_ user-facing change?
    no
    
    ### How was this patch tested?
    1. add UT
    2. setup a Gravitino server which returns credential, test with python
    client
---
 .../gravitino/api/credential/__init__.py           |  16 +++
 .../gravitino/api/credential/credential.py         |  51 ++++++++++
 .../api/credential/gcs_token_credential.py         |  75 ++++++++++++++
 .../api/credential/oss_token_credential.py         | 105 ++++++++++++++++++++
 .../api/credential/s3_secret_key_credential.py     |  91 +++++++++++++++++
 .../api/credential/s3_token_credential.py          | 110 +++++++++++++++++++++
 .../api/credential/supports_credentials.py         |  73 ++++++++++++++
 .../client-python/gravitino/api/metadata_object.py |  56 +++++++++++
 .../gravitino/catalog/base_schema_catalog.py       |  13 +++
 .../gravitino/catalog/fileset_catalog.py           |  13 ++-
 .../gravitino/client/generic_fileset.py            |  75 ++++++++++++++
 .../metadata_object_credential_operations.py       |  74 ++++++++++++++
 .../gravitino/client/metadata_object_impl.py       |  35 +++++++
 .../client-python/gravitino/dto/credential_dto.py  |  42 ++++++++
 .../gravitino/dto/responses/credential_response.py |  44 +++++++++
 clients/client-python/gravitino/exceptions/base.py |   8 ++
 .../handlers/credential_error_handler.py           |  45 +++++++++
 .../gravitino/utils/credential_factory.py          |  39 ++++++++
 .../client-python/gravitino/utils/precondition.py  |  48 +++++++++
 clients/client-python/tests/unittests/mock_base.py |   4 +
 .../tests/unittests/test_credential_api.py         | 105 ++++++++++++++++++++
 .../tests/unittests/test_credential_factory.py     | 101 +++++++++++++++++++
 .../tests/unittests/test_error_handler.py          |  23 +++++
 .../tests/unittests/test_precondition.py           |  46 +++++++++
 .../tests/unittests/test_responses.py              |  35 +++++++
 25 files changed, 1325 insertions(+), 2 deletions(-)

diff --git a/clients/client-python/gravitino/api/credential/__init__.py 
b/clients/client-python/gravitino/api/credential/__init__.py
new file mode 100644
index 000000000..325597ecf
--- /dev/null
+++ b/clients/client-python/gravitino/api/credential/__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/gravitino/api/credential/credential.py 
b/clients/client-python/gravitino/api/credential/credential.py
new file mode 100644
index 000000000..37b97694a
--- /dev/null
+++ b/clients/client-python/gravitino/api/credential/credential.py
@@ -0,0 +1,51 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from abc import ABC, abstractmethod
+from typing import Dict
+
+
+class Credential(ABC):
+    """Represents the credential in Gravitino."""
+
+    @abstractmethod
+    def credential_type(self) -> str:
+        """The type of the credential.
+
+        Returns:
+             the type of the credential.
+        """
+        pass
+
+    @abstractmethod
+    def expire_time_in_ms(self) -> int:
+        """Returns the expiration time of the credential in milliseconds since
+        the epoch, 0 means it will never expire.
+
+        Returns:
+             The expiration time of the credential.
+        """
+        pass
+
+    @abstractmethod
+    def credential_info(self) -> Dict[str, str]:
+        """The credential information.
+
+        Returns:
+             The credential information.
+        """
+        pass
diff --git 
a/clients/client-python/gravitino/api/credential/gcs_token_credential.py 
b/clients/client-python/gravitino/api/credential/gcs_token_credential.py
new file mode 100644
index 000000000..1362383f0
--- /dev/null
+++ b/clients/client-python/gravitino/api/credential/gcs_token_credential.py
@@ -0,0 +1,75 @@
+# 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 abc import ABC
+from typing import Dict
+
+from gravitino.api.credential.credential import Credential
+from gravitino.utils.precondition import Precondition
+
+
+class GCSTokenCredential(Credential, ABC):
+    """Represents the GCS token credential."""
+
+    GCS_TOKEN_CREDENTIAL_TYPE: str = "gcs-token"
+    _GCS_TOKEN_NAME: str = "token"
+
+    _expire_time_in_ms: int = 0
+
+    def __init__(self, credential_info: Dict[str, str], expire_time_in_ms: 
int):
+        self._token = credential_info[self._GCS_TOKEN_NAME]
+        self._expire_time_in_ms = expire_time_in_ms
+        Precondition.check_string_not_empty(
+            self._token, "GCS token should not be empty"
+        )
+        Precondition.check_argument(
+            self._expire_time_in_ms > 0,
+            "The expiration time of GCS token credential should be greater 
than 0",
+        )
+
+    def credential_type(self) -> str:
+        """The type of the credential.
+
+        Returns:
+             the type of the credential.
+        """
+        return self.GCS_TOKEN_CREDENTIAL_TYPE
+
+    def expire_time_in_ms(self) -> int:
+        """Returns the expiration time of the credential in milliseconds since
+        the epoch, 0 means it will never expire.
+
+        Returns:
+             The expiration time of the credential.
+        """
+        return self._expire_time_in_ms
+
+    def credential_info(self) -> Dict[str, str]:
+        """The credential information.
+
+        Returns:
+             The credential information.
+        """
+        return {self._GCS_TOKEN_NAME: self._token}
+
+    def token(self) -> str:
+        """The GCS token.
+
+        Returns:
+            The GCS token.
+        """
+        return self._token
diff --git 
a/clients/client-python/gravitino/api/credential/oss_token_credential.py 
b/clients/client-python/gravitino/api/credential/oss_token_credential.py
new file mode 100644
index 000000000..70dad14a1
--- /dev/null
+++ b/clients/client-python/gravitino/api/credential/oss_token_credential.py
@@ -0,0 +1,105 @@
+# 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 abc import ABC
+from typing import Dict
+
+from gravitino.api.credential.credential import Credential
+from gravitino.utils.precondition import Precondition
+
+
+class OSSTokenCredential(Credential, ABC):
+    """Represents OSS token credential."""
+
+    OSS_TOKEN_CREDENTIAL_TYPE: str = "oss-token"
+    _GRAVITINO_OSS_SESSION_ACCESS_KEY_ID: str = "oss-access-key-id"
+    _GRAVITINO_OSS_SESSION_SECRET_ACCESS_KEY: str = "oss-secret-access-key"
+    _GRAVITINO_OSS_TOKEN: str = "oss-security-token"
+
+    def __init__(self, credential_info: Dict[str, str], expire_time_in_ms: 
int):
+        self._access_key_id = 
credential_info[self._GRAVITINO_OSS_SESSION_ACCESS_KEY_ID]
+        self._secret_access_key = credential_info[
+            self._GRAVITINO_OSS_SESSION_SECRET_ACCESS_KEY
+        ]
+        self._security_token = credential_info[self._GRAVITINO_OSS_TOKEN]
+        self._expire_time_in_ms = expire_time_in_ms
+        Precondition.check_string_not_empty(
+            self._access_key_id, "The OSS access key ID should not be empty"
+        )
+        Precondition.check_string_not_empty(
+            self._secret_access_key, "The OSS secret access key should not be 
empty"
+        )
+        Precondition.check_string_not_empty(
+            self._security_token, "The OSS security token should not be empty"
+        )
+        Precondition.check_argument(
+            self._expire_time_in_ms > 0,
+            "The expiration time of OSS token credential should be greater 
than 0",
+        )
+
+    def credential_type(self) -> str:
+        """The type of the credential.
+
+        Returns:
+             the type of the credential.
+        """
+        return self.OSS_TOKEN_CREDENTIAL_TYPE
+
+    def expire_time_in_ms(self) -> int:
+        """Returns the expiration time of the credential in milliseconds since
+        the epoch, 0 means it will never expire.
+
+        Returns:
+             The expiration time of the credential.
+        """
+        return self._expire_time_in_ms
+
+    def credential_info(self) -> Dict[str, str]:
+        """The credential information.
+
+        Returns:
+             The credential information.
+        """
+        return {
+            self._GRAVITINO_OSS_TOKEN: self._security_token,
+            self._GRAVITINO_OSS_SESSION_ACCESS_KEY_ID: self._access_key_id,
+            self._GRAVITINO_OSS_SESSION_SECRET_ACCESS_KEY: 
self._secret_access_key,
+        }
+
+    def access_key_id(self) -> str:
+        """The OSS access key id.
+
+        Returns:
+            The OSS access key id.
+        """
+        return self._access_key_id
+
+    def secret_access_key(self) -> str:
+        """The OSS secret access key.
+
+        Returns:
+            The OSS secret access key.
+        """
+        return self._secret_access_key
+
+    def security_token(self) -> str:
+        """The OSS security token.
+
+        Returns:
+            The OSS security token.
+        """
+        return self._security_token
diff --git 
a/clients/client-python/gravitino/api/credential/s3_secret_key_credential.py 
b/clients/client-python/gravitino/api/credential/s3_secret_key_credential.py
new file mode 100644
index 000000000..735c41e2e
--- /dev/null
+++ b/clients/client-python/gravitino/api/credential/s3_secret_key_credential.py
@@ -0,0 +1,91 @@
+# 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 abc import ABC
+from typing import Dict
+
+from gravitino.api.credential.credential import Credential
+from gravitino.utils.precondition import Precondition
+
+
+class S3SecretKeyCredential(Credential, ABC):
+    """Represents S3 secret key credential."""
+
+    S3_SECRET_KEY_CREDENTIAL_TYPE: str = "s3-secret-key"
+    _GRAVITINO_S3_STATIC_ACCESS_KEY_ID: str = "s3-access-key-id"
+    _GRAVITINO_S3_STATIC_SECRET_ACCESS_KEY: str = "s3-secret-access-key"
+
+    def __init__(self, credential_info: Dict[str, str], expire_time: int):
+        self._access_key_id = 
credential_info[self._GRAVITINO_S3_STATIC_ACCESS_KEY_ID]
+        self._secret_access_key = credential_info[
+            self._GRAVITINO_S3_STATIC_SECRET_ACCESS_KEY
+        ]
+        Precondition.check_string_not_empty(
+            self._access_key_id, "S3 access key id should not be empty"
+        )
+        Precondition.check_string_not_empty(
+            self._secret_access_key, "S3 secret access key should not be empty"
+        )
+        Precondition.check_argument(
+            expire_time == 0,
+            "The expiration time of S3 secret key credential should be 0",
+        )
+
+    def credential_type(self) -> str:
+        """Returns the expiration time of the credential in milliseconds since
+        the epoch, 0 means it will never expire.
+
+        Returns:
+             The expiration time of the credential.
+        """
+        return self.S3_SECRET_KEY_CREDENTIAL_TYPE
+
+    def expire_time_in_ms(self) -> int:
+        """Returns the expiration time of the credential in milliseconds since
+        the epoch, 0 means it will never expire.
+
+        Returns:
+             The expiration time of the credential.
+        """
+        return 0
+
+    def credential_info(self) -> Dict[str, str]:
+        """The credential information.
+
+        Returns:
+             The credential information.
+        """
+        return {
+            self._GRAVITINO_S3_STATIC_SECRET_ACCESS_KEY: 
self._secret_access_key,
+            self._GRAVITINO_S3_STATIC_ACCESS_KEY_ID: self._access_key_id,
+        }
+
+    def access_key_id(self) -> str:
+        """The S3 access key id.
+
+        Returns:
+            The S3 access key id.
+        """
+        return self._access_key_id
+
+    def secret_access_key(self) -> str:
+        """The S3 secret access key.
+
+        Returns:
+            The S3 secret access key.
+        """
+        return self._secret_access_key
diff --git 
a/clients/client-python/gravitino/api/credential/s3_token_credential.py 
b/clients/client-python/gravitino/api/credential/s3_token_credential.py
new file mode 100644
index 000000000..c72d9f02a
--- /dev/null
+++ b/clients/client-python/gravitino/api/credential/s3_token_credential.py
@@ -0,0 +1,110 @@
+# 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 abc import ABC
+from typing import Dict
+
+from gravitino.api.credential.credential import Credential
+from gravitino.utils.precondition import Precondition
+
+
+class S3TokenCredential(Credential, ABC):
+    """Represents the S3 token credential."""
+
+    S3_TOKEN_CREDENTIAL_TYPE: str = "s3-token"
+    _GRAVITINO_S3_SESSION_ACCESS_KEY_ID: str = "s3-access-key-id"
+    _GRAVITINO_S3_SESSION_SECRET_ACCESS_KEY: str = "s3-secret-access-key"
+    _GRAVITINO_S3_TOKEN: str = "s3-session-token"
+
+    _expire_time_in_ms: int = 0
+    _access_key_id: str = None
+    _secret_access_key: str = None
+    _session_token: str = None
+
+    def __init__(self, credential_info: Dict[str, str], expire_time_in_ms: 
int):
+        self._access_key_id = 
credential_info[self._GRAVITINO_S3_SESSION_ACCESS_KEY_ID]
+        self._secret_access_key = credential_info[
+            self._GRAVITINO_S3_SESSION_SECRET_ACCESS_KEY
+        ]
+        self._session_token = credential_info[self._GRAVITINO_S3_TOKEN]
+        self._expire_time_in_ms = expire_time_in_ms
+        Precondition.check_string_not_empty(
+            self._access_key_id, "The S3 access key ID should not be empty"
+        )
+        Precondition.check_string_not_empty(
+            self._secret_access_key, "The S3 secret access key should not be 
empty"
+        )
+        Precondition.check_string_not_empty(
+            self._session_token, "The S3 session token should not be empty"
+        )
+        Precondition.check_argument(
+            self._expire_time_in_ms > 0,
+            "The expiration time of S3 token credential should be greater than 
0",
+        )
+
+    def credential_type(self) -> str:
+        """The type of the credential.
+
+        Returns:
+             the type of the credential.
+        """
+        return self.S3_TOKEN_CREDENTIAL_TYPE
+
+    def expire_time_in_ms(self) -> int:
+        """Returns the expiration time of the credential in milliseconds since
+        the epoch, 0 means it will never expire.
+
+        Returns:
+             The expiration time of the credential.
+        """
+        return self._expire_time_in_ms
+
+    def credential_info(self) -> Dict[str, str]:
+        """The credential information.
+
+        Returns:
+             The credential information.
+        """
+        return {
+            self._GRAVITINO_S3_TOKEN: self._session_token,
+            self._GRAVITINO_S3_SESSION_ACCESS_KEY_ID: self._access_key_id,
+            self._GRAVITINO_S3_SESSION_SECRET_ACCESS_KEY: 
self._secret_access_key,
+        }
+
+    def access_key_id(self) -> str:
+        """The S3 access key id.
+
+        Returns:
+            The S3 access key id.
+        """
+        return self._access_key_id
+
+    def secret_access_key(self) -> str:
+        """The S3 secret access key.
+
+        Returns:
+            The S3 secret access key.
+        """
+        return self._secret_access_key
+
+    def session_token(self) -> str:
+        """The S3 session token.
+
+        Returns:
+            The S3 session token.
+        """
+        return self._session_token
diff --git 
a/clients/client-python/gravitino/api/credential/supports_credentials.py 
b/clients/client-python/gravitino/api/credential/supports_credentials.py
new file mode 100644
index 000000000..cf4856667
--- /dev/null
+++ b/clients/client-python/gravitino/api/credential/supports_credentials.py
@@ -0,0 +1,73 @@
+# 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 abc import ABC, abstractmethod
+from typing import List
+from gravitino.api.credential.credential import Credential
+from gravitino.exceptions.base import (
+    NoSuchCredentialException,
+    IllegalStateException,
+)
+
+
+class SupportsCredentials(ABC):
+    """Represents interface to get credentials."""
+
+    @abstractmethod
+    def get_credentials(self) -> List[Credential]:
+        """Retrieves a List of Credential objects.
+
+        Returns:
+            A List of Credential objects. In most cases the array only contains
+        one credential. If the object like Fileset contains multiple locations
+        for different storages like HDFS, S3, the array will contain multiple
+        credentials. The array could be empty if you request a credential for
+        a catalog but the credential provider couldn't generate the credential
+        for the catalog, like S3 token credential provider only generate
+        credential for the specific object like Fileset,Table. There will be at
+        most one credential for one credential type.
+        """
+        pass
+
+    def get_credential(self, credential_type: str) -> Credential:
+        """Retrieves Credential object based on the specified credential type.
+
+        Args:
+            credential_type: The type of the credential like s3-token,
+            s3-secret-key which are defined in the specific credentials.
+        Returns:
+            An Credential object with the specified credential type.
+        Raises:
+            NoSuchCredentialException If the specific credential cannot be 
found.
+            IllegalStateException if multiple credential can be found.
+        """
+
+        credentials = self.get_credentials()
+        matched_credentials = [
+            credential
+            for credential in credentials
+            if credential.credential_type == credential_type
+        ]
+        if len(matched_credentials) == 0:
+            raise NoSuchCredentialException(
+                f"No credential found for the credential type: 
{credential_type}"
+            )
+        if len(matched_credentials) > 1:
+            raise IllegalStateException(
+                f"Multiple credentials found for the credential type: 
{credential_type}"
+            )
+        return matched_credentials[0]
diff --git a/clients/client-python/gravitino/api/metadata_object.py 
b/clients/client-python/gravitino/api/metadata_object.py
new file mode 100644
index 000000000..f0429893e
--- /dev/null
+++ b/clients/client-python/gravitino/api/metadata_object.py
@@ -0,0 +1,56 @@
+# 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 abc import ABC, abstractmethod
+from enum import Enum
+
+
+class MetadataObject(ABC):
+    """The MetadataObject is the basic unit of the Gravitino system. It
+    represents the metadata object in the Apache Gravitino system. The object
+    can be a metalake, catalog, schema, table, topic, etc.
+    """
+
+    class Type(Enum):
+        """The type of object in the Gravitino system. Every type will map one
+        kind of the entity of the underlying system."""
+
+        CATALOG = "catalog"
+        """"Metadata Type for catalog."""
+
+        FILESET = "fileset"
+        """Metadata Type for Fileset System (including HDFS, S3, etc.), like 
path/to/file"""
+
+    @abstractmethod
+    def type(self) -> Type:
+        """
+        The type of the object.
+
+        Returns:
+            The type of the object.
+        """
+        pass
+
+    @abstractmethod
+    def name(self) -> str:
+        """
+        The name of the object.
+
+        Returns:
+            The name of the object.
+        """
+        pass
diff --git a/clients/client-python/gravitino/catalog/base_schema_catalog.py 
b/clients/client-python/gravitino/catalog/base_schema_catalog.py
index a04e7698d..6e5d212a2 100644
--- a/clients/client-python/gravitino/catalog/base_schema_catalog.py
+++ b/clients/client-python/gravitino/catalog/base_schema_catalog.py
@@ -19,9 +19,14 @@ import logging
 from typing import Dict, List
 
 from gravitino.api.catalog import Catalog
+from gravitino.api.metadata_object import MetadataObject
 from gravitino.api.schema import Schema
 from gravitino.api.schema_change import SchemaChange
 from gravitino.api.supports_schemas import SupportsSchemas
+from gravitino.client.metadata_object_credential_operations import (
+    MetadataObjectCredentialOperations,
+)
+from gravitino.client.metadata_object_impl import MetadataObjectImpl
 from gravitino.dto.audit_dto import AuditDTO
 from gravitino.dto.catalog_dto import CatalogDTO
 from gravitino.dto.requests.schema_create_request import SchemaCreateRequest
@@ -52,6 +57,9 @@ class BaseSchemaCatalog(CatalogDTO, SupportsSchemas):
     # The namespace of current catalog, which is the metalake name.
     _catalog_namespace: Namespace
 
+    # The metadata object credential operations
+    _object_credential_operations: MetadataObjectCredentialOperations
+
     def __init__(
         self,
         catalog_namespace: Namespace,
@@ -74,6 +82,11 @@ class BaseSchemaCatalog(CatalogDTO, SupportsSchemas):
         self.rest_client = rest_client
         self._catalog_namespace = catalog_namespace
 
+        metadata_object = MetadataObjectImpl([name], 
MetadataObject.Type.CATALOG)
+        self._object_credential_operations = 
MetadataObjectCredentialOperations(
+            catalog_namespace.level(0), metadata_object, rest_client
+        )
+
         self.validate()
 
     def as_schemas(self):
diff --git a/clients/client-python/gravitino/catalog/fileset_catalog.py 
b/clients/client-python/gravitino/catalog/fileset_catalog.py
index ffa252e62..f7ad2aebd 100644
--- a/clients/client-python/gravitino/catalog/fileset_catalog.py
+++ b/clients/client-python/gravitino/catalog/fileset_catalog.py
@@ -19,10 +19,13 @@ import logging
 from typing import List, Dict
 
 from gravitino.api.catalog import Catalog
+from gravitino.api.credential.supports_credentials import SupportsCredentials
+from gravitino.api.credential.credential import Credential
 from gravitino.api.fileset import Fileset
 from gravitino.api.fileset_change import FilesetChange
 from gravitino.audit.caller_context import CallerContextHolder, CallerContext
 from gravitino.catalog.base_schema_catalog import BaseSchemaCatalog
+from gravitino.client.generic_fileset import GenericFileset
 from gravitino.dto.audit_dto import AuditDTO
 from gravitino.dto.requests.fileset_create_request import FilesetCreateRequest
 from gravitino.dto.requests.fileset_update_request import FilesetUpdateRequest
@@ -40,7 +43,7 @@ from gravitino.exceptions.handlers.fileset_error_handler 
import FILESET_ERROR_HA
 logger = logging.getLogger(__name__)
 
 
-class FilesetCatalog(BaseSchemaCatalog):
+class FilesetCatalog(BaseSchemaCatalog, SupportsCredentials):
     """
     Fileset catalog is a catalog implementation that supports fileset like 
metadata operations, for
     example, schemas and filesets list, creation, update and deletion. A 
Fileset catalog is under the metalake.
@@ -124,7 +127,7 @@ class FilesetCatalog(BaseSchemaCatalog):
         fileset_resp = FilesetResponse.from_json(resp.body, infer_missing=True)
         fileset_resp.validate()
 
-        return fileset_resp.fileset()
+        return GenericFileset(fileset_resp.fileset(), self.rest_client, 
full_namespace)
 
     def create_fileset(
         self,
@@ -321,3 +324,9 @@ class FilesetCatalog(BaseSchemaCatalog):
         if isinstance(change, FilesetChange.RemoveComment):
             return FilesetUpdateRequest.UpdateFilesetCommentRequest(None)
         raise ValueError(f"Unknown change type: {type(change).__name__}")
+
+    def support_credentials(self) -> SupportsCredentials:
+        return self
+
+    def get_credentials(self) -> List[Credential]:
+        return self._object_credential_operations.get_credentials()
diff --git a/clients/client-python/gravitino/client/generic_fileset.py 
b/clients/client-python/gravitino/client/generic_fileset.py
new file mode 100644
index 000000000..3b7aa5326
--- /dev/null
+++ b/clients/client-python/gravitino/client/generic_fileset.py
@@ -0,0 +1,75 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from typing import Optional, Dict, List
+
+from gravitino.api.fileset import Fileset
+from gravitino.api.metadata_object import MetadataObject
+from gravitino.api.credential.supports_credentials import SupportsCredentials
+from gravitino.api.credential.credential import Credential
+from gravitino.client.metadata_object_credential_operations import (
+    MetadataObjectCredentialOperations,
+)
+from gravitino.client.metadata_object_impl import MetadataObjectImpl
+from gravitino.dto.audit_dto import AuditDTO
+from gravitino.dto.fileset_dto import FilesetDTO
+from gravitino.namespace import Namespace
+from gravitino.utils import HTTPClient
+
+
+class GenericFileset(Fileset, SupportsCredentials):
+
+    _fileset: FilesetDTO
+    """The fileset data transfer object"""
+
+    _object_credential_operations: MetadataObjectCredentialOperations
+    """The metadata object credential operations"""
+
+    def __init__(
+        self, fileset: FilesetDTO, rest_client: HTTPClient, full_namespace: 
Namespace
+    ):
+        self._fileset = fileset
+        metadata_object = MetadataObjectImpl(
+            [full_namespace.level(1), full_namespace.level(2), fileset.name()],
+            MetadataObject.Type.FILESET,
+        )
+        self._object_credential_operations = 
MetadataObjectCredentialOperations(
+            full_namespace.level(0), metadata_object, rest_client
+        )
+
+    def name(self) -> str:
+        return self._fileset.name()
+
+    def type(self) -> Fileset.Type:
+        return self._fileset.type()
+
+    def storage_location(self) -> str:
+        return self._fileset.storage_location()
+
+    def comment(self) -> Optional[str]:
+        return self._fileset.comment()
+
+    def properties(self) -> Dict[str, str]:
+        return self._fileset.properties()
+
+    def audit_info(self) -> AuditDTO:
+        return self._fileset.audit_info()
+
+    def support_credentials(self) -> SupportsCredentials:
+        return self
+
+    def get_credentials(self) -> List[Credential]:
+        return self._object_credential_operations.get_credentials()
diff --git 
a/clients/client-python/gravitino/client/metadata_object_credential_operations.py
 
b/clients/client-python/gravitino/client/metadata_object_credential_operations.py
new file mode 100644
index 000000000..93d538cfa
--- /dev/null
+++ 
b/clients/client-python/gravitino/client/metadata_object_credential_operations.py
@@ -0,0 +1,74 @@
+# 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 logging
+from typing import List
+from gravitino.api.credential.supports_credentials import SupportsCredentials
+from gravitino.api.credential.credential import Credential
+from gravitino.api.metadata_object import MetadataObject
+from gravitino.dto.credential_dto import CredentialDTO
+from gravitino.dto.responses.credential_response import CredentialResponse
+from gravitino.exceptions.handlers.credential_error_handler import (
+    CREDENTIAL_ERROR_HANDLER,
+)
+from gravitino.utils import HTTPClient
+from gravitino.utils.credential_factory import CredentialFactory
+
+logger = logging.getLogger(__name__)
+
+
+class MetadataObjectCredentialOperations(SupportsCredentials):
+    _rest_client: HTTPClient
+    """The REST client to communicate with the REST server"""
+
+    _request_path: str
+    """The REST API path to do credential operations"""
+
+    def __init__(
+        self,
+        metalake_name: str,
+        metadata_object: MetadataObject,
+        rest_client: HTTPClient,
+    ):
+        self._rest_client = rest_client
+        metadata_object_type = metadata_object.type().value
+        metadata_object_name = metadata_object.name()
+        self._request_path = (
+            f"api/metalakes/{metalake_name}objects/{metadata_object_type}/"
+            f"{metadata_object_name}/credentials"
+        )
+
+    def get_credentials(self) -> List[Credential]:
+        resp = self._rest_client.get(
+            self._request_path,
+            error_handler=CREDENTIAL_ERROR_HANDLER,
+        )
+
+        credential_resp = CredentialResponse.from_json(resp.body, 
infer_missing=True)
+        credential_resp.validate()
+        credential_dtos = credential_resp.credentials()
+        return self.to_credentials(credential_dtos)
+
+    def to_credentials(self, credentials: List[CredentialDTO]) -> 
List[Credential]:
+        return [self.to_credential(credential) for credential in credentials]
+
+    def to_credential(self, credential_dto: CredentialDTO) -> Credential:
+        return CredentialFactory.create(
+            credential_dto.credential_type(),
+            credential_dto.credential_info(),
+            credential_dto.expire_time_in_ms(),
+        )
diff --git a/clients/client-python/gravitino/client/metadata_object_impl.py 
b/clients/client-python/gravitino/client/metadata_object_impl.py
new file mode 100644
index 000000000..af16b71c4
--- /dev/null
+++ b/clients/client-python/gravitino/client/metadata_object_impl.py
@@ -0,0 +1,35 @@
+# 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, ClassVar
+
+from gravitino.api.metadata_object import MetadataObject
+
+
+class MetadataObjectImpl(MetadataObject):
+
+    _DOT: ClassVar[str] = "."
+
+    def __init__(self, names: List[str], metadata_object_type: 
MetadataObject.Type):
+        self._name = self._DOT.join(names)
+        self._metadata_object_type = metadata_object_type
+
+    def type(self) -> MetadataObject.Type:
+        return self._metadata_object_type
+
+    def name(self) -> str:
+        return self._name
diff --git a/clients/client-python/gravitino/dto/credential_dto.py 
b/clients/client-python/gravitino/dto/credential_dto.py
new file mode 100644
index 000000000..518c0460c
--- /dev/null
+++ b/clients/client-python/gravitino/dto/credential_dto.py
@@ -0,0 +1,42 @@
+# 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 dataclasses import dataclass, field
+from typing import Dict
+
+from dataclasses_json import config, DataClassJsonMixin
+
+from gravitino.api.credential.credential import Credential
+
+
+@dataclass
+class CredentialDTO(Credential, DataClassJsonMixin):
+    """Represents a Credential DTO (Data Transfer Object)."""
+
+    _credential_type: str = field(metadata=config(field_name="credentialType"))
+    _expire_time_in_ms: int = 
field(metadata=config(field_name="expireTimeInMs"))
+    _credential_info: Dict[str, str] = field(
+        metadata=config(field_name="credentialInfo")
+    )
+
+    def credential_type(self) -> str:
+        return self._credential_type
+
+    def expire_time_in_ms(self) -> int:
+        return self._expire_time_in_ms
+
+    def credential_info(self) -> Dict[str, str]:
+        return self._credential_info
diff --git 
a/clients/client-python/gravitino/dto/responses/credential_response.py 
b/clients/client-python/gravitino/dto/responses/credential_response.py
new file mode 100644
index 000000000..1883c7580
--- /dev/null
+++ b/clients/client-python/gravitino/dto/responses/credential_response.py
@@ -0,0 +1,44 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from typing import List
+from dataclasses import dataclass, field
+from dataclasses_json import config
+
+from gravitino.dto.credential_dto import CredentialDTO
+from gravitino.dto.responses.base_response import BaseResponse
+from gravitino.exceptions.base import IllegalArgumentException
+
+
+@dataclass
+class CredentialResponse(BaseResponse):
+    """Response for credential response."""
+
+    _credentials: List[CredentialDTO] = 
field(metadata=config(field_name="credentials"))
+
+    def credentials(self) -> List[CredentialDTO]:
+        return self._credentials
+
+    def validate(self):
+        """Validates the response data.
+
+        Raises:
+            IllegalArgumentException if credentials are None.
+        """
+        if self._credentials is None:
+            raise IllegalArgumentException("credentials should be set")
+        super().validate()
diff --git a/clients/client-python/gravitino/exceptions/base.py 
b/clients/client-python/gravitino/exceptions/base.py
index cd71de236..9091116dd 100644
--- a/clients/client-python/gravitino/exceptions/base.py
+++ b/clients/client-python/gravitino/exceptions/base.py
@@ -61,6 +61,10 @@ class NoSuchFilesetException(NotFoundException):
     """Exception thrown when a file with specified name is not existed."""
 
 
+class NoSuchCredentialException(NotFoundException):
+    """Exception thrown when a credential with specified credential type is 
not existed."""
+
+
 class NoSuchMetalakeException(NotFoundException):
     """An exception thrown when a metalake is not found."""
 
@@ -135,3 +139,7 @@ class UnauthorizedException(GravitinoRuntimeException):
 
 class BadRequestException(GravitinoRuntimeException):
     """An exception thrown when the request is invalid."""
+
+
+class IllegalStateException(GravitinoRuntimeException):
+    """An exception thrown when the state is invalid."""
diff --git 
a/clients/client-python/gravitino/exceptions/handlers/credential_error_handler.py
 
b/clients/client-python/gravitino/exceptions/handlers/credential_error_handler.py
new file mode 100644
index 000000000..542fb27cf
--- /dev/null
+++ 
b/clients/client-python/gravitino/exceptions/handlers/credential_error_handler.py
@@ -0,0 +1,45 @@
+# 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 (
+    CatalogNotInUseException,
+    NoSuchCredentialException,
+)
+
+
+class CredentialErrorHandler(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 == NoSuchCredentialException.__name__:
+                raise NoSuchCredentialException(error_message)
+
+        if code == ErrorConstants.NOT_IN_USE_CODE:
+            raise CatalogNotInUseException(error_message)
+
+        super().handle(error_response)
+
+
+CREDENTIAL_ERROR_HANDLER = CredentialErrorHandler()
diff --git a/clients/client-python/gravitino/utils/credential_factory.py 
b/clients/client-python/gravitino/utils/credential_factory.py
new file mode 100644
index 000000000..2dfbf619b
--- /dev/null
+++ b/clients/client-python/gravitino/utils/credential_factory.py
@@ -0,0 +1,39 @@
+# 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 Dict
+from gravitino.api.credential.credential import Credential
+from gravitino.api.credential.gcs_token_credential import GCSTokenCredential
+from gravitino.api.credential.oss_token_credential import OSSTokenCredential
+from gravitino.api.credential.s3_secret_key_credential import 
S3SecretKeyCredential
+from gravitino.api.credential.s3_token_credential import S3TokenCredential
+
+
+class CredentialFactory:
+    @staticmethod
+    def create(
+        credential_type: str, credential_info: Dict[str, str], 
expire_time_in_ms: int
+    ) -> Credential:
+        if credential_type == S3TokenCredential.S3_TOKEN_CREDENTIAL_TYPE:
+            return S3TokenCredential(credential_info, expire_time_in_ms)
+        if credential_type == 
S3SecretKeyCredential.S3_SECRET_KEY_CREDENTIAL_TYPE:
+            return S3SecretKeyCredential(credential_info, expire_time_in_ms)
+        if credential_type == GCSTokenCredential.GCS_TOKEN_CREDENTIAL_TYPE:
+            return GCSTokenCredential(credential_info, expire_time_in_ms)
+        if credential_type == OSSTokenCredential.OSS_TOKEN_CREDENTIAL_TYPE:
+            return OSSTokenCredential(credential_info, expire_time_in_ms)
+        raise NotImplementedError(f"Credential type {credential_type} is not 
supported")
diff --git a/clients/client-python/gravitino/utils/precondition.py 
b/clients/client-python/gravitino/utils/precondition.py
new file mode 100644
index 000000000..da3490555
--- /dev/null
+++ b/clients/client-python/gravitino/utils/precondition.py
@@ -0,0 +1,48 @@
+# 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.base import IllegalArgumentException
+
+
+class Precondition:
+    @staticmethod
+    def check_argument(expression_result: bool, error_message: str):
+        """Ensures the truth of an expression involving one or more parameters
+        to the calling method.
+
+        Args:
+          expression_result: A boolean expression.
+          error_message: The error message to use if the check fails.
+        Raises:
+          IllegalArgumentException – if expression is false
+        """
+        if not expression_result:
+            raise IllegalArgumentException(error_message)
+
+    @staticmethod
+    def check_string_not_empty(check_string: str, error_message: str):
+        """Ensures the string is not empty.
+
+        Args:
+          check_string: The string to check.
+          error_message: The error message to use if the check fails.
+        Raises:
+          IllegalArgumentException – if the check fails.
+        """
+        Precondition.check_argument(
+            check_string is not None and check_string.strip() != "", 
error_message
+        )
diff --git a/clients/client-python/tests/unittests/mock_base.py 
b/clients/client-python/tests/unittests/mock_base.py
index 9fd60a702..16a3d03c3 100644
--- a/clients/client-python/tests/unittests/mock_base.py
+++ b/clients/client-python/tests/unittests/mock_base.py
@@ -93,6 +93,10 @@ def mock_data(cls):
         "gravitino.client.gravitino_metalake.GravitinoMetalake.load_catalog",
         return_value=mock_load_fileset_catalog(),
     )
+    @patch(
+        "gravitino.catalog.fileset_catalog.FilesetCatalog.load_fileset",
+        return_value=mock_load_fileset("fileset", ""),
+    )
     @patch(
         
"gravitino.client.gravitino_client_base.GravitinoClientBase.check_version",
         return_value=True,
diff --git a/clients/client-python/tests/unittests/test_credential_api.py 
b/clients/client-python/tests/unittests/test_credential_api.py
new file mode 100644
index 000000000..2811a226f
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_credential_api.py
@@ -0,0 +1,105 @@
+# 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
+import json
+import unittest
+from http.client import HTTPResponse
+from unittest.mock import patch, Mock
+
+from gravitino import GravitinoClient, NameIdentifier
+from gravitino.api.credential.credential import Credential
+from gravitino.api.credential.s3_token_credential import S3TokenCredential
+from gravitino.client.generic_fileset import GenericFileset
+from gravitino.namespace import Namespace
+from gravitino.utils import Response, HTTPClient
+from tests.unittests import mock_base
+
+
+@mock_base.mock_data
+class TestCredentialApi(unittest.TestCase):
+
+    def test_get_credentials(self, *mock_method):
+        json_str = self._get_s3_token_str()
+        mock_resp = self._get_mock_http_resp(json_str)
+
+        metalake_name: str = "metalake_demo"
+        catalog_name: str = "fileset_catalog"
+        gravitino_client = GravitinoClient(
+            uri="http://localhost:8090";, metalake_name=metalake_name
+        )
+        catalog = gravitino_client.load_catalog(catalog_name)
+
+        with patch(
+            "gravitino.utils.http_client.HTTPClient.get",
+            return_value=mock_resp,
+        ):
+            credentials = (
+                
catalog.as_fileset_catalog().support_credentials().get_credentials()
+            )
+            self._check_credential(credentials)
+
+        fileset_dto = catalog.as_fileset_catalog().load_fileset(
+            NameIdentifier.of("schema", "fileset")
+        )
+        fileset = GenericFileset(
+            fileset_dto,
+            HTTPClient("http://localhost:8090";),
+            Namespace.of(metalake_name, catalog_name, "schema"),
+        )
+        with patch(
+            "gravitino.utils.http_client.HTTPClient.get",
+            return_value=mock_resp,
+        ):
+            credentials = fileset.support_credentials().get_credentials()
+            self._check_credential(credentials)
+
+    def _get_mock_http_resp(self, json_str: str):
+        mock_http_resp = Mock(HTTPResponse)
+        mock_http_resp.getcode.return_value = 200
+        mock_http_resp.read.return_value = json_str
+        mock_http_resp.info.return_value = None
+        mock_http_resp.url = None
+        mock_resp = Response(mock_http_resp)
+        return mock_resp
+
+    def _get_s3_token_str(self):
+        json_data = {
+            "code": 0,
+            "credentials": [
+                {
+                    "credentialType": "s3-token",
+                    "expireTimeInMs": 1000,
+                    "credentialInfo": {
+                        "s3-access-key-id": "access_id",
+                        "s3-secret-access-key": "secret_key",
+                        "s3-session-token": "token",
+                    },
+                }
+            ],
+        }
+        return json.dumps(json_data)
+
+    def _check_credential(self, credentials: List[Credential]):
+        self.assertEqual(1, len(credentials))
+        s3_credential: S3TokenCredential = credentials[0]
+        self.assertEqual(
+            S3TokenCredential.S3_TOKEN_CREDENTIAL_TYPE, 
s3_credential.credential_type()
+        )
+        self.assertEqual("access_id", s3_credential.access_key_id())
+        self.assertEqual("secret_key", s3_credential.secret_access_key())
+        self.assertEqual("token", s3_credential.session_token())
+        self.assertEqual(1000, s3_credential.expire_time_in_ms())
diff --git a/clients/client-python/tests/unittests/test_credential_factory.py 
b/clients/client-python/tests/unittests/test_credential_factory.py
new file mode 100644
index 000000000..0a7e78251
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_credential_factory.py
@@ -0,0 +1,101 @@
+# 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.
+
+# pylint: disable=protected-access,too-many-lines,too-many-locals
+
+import unittest
+
+from gravitino.api.credential.gcs_token_credential import GCSTokenCredential
+from gravitino.api.credential.oss_token_credential import OSSTokenCredential
+from gravitino.api.credential.s3_secret_key_credential import 
S3SecretKeyCredential
+from gravitino.api.credential.s3_token_credential import S3TokenCredential
+from gravitino.utils.credential_factory import CredentialFactory
+
+
+class TestCredentialFactory(unittest.TestCase):
+
+    def test_s3_token_credential(self):
+        s3_credential_info = {
+            S3TokenCredential._GRAVITINO_S3_SESSION_ACCESS_KEY_ID: 
"access_key",
+            S3TokenCredential._GRAVITINO_S3_SESSION_SECRET_ACCESS_KEY: 
"secret_key",
+            S3TokenCredential._GRAVITINO_S3_TOKEN: "session_token",
+        }
+        s3_credential = S3TokenCredential(s3_credential_info, 1000)
+        credential_info = s3_credential.credential_info()
+        expire_time = s3_credential.expire_time_in_ms()
+
+        check_credential = CredentialFactory.create(
+            s3_credential.S3_TOKEN_CREDENTIAL_TYPE, credential_info, 
expire_time
+        )
+        self.assertEqual("access_key", check_credential.access_key_id())
+        self.assertEqual("secret_key", check_credential.secret_access_key())
+        self.assertEqual("session_token", check_credential.session_token())
+        self.assertEqual(1000, check_credential.expire_time_in_ms())
+
+    def test_s3_secret_key_credential(self):
+        s3_credential_info = {
+            S3SecretKeyCredential._GRAVITINO_S3_STATIC_ACCESS_KEY_ID: 
"access_key",
+            S3SecretKeyCredential._GRAVITINO_S3_STATIC_SECRET_ACCESS_KEY: 
"secret_key",
+        }
+        s3_credential = S3SecretKeyCredential(s3_credential_info, 0)
+        credential_info = s3_credential.credential_info()
+        expire_time = s3_credential.expire_time_in_ms()
+
+        check_credential = CredentialFactory.create(
+            s3_credential.S3_SECRET_KEY_CREDENTIAL_TYPE, credential_info, 
expire_time
+        )
+        self.assertEqual("access_key", check_credential.access_key_id())
+        self.assertEqual("secret_key", check_credential.secret_access_key())
+        self.assertEqual(0, check_credential.expire_time_in_ms())
+
+    def test_gcs_token_credential(self):
+        credential_info = {GCSTokenCredential._GCS_TOKEN_NAME: "token"}
+        credential = GCSTokenCredential(credential_info, 1000)
+        credential_info = credential.credential_info()
+        expire_time = credential.expire_time_in_ms()
+
+        check_credential = CredentialFactory.create(
+            credential.credential_type(), credential_info, expire_time
+        )
+        self.assertEqual(
+            GCSTokenCredential.GCS_TOKEN_CREDENTIAL_TYPE,
+            check_credential.credential_type(),
+        )
+        self.assertEqual("token", check_credential.token())
+        self.assertEqual(1000, check_credential.expire_time_in_ms())
+
+    def test_oss_token_credential(self):
+        credential_info = {
+            OSSTokenCredential._GRAVITINO_OSS_TOKEN: "token",
+            OSSTokenCredential._GRAVITINO_OSS_SESSION_ACCESS_KEY_ID: 
"access_id",
+            OSSTokenCredential._GRAVITINO_OSS_SESSION_SECRET_ACCESS_KEY: 
"secret_key",
+        }
+        credential = OSSTokenCredential(credential_info, 1000)
+        credential_info = credential.credential_info()
+        expire_time = credential.expire_time_in_ms()
+
+        check_credential = CredentialFactory.create(
+            credential.credential_type(), credential_info, expire_time
+        )
+        self.assertEqual(
+            OSSTokenCredential.OSS_TOKEN_CREDENTIAL_TYPE,
+            check_credential.credential_type(),
+        )
+        self.assertEqual("token", check_credential.security_token())
+        self.assertEqual("access_id", check_credential.access_key_id())
+        self.assertEqual("secret_key", check_credential.secret_access_key())
+        self.assertEqual(1000, check_credential.expire_time_in_ms())
diff --git a/clients/client-python/tests/unittests/test_error_handler.py 
b/clients/client-python/tests/unittests/test_error_handler.py
index 4b9cbf1ca..a402ae111 100644
--- a/clients/client-python/tests/unittests/test_error_handler.py
+++ b/clients/client-python/tests/unittests/test_error_handler.py
@@ -35,6 +35,10 @@ from gravitino.exceptions.base import (
     UnsupportedOperationException,
     ConnectionFailedException,
     CatalogAlreadyExistsException,
+    NoSuchCredentialException,
+)
+from gravitino.exceptions.handlers.credential_error_handler import (
+    CREDENTIAL_ERROR_HANDLER,
 )
 
 from gravitino.exceptions.handlers.rest_error_handler import REST_ERROR_HANDLER
@@ -127,6 +131,25 @@ class TestErrorHandler(unittest.TestCase):
                 ErrorResponse.generate_error_response(Exception, "mock error")
             )
 
+    def test_credential_error_handler(self):
+
+        with self.assertRaises(NoSuchCredentialException):
+            CREDENTIAL_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(
+                    NoSuchCredentialException, "mock error"
+                )
+            )
+
+        with self.assertRaises(InternalError):
+            CREDENTIAL_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(InternalError, "mock 
error")
+            )
+
+        with self.assertRaises(RESTException):
+            CREDENTIAL_ERROR_HANDLER.handle(
+                ErrorResponse.generate_error_response(Exception, "mock error")
+            )
+
     def test_metalake_error_handler(self):
 
         with self.assertRaises(NoSuchMetalakeException):
diff --git a/clients/client-python/tests/unittests/test_precondition.py 
b/clients/client-python/tests/unittests/test_precondition.py
new file mode 100644
index 000000000..78a246597
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_precondition.py
@@ -0,0 +1,46 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# pylint: disable=protected-access,too-many-lines,too-many-locals
+
+import unittest
+
+from gravitino.exceptions.base import IllegalArgumentException
+from gravitino.utils.precondition import Precondition
+
+
+class TestPrecondition(unittest.TestCase):
+
+    def test_check_argument(self):
+        with self.assertRaises(IllegalArgumentException):
+            Precondition.check_argument(False, "error")
+        try:
+            Precondition.check_argument(True, "error")
+        except IllegalArgumentException:
+            self.fail("should not raise IllegalArgumentException")
+
+    def test_check_string_empty(self):
+        with self.assertRaises(IllegalArgumentException):
+            Precondition.check_string_not_empty("", "empty")
+        with self.assertRaises(IllegalArgumentException):
+            Precondition.check_string_not_empty(" ", "empty")
+        with self.assertRaises(IllegalArgumentException):
+            Precondition.check_string_not_empty(None, "empty")
+        try:
+            Precondition.check_string_not_empty("test", "empty")
+        except IllegalArgumentException:
+            self.fail("should not raised an exception")
diff --git a/clients/client-python/tests/unittests/test_responses.py 
b/clients/client-python/tests/unittests/test_responses.py
index 19d403ad3..da8340bdf 100644
--- a/clients/client-python/tests/unittests/test_responses.py
+++ b/clients/client-python/tests/unittests/test_responses.py
@@ -17,6 +17,7 @@
 import json
 import unittest
 
+from gravitino.dto.responses.credential_response import CredentialResponse
 from gravitino.dto.responses.file_location_response import FileLocationResponse
 from gravitino.exceptions.base import IllegalArgumentException
 
@@ -39,3 +40,37 @@ class TestResponses(unittest.TestCase):
         )
         with self.assertRaises(IllegalArgumentException):
             file_location_resp.validate()
+
+    def test_credential_response(self):
+        json_data = {"code": 0, "credentials": []}
+        json_str = json.dumps(json_data)
+        credential_resp: CredentialResponse = 
CredentialResponse.from_json(json_str)
+        self.assertEqual(0, len(credential_resp.credentials()))
+        credential_resp.validate()
+
+        json_data = {
+            "code": 0,
+            "credentials": [
+                {
+                    "credentialType": "s3-token",
+                    "expireTimeInMs": 1000,
+                    "credentialInfo": {
+                        "s3-access-key-id": "access-id",
+                        "s3-secret-access-key": "secret-key",
+                        "s3-session-token": "token",
+                    },
+                }
+            ],
+        }
+        json_str = json.dumps(json_data)
+        credential_resp: CredentialResponse = 
CredentialResponse.from_json(json_str)
+        credential_resp.validate()
+        self.assertEqual(1, len(credential_resp.credentials()))
+        credential = credential_resp.credentials()[0]
+        self.assertEqual("s3-token", credential.credential_type())
+        self.assertEqual(1000, credential.expire_time_in_ms())
+        self.assertEqual("access-id", 
credential.credential_info()["s3-access-key-id"])
+        self.assertEqual(
+            "secret-key", credential.credential_info()["s3-secret-access-key"]
+        )
+        self.assertEqual("token", 
credential.credential_info()["s3-session-token"])

Reply via email to