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

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


The following commit(s) were added to refs/heads/branch-1.0 by this push:
     new 15aef291c6 [#8627] feat(client-python): add metadata objects (#8966)
15aef291c6 is described below

commit 15aef291c6f27eed4d8a4192e23c6618a1214e3a
Author: George T. C. Lai <[email protected]>
AuthorDate: Thu Oct 30 15:58:29 2025 +0800

    [#8627] feat(client-python): add metadata objects (#8966)
    
    <!--
    1. Title: [#<issue>] <type>(<scope>): <subject>
       Examples:
         - "[#123] feat(operator): support xxx"
         - "[#233] fix: check null before access result in xxx"
         - "[MINOR] refactor: fix typo in variable name"
         - "[MINOR] docs: fix typo in README"
         - "[#255] test: fix flaky test NameOfTheTest"
       Reference: https://www.conventionalcommits.org/en/v1.0.0/
    2. If the PR is unfinished, please mark this PR as draft.
    -->
    
    ### What changes were proposed in this pull request?
    
    This PR is aimed at implementing the following classes corresponding to
    the Java client.
    
    MetadataObjects.java
    
    A refactor of `MetadataObject` and of `MetadataObjectImpl` are included
    as well to conform with the up-to-date implementation in Java client.
    
    ### Why are the changes needed?
    
    We need to support python client for table operations.
    
    #8627
    
    ### Does this PR introduce _any_ user-facing change?
    
    No
    
    ### How was this patch tested?
    
    Unit tests
    
    ---------
    
    Signed-off-by: George T. C. Lai <[email protected]>
---
 .../client-python/gravitino/api/metadata_object.py |  76 +++++-
 .../gravitino/api/metadata_objects.py              | 221 ++++++++++++++++++
 .../gravitino/client/base_schema_catalog.py        |   8 +-
 .../gravitino/client/generic_fileset.py            |  10 +-
 .../tests/unittests/test_metadata_objects.py       | 260 +++++++++++++++++++++
 5 files changed, 556 insertions(+), 19 deletions(-)

diff --git a/clients/client-python/gravitino/api/metadata_object.py 
b/clients/client-python/gravitino/api/metadata_object.py
index f0429893ed..20a498c0ad 100644
--- a/clients/client-python/gravitino/api/metadata_object.py
+++ b/clients/client-python/gravitino/api/metadata_object.py
@@ -17,6 +17,7 @@
 
 from abc import ABC, abstractmethod
 from enum import Enum
+from typing import Optional
 
 
 class MetadataObject(ABC):
@@ -27,30 +28,85 @@ class MetadataObject(ABC):
 
     class Type(Enum):
         """The type of object in the Gravitino system. Every type will map one
-        kind of the entity of the underlying system."""
+        kind of the entity of the underlying system.
+        """
+
+        METALAKE = "metalake"
+        """A metalake is a concept of tenant. It means an organization. A 
metalake
+        contains many data sources.
+        """
 
         CATALOG = "catalog"
-        """"Metadata Type for catalog."""
+        """A catalog is a collection of metadata from a specific metadata 
source,
+        like Apache Hive catalog, Apache Iceberg catalog, JDBC catalog, etc.
+        """
+
+        SCHEMA = "schema"
+        """"A schema is a sub collection of the catalog. The schema can 
contain filesets,
+        tables, topics, etc.
+        """
 
         FILESET = "fileset"
-        """Metadata Type for Fileset System (including HDFS, S3, etc.), like 
path/to/file"""
+        """A fileset is mapped to a directory on a file system like HDFS, S3, 
ADLS,
+        GCS, etc.
+        """
+
+        TABLE = "table"
+        """A table is mapped the table of relational data sources like Apache 
Hive,
+        MySQL, etc.
+        """
+
+        TOPIC = "topic"
+        """A topic is mapped the topic of messaging data sources like Apache 
Kafka,
+        Apache Pulsar, etc.
+        """
+
+        COLUMN = "column"
+        """A column is a sub-collection of the table that represents a group of
+        same type data.
+        """
+
+        ROLE = "role"
+        """A role is an object contains specific securable objects with 
privileges."""
+
+        MODEL = "model"
+        """A model is mapped to the model artifact in ML."""
 
     @abstractmethod
-    def type(self) -> Type:
+    def parent(self) -> Optional[str]:
+        """The parent full name of the object.
+
+        If the object doesn't have parent, this method will return `None`.
+
+        Returns:
+            Optional[str]: The parent full name of the object.
         """
-        The type of the object.
+
+    @abstractmethod
+    def type(self) -> Type:
+        """The type of the object.
 
         Returns:
-            The type of the object.
+            Type: The type of the object.
         """
-        pass
 
     @abstractmethod
     def name(self) -> str:
+        """The name of the object.
+
+        Returns:
+            str: The name of the object.
         """
-        The name of the object.
+
+    def full_name(self) -> str:
+        """The full name of the object.
+
+        Full name will be separated by "." to represent a string identifier of 
the object,
+        like catalog, catalog.table, etc.
 
         Returns:
-            The name of the object.
+            str: The name of the object.
         """
-        pass
+
+        parent = self.parent()
+        return ".".join([parent, self.name()]) if parent else self.name()
diff --git a/clients/client-python/gravitino/api/metadata_objects.py 
b/clients/client-python/gravitino/api/metadata_objects.py
new file mode 100644
index 0000000000..8a29c612e1
--- /dev/null
+++ b/clients/client-python/gravitino/api/metadata_objects.py
@@ -0,0 +1,221 @@
+# 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 ClassVar, Optional, Union, overload
+
+from gravitino.api.metadata_object import MetadataObject
+from gravitino.exceptions.base import IllegalArgumentException
+from gravitino.utils.precondition import Precondition
+
+
+class MetadataObjects:
+    """The helper class for `MetadataObject`."""
+
+    METADATA_OBJECT_RESERVED_NAME: ClassVar[str] = "*"
+    """The reserved name for the metadata object."""
+
+    DOT_SPLITTER: ClassVar[str] = "."
+    DOT_JOINER: ClassVar[str] = "."
+
+    _NAMES_LEN_CONDS: ClassVar[dict[int, set[MetadataObject.Type]]] = {
+        1: {
+            MetadataObject.Type.METALAKE,
+            MetadataObject.Type.CATALOG,
+            MetadataObject.Type.ROLE,
+        },
+        2: {MetadataObject.Type.SCHEMA},
+        3: {
+            MetadataObject.Type.FILESET,
+            MetadataObject.Type.TABLE,
+            MetadataObject.Type.TOPIC,
+            MetadataObject.Type.MODEL,
+        },
+        4: {MetadataObject.Type.COLUMN},
+    }
+
+    @staticmethod
+    @overload
+    def of(name_or_names: list[str], type_: MetadataObject.Type) -> 
MetadataObject: ...
+
+    @staticmethod
+    @overload
+    def of(
+        name_or_names: str, type_: MetadataObject.Type, parent: Optional[str] 
= None
+    ) -> MetadataObject: ...
+
+    @staticmethod
+    def of(
+        name_or_names: Union[str, list[str]],
+        type_: MetadataObject.Type,
+        parent: Optional[str] = None,
+    ) -> MetadataObject:
+        if isinstance(name_or_names, str):
+            name = name_or_names
+            full_name = (
+                MetadataObjects.DOT_JOINER.join([parent, name]) if parent else 
name
+            )
+            return MetadataObjects.parse(full_name, type_)
+        names_len = len(name_or_names)
+        Precondition.check_argument(
+            names_len > 0,
+            "Cannot create a metadata object with no names",
+        )
+        Precondition.check_argument(
+            names_len <= 4,
+            "Cannot create a metadata object with the name length which is 
greater than 4",
+        )
+        Precondition.check_argument(
+            names_len != 1 or type_ in MetadataObjects._NAMES_LEN_CONDS[1],
+            "If the length of names is 1, it must be the CATALOG, METALAKE, or 
ROLE type",
+        )
+        Precondition.check_argument(
+            names_len != 2 or type_ in MetadataObjects._NAMES_LEN_CONDS[2],
+            "If the length of names is 2, it must be the SCHEMA type",
+        )
+        Precondition.check_argument(
+            names_len != 3 or type_ in MetadataObjects._NAMES_LEN_CONDS[3],
+            "If the length of names is 3, it must be FILESET, TABLE, TOPIC or 
MODEL",
+        )
+        Precondition.check_argument(
+            names_len != 4 or type_ in MetadataObjects._NAMES_LEN_CONDS[4],
+            "If the length of names is 4, it must be COLUMN",
+        )
+        names = name_or_names
+        for name in names:
+            MetadataObjects.check_name(name)
+        return MetadataObjects.MetadataObjectImpl(
+            MetadataObjects.get_last_name(names),
+            type_,
+            MetadataObjects.get_parent_full_name(names),
+        )
+
+    @staticmethod
+    def parent(object_: MetadataObject) -> Optional[MetadataObject]:
+        """Get the parent metadata object of the given metadata object.
+
+        Args:
+            object_ (MetadataObject): The metadata object
+
+        Returns:
+            Optional[MetadataObject]:
+                The parent metadata object if it exists, otherwise `None`
+        """
+        if object_ is None:
+            return None
+
+        object_type = object_.type()
+        # Return None if the object is the root object
+        if object_type in {
+            MetadataObject.Type.METALAKE,
+            MetadataObject.Type.CATALOG,
+            MetadataObject.Type.ROLE,
+        }:
+            return None
+
+        parent_type = None
+        if object_type is MetadataObject.Type.COLUMN:
+            parent_type = MetadataObject.Type.TABLE
+        elif object_type in {
+            MetadataObject.Type.TABLE,
+            MetadataObject.Type.FILESET,
+            MetadataObject.Type.TOPIC,
+            MetadataObject.Type.MODEL,
+        }:
+            parent_type = MetadataObject.Type.SCHEMA
+        elif object_type is MetadataObject.Type.SCHEMA:
+            parent_type = MetadataObject.Type.CATALOG
+        else:
+            raise IllegalArgumentException(
+                f"Unexpected to reach here for metadata object type: 
{object_type.value}"
+            )
+
+        return MetadataObjects.parse(object_.parent(), parent_type)
+
+    @staticmethod
+    def get_parent_full_name(names: list[str]) -> Optional[str]:
+        """Get the parent full name of the given full name.
+
+        Args:
+            names (list[str]): The names of the metadata object
+
+        Returns:
+            Optional[str]: The parent full name if it exists, otherwise `None`.
+        """
+        names_len = len(names)
+        if names_len <= 1:
+            return None
+        return MetadataObjects.DOT_JOINER.join(names[:-1])
+
+    @staticmethod
+    def get_last_name(names: list[str]) -> str:
+        return names[-1]
+
+    @staticmethod
+    def check_name(name: str) -> None:
+        Precondition.check_argument(
+            name is not None,
+            "Cannot create a metadata object with null name",
+        )
+        Precondition.check_argument(
+            name != MetadataObjects.METADATA_OBJECT_RESERVED_NAME,
+            "Cannot create a metadata object with `*` name.",
+        )
+
+    @staticmethod
+    def parse(full_name: str, type_: MetadataObject.Type) -> MetadataObject:
+        Precondition.check_argument(
+            full_name is not None and len(full_name.strip()) > 0,
+            "Metadata object full name cannot be blank",
+        )
+
+        parts = full_name.split(MetadataObjects.DOT_SPLITTER)
+        if type_ is MetadataObject.Type.ROLE:
+            return MetadataObjects.of([full_name], MetadataObject.Type.ROLE)
+
+        return MetadataObjects.of(parts, type_)
+
+    class MetadataObjectImpl(MetadataObject):
+        def __init__(
+            self, name: str, type_: MetadataObject.Type, parent: Optional[str] 
= None
+        ) -> None:
+            self._name = name
+            self._type = type_
+            self._parent = parent
+
+        def name(self) -> str:
+            return self._name
+
+        def type(self) -> MetadataObject.Type:
+            return self._type
+
+        def parent(self) -> Optional[str]:
+            return self._parent
+
+        def __eq__(self, value: object) -> bool:
+            if not isinstance(value, MetadataObjects.MetadataObjectImpl):
+                return False
+            return (
+                self._name == value.name()
+                and self._type == value.type()
+                and self._parent == value.parent()
+            )
+
+        def __hash__(self) -> int:
+            return hash((self._name, self._parent, self._type))
+
+        def __str__(self) -> str:
+            return f"MetadataObject: [fullName={self.full_name()}], 
[type={self.type().value}]"
diff --git a/clients/client-python/gravitino/client/base_schema_catalog.py 
b/clients/client-python/gravitino/client/base_schema_catalog.py
index 1555b8e0cb..74dc5e8911 100644
--- a/clients/client-python/gravitino/client/base_schema_catalog.py
+++ b/clients/client-python/gravitino/client/base_schema_catalog.py
@@ -20,13 +20,13 @@ from typing import Dict, List
 
 from gravitino.api.catalog import Catalog
 from gravitino.api.metadata_object import MetadataObject
+from gravitino.api.metadata_objects import MetadataObjects
 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
@@ -35,11 +35,11 @@ from gravitino.dto.requests.schema_updates_request import 
SchemaUpdatesRequest
 from gravitino.dto.responses.drop_response import DropResponse
 from gravitino.dto.responses.entity_list_response import EntityListResponse
 from gravitino.dto.responses.schema_response import SchemaResponse
+from gravitino.exceptions.base import IllegalArgumentException
 from gravitino.exceptions.handlers.schema_error_handler import 
SCHEMA_ERROR_HANDLER
 from gravitino.namespace import Namespace
-from gravitino.utils import HTTPClient
 from gravitino.rest.rest_utils import encode_string
-from gravitino.exceptions.base import IllegalArgumentException
+from gravitino.utils import HTTPClient
 
 logger = logging.getLogger(__name__)
 
@@ -82,7 +82,7 @@ class BaseSchemaCatalog(CatalogDTO, SupportsSchemas):
         self.rest_client = rest_client
         self._catalog_namespace = catalog_namespace
 
-        metadata_object = MetadataObjectImpl([name], 
MetadataObject.Type.CATALOG)
+        metadata_object = MetadataObjects.of([name], 
MetadataObject.Type.CATALOG)
         self._object_credential_operations = 
MetadataObjectCredentialOperations(
             catalog_namespace.level(0), metadata_object, rest_client
         )
diff --git a/clients/client-python/gravitino/client/generic_fileset.py 
b/clients/client-python/gravitino/client/generic_fileset.py
index ccadb26335..b2adb21cc4 100644
--- a/clients/client-python/gravitino/client/generic_fileset.py
+++ b/clients/client-python/gravitino/client/generic_fileset.py
@@ -14,16 +14,16 @@
 # 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 typing import Dict, List, Optional
 
+from gravitino.api.credential.credential import Credential
+from gravitino.api.credential.supports_credentials import SupportsCredentials
 from gravitino.api.file.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.api.metadata_objects import MetadataObjects
 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
@@ -41,7 +41,7 @@ class GenericFileset(Fileset, SupportsCredentials):
         self, fileset: FilesetDTO, rest_client: HTTPClient, full_namespace: 
Namespace
     ):
         self._fileset = fileset
-        metadata_object = MetadataObjectImpl(
+        metadata_object = MetadataObjects.of(
             [full_namespace.level(1), full_namespace.level(2), fileset.name()],
             MetadataObject.Type.FILESET,
         )
diff --git a/clients/client-python/tests/unittests/test_metadata_objects.py 
b/clients/client-python/tests/unittests/test_metadata_objects.py
new file mode 100644
index 0000000000..412f4e02b3
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_metadata_objects.py
@@ -0,0 +1,260 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import unittest
+
+from gravitino.api.metadata_object import MetadataObject
+from gravitino.api.metadata_objects import MetadataObjects
+from gravitino.exceptions.base import IllegalArgumentException
+
+
+class TestMetadataObjects(unittest.TestCase):
+    def test_of_with_string_name(self):
+        obj = MetadataObjects.of("catalog1", MetadataObject.Type.CATALOG)
+        self.assertEqual("catalog1", obj.name())
+        self.assertEqual(MetadataObject.Type.CATALOG, obj.type())
+        self.assertIsNone(obj.parent())
+
+    def test_of_with_string_name_and_parent(self):
+        obj = MetadataObjects.of("schema1", MetadataObject.Type.SCHEMA, 
"catalog1")
+        self.assertEqual("schema1", obj.name())
+        self.assertEqual(MetadataObject.Type.SCHEMA, obj.type())
+        self.assertEqual("catalog1", obj.parent())
+
+    def test_of_with_list_names_metalake(self):
+        obj = MetadataObjects.of(["metalake1"], MetadataObject.Type.METALAKE)
+        self.assertEqual("metalake1", obj.name())
+        self.assertEqual(MetadataObject.Type.METALAKE, obj.type())
+        self.assertIsNone(obj.parent())
+
+    def test_of_with_list_names_catalog(self):
+        obj = MetadataObjects.of(["catalog1"], MetadataObject.Type.CATALOG)
+        self.assertEqual("catalog1", obj.name())
+        self.assertEqual(MetadataObject.Type.CATALOG, obj.type())
+        self.assertIsNone(obj.parent())
+
+    def test_of_with_list_names_role(self):
+        obj = MetadataObjects.of(["role1"], MetadataObject.Type.ROLE)
+        self.assertEqual("role1", obj.name())
+        self.assertEqual(MetadataObject.Type.ROLE, obj.type())
+        self.assertIsNone(obj.parent())
+
+    def test_of_with_list_names_schema(self):
+        obj = MetadataObjects.of(["catalog1", "schema1"], 
MetadataObject.Type.SCHEMA)
+        self.assertEqual("schema1", obj.name())
+        self.assertEqual(MetadataObject.Type.SCHEMA, obj.type())
+        self.assertEqual("catalog1", obj.parent())
+
+    def test_of_with_list_names_table(self):
+        obj = MetadataObjects.of(
+            ["catalog1", "schema1", "table1"], MetadataObject.Type.TABLE
+        )
+        self.assertEqual("table1", obj.name())
+        self.assertEqual(MetadataObject.Type.TABLE, obj.type())
+        self.assertEqual("catalog1.schema1", obj.parent())
+
+    def test_of_with_list_names_column(self):
+        obj = MetadataObjects.of(
+            ["catalog1", "schema1", "table1", "col1"], 
MetadataObject.Type.COLUMN
+        )
+        self.assertEqual("col1", obj.name())
+        self.assertEqual(MetadataObject.Type.COLUMN, obj.type())
+        self.assertEqual("catalog1.schema1.table1", obj.parent())
+
+    def test_of_invalid_names(self):
+        with self.assertRaisesRegex(
+            IllegalArgumentException, "Cannot create a metadata object with no 
names"
+        ):
+            MetadataObjects.of([], MetadataObject.Type.CATALOG)
+        with self.assertRaisesRegex(
+            IllegalArgumentException,
+            "Cannot create a metadata object with the name length which is 
greater than 4",
+        ):
+            MetadataObjects.of(["a", "b", "c", "d", "e"], 
MetadataObject.Type.CATALOG)
+
+    def test_of_invalid_name_length_type_mismatch(self):
+        with self.assertRaisesRegex(
+            IllegalArgumentException,
+            "If the length of names is 1, it must be the CATALOG, METALAKE, or 
ROLE type",
+        ):
+            MetadataObjects.of(["catalog1"], MetadataObject.Type.SCHEMA)
+
+        with self.assertRaisesRegex(
+            IllegalArgumentException,
+            "If the length of names is 2, it must be the SCHEMA type",
+        ):
+            MetadataObjects.of(["catalog1", "schema1"], 
MetadataObject.Type.CATALOG)
+
+        with self.assertRaisesRegex(
+            IllegalArgumentException,
+            "If the length of names is 3, it must be FILESET, TABLE, TOPIC or 
MODEL",
+        ):
+            MetadataObjects.of(
+                ["catalog1", "schema1", "table1"], MetadataObject.Type.COLUMN
+            )
+
+        with self.assertRaisesRegex(
+            IllegalArgumentException, "If the length of names is 4, it must be 
COLUMN"
+        ):
+            MetadataObjects.of(
+                ["catalog1", "schema1", "table1", "column1"], 
MetadataObject.Type.TABLE
+            )
+
+    def test_of_invalid_reserved_name(self):
+        with self.assertRaises(IllegalArgumentException):
+            MetadataObjects.of(["*"], MetadataObject.Type.CATALOG)
+
+    def test_parent_root_object(self):
+        obj = MetadataObjects.of(["metalake1"], MetadataObject.Type.METALAKE)
+        self.assertIsNone(MetadataObjects.parent(obj))
+
+        obj = MetadataObjects.of(["catalog1"], MetadataObject.Type.CATALOG)
+        self.assertIsNone(MetadataObjects.parent(obj))
+
+        obj = MetadataObjects.of(["role1"], MetadataObject.Type.ROLE)
+        self.assertIsNone(MetadataObjects.parent(obj))
+
+    def test_parent_schema(self):
+        obj = MetadataObjects.of(["catalog1", "schema1"], 
MetadataObject.Type.SCHEMA)
+        parent = MetadataObjects.parent(obj)
+        self.assertIsNotNone(parent)
+        self.assertEqual("catalog1", parent.name())
+        self.assertEqual(MetadataObject.Type.CATALOG, parent.type())
+
+    def test_parent_table(self):
+        obj = MetadataObjects.of(
+            ["catalog1", "schema1", "table1"], MetadataObject.Type.TABLE
+        )
+        parent = MetadataObjects.parent(obj)
+        self.assertIsNotNone(parent)
+        self.assertEqual("schema1", parent.name())
+        self.assertEqual(MetadataObject.Type.SCHEMA, parent.type())
+        self.assertEqual("catalog1", parent.parent())
+
+    def test_parent_column(self):
+        obj = MetadataObjects.of(
+            ["catalog1", "schema1", "table1", "col1"], 
MetadataObject.Type.COLUMN
+        )
+        parent = MetadataObjects.parent(obj)
+        self.assertIsNotNone(parent)
+        self.assertEqual("table1", parent.name())
+        self.assertEqual(MetadataObject.Type.TABLE, parent.type())
+        self.assertEqual("catalog1.schema1", parent.parent())
+
+    def test_parent_none_input(self):
+        self.assertIsNone(MetadataObjects.parent(None))
+
+    def test_get_parent_full_name(self):
+        self.assertIsNone(MetadataObjects.get_parent_full_name(["single"]))
+        self.assertEqual(
+            "catalog1", MetadataObjects.get_parent_full_name(["catalog1", 
"schema1"])
+        )
+        self.assertEqual(
+            "catalog1.schema1",
+            MetadataObjects.get_parent_full_name(["catalog1", "schema1", 
"table1"]),
+        )
+        self.assertEqual(
+            "catalog1.schema1.table1",
+            MetadataObjects.get_parent_full_name(
+                ["catalog1", "schema1", "table1", "col1"]
+            ),
+        )
+
+    def test_get_last_name(self):
+        self.assertEqual("single", MetadataObjects.get_last_name(["single"]))
+        self.assertEqual(
+            "schema1", MetadataObjects.get_last_name(["catalog1", "schema1"])
+        )
+        self.assertEqual(
+            "table1", MetadataObjects.get_last_name(["catalog1", "schema1", 
"table1"])
+        )
+
+    # def test_check_name_valid(self):
+    #     MetadataObjects.check_name("valid_name")
+    #     MetadataObjects.check_name("catalog1")
+
+    # def test_check_name_invalid_none(self):
+    #     with self.assertRaises(IllegalArgumentException):
+    #         MetadataObjects.check_name(None)
+
+    # def test_check_name_invalid_reserved(self):
+    #     with self.assertRaises(IllegalArgumentException):
+    #         MetadataObjects.check_name("*")
+
+    def test_parse_root_object(self):
+        obj = MetadataObjects.parse("metalake1", MetadataObject.Type.METALAKE)
+        self.assertEqual("metalake1", obj.name())
+        self.assertEqual(MetadataObject.Type.METALAKE, obj.type())
+        self.assertIsNone(obj.parent())
+
+        obj = MetadataObjects.parse("catalog1", MetadataObject.Type.CATALOG)
+        self.assertEqual("catalog1", obj.name())
+        self.assertEqual(MetadataObject.Type.CATALOG, obj.type())
+        self.assertIsNone(obj.parent())
+
+        obj = MetadataObjects.parse("role.with.dots", MetadataObject.Type.ROLE)
+        self.assertEqual("role.with.dots", obj.name())
+        self.assertEqual(MetadataObject.Type.ROLE, obj.type())
+        self.assertIsNone(obj.parent())
+
+    def test_parse_schema(self):
+        obj = MetadataObjects.parse("catalog1.schema1", 
MetadataObject.Type.SCHEMA)
+        self.assertEqual("schema1", obj.name())
+        self.assertEqual(MetadataObject.Type.SCHEMA, obj.type())
+        self.assertEqual("catalog1", obj.parent())
+
+    def test_parse_table(self):
+        obj = MetadataObjects.parse(
+            "catalog1.schema1.table1", MetadataObject.Type.TABLE
+        )
+        self.assertEqual("table1", obj.name())
+        self.assertEqual(MetadataObject.Type.TABLE, obj.type())
+        self.assertEqual("catalog1.schema1", obj.parent())
+
+    def test_parse_invalid_blank_name(self):
+        with self.assertRaisesRegex(
+            IllegalArgumentException, "Metadata object full name cannot be 
blank"
+        ):
+            MetadataObjects.parse("", MetadataObject.Type.CATALOG)
+
+        with self.assertRaisesRegex(
+            IllegalArgumentException, "Metadata object full name cannot be 
blank"
+        ):
+            MetadataObjects.parse("   ", MetadataObject.Type.CATALOG)
+
+        with self.assertRaisesRegex(
+            IllegalArgumentException, "Metadata object full name cannot be 
blank"
+        ):
+            MetadataObjects.parse(None, MetadataObject.Type.CATALOG)
+
+    def test_metadata_object_impl_equal_and_hash(self):
+        obj1 = MetadataObjects.of(["catalog1"], MetadataObject.Type.CATALOG)
+        obj2 = MetadataObjects.of(["catalog1"], MetadataObject.Type.CATALOG)
+        obj3 = MetadataObjects.of(["catalog2"], MetadataObject.Type.CATALOG)
+
+        self.assertEqual(obj1, obj2)
+        self.assertNotEqual(obj1, obj3)
+        self.assertNotEqual(obj1, "not_a_metadata_object")
+        self.assertEqual(hash(obj1), hash(obj2))
+        self.assertNotEqual(hash(obj1), hash(obj3))
+
+    def test_metadata_object_impl_str(self):
+        obj = MetadataObjects.of(["catalog1", "schema1"], 
MetadataObject.Type.SCHEMA)
+        expected_str = (
+            f"MetadataObject: [fullName={obj.full_name()}], 
[type={obj.type().value}]"
+        )
+        self.assertEqual(expected_str, str(obj))

Reply via email to