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))