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

lzljs3620320 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 160d8e7735 [python] add some validations for rest apis (#6812)
160d8e7735 is described below

commit 160d8e7735abcb076374e5946b93fd1aebc6e04f
Author: Jiajia Li <[email protected]>
AuthorDate: Sun Dec 14 20:17:07 2025 +0800

    [python] add some validations for rest apis (#6812)
---
 paimon-python/pypaimon/api/rest_api.py        |  88 ++++++++++++++++++----
 paimon-python/pypaimon/tests/rest/api_test.py | 103 ++++++++++++++++++++++++++
 2 files changed, 178 insertions(+), 13 deletions(-)

diff --git a/paimon-python/pypaimon/api/rest_api.py 
b/paimon-python/pypaimon/api/rest_api.py
index d715c8a4aa..2992a772df 100755
--- a/paimon-python/pypaimon/api/rest_api.py
+++ b/paimon-python/pypaimon/api/rest_api.py
@@ -48,17 +48,26 @@ class RESTApi:
     TOKEN_EXPIRATION_SAFE_TIME_MILLIS = 3_600_000
 
     def __init__(self, options: Dict[str, str], config_required: bool = True):
+        if not options:
+            raise ValueError("Options cannot be None or empty")
+
+        uri = options.get(CatalogOptions.URI)
+        if not uri or not uri.strip():
+            raise ValueError("URI cannot be empty")
+
         self.logger = logging.getLogger(self.__class__.__name__)
-        self.client = HttpClient(options.get(CatalogOptions.URI))
+        self.client = HttpClient(uri)
         auth_provider = AuthProviderFactory.create_auth_provider(options)
         base_headers = RESTUtil.extract_prefix_map(options, self.HEADER_PREFIX)
 
         if config_required:
             warehouse = options.get(CatalogOptions.WAREHOUSE)
-            query_params = {}
-            if warehouse:
-                query_params[CatalogOptions.WAREHOUSE] = 
RESTUtil.encode_string(
-                    warehouse)
+            if not warehouse or not warehouse.strip():
+                raise ValueError("Warehouse name cannot be empty")
+
+            query_params = {
+                CatalogOptions.WAREHOUSE: RESTUtil.encode_string(warehouse)
+            }
 
             config_response = self.client.get_with_params(
                 ResourcePaths.config(),
@@ -153,12 +162,18 @@ class RESTApi:
         return PagedList(databases, response.get_next_page_token())
 
     def create_database(self, name: str, options: Dict[str, str]) -> None:
+        if not name or not name.strip():
+            raise ValueError("Database name cannot be empty")
+
         request = CreateDatabaseRequest(name, options)
         self.client.post(
             self.resource_paths.databases(), request, self.rest_auth_function
         )
 
     def get_database(self, name: str) -> GetDatabaseResponse:
+        if not name or not name.strip():
+            raise ValueError("Database name cannot be empty")
+
         return self.client.get(
             self.resource_paths.database(name),
             GetDatabaseResponse,
@@ -166,6 +181,9 @@ class RESTApi:
         )
 
     def drop_database(self, name: str) -> None:
+        if not name or not name.strip():
+            raise ValueError("Database name cannot be empty")
+
         self.client.delete(
             self.resource_paths.database(name),
             self.rest_auth_function)
@@ -188,6 +206,9 @@ class RESTApi:
             self.rest_auth_function)
 
     def list_tables(self, database_name: str) -> List[str]:
+        if not database_name or not database_name.strip():
+            raise ValueError("Database name cannot be empty")
+
         return self.__list_data_from_page_api(
             lambda query_params: self.client.get_with_params(
                 self.resource_paths.tables(database_name),
@@ -204,6 +225,9 @@ class RESTApi:
             page_token: Optional[str] = None,
             table_name_pattern: Optional[str] = None,
     ) -> PagedList[str]:
+        if not database_name or not database_name.strip():
+            raise ValueError("Database name cannot be empty")
+
         response = self.client.get_with_params(
             self.resource_paths.tables(database_name),
             self.__build_paged_query_params(
@@ -217,30 +241,45 @@ class RESTApi:
         return PagedList(tables, response.get_next_page_token())
 
     def create_table(self, identifier: Identifier, schema: Schema) -> None:
+        database_name, _ = self.__validate_identifier(identifier)
+        if not schema:
+            raise ValueError("Schema cannot be None")
+
         request = CreateTableRequest(identifier, schema)
         return self.client.post(
-            self.resource_paths.tables(identifier.get_database_name()),
+            self.resource_paths.tables(database_name),
             request,
             self.rest_auth_function)
 
     def get_table(self, identifier: Identifier) -> GetTableResponse:
+        database_name, table_name = self.__validate_identifier(identifier)
+
         return self.client.get(
             self.resource_paths.table(
-                identifier.get_database_name(),
-                identifier.get_object_name()),
+                database_name,
+                table_name),
             GetTableResponse,
             self.rest_auth_function,
         )
 
     def drop_table(self, identifier: Identifier) -> GetTableResponse:
+        database_name, table_name = self.__validate_identifier(identifier)
+
         return self.client.delete(
             self.resource_paths.table(
-                identifier.get_database_name(),
-                identifier.get_object_name()),
+                database_name,
+                table_name),
             self.rest_auth_function,
         )
 
     def rename_table(self, source_identifier: Identifier, target_identifier: 
Identifier) -> None:
+        if not source_identifier:
+            raise ValueError("Source identifier cannot be None")
+        if not target_identifier:
+            raise ValueError("Target identifier cannot be None")
+        self.__validate_identifier(source_identifier)
+        self.__validate_identifier(target_identifier)
+
         request = RenameTableRequest(source_identifier, target_identifier)
         return self.client.post(
             self.resource_paths.rename_table(),
@@ -248,10 +287,12 @@ class RESTApi:
             self.rest_auth_function)
 
     def load_table_token(self, identifier: Identifier) -> 
GetTableTokenResponse:
+        database_name, table_name = self.__validate_identifier(identifier)
+
         return self.client.get(
             self.resource_paths.table_token(
-                identifier.get_database_name(),
-                identifier.get_object_name()),
+                database_name,
+                table_name),
             GetTableTokenResponse,
             self.rest_auth_function,
         )
@@ -279,12 +320,33 @@ class RESTApi:
             NoSuchResourceException: Exception thrown on HTTP 404 means the 
table not exists
             ForbiddenException: Exception thrown on HTTP 403 means don't have 
the permission for this table
         """
+        database_name, table_name = self.__validate_identifier(identifier)
+        if not snapshot:
+            raise ValueError("Snapshot cannot be None")
+        if statistics is None:
+            raise ValueError("Statistics cannot be None")
+
         request = CommitTableRequest(table_uuid, snapshot, statistics)
         response = self.client.post_with_response_type(
             self.resource_paths.commit_table(
-                identifier.get_database_name(), identifier.get_object_name()),
+                database_name, table_name),
             request,
             CommitTableResponse,
             self.rest_auth_function
         )
         return response.is_success()
+
+    @staticmethod
+    def __validate_identifier(identifier: Identifier):
+        if not identifier:
+            raise ValueError("Identifier cannot be None")
+
+        database_name = identifier.get_database_name()
+        if not database_name or not database_name.strip():
+            raise ValueError("Database name cannot be empty")
+
+        table_name = identifier.get_object_name()
+        if not table_name or not table_name.strip():
+            raise ValueError("Table name cannot be None")
+
+        return database_name.strip(), table_name.strip()
diff --git a/paimon-python/pypaimon/tests/rest/api_test.py 
b/paimon-python/pypaimon/tests/rest/api_test.py
index 374765cea7..0443fc7709 100644
--- a/paimon-python/pypaimon/tests/rest/api_test.py
+++ b/paimon-python/pypaimon/tests/rest/api_test.py
@@ -18,6 +18,7 @@
 import logging
 import unittest
 import uuid
+from unittest.mock import Mock
 
 from pypaimon.api.api_response import ConfigResponse
 from pypaimon.api.auth import BearTokenAuthProvider
@@ -227,3 +228,105 @@ class ApiTest(unittest.TestCase):
             # Shutdown server
             server.shutdown()
             print("Server stopped")
+
+    def test_rest_api_parameter_validation(self):
+        rest_api = RESTApi.__new__(RESTApi)
+        # Test __init__ with missing URI
+        with self.assertRaises(ValueError) as context:
+            RESTApi({"warehouse": "test"}, config_required=False)
+        self.assertIn("URI cannot be empty", str(context.exception))
+
+        # Test __init__ with empty URI
+        with self.assertRaises(ValueError) as context:
+            RESTApi({CatalogOptions.URI: "   "}, config_required=False)
+        self.assertIn("URI cannot be empty", str(context.exception))
+
+        # Test create_database with empty name
+        with self.assertRaises(ValueError) as context:
+            rest_api.create_database("", {})
+        self.assertIn("Database name cannot be empty", str(context.exception))
+
+        # Test create_database with whitespace name
+        with self.assertRaises(ValueError) as context:
+            rest_api.create_database("   ", {})
+        self.assertIn("Database name cannot be empty", str(context.exception))
+
+        # Test get_database with empty name
+        with self.assertRaises(ValueError) as context:
+            rest_api.get_database("")
+        self.assertIn("Database name cannot be empty", str(context.exception))
+
+        # Test get_database with whitespace name
+        with self.assertRaises(ValueError) as context:
+            rest_api.get_database("   ")
+        self.assertIn("Database name cannot be empty", str(context.exception))
+
+        # Test drop_database with empty name
+        with self.assertRaises(ValueError) as context:
+            rest_api.drop_database("")
+        self.assertIn("Database name cannot be empty", str(context.exception))
+
+        # Test alter_database with empty name
+        with self.assertRaises(ValueError) as context:
+            rest_api.alter_database("", [], {})
+        self.assertIn("Database name cannot be empty", str(context.exception))
+
+        # Test list_tables with empty database_name
+        with self.assertRaises(ValueError) as context:
+            rest_api.list_tables("")
+        self.assertIn("Database name cannot be empty", str(context.exception))
+
+        # Test list_tables_paged with empty database_name
+        with self.assertRaises(ValueError) as context:
+            rest_api.list_tables_paged("")
+        self.assertIn("Database name cannot be empty", str(context.exception))
+
+        # Test create_table with None identifier
+        with self.assertRaises(ValueError) as context:
+            rest_api.create_table(None, Mock())
+        self.assertIn("Identifier cannot be None", str(context.exception))
+
+        # Test create_table with None schema
+        with self.assertRaises(ValueError) as context:
+            rest_api.create_table(Mock(), None)
+        self.assertIn("Schema cannot be None", str(context.exception))
+
+        # Test get_table with None identifier
+        with self.assertRaises(ValueError) as context:
+            rest_api.get_table(None)
+        self.assertIn("Identifier cannot be None", str(context.exception))
+
+        # Test drop_table with None identifier
+        with self.assertRaises(ValueError) as context:
+            rest_api.drop_table(None)
+        self.assertIn("Identifier cannot be None", str(context.exception))
+
+        # Test rename_table with None source_identifier
+        with self.assertRaises(ValueError) as context:
+            rest_api.rename_table(None, Mock())
+        self.assertIn("Source identifier cannot be None", 
str(context.exception))
+
+        # Test rename_table with None target_identifier
+        with self.assertRaises(ValueError) as context:
+            rest_api.rename_table(Mock(), None)
+        self.assertIn("Target identifier cannot be None", 
str(context.exception))
+
+        # Test load_table_token with None identifier
+        with self.assertRaises(ValueError) as context:
+            rest_api.load_table_token(None)
+        self.assertIn("Identifier cannot be None", str(context.exception))
+
+        # Test commit_snapshot with None identifier
+        with self.assertRaises(ValueError) as context:
+            rest_api.commit_snapshot(None, "uuid", Mock(), [])
+        self.assertIn("Identifier cannot be None", str(context.exception))
+
+        # Test commit_snapshot with None snapshot
+        with self.assertRaises(ValueError) as context:
+            rest_api.commit_snapshot(Mock(), "uuid", None, [])
+        self.assertIn("Snapshot cannot be None", str(context.exception))
+
+        # Test commit_snapshot with None statistics
+        with self.assertRaises(ValueError) as context:
+            rest_api.commit_snapshot(Mock(), "uuid", Mock(), None)
+        self.assertIn("Statistics cannot be None", str(context.exception))

Reply via email to