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

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


The following commit(s) were added to refs/heads/main by this push:
     new b97bd9412b [#8797] feat(client-python): add class AddPartitionsRequest 
(#9045)
b97bd9412b is described below

commit b97bd9412b2fef17cc36e4df14fa4cbea38b0620
Author: George T. C. Lai <[email protected]>
AuthorDate: Fri Nov 14 01:47:37 2025 +0800

    [#8797] feat(client-python): add class AddPartitionsRequest (#9045)
    
    ### What changes were proposed in this pull request?
    
    The following methods and classes were included in this PR:
    - method `toDTO(Partition partition)` in `DTOConverters`,
    - method `toFunctionArg(Expression expression)` in `DTOConverters`, and
    - class `AddPartitionsRequest`.
    
    ### Why are the changes needed?
    
    We need to enable table operations in Python client that requires
    implementation of the aforementioned methods and classes used in
    `RelationalTable`.
    
    Fix: #8797
    
    ### 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]>
---
 .../dto/requests/add_partitions_request.py         |  39 +++++
 .../gravitino/dto/util/dto_converters.py           | 107 +++++++++++-
 .../unittests/dto/util/test_dto_converters.py      | 182 ++++++++++++++++++++-
 .../client-python/tests/unittests/test_requests.py |  41 +++++
 4 files changed, 366 insertions(+), 3 deletions(-)

diff --git 
a/clients/client-python/gravitino/dto/requests/add_partitions_request.py 
b/clients/client-python/gravitino/dto/requests/add_partitions_request.py
new file mode 100644
index 0000000000..e54b9f9200
--- /dev/null
+++ b/clients/client-python/gravitino/dto/requests/add_partitions_request.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 dataclasses import dataclass, field
+
+from dataclasses_json import config
+
+from gravitino.dto.rel.partitions.partition_dto import PartitionDTO
+from gravitino.rest.rest_message import RESTRequest
+from gravitino.utils.precondition import Precondition
+
+
+@dataclass
+class AddPartitionsRequest(RESTRequest):
+    """Request to add partitions to a table."""
+
+    _partitions: list[PartitionDTO] = 
field(metadata=config(field_name="partitions"))
+
+    def validate(self):
+        Precondition.check_argument(
+            self._partitions is not None, "partitions must not be null"
+        )
+        Precondition.check_argument(
+            len(self._partitions) == 1, "Haven't yet implemented multiple 
partitions"
+        )
diff --git a/clients/client-python/gravitino/dto/util/dto_converters.py 
b/clients/client-python/gravitino/dto/util/dto_converters.py
index e2a781e19a..94d7224379 100644
--- a/clients/client-python/gravitino/dto/util/dto_converters.py
+++ b/clients/client-python/gravitino/dto/util/dto_converters.py
@@ -23,8 +23,9 @@ from gravitino.api.rel.expressions.distributions.distribution 
import Distributio
 from gravitino.api.rel.expressions.distributions.distributions import 
Distributions
 from gravitino.api.rel.expressions.expression import Expression
 from gravitino.api.rel.expressions.function_expression import 
FunctionExpression
+from gravitino.api.rel.expressions.literals.literal import Literal
 from gravitino.api.rel.expressions.literals.literals import Literals
-from gravitino.api.rel.expressions.named_reference import NamedReference
+from gravitino.api.rel.expressions.named_reference import FieldReference, 
NamedReference
 from gravitino.api.rel.expressions.sorts.sort_order import SortOrder
 from gravitino.api.rel.expressions.sorts.sort_orders import SortOrders
 from gravitino.api.rel.expressions.transforms.transform import Transform
@@ -32,7 +33,9 @@ from gravitino.api.rel.expressions.transforms.transforms 
import Transforms
 from gravitino.api.rel.expressions.unparsed_expression import 
UnparsedExpression
 from gravitino.api.rel.indexes.index import Index
 from gravitino.api.rel.indexes.indexes import Indexes
+from gravitino.api.rel.partitions.identity_partition import IdentityPartition
 from gravitino.api.rel.partitions.list_partition import ListPartition
+from gravitino.api.rel.partitions.partition import Partition
 from gravitino.api.rel.partitions.range_partition import RangePartition
 from gravitino.api.rel.table import Table
 from gravitino.api.rel.types.types import Types
@@ -58,6 +61,10 @@ from gravitino.dto.rel.partitioning.range_partitioning_dto 
import RangePartition
 from gravitino.dto.rel.partitioning.truncate_partitioning_dto import (
     TruncatePartitioningDTO,
 )
+from gravitino.dto.rel.partitions.identity_partition_dto import 
IdentityPartitionDTO
+from gravitino.dto.rel.partitions.list_partition_dto import ListPartitionDTO
+from gravitino.dto.rel.partitions.partition_dto import PartitionDTO
+from gravitino.dto.rel.partitions.range_partition_dto import RangePartitionDTO
 from gravitino.dto.rel.sort_order_dto import SortOrderDTO
 from gravitino.dto.rel.table_dto import TableDTO
 from gravitino.exceptions.base import IllegalArgumentException
@@ -302,3 +309,101 @@ class DTOConverters:
         if not dtos:
             return []
         return [DTOConverters.from_dto(dto) for dto in dtos]
+
+    @staticmethod
+    def to_function_arg(expression: Expression) -> FunctionArg:
+        """Converts an Expression to an FunctionArg DTO.
+
+        Args:
+            expression (Expression): The expression to be converted.
+
+        Returns:
+            FunctionArg: The expression DTO.
+        """
+
+        if isinstance(expression, FunctionArg):
+            return cast(FunctionArg, expression)
+        if isinstance(expression, Literal):
+            if expression is Literals.NULL:
+                return LiteralDTO.NULL
+            return (
+                LiteralDTO.builder()
+                .with_value(str(expression.value()))
+                .with_data_type(expression.data_type())
+                .build()
+            )
+        if isinstance(expression, FieldReference):
+            return (
+                FieldReferenceDTO.builder()
+                .with_field_name(expression.field_name())
+                .build()
+            )
+        if isinstance(expression, FunctionExpression):
+            return (
+                FuncExpressionDTO.builder()
+                .with_function_name(expression.function_name())
+                .with_function_args(
+                    [
+                        DTOConverters.to_function_arg(arg)
+                        for arg in expression.arguments()
+                    ]
+                )
+                .build()
+            )
+        if isinstance(expression, UnparsedExpression):
+            return (
+                UnparsedExpressionDTO.builder()
+                .with_unparsed_expression(expression.unparsed_expression())
+                .build()
+            )
+        raise IllegalArgumentException(
+            f"Unsupported expression type: {expression.__class__.__name__}"
+        )
+
+    @singledispatchmethod
+    @staticmethod
+    def to_dto(obj) -> object:
+        raise IllegalArgumentException(f"Unsupported type: {type(obj)}")
+
+    @to_dto.register
+    @staticmethod
+    def _(obj: Partition) -> PartitionDTO:
+        if isinstance(obj, RangePartition):
+            range_partition = cast(RangePartition, obj)
+            return RangePartitionDTO(
+                name=range_partition.name(),
+                upper=cast(
+                    LiteralDTO, 
DTOConverters.to_function_arg(range_partition.upper())
+                ),
+                lower=cast(
+                    LiteralDTO, 
DTOConverters.to_function_arg(range_partition.lower())
+                ),
+                properties=range_partition.properties(),
+            )
+        if isinstance(obj, IdentityPartition):
+            identity_partition = cast(IdentityPartition, obj)
+            return IdentityPartitionDTO(
+                name=identity_partition.name(),
+                values=[
+                    cast(LiteralDTO, DTOConverters.to_function_arg(v))
+                    for v in identity_partition.values()
+                ],
+                field_names=identity_partition.field_names(),
+                properties=identity_partition.properties(),
+            )
+        if isinstance(obj, ListPartition):
+            list_partition = cast(ListPartition, obj)
+            return ListPartitionDTO(
+                name=list_partition.name(),
+                lists=[
+                    [
+                        cast(LiteralDTO, DTOConverters.to_function_arg(v))
+                        for v in list_item
+                    ]
+                    for list_item in list_partition.lists()
+                ],
+                properties=list_partition.properties(),
+            )
+        raise IllegalArgumentException(
+            f"Unsupported partition type: {obj.__class__.__name__}"
+        )
diff --git 
a/clients/client-python/tests/unittests/dto/util/test_dto_converters.py 
b/clients/client-python/tests/unittests/dto/util/test_dto_converters.py
index 2fd7a1ec55..171f7ee864 100644
--- a/clients/client-python/tests/unittests/dto/util/test_dto_converters.py
+++ b/clients/client-python/tests/unittests/dto/util/test_dto_converters.py
@@ -16,10 +16,10 @@
 # under the License.
 
 import unittest
-from datetime import datetime
+from datetime import date, datetime
 from itertools import product
 from random import randint, random
-from typing import cast
+from typing import Dict, cast
 from unittest.mock import MagicMock, patch
 
 from gravitino.api.rel.column import Column
@@ -36,6 +36,8 @@ from gravitino.api.rel.expressions.transforms.transforms 
import Transforms
 from gravitino.api.rel.expressions.unparsed_expression import 
UnparsedExpression
 from gravitino.api.rel.indexes.index import Index
 from gravitino.api.rel.indexes.indexes import Indexes
+from gravitino.api.rel.partitions.partition import Partition
+from gravitino.api.rel.partitions.partitions import Partitions
 from gravitino.api.rel.table import Table
 from gravitino.api.rel.types.types import Types
 from gravitino.dto.rel.column_dto import ColumnDTO
@@ -63,6 +65,9 @@ from gravitino.dto.rel.partitioning.truncate_partitioning_dto 
import (
     TruncatePartitioningDTO,
 )
 from gravitino.dto.rel.partitioning.year_partitioning_dto import 
YearPartitioningDTO
+from gravitino.dto.rel.partitions.identity_partition_dto import 
IdentityPartitionDTO
+from gravitino.dto.rel.partitions.list_partition_dto import ListPartitionDTO
+from gravitino.dto.rel.partitions.range_partition_dto import RangePartitionDTO
 from gravitino.dto.rel.sort_order_dto import SortOrderDTO
 from gravitino.dto.rel.table_dto import TableDTO
 from gravitino.dto.util.dto_converters import DTOConverters
@@ -93,6 +98,16 @@ class TestDTOConverters(unittest.TestCase):
             Types.VarCharType.of(10): "test",
             Types.FixedCharType.of(10): "test",
         }
+        cls.function_args = {
+            FieldReference(field_names=["score"]): FieldReferenceDTO.builder()
+            .with_field_name(field_name=["score"])
+            .build(),
+            UnparsedExpression.of(
+                unparsed_expression="unparsed"
+            ): UnparsedExpressionDTO.builder()
+            .with_unparsed_expression("unparsed")
+            .build(),
+        }
         cls.table_dto_json = """
         {
             "name": "example_table",
@@ -590,3 +605,166 @@ class TestDTOConverters(unittest.TestCase):
         )
         self.assertEqual(table.audit_info(), dto.audit_info())
         self.assertEqual(table.properties(), dto.properties())
+
+    def test_to_function_arg_function_arg(self):
+        for expression, function_arg in 
TestDTOConverters.function_args.items():
+            converted = DTOConverters.to_function_arg(function_arg)
+            self.assertTrue(converted == function_arg)
+            converted = DTOConverters.to_function_arg(expression)
+            self.assertTrue(converted == function_arg)
+
+        for data_type, value in TestDTOConverters.literals.items():
+            literal = Literals.of(value=value, data_type=data_type)
+            expected = (
+                LiteralDTO.builder()
+                .with_data_type(data_type)
+                .with_value(str(value))
+                .build()
+            )
+            self.assertTrue(DTOConverters.to_function_arg(literal) == expected)
+        self.assertTrue(DTOConverters.to_function_arg(Literals.NULL) == 
LiteralDTO.NULL)
+
+        function_name = "test_function"
+        args: list[FunctionArg] = [
+            LiteralDTO.builder()
+            .with_data_type(Types.IntegerType.get())
+            .with_value("-1")
+            .build(),
+            LiteralDTO.builder()
+            .with_data_type(Types.BooleanType.get())
+            .with_value("True")
+            .build(),
+        ]
+        expected = (
+            FuncExpressionDTO.builder()
+            .with_function_name(function_name)
+            .with_function_args(args)
+            .build()
+        )
+        func_expression = FunctionExpression.of(
+            function_name,
+            Literals.of(value="-1", data_type=Types.IntegerType.get()),
+            Literals.of(value="True", data_type=Types.BooleanType.get()),
+        )
+        converted = DTOConverters.to_function_arg(func_expression)
+        self.assertTrue(converted == expected)
+
+        with self.assertRaisesRegex(
+            IllegalArgumentException, "Unsupported expression type"
+        ):
+            DTOConverters.to_function_arg(DistributionDTO.NONE)
+
+    def test_to_dto_raise_exception(self):
+        with self.assertRaisesRegex(IllegalArgumentException, "Unsupported 
type"):
+            DTOConverters.to_dto("test")
+
+    def test_to_dto_range_partition(self):
+        converted = DTOConverters.to_dto(
+            Partitions.range(
+                name="p0",
+                upper=Literals.NULL,
+                lower=Literals.integer_literal(6),
+                properties={},
+            )
+        )
+        expected = RangePartitionDTO(
+            name="p0",
+            upper=LiteralDTO.NULL,
+            lower=LiteralDTO.builder()
+            .with_value("6")
+            .with_data_type(Types.IntegerType.get())
+            .build(),
+            properties={},
+        )
+        self.assertTrue(converted == expected)
+
+    def test_to_dto_partition_raise_exception(self):
+        class InvalidPartition(Partition):
+            def name(self) -> str:
+                return "invalid_partition"
+
+            def properties(self) -> Dict[str, str]:
+                return {}
+
+        with self.assertRaisesRegex(
+            IllegalArgumentException, "Unsupported partition type"
+        ):
+            DTOConverters.to_dto(InvalidPartition())
+
+    def test_to_dto_identity_partition(self):
+        partition_name = "dt=2025-08-08/country=us"
+        field_names = [["dt"], ["country"]]
+        properties = {"location": 
"/user/hive/warehouse/tpch_flat_orc_2.db/orders"}
+        values = [
+            LiteralDTO.builder()
+            .with_data_type(data_type=Types.DateType.get())
+            .with_value(value="2025-08-08")
+            .build(),
+            LiteralDTO.builder()
+            .with_data_type(data_type=Types.StringType.get())
+            .with_value(value="us")
+            .build(),
+        ]
+        expected = IdentityPartitionDTO(
+            name=partition_name,
+            field_names=field_names,
+            values=values,
+            properties=properties,
+        )
+        partition = Partitions.identity(
+            name=partition_name,
+            field_names=field_names,
+            values=[
+                Literals.date_literal(date(2025, 8, 8)),
+                Literals.string_literal("us"),
+            ],
+            properties=properties,
+        )
+        converted = DTOConverters.to_dto(partition)
+        self.assertTrue(converted == expected)
+
+    def test_to_dto_list_partition(self):
+        partition_name = "p202508_California"
+        properties = {"key": "value"}
+        partition = Partitions.list(
+            partition_name,
+            [
+                [
+                    Literals.date_literal(date(2025, 8, 8)),
+                    Literals.string_literal("Los Angeles"),
+                ],
+                [
+                    Literals.date_literal(date(2025, 8, 8)),
+                    Literals.string_literal("San Francisco"),
+                ],
+            ],
+            properties,
+        )
+        expected = ListPartitionDTO(
+            name=partition_name,
+            lists=[
+                [
+                    LiteralDTO.builder()
+                    .with_data_type(data_type=Types.DateType.get())
+                    .with_value(value="2025-08-08")
+                    .build(),
+                    LiteralDTO.builder()
+                    .with_data_type(data_type=Types.StringType.get())
+                    .with_value(value="Los Angeles")
+                    .build(),
+                ],
+                [
+                    LiteralDTO.builder()
+                    .with_data_type(data_type=Types.DateType.get())
+                    .with_value(value="2025-08-08")
+                    .build(),
+                    LiteralDTO.builder()
+                    .with_data_type(data_type=Types.StringType.get())
+                    .with_value(value="San Francisco")
+                    .build(),
+                ],
+            ],
+            properties=properties,
+        )
+        converted = DTOConverters.to_dto(partition)
+        self.assertTrue(converted == expected)
diff --git a/clients/client-python/tests/unittests/test_requests.py 
b/clients/client-python/tests/unittests/test_requests.py
new file mode 100644
index 0000000000..260855965b
--- /dev/null
+++ b/clients/client-python/tests/unittests/test_requests.py
@@ -0,0 +1,41 @@
+# 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 json
+import unittest
+from typing import cast
+
+from gravitino.dto.requests.add_partitions_request import AddPartitionsRequest
+from gravitino.exceptions.base import IllegalArgumentException
+
+
+class TestRequests(unittest.TestCase):
+    def test_add_partitions_request(self):
+        partitions = ["p202508_California"]
+        json_str = json.dumps({"partitions": partitions})
+        req = AddPartitionsRequest.from_json(json_str)
+        req_dict = cast(dict, req.to_dict())
+        self.assertListEqual(req_dict["partitions"], partitions)
+
+        exceptions = {
+            "partitions must not be null": '{"partitions": null}',
+            "Haven't yet implemented multiple partitions": '{"partitions": 
["p1", "p2"]}',
+        }
+        for exception_str, json_str in exceptions.items():
+            with self.assertRaisesRegex(IllegalArgumentException, 
exception_str):
+                req = AddPartitionsRequest.from_json(json_str)
+                req.validate()

Reply via email to