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