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

imbajin pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hugegraph-ai.git


The following commit(s) were added to refs/heads/main by this push:
     new a0537bb9 feat(llm): support number vertexid query (#329)
a0537bb9 is described below

commit a0537bb95c39a6cbb25237809652c8130084bb68
Author: Neo Ko <[email protected]>
AuthorDate: Sat Jun 20 23:59:41 2026 +0800

    feat(llm): support number vertexid query (#329)
    
    1. make vertex api compactible with number type vertex id
    2. add test case for add/append/eliminate/remove vertex with number type
    vertex id
    
    Co-authored-by: imbajin <[email protected]>
---
 .../src/pyhugegraph/api/graph.py                   | 440 +++++++++++----------
 .../src/pyhugegraph/api/traverser.py               | 134 ++++---
 .../src/pyhugegraph/utils/id_format.py             |  54 +++
 .../src/tests/api/test_graph.py                    |  46 +++
 .../src/tests/api/test_schema.py                   |  13 +-
 .../src/tests/api/test_vertex_id_format.py         | 156 ++++++++
 hugegraph-python-client/src/tests/client_utils.py  |   8 +
 7 files changed, 566 insertions(+), 285 deletions(-)

diff --git a/hugegraph-python-client/src/pyhugegraph/api/graph.py 
b/hugegraph-python-client/src/pyhugegraph/api/graph.py
index 4b6aab1c..b30cc8a3 100644
--- a/hugegraph-python-client/src/pyhugegraph/api/graph.py
+++ b/hugegraph-python-client/src/pyhugegraph/api/graph.py
@@ -1,219 +1,221 @@
-# 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
-
-from pyhugegraph.api.common import HugeParamsBase
-from pyhugegraph.structure.edge_data import EdgeData
-from pyhugegraph.structure.vertex_data import VertexData
-from pyhugegraph.utils import huge_router as router
-from pyhugegraph.utils.exceptions import NotFoundError
-
-
-class GraphManager(HugeParamsBase):
-    @router.http("POST", "graph/vertices")
-    def addVertex(self, label, properties, id=None):
-        data = {}
-        if id is not None:
-            data["id"] = id
-        data["label"] = label
-        data["properties"] = properties
-        if response := self._invoke_request(data=json.dumps(data)):
-            return VertexData(response)
-        return None
-
-    @router.http("POST", "graph/vertices/batch")
-    def addVertices(self, input_data):
-        data = []
-        for item in input_data:
-            data.append({"label": item[0], "properties": item[1]})
-        if response := self._invoke_request(data=json.dumps(data)):
-            return [VertexData({"id": item}) for item in response]
-        return None
-
-    @router.http("PUT", 'graph/vertices/"{vertex_id}"?action=append')
-    def appendVertex(self, vertex_id, properties):  # pylint: 
disable=unused-argument
-        data = {"properties": properties}
-        if response := self._invoke_request(data=json.dumps(data)):
-            return VertexData(response)
-        return None
-
-    @router.http("PUT", 'graph/vertices/"{vertex_id}"?action=eliminate')
-    def eliminateVertex(self, vertex_id, properties):  # pylint: 
disable=unused-argument
-        data = {"properties": properties}
-        if response := self._invoke_request(data=json.dumps(data)):
-            return VertexData(response)
-        return None
-
-    @router.http("GET", 'graph/vertices/"{vertex_id}"')
-    def getVertexById(self, vertex_id):  # pylint: disable=unused-argument
-        if response := self._invoke_request():
-            return VertexData(response)
-        return None
-
-    def getVertexByPage(self, label, limit, page=None, properties=None):
-        path = "graph/vertices?"
-        para = ""
-        para = para + "&label=" + label
-        if properties:
-            para = para + "&properties=" + json.dumps(properties)
-        if page:
-            para += f"&page={page}"
-        else:
-            para += "&page"
-        para = para + "&limit=" + str(limit)
-        path = path + para[1:]
-        if response := self._sess.request(path):
-            res = [VertexData(item) for item in response["vertices"]]
-            next_page = response["page"]
-            return res, next_page
-        return None, None
-
-    def getVertexByCondition(self, label="", limit=0, page=None, 
properties=None):
-        path = "graph/vertices?"
-        para = ""
-        if label:
-            para = para + "&label=" + label
-        if properties:
-            para = para + "&properties=" + json.dumps(properties)
-        if limit > 0:
-            para = para + "&limit=" + str(limit)
-        if page:
-            para += f"&page={page}"
-        else:
-            para += "&page"
-        path = path + para[1:]
-        if response := self._sess.request(path):
-            return [VertexData(item) for item in response["vertices"]]
-        return None
-
-    @router.http("DELETE", 'graph/vertices/"{vertex_id}"')
-    def removeVertexById(self, vertex_id):  # pylint: disable=unused-argument
-        return self._invoke_request()
-
-    @router.http("POST", "graph/edges")
-    def addEdge(self, edge_label, out_id, in_id, properties) -> EdgeData | 
None:
-        data = {
-            "label": edge_label,
-            "outV": out_id,
-            "inV": in_id,
-            "properties": properties,
-        }
-        if response := self._invoke_request(data=json.dumps(data)):
-            return EdgeData(response)
-        return None
-
-    @router.http("POST", "graph/edges/batch")
-    def addEdges(self, input_data) -> list[EdgeData] | None:
-        data = []
-        for item in input_data:
-            data.append(
-                {
-                    "label": item[0],
-                    "outV": item[1],
-                    "inV": item[2],
-                    "outVLabel": item[3],
-                    "inVLabel": item[4],
-                    "properties": item[5],
-                }
-            )
-        if response := self._invoke_request(data=json.dumps(data)):
-            return [EdgeData({"id": item}) for item in response]
-        return None
-
-    @router.http("PUT", "graph/edges/{edge_id}?action=append")
-    def appendEdge(
-        self,
-        edge_id,
-        properties,  # pylint: disable=unused-argument
-    ) -> EdgeData | None:
-        if response := self._invoke_request(data=json.dumps({"properties": 
properties})):
-            return EdgeData(response)
-        return None
-
-    @router.http("PUT", "graph/edges/{edge_id}?action=eliminate")
-    def eliminateEdge(
-        self,
-        edge_id,
-        properties,  # pylint: disable=unused-argument
-    ) -> EdgeData | None:
-        if response := self._invoke_request(data=json.dumps({"properties": 
properties})):
-            return EdgeData(response)
-        return None
-
-    @router.http("GET", "graph/edges/{edge_id}")
-    def getEdgeById(self, edge_id) -> EdgeData | None:  # pylint: 
disable=unused-argument
-        if response := self._invoke_request():
-            return EdgeData(response)
-        return None
-
-    def getEdgeByPage(
-        self,
-        label=None,
-        vertex_id=None,
-        direction=None,
-        limit=0,
-        page=None,
-        properties=None,
-    ):
-        path = "graph/edges?"
-        para = ""
-        if vertex_id:
-            if direction:
-                para = para + '&vertex_id="' + vertex_id + '"&direction=' + 
direction
-            else:
-                raise NotFoundError("Direction can not be empty.")
-        if label:
-            para = para + "&label=" + label
-        if properties:
-            para = para + "&properties=" + json.dumps(properties)
-        if page:
-            para += f"&page={page}"
-        else:
-            para += "&page"
-        if limit > 0:
-            para = para + "&limit=" + str(limit)
-        path = path + para[1:]
-        if response := self._sess.request(path):
-            return [EdgeData(item) for item in response["edges"]], 
response["page"]
-        return None, None
-
-    @router.http("DELETE", "graph/edges/{edge_id}")
-    def removeEdgeById(self, edge_id) -> dict:  # pylint: 
disable=unused-argument
-        return self._invoke_request()
-
-    def getVerticesById(self, vertex_ids) -> list[VertexData] | None:
-        if not vertex_ids:
-            return []
-        path = "traversers/vertices?"
-        for vertex_id in vertex_ids:
-            path += f'ids="{vertex_id}"&'  # pylint: 
disable=consider-using-join
-        path = path.rstrip("&")
-        if response := self._sess.request(path):
-            return [VertexData(item) for item in response["vertices"]]
-        return None
-
-    def getEdgesById(self, edge_ids) -> list[EdgeData] | None:
-        if not edge_ids:
-            return []
-        path = "traversers/edges?"
-        for vertex_id in edge_ids:
-            path += f"ids={vertex_id}&"  # pylint: disable=consider-using-join
-        path = path.rstrip("&")
-        if response := self._sess.request(path):
-            return [EdgeData(item) for item in response["edges"]]
-        return None
+# 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
+from urllib.parse import urlencode
+
+from pyhugegraph.api.common import HugeParamsBase
+from pyhugegraph.structure.edge_data import EdgeData
+from pyhugegraph.structure.vertex_data import VertexData
+from pyhugegraph.utils import huge_router as router
+from pyhugegraph.utils.exceptions import NotFoundError
+from pyhugegraph.utils.id_format import format_vertex_id, format_vertex_id_path
+
+
+class GraphManager(HugeParamsBase):
+    @router.http("POST", "graph/vertices")
+    def addVertex(self, label, properties, id=None):
+        data = {}
+        if id is not None:
+            data["id"] = id
+        data["label"] = label
+        data["properties"] = properties
+        if response := self._invoke_request(data=json.dumps(data)):
+            return VertexData(response)
+        return None
+
+    @router.http("POST", "graph/vertices/batch")
+    def addVertices(self, input_data):
+        data = []
+        for item in input_data:
+            data.append({"label": item[0], "properties": item[1]})
+        if response := self._invoke_request(data=json.dumps(data)):
+            return [VertexData({"id": item}) for item in response]
+        return None
+
+    def appendVertex(self, vertex_id, properties):
+        data = {"properties": properties}
+        path = 
f"graph/vertices/{format_vertex_id_path(vertex_id)}?action=append"
+        if response := self._sess.request(path, "PUT", data=json.dumps(data)):
+            return VertexData(response)
+        return None
+
+    def eliminateVertex(self, vertex_id, properties):
+        data = {"properties": properties}
+        path = 
f"graph/vertices/{format_vertex_id_path(vertex_id)}?action=eliminate"
+        if response := self._sess.request(path, "PUT", data=json.dumps(data)):
+            return VertexData(response)
+        return None
+
+    def getVertexById(self, vertex_id):
+        path = f"graph/vertices/{format_vertex_id_path(vertex_id)}"
+        if response := self._sess.request(path):
+            return VertexData(response)
+        return None
+
+    def getVertexByPage(self, label, limit, page=None, properties=None):
+        path = "graph/vertices?"
+        para = ""
+        para = para + "&label=" + label
+        if properties:
+            para = para + "&properties=" + json.dumps(properties)
+        if page:
+            para += f"&page={page}"
+        else:
+            para += "&page"
+        para = para + "&limit=" + str(limit)
+        path = path + para[1:]
+        if response := self._sess.request(path):
+            res = [VertexData(item) for item in response["vertices"]]
+            next_page = response["page"]
+            return res, next_page
+        return None, None
+
+    def getVertexByCondition(self, label="", limit=0, page=None, 
properties=None):
+        path = "graph/vertices?"
+        para = ""
+        if label:
+            para = para + "&label=" + label
+        if properties:
+            para = para + "&properties=" + json.dumps(properties)
+        if limit > 0:
+            para = para + "&limit=" + str(limit)
+        if page:
+            para += f"&page={page}"
+        else:
+            para += "&page"
+        path = path + para[1:]
+        if response := self._sess.request(path):
+            return [VertexData(item) for item in response["vertices"]]
+        return None
+
+    def removeVertexById(self, vertex_id):
+        path = f"graph/vertices/{format_vertex_id_path(vertex_id)}"
+        return self._sess.request(path, "DELETE")
+
+    @router.http("POST", "graph/edges")
+    def addEdge(self, edge_label, out_id, in_id, properties) -> EdgeData | 
None:
+        data = {
+            "label": edge_label,
+            "outV": out_id,
+            "inV": in_id,
+            "properties": properties,
+        }
+        if response := self._invoke_request(data=json.dumps(data)):
+            return EdgeData(response)
+        return None
+
+    @router.http("POST", "graph/edges/batch")
+    def addEdges(self, input_data) -> list[EdgeData] | None:
+        data = []
+        for item in input_data:
+            data.append(
+                {
+                    "label": item[0],
+                    "outV": item[1],
+                    "inV": item[2],
+                    "outVLabel": item[3],
+                    "inVLabel": item[4],
+                    "properties": item[5],
+                }
+            )
+        if response := self._invoke_request(data=json.dumps(data)):
+            return [EdgeData({"id": item}) for item in response]
+        return None
+
+    @router.http("PUT", "graph/edges/{edge_id}?action=append")
+    def appendEdge(
+        self,
+        edge_id,
+        properties,  # pylint: disable=unused-argument
+    ) -> EdgeData | None:
+        if response := self._invoke_request(data=json.dumps({"properties": 
properties})):
+            return EdgeData(response)
+        return None
+
+    @router.http("PUT", "graph/edges/{edge_id}?action=eliminate")
+    def eliminateEdge(
+        self,
+        edge_id,
+        properties,  # pylint: disable=unused-argument
+    ) -> EdgeData | None:
+        if response := self._invoke_request(data=json.dumps({"properties": 
properties})):
+            return EdgeData(response)
+        return None
+
+    @router.http("GET", "graph/edges/{edge_id}")
+    def getEdgeById(self, edge_id) -> EdgeData | None:  # pylint: 
disable=unused-argument
+        if response := self._invoke_request():
+            return EdgeData(response)
+        return None
+
+    def getEdgeByPage(
+        self,
+        label=None,
+        vertex_id=None,
+        direction=None,
+        limit=0,
+        page=None,
+        properties=None,
+    ):
+        path = "graph/edges?"
+        para = ""
+        if vertex_id is not None:
+            if direction:
+                vertex_query = urlencode({"vertex_id": 
format_vertex_id(vertex_id)})
+                para = para + "&" + vertex_query + "&direction=" + direction
+            else:
+                raise NotFoundError("Direction can not be empty.")
+        if label:
+            para = para + "&label=" + label
+        if properties:
+            para = para + "&properties=" + json.dumps(properties)
+        if page:
+            para += f"&page={page}"
+        else:
+            para += "&page"
+        if limit > 0:
+            para = para + "&limit=" + str(limit)
+        path = path + para[1:]
+        if response := self._sess.request(path):
+            return [EdgeData(item) for item in response["edges"]], 
response["page"]
+        return None, None
+
+    @router.http("DELETE", "graph/edges/{edge_id}")
+    def removeEdgeById(self, edge_id) -> dict:  # pylint: 
disable=unused-argument
+        return self._invoke_request()
+
+    def getVerticesById(self, vertex_ids) -> list[VertexData] | None:
+        if not vertex_ids:
+            return []
+        path = "traversers/vertices?"
+        query = urlencode([("ids", format_vertex_id(vertex_id)) for vertex_id 
in vertex_ids])
+        path += query
+        if response := self._sess.request(path):
+            return [VertexData(item) for item in response["vertices"]]
+        return None
+
+    def getEdgesById(self, edge_ids) -> list[EdgeData] | None:
+        if not edge_ids:
+            return []
+        path = "traversers/edges?"
+        for vertex_id in edge_ids:
+            path += f"ids={vertex_id}&"  # pylint: disable=consider-using-join
+        path = path.rstrip("&")
+        if response := self._sess.request(path):
+            return [EdgeData(item) for item in response["edges"]]
+        return None
diff --git a/hugegraph-python-client/src/pyhugegraph/api/traverser.py 
b/hugegraph-python-client/src/pyhugegraph/api/traverser.py
index f296313e..1d8363f9 100644
--- a/hugegraph-python-client/src/pyhugegraph/api/traverser.py
+++ b/hugegraph-python-client/src/pyhugegraph/api/traverser.py
@@ -15,56 +15,66 @@
 # specific language governing permissions and limitations
 # under the License.
 import json
+from urllib.parse import urlencode
 
 from pyhugegraph.api.common import HugeParamsBase
 from pyhugegraph.utils import huge_router as router
+from pyhugegraph.utils.id_format import format_vertex_id
 
 
 class TraverserManager(HugeParamsBase):
-    @router.http("GET", 
'traversers/kout?source="{source_id}"&max_depth={max_depth}')
-    def k_out(self, source_id, max_depth):  # pylint: disable=unused-argument
-        return self._invoke_request()
-
-    @router.http("GET", 
'traversers/kneighbor?source="{source_id}"&max_depth={max_depth}')
-    def k_neighbor(self, source_id, max_depth):  # pylint: 
disable=unused-argument
-        return self._invoke_request()
-
-    @router.http("GET", 
'traversers/sameneighbors?vertex="{vertex_id}"&other="{other_id}"')
-    def same_neighbors(self, vertex_id, other_id):  # pylint: 
disable=unused-argument
-        return self._invoke_request()
-
-    @router.http("GET", 
'traversers/jaccardsimilarity?vertex="{vertex_id}"&other="{other_id}"')
-    def jaccard_similarity(self, vertex_id, other_id):  # pylint: 
disable=unused-argument
-        return self._invoke_request()
-
-    @router.http(
-        "GET",
-        
'traversers/shortestpath?source="{source_id}"&target="{target_id}"&max_depth={max_depth}',
-    )
-    def shortest_path(self, source_id, target_id, max_depth):  # pylint: 
disable=unused-argument
-        return self._invoke_request()
-
-    @router.http(
-        "GET",
-        
'traversers/allshortestpaths?source="{source_id}"&target="{target_id}"&max_depth={max_depth}',
-    )
-    def all_shortest_paths(self, source_id, target_id, max_depth):  # pylint: 
disable=unused-argument
-        return self._invoke_request()
-
-    @router.http(
-        "GET",
-        
'traversers/weightedshortestpath?source="{source_id}"&target="{target_id}"'
-        "&weight={weight}&max_depth={max_depth}",
-    )
-    def weighted_shortest_path(self, source_id, target_id, weight, max_depth): 
 # pylint: disable=unused-argument
-        return self._invoke_request()
-
-    @router.http(
-        "GET",
-        
'traversers/singlesourceshortestpath?source="{source_id}"&max_depth={max_depth}',
-    )
-    def single_source_shortest_path(self, source_id, max_depth):  # pylint: 
disable=unused-argument
-        return self._invoke_request()
+    def _get_with_vertex_ids(self, endpoint, vertex_params, query_params):
+        params = [(name, format_vertex_id(value)) for name, value in 
vertex_params]
+        params.extend(query_params)
+        return self._sess.request(f"{endpoint}?{urlencode(params)}")
+
+    def k_out(self, source_id, max_depth):
+        return self._get_with_vertex_ids("traversers/kout", [("source", 
source_id)], [("max_depth", max_depth)])
+
+    def k_neighbor(self, source_id, max_depth):
+        return self._get_with_vertex_ids("traversers/kneighbor", [("source", 
source_id)], [("max_depth", max_depth)])
+
+    def same_neighbors(self, vertex_id, other_id):
+        return self._get_with_vertex_ids(
+            "traversers/sameneighbors",
+            [("vertex", vertex_id), ("other", other_id)],
+            [],
+        )
+
+    def jaccard_similarity(self, vertex_id, other_id):
+        return self._get_with_vertex_ids(
+            "traversers/jaccardsimilarity",
+            [("vertex", vertex_id), ("other", other_id)],
+            [],
+        )
+
+    def shortest_path(self, source_id, target_id, max_depth):
+        return self._get_with_vertex_ids(
+            "traversers/shortestpath",
+            [("source", source_id), ("target", target_id)],
+            [("max_depth", max_depth)],
+        )
+
+    def all_shortest_paths(self, source_id, target_id, max_depth):
+        return self._get_with_vertex_ids(
+            "traversers/allshortestpaths",
+            [("source", source_id), ("target", target_id)],
+            [("max_depth", max_depth)],
+        )
+
+    def weighted_shortest_path(self, source_id, target_id, weight, max_depth):
+        return self._get_with_vertex_ids(
+            "traversers/weightedshortestpath",
+            [("source", source_id), ("target", target_id)],
+            [("weight", weight), ("max_depth", max_depth)],
+        )
+
+    def single_source_shortest_path(self, source_id, max_depth):
+        return self._get_with_vertex_ids(
+            "traversers/singlesourceshortestpath",
+            [("source", source_id)],
+            [("max_depth", max_depth)],
+        )
 
     @router.http("POST", "traversers/multinodeshortestpath")
     def multi_node_shortest_path(
@@ -90,12 +100,12 @@ class TraverserManager(HugeParamsBase):
             )
         )
 
-    @router.http(
-        "GET",
-        
'traversers/paths?source="{source_id}"&target="{target_id}"&max_depth={max_depth}',
-    )
-    def paths(self, source_id, target_id, max_depth):  # pylint: 
disable=unused-argument
-        return self._invoke_request()
+    def paths(self, source_id, target_id, max_depth):
+        return self._get_with_vertex_ids(
+            "traversers/paths",
+            [("source", source_id), ("target", target_id)],
+            [("max_depth", max_depth)],
+        )
 
     @router.http("POST", "traversers/paths")
     def advanced_paths(
@@ -154,12 +164,12 @@ class TraverserManager(HugeParamsBase):
             )
         )
 
-    @router.http(
-        "GET",
-        
'traversers/crosspoints?source="{source_id}"&target="{target_id}"&max_depth={max_depth}',
-    )
-    def crosspoints(self, source_id, target_id, max_depth):  # pylint: 
disable=unused-argument
-        return self._invoke_request()
+    def crosspoints(self, source_id, target_id, max_depth):
+        return self._get_with_vertex_ids(
+            "traversers/crosspoints",
+            [("source", source_id), ("target", target_id)],
+            [("max_depth", max_depth)],
+        )
 
     @router.http("POST", "traversers/customizedcrosspoints")
     def customized_crosspoints(
@@ -184,13 +194,11 @@ class TraverserManager(HugeParamsBase):
             )
         )
 
-    @router.http("GET", 
'traversers/rings?source="{source_id}"&max_depth={max_depth}')
-    def rings(self, source_id, max_depth):  # pylint: disable=unused-argument
-        return self._invoke_request()
+    def rings(self, source_id, max_depth):
+        return self._get_with_vertex_ids("traversers/rings", [("source", 
source_id)], [("max_depth", max_depth)])
 
-    @router.http("GET", 
'traversers/rays?source="{source_id}"&max_depth={max_depth}')
-    def rays(self, source_id, max_depth):  # pylint: disable=unused-argument
-        return self._invoke_request()
+    def rays(self, source_id, max_depth):
+        return self._get_with_vertex_ids("traversers/rays", [("source", 
source_id)], [("max_depth", max_depth)])
 
     @router.http("POST", "traversers/fusiformsimilarity")
     def fusiform_similarity(
@@ -233,7 +241,7 @@ class TraverserManager(HugeParamsBase):
 
     @router.http("GET", "traversers/vertices")
     def vertices(self, ids):
-        params = {"ids": f'"{ids}"'}
+        params = {"ids": format_vertex_id(ids)}
         return self._invoke_request(params=params)
 
     @router.http("GET", "traversers/edges")
diff --git a/hugegraph-python-client/src/pyhugegraph/utils/id_format.py 
b/hugegraph-python-client/src/pyhugegraph/utils/id_format.py
new file mode 100644
index 00000000..7b4dbbfb
--- /dev/null
+++ b/hugegraph-python-client/src/pyhugegraph/utils/id_format.py
@@ -0,0 +1,54 @@
+# 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
+from numbers import Integral
+from urllib.parse import quote
+from uuid import UUID
+
+JAVA_LONG_MIN = -(2**63)
+JAVA_LONG_MAX = 2**63 - 1
+
+
+def format_vertex_id(vertex_id, allow_none: bool = False) -> str | None:
+    if vertex_id is None:
+        if allow_none:
+            return None
+        raise ValueError("The vertex id can't be None")
+
+    if isinstance(vertex_id, bool):
+        raise TypeError("The vertex id must be either str or integer")
+
+    uuid_prefix = ""
+    if isinstance(vertex_id, UUID):
+        uuid_prefix = "U"
+        vertex_id = str(vertex_id)
+
+    if isinstance(vertex_id, Integral):
+        if vertex_id < JAVA_LONG_MIN or vertex_id > JAVA_LONG_MAX:
+            raise ValueError("The vertex id integer must fit in Java signed 
long range")
+    elif not isinstance(vertex_id, str):
+        raise TypeError("The vertex id must be either str or integer")
+
+    return uuid_prefix + json.dumps(vertex_id, allow_nan=False)
+
+
+def format_vertex_id_path(vertex_id, allow_none: bool = False) -> str | None:
+    formatted_id = format_vertex_id(vertex_id, allow_none=allow_none)
+    if formatted_id is None:
+        return None
+    return quote(formatted_id, safe="")
diff --git a/hugegraph-python-client/src/tests/api/test_graph.py 
b/hugegraph-python-client/src/tests/api/test_graph.py
index 1c402dcd..f4872a4b 100644
--- a/hugegraph-python-client/src/tests/api/test_graph.py
+++ b/hugegraph-python-client/src/tests/api/test_graph.py
@@ -63,17 +63,33 @@ class TestGraphManager(unittest.TestCase):
         appended_vertex = self.graph.appendVertex(vertex.id, {"city": 
"Beijing"})
         self.assertEqual(appended_vertex.properties["city"], "Beijing")
 
+    def test_append_vertex_with_number_id(self):
+        vertex = self.graph.addVertex("department", {"name": "DepartmentA", 
"headcount": 10, "floor": 1})
+        appended_vertex = self.graph.appendVertex(vertex.id, {"headcount": 15})
+        self.assertEqual(appended_vertex.properties["headcount"], 15)
+
     def test_eliminate_vertex(self):
         vertex = self.graph.addVertex("person", {"name": "marko", "age": 29, 
"city": "Beijing"})
         self.graph.eliminateVertex(vertex.id, {"city": "Beijing"})
         eliminated_vertex = self.graph.getVertexById(vertex.id)
         self.assertIsNone(eliminated_vertex.properties.get("city"))
 
+    def test_eliminate_vertex_with_number_id(self):
+        vertex = self.graph.addVertex("department", {"name": "DepartmentA", 
"headcount": 10, "floor": 1})
+        self.graph.eliminateVertex(vertex.id, {"floor": 1})
+        eliminated_vertex = self.graph.getVertexById(vertex.id)
+        self.assertIsNone(eliminated_vertex.properties.get("floor"))
+
     def test_get_vertex_by_id(self):
         vertex = self.graph.addVertex("person", {"name": "Alice", "age": 20})
         retrieved_vertex = self.graph.getVertexById(vertex.id)
         self.assertEqual(retrieved_vertex.id, vertex.id)
 
+    def test_get_vertex_by_number_id(self):
+        vertex = self.graph.addVertex("department", {"name": "DepartmentA", 
"headcount": 10, "floor": 1})
+        retrieved_vertex = self.graph.getVertexById(vertex.id)
+        self.assertEqual(retrieved_vertex.id, vertex.id)
+
     def test_get_vertex_by_page(self):
         self.graph.addVertex("person", {"name": "Alice", "age": 20})
         self.graph.addVertex("person", {"name": "Bob", "age": 23})
@@ -97,6 +113,17 @@ class TestGraphManager(unittest.TestCase):
         except NotFoundError as e:
             self.assertTrue("Alice\\' does not exist" in str(e))
 
+    def test_remove_vertex_by_number_id(self):
+        vertex = self.graph.addVertex("department", {"name": "DepartmentA", 
"headcount": 10, "floor": 1})
+        self.graph.removeVertexById(vertex.id)
+        try:
+            self.graph.getVertexById(vertex.id)
+        except NotFoundError as e:
+            msg = f"\\'{vertex.id}\\' does not exist"
+            self.assertTrue(msg in str(e))
+        else:
+            self.fail("Expected NotFoundError after removing numeric-id 
vertex")
+
     def test_add_edge(self):
         vertex1 = self.graph.addVertex("person", {"name": "Alice", "age": 20})
         vertex2 = self.graph.addVertex("person", {"name": "Bob", "age": 23})
@@ -161,6 +188,25 @@ class TestGraphManager(unittest.TestCase):
         vertices = self.graph.getVerticesById([vertex1.id, vertex2.id])
         self.assertEqual(len(vertices), 2)
 
+    def test_number_id_query_and_traverser_paths(self):
+        department1 = self.graph.addVertex("department", {"name": 
"DepartmentA", "headcount": 10, "floor": 1})
+        department2 = self.graph.addVertex("department", {"name": 
"DepartmentB", "headcount": 20, "floor": 2})
+        self.graph.addEdge("reports_to", department1.id, department2.id, 
{"date": "2026-06-20"})
+
+        edges, _ = self.graph.getEdgeByPage("reports_to", department1.id, 
"OUT")
+        self.assertEqual(len(edges), 1)
+        self.assertEqual(edges[0].outV, department1.id)
+        self.assertEqual(edges[0].inV, department2.id)
+
+        vertices = self.graph.getVerticesById([department1.id, department2.id])
+        self.assertEqual({vertex.id for vertex in vertices}, {department1.id, 
department2.id})
+
+        traverser_vertex = self.client.traverser.vertices(department1.id)
+        self.assertEqual(traverser_vertex["vertices"][0]["id"], department1.id)
+
+        k_out_result = self.client.traverser.k_out(department1.id, 1)
+        self.assertIn(department2.id, k_out_result["vertices"])
+
     def test_get_edges_by_id(self):
         vertex1 = self.graph.addVertex("person", {"name": "Alice", "age": 20})
         vertex2 = self.graph.addVertex("person", {"name": "Bob", "age": 23})
diff --git a/hugegraph-python-client/src/tests/api/test_schema.py 
b/hugegraph-python-client/src/tests/api/test_schema.py
index 5c5b2148..56e8a12a 100644
--- a/hugegraph-python-client/src/tests/api/test_schema.py
+++ b/hugegraph-python-client/src/tests/api/test_schema.py
@@ -58,7 +58,12 @@ class TestSchemaManager(unittest.TestCase):
 
     def test_get_property_keys(self):
         property_keys = self.schema.getPropertyKeys()
-        self.assertEqual(7, len(property_keys))
+        property_key_names = {property_key.name for property_key in 
property_keys}
+        self.assertTrue(
+            {"name", "age", "city", "lang", "date", "price", "weight", 
"headcount", "floor"}.issubset(
+                property_key_names
+            )
+        )
 
     def test_get_property_key(self):
         property_key = self.schema.getPropertyKey("name")
@@ -66,7 +71,8 @@ class TestSchemaManager(unittest.TestCase):
 
     def test_get_vertex_labels(self):
         vertex_labels = self.schema.getVertexLabels()
-        self.assertEqual(3, len(vertex_labels))
+        vertex_label_names = {vertex_label.name for vertex_label in 
vertex_labels}
+        self.assertTrue({"person", "software", "book", 
"department"}.issubset(vertex_label_names))
 
     def test_get_vertex_label(self):
         vertex_label = self.schema.getVertexLabel("person")
@@ -74,7 +80,8 @@ class TestSchemaManager(unittest.TestCase):
 
     def test_get_edge_labels(self):
         edge_labels = self.schema.getEdgeLabels()
-        self.assertEqual(2, len(edge_labels))
+        edge_label_names = {edge_label.name for edge_label in edge_labels}
+        self.assertTrue({"knows", "created", 
"reports_to"}.issubset(edge_label_names))
 
     def test_get_edge_label(self):
         edge_label = self.schema.getEdgeLabel("knows")
diff --git a/hugegraph-python-client/src/tests/api/test_vertex_id_format.py 
b/hugegraph-python-client/src/tests/api/test_vertex_id_format.py
new file mode 100644
index 00000000..24e9950a
--- /dev/null
+++ b/hugegraph-python-client/src/tests/api/test_vertex_id_format.py
@@ -0,0 +1,156 @@
+# 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 decimal import Decimal
+from fractions import Fraction
+from types import SimpleNamespace
+from uuid import UUID
+
+import pytest
+from pyhugegraph.api.graph import GraphManager
+from pyhugegraph.api.traverser import TraverserManager
+from pyhugegraph.utils.id_format import format_vertex_id, format_vertex_id_path
+
+pytestmark = [pytest.mark.unit]
+
+
+class FakeSession:
+    def __init__(self, responses=None):
+        self.cfg = SimpleNamespace(graphspace=None, gs_supported=False)
+        self.calls = []
+        self.responses = list(responses or [])
+
+    def request(self, path, method="GET", **kwargs):
+        self.calls.append((path, method, kwargs))
+        if self.responses:
+            return self.responses.pop(0)
+        return {}
+
+
[email protected](
+    ("vertex_id", "expected"),
+    [
+        ("person:marko", '"person:marko"'),
+        ('a"b\\c', '"a\\"b\\\\c"'),
+        (123, "123"),
+        (-(2**63), str(-(2**63))),
+        (2**63 - 1, str(2**63 - 1)),
+        (UUID("12345678-1234-5678-1234-567812345678"), 
'U"12345678-1234-5678-1234-567812345678"'),
+    ],
+)
+def test_format_vertex_id_matches_hugegraph_json_literal_contract(vertex_id, 
expected):
+    assert format_vertex_id(vertex_id) == expected
+
+
+def test_format_vertex_id_rejects_bool_even_though_bool_is_int():
+    with pytest.raises(TypeError):
+        format_vertex_id(True)
+
+
[email protected]("vertex_id", [1.5, Decimal("1"), Fraction(1, 1), 1 + 
0j, 2**63, -(2**63) - 1])
+def test_format_vertex_id_rejects_non_java_long_numeric_values(vertex_id):
+    with pytest.raises((TypeError, ValueError)):
+        format_vertex_id(vertex_id)
+
+
+def test_format_vertex_id_allows_none_only_when_requested():
+    with pytest.raises(ValueError):
+        format_vertex_id(None)
+    assert format_vertex_id(None, allow_none=True) is None
+
+
+def test_format_vertex_id_path_quotes_json_literal_as_one_url_segment():
+    assert format_vertex_id_path("a/b?c#d&e") == "%22a%2Fb%3Fc%23d%26e%22"
+    assert format_vertex_id_path(123) == "123"
+    assert (
+        format_vertex_id_path(UUID("12345678-1234-5678-1234-567812345678"))
+        == "U%2212345678-1234-5678-1234-567812345678%22"
+    )
+
+
+def test_graph_vertex_path_formats_json_literal_ids():
+    session = FakeSession(
+        responses=[
+            {"id": "person:marko", "label": "person", "properties": {}},
+            {"id": 123, "label": "department", "properties": {}},
+            {"id": "a/b?c#d&e", "label": "book", "properties": {}},
+        ]
+    )
+    graph = GraphManager(session)
+
+    graph.getVertexById("person:marko")
+    graph.getVertexById(123)
+    graph.getVertexById("a/b?c#d&e")
+
+    assert session.calls[0][0] == "graph/vertices/%22person%3Amarko%22"
+    assert session.calls[1][0] == "graph/vertices/123"
+    assert session.calls[2][0] == "graph/vertices/%22a%2Fb%3Fc%23d%26e%22"
+
+
+def test_graph_edge_page_formats_and_encodes_vertex_id_query():
+    session = FakeSession(
+        responses=[
+            {"edges": [], "page": None},
+            {"edges": [], "page": None},
+        ]
+    )
+    graph = GraphManager(session)
+
+    graph.getEdgeByPage("knows", "person:marko", "OUT")
+    graph.getEdgeByPage("knows", 123, "OUT")
+
+    assert session.calls[0][0] == 
"graph/edges?vertex_id=%22person%3Amarko%22&direction=OUT&label=knows&page"
+    assert session.calls[1][0] == 
"graph/edges?vertex_id=123&direction=OUT&label=knows&page"
+
+
+def test_graph_vertices_by_id_formats_repeated_json_literal_query_params():
+    session = FakeSession(
+        responses=[
+            {
+                "vertices": [
+                    {"id": 123},
+                    {"id": "person:marko"},
+                    {"id": "a&b"},
+                ]
+            }
+        ]
+    )
+    graph = GraphManager(session)
+
+    vertices = graph.getVerticesById([123, "person:marko", "a&b"])
+
+    assert [vertex.id for vertex in vertices] == [123, "person:marko", "a&b"]
+    assert session.calls[0][0] == 
"traversers/vertices?ids=123&ids=%22person%3Amarko%22&ids=%22a%26b%22"
+
+
+def test_traverser_formats_vertex_id_query_params():
+    session = FakeSession(
+        responses=[
+            {"same_neighbors": []},
+            {"path": []},
+            {"vertices": []},
+        ]
+    )
+    traverser = TraverserManager(session)
+
+    traverser.same_neighbors("person:marko", 456)
+    traverser.shortest_path(123, "person:josh", 3)
+    traverser.k_out(123, 2)
+
+    assert session.calls[0][0] == 
"traversers/sameneighbors?vertex=%22person%3Amarko%22&other=456"
+    assert session.calls[1][0] == 
"traversers/shortestpath?source=123&target=%22person%3Ajosh%22&max_depth=3"
+    assert session.calls[2][0] == "traversers/kout?source=123&max_depth=2"
diff --git a/hugegraph-python-client/src/tests/client_utils.py 
b/hugegraph-python-client/src/tests/client_utils.py
index 7d209cc6..73309187 100644
--- a/hugegraph-python-client/src/tests/client_utils.py
+++ b/hugegraph-python-client/src/tests/client_utils.py
@@ -70,6 +70,8 @@ class ClientUtils:
         schema.propertyKey("date").asDate().ifNotExist().create()
         schema.propertyKey("price").asInt().ifNotExist().create()
         schema.propertyKey("weight").asDouble().ifNotExist().create()
+        schema.propertyKey("headcount").asInt().ifNotExist().create()
+        schema.propertyKey("floor").asInt().ifNotExist().create()
 
     def init_vertex_label(self):
         schema = self.schema
@@ -82,6 +84,9 @@ class ClientUtils:
         schema.vertexLabel("book").useCustomizeStringId().properties("name", 
"price").nullableKeys(
             "price"
         ).ifNotExist().create()
+        schema.vertexLabel("department").properties("name", "headcount", 
"floor").nullableKeys(
+            "floor"
+        ).ifNotExist().create()
 
     def init_edge_label(self):
         schema = self.schema
@@ -91,6 +96,9 @@ class ClientUtils:
         
schema.edgeLabel("created").sourceLabel("person").targetLabel("software").properties(
             "date", "city"
         ).nullableKeys("city").ifNotExist().create()
+        
schema.edgeLabel("reports_to").sourceLabel("department").targetLabel("department").properties(
+            "date"
+        ).ifNotExist().create()
 
     def init_index_label(self):
         schema = self.schema


Reply via email to