Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion hugegraph-python-client/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ dependencies = [
"requests",
"setuptools",
"urllib3",
"rich",
"rich"
]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Removing the trailing comma after "rich" is unrelated to this bug fix and adds diff noise. TOML allows trailing commas; the original style is consistent with the rest of the list. Please revert this hunk.


[project.urls]
Expand Down
14 changes: 8 additions & 6 deletions hugegraph-python-client/src/pyhugegraph/api/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,21 @@ def addVertices(self, input_data):
return [VertexData({"id": item}) for item in response]
return None

@router.http("PUT", 'graph/vertices/"{vertex_id}"?action=append')
@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')
@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}"')
@router.http("GET", 'graph/vertices/{vertex_id}')
def getVertexById(self, vertex_id): # pylint: disable=unused-argument
if response := self._invoke_request():
return VertexData(response)
Expand Down Expand Up @@ -101,7 +101,7 @@ def getVertexByCondition(self, label="", limit=0, page=None, properties=None):
return [VertexData(item) for item in response["vertices"]]
return None

@router.http("DELETE", 'graph/vertices/"{vertex_id}"')
@router.http("DELETE", 'graph/vertices/{vertex_id}')
def removeVertexById(self, vertex_id): # pylint: disable=unused-argument
return self._invoke_request()

Expand Down Expand Up @@ -200,8 +200,10 @@ 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
quoted_vertex_ids = map(json.dumps, vertex_ids)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ json.dumps without ensure_ascii=False will Unicode-escape non-ASCII vertex IDs — json.dumps("\u4eba\u540d") produces '"\\u4eba\\u540d"' instead of '"\u4eba\u540d"', causing HugeGraph lookups to fail for any non-ASCII string vertex ID.

Suggested change
quoted_vertex_ids = (json.dumps(vid, ensure_ascii=False) for vid in vertex_ids)

for vertex_id in quoted_vertex_ids:
path += f'ids={vertex_id}&' # pylint: disable=consider-using-join
path = path.rstrip("&")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ No test added for getVerticesById with numeric IDs. This is the only changed code path in this method but none of the new test cases call it with an integer vertex ID. Please add a test (once the department schema ID strategy is fixed):

def test_get_vertices_by_number_id(self):
    vertex = self.graph.addVertex("department", {"name": "DeptA", "headcount": 10, "floor": 1})
    result = self.graph.getVerticesById([vertex.id])
    self.assertIsNotNone(result)
    self.assertEqual(result[0].id, vertex.id)

if response := self._sess.request(path):
return [VertexData(item) for item in response["vertices"]]
Expand Down
32 changes: 23 additions & 9 deletions hugegraph-python-client/src/pyhugegraph/utils/huge_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@

import functools
import inspect
import re
import threading
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, ClassVar

from pyhugegraph.utils.log import log
from pyhugegraph.utils.path_parser import PathParser
from pyhugegraph.utils.util import ResponseValidation

if TYPE_CHECKING:
Expand Down Expand Up @@ -100,6 +100,8 @@ def http(method: str, path: str) -> Callable:
Returns:
Callable: The decorator function.
"""
# Pre-parse the path template for efficiency
path_parser = PathParser(path)

def decorator(func: Callable) -> Callable:
"""Decorator function that modifies the original function."""
Expand All @@ -118,8 +120,8 @@ def wrapper(self: "HGraphContext", *args: Any, **kwargs: Any) -> Any:
Returns:
Any: The result of the decorated function.
"""
# If the pathinfo contains placeholders, format it with the actual arguments
if re.search(r"{\w+}", path):
# If the path has parameters, format it with actual arguments
if path_parser.params:
sig = inspect.signature(func)
bound_args = sig.bind(self, *args, **kwargs)
bound_args.apply_defaults()
Expand All @@ -131,7 +133,7 @@ def wrapper(self: "HGraphContext", *args: Any, **kwargs: Any) -> Any:
# only mounts UserAPI/AccessAPI/BelongAPI/TargetAPI under
# /graphspaces/{graphspace}/auth/..., so we fail fast when the
# session lacks one rather than producing an unreachable URL.
if "{graphspace}" in path:
if "graphspace" in path_parser.params:
graphspace_arg = all_kwargs.get("graphspace")
graphspace_cfg = getattr(self.session.cfg, "graphspace", None)
gs_supported = getattr(self.session.cfg, "gs_supported", False)
Expand All @@ -147,9 +149,16 @@ def wrapper(self: "HGraphContext", *args: Any, **kwargs: Any) -> Any:
raise ValueError(f"Expected graphspace-prefixed path, got: {path}")

all_kwargs["graphspace"] = graphspace_arg or graphspace_cfg
formatted_path = path.format(**all_kwargs)
formatted_path = path_parser.format(all_kwargs)
elif "vertex_id" in path_parser.params:
# Handle vertex_id quoting: string types need quotes, numbers don't
vertex_id = all_kwargs.get("vertex_id")
is_string = isinstance(vertex_id, str)
formatted_path = path_parser.format(
all_kwargs, quoted_override={"vertex_id": is_string}
)
else:
formatted_path = path.format(**all_kwargs)
formatted_path = path_parser.format(all_kwargs)
else:
formatted_path = path

Expand All @@ -166,10 +175,13 @@ def wrapper(self: "HGraphContext", *args: Any, **kwargs: Any) -> Any:


class RouterMixin:
def _invoke_request_registered(self, placeholders: dict | None = None, validator=None, **kwargs: Any):
def _invoke_request_registered(
self, placeholders: dict | None = None, validator=None, **kwargs: Any
):
"""
Make an HTTP request using the stored partial request function.
Args:
placeholders: Dictionary of placeholder values for path formatting.
**kwargs (Any): Keyword arguments to be passed to the request function.
Returns:
Any: The response from the HTTP request.
Expand All @@ -180,9 +192,11 @@ def _invoke_request_registered(self, placeholders: dict | None = None, validator
fname = frame.f_code.co_name
route = RouterRegistry().routers.get(f"{self.__class__.__name__}.{fname}")

if re.search(r"{\w+}", route.path):
# Use PathParser to format the path
path_parser = PathParser(route.path)
if path_parser.params:
assert placeholders is not None, "Placeholders must be provided"
formatted_path = route.path.format(**placeholders)
formatted_path = path_parser.format(placeholders)
else:
formatted_path = route.path

Expand Down
Loading
Loading