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 <[email protected]>
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"])