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

Reply via email to