This is an automated email from the ASF dual-hosted git repository.
JingsongLi 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 98400f887d [python] Send the warehouse config parameter encoded once
(#10357)
98400f887d is described below
commit 98400f887deb7aac26f585d9d005471de74dd3e7
Author: Jiajia Li <[email protected]>
AuthorDate: Mon Oct 12 21:29:47 2026 +0800
[python] Send the warehouse config parameter encoded once (#10357)
---
paimon-python/pypaimon/api/resource_paths.py | 4 +-
paimon-python/pypaimon/api/rest_api.py | 2 +-
.../pypaimon/tests/rest/resource_paths_test.py | 101 +++++++++++++++++++++
.../tests/rest/rest_api_warehouse_encoding_test.py | 73 +++++++++++++++
4 files changed, 178 insertions(+), 2 deletions(-)
diff --git a/paimon-python/pypaimon/api/resource_paths.py
b/paimon-python/pypaimon/api/resource_paths.py
index 45f82364f6..309d829d59 100644
--- a/paimon-python/pypaimon/api/resource_paths.py
+++ b/paimon-python/pypaimon/api/resource_paths.py
@@ -72,7 +72,9 @@ class ResourcePaths:
self.TABLES, RESTUtil.encode_string(table_name)))
def table_details(self, database_name: str) -> str:
- return "{}/{}/{}/{}".format(self.base_path, self.DATABASES,
database_name, self.TABLE_DETAILS)
+ database_name = urllib.parse.quote_plus(database_name, safe="*~",
errors="replace")
+ return "{}/{}/{}/{}".format(
+ self.base_path, self.DATABASES, database_name, self.TABLE_DETAILS)
def table_token(self, database_name: str, table_name: str) -> str:
return ("{}/{}/{}/{}/{}/token".format(self.base_path, self.DATABASES,
RESTUtil.encode_string(database_name),
diff --git a/paimon-python/pypaimon/api/rest_api.py
b/paimon-python/pypaimon/api/rest_api.py
index 543c0858b3..d69f49d0dc 100755
--- a/paimon-python/pypaimon/api/rest_api.py
+++ b/paimon-python/pypaimon/api/rest_api.py
@@ -109,7 +109,7 @@ class RESTApi:
raise ValueError("Warehouse name cannot be empty")
query_params = {
- CatalogOptions.WAREHOUSE.key():
RESTUtil.encode_string(warehouse)
+ CatalogOptions.WAREHOUSE.key(): warehouse
}
config_response = self.client.get_with_params(
diff --git a/paimon-python/pypaimon/tests/rest/resource_paths_test.py
b/paimon-python/pypaimon/tests/rest/resource_paths_test.py
new file mode 100644
index 0000000000..eb3b9ed660
--- /dev/null
+++ b/paimon-python/pypaimon/tests/rest/resource_paths_test.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.
+
+
+import threading
+import unittest
+from http.server import BaseHTTPRequestHandler, HTTPServer
+
+from pypaimon.api.auth import DLFAuthProvider
+from pypaimon.api.client import HttpClient
+from pypaimon.api.resource_paths import ResourcePaths
+from pypaimon.api.token_loader import DLFToken
+from pypaimon.api.typedef import RESTAuthParameter
+
+
+class ResourcePathsTest(unittest.TestCase):
+
+ def test_url_encode(self):
+ database = "test_db"
+ object_name = "test_table$snapshot"
+ resource_paths = ResourcePaths("paimon")
+ self.assertEqual(
+ "/v1/paimon/databases/test_db/tables/test_table%24snapshot",
+ resource_paths.table(database, object_name))
+ resource_paths = ResourcePaths("paimon/aaaa")
+ self.assertEqual(
+ "/v1/paimon%2Faaaa/databases/test_db/tables/test_table%24snapshot",
+ resource_paths.table(database, object_name))
+
+ def test_database_and_table_names_are_url_encoded(self):
+ paths = ResourcePaths("catalog/id")
+ self.assertEqual(
+ "/v1/catalog%2Fid/databases/sales+db/tables/orders%2Fall",
+ paths.table("sales db", "orders/all"))
+ self.assertEqual(
+ "/v1/catalog%2Fid/databases/sales+db/table-details",
paths.table_details("sales db"))
+
+ def test_table_details_signature_matches_the_path_sent(self):
+ received = []
+
+ class Handler(BaseHTTPRequestHandler):
+
+ def log_message(self, *args):
+ pass
+
+ def do_GET(self):
+ received.append((self.path, dict(self.headers)))
+ self.send_response(204)
+ self.end_headers()
+
+ server = HTTPServer(("127.0.0.1", 0), Handler)
+ self.addCleanup(server.server_close)
+ thread = threading.Thread(target=server.serve_forever, daemon=True)
+ thread.start()
+ self.addCleanup(thread.join)
+ self.addCleanup(server.shutdown)
+ uri = "http://127.0.0.1:{}".format(server.server_port)
+ client = HttpClient(uri)
+ self.addCleanup(client.session.close)
+ token = DLFToken("test-ak", "test-sk", "test-token", None)
+ paths = ResourcePaths("catalog~id")
+
+ for algorithm in ("default", "openapi-v4"):
+ provider = DLFAuthProvider(uri, "cn-hangzhou", algorithm,
token=token)
+ for database, encoded in (("sales~db", "sales~db"),
+ ("sales~ db/a+b*", "sales~+db%2Fa%2Bb*"),
+ ("sales%7Edb", "sales%257Edb")):
+ with self.subTest(algorithm=algorithm, database=database):
+ signed = []
+
+ def sign(parameter):
+ signed.append(parameter.path)
+ return provider.merge_auth_header({}, parameter)
+
+ client.get(paths.table_details(database), None, sign)
+ path, headers = received[-1]
+ self.assertEqual(
+
"/v1/catalog~id/databases/{}/table-details".format(encoded), path)
+ expected = provider.signer.authorization(
+ RESTAuthParameter("GET", path, "", {}), token,
+ provider.extract_host(uri), headers)
+ self.assertEqual(expected, headers["Authorization"])
+ self.assertEqual([path], signed)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git
a/paimon-python/pypaimon/tests/rest/rest_api_warehouse_encoding_test.py
b/paimon-python/pypaimon/tests/rest/rest_api_warehouse_encoding_test.py
new file mode 100644
index 0000000000..669004896d
--- /dev/null
+++ b/paimon-python/pypaimon/tests/rest/rest_api_warehouse_encoding_test.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.
+
+
+import threading
+import unittest
+from http.server import BaseHTTPRequestHandler, HTTPServer
+from urllib.parse import parse_qs, urlsplit
+
+from pypaimon.api.api_response import ConfigResponse
+from pypaimon.api.rest_api import RESTApi
+from pypaimon.common.json_util import JSON
+
+
+class RESTApiWarehouseEncodingTest(unittest.TestCase):
+
+ server = None
+
+ def tearDown(self):
+ if self.server is not None:
+ self.server.shutdown()
+ self.server.server_close()
+
+ def test_warehouse_query_parameter_not_double_encoded(self):
+ warehouse = "file:///tmp/paimon-warehouse"
+ config = JSON.to_json(ConfigResponse(
+ defaults={"warehouse": warehouse, "prefix": "paimon"},
overrides={}))
+ received = []
+
+ class Handler(BaseHTTPRequestHandler):
+
+ def log_message(self, *args):
+ pass
+
+ def do_GET(self):
+ received.append(self.path)
+ body = config.encode("utf-8")
+ self.send_response(200)
+ self.send_header("Content-Type", "application/json")
+ self.send_header("Content-Length", str(len(body)))
+ self.end_headers()
+ self.wfile.write(body)
+
+ self.server = HTTPServer(("127.0.0.1", 0), Handler)
+ threading.Thread(target=self.server.serve_forever, daemon=True).start()
+
+ RESTApi({
+ "uri": "http://127.0.0.1:{}".format(self.server.server_port),
+ "warehouse": warehouse,
+ "token": "token",
+ "token.provider": "bear",
+ })
+
+ received_warehouse =
parse_qs(urlsplit(received[0]).query)["warehouse"][0]
+ self.assertEqual(warehouse, received_warehouse, "warehouse query param
was double-encoded")
+
+
+if __name__ == '__main__':
+ unittest.main()