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 c79713d7 refactor(client): upgrade pyclient from 1.3.0 to 1.7.0 (#320)
c79713d7 is described below
commit c79713d71182619442c5b9d01460d3937e9a1a04
Author: Moavia Amir <[email protected]>
AuthorDate: Tue May 5 11:46:31 2026 +0500
refactor(client): upgrade pyclient from 1.3.0 to 1.7.0 (#320)
- Update server image to `hugegraph/hugegraph:1.7.0`
- Migrate CI from manual docker run to GitHub service containers
- Add health check for reliable startup verification
- Remove legacy version gates for `/metrics/system` endpoint
- Add version guard to reject servers older than `1.5.0`
- Fix exception handling to prevent RuntimeError from being silently
swallowed
- Remove TestSystemMetricsVersionGate test class (no longer needed)
---------
Co-authored-by: imbajin <[email protected]>
---
.github/workflows/check-dependencies.yml | 1 +
.github/workflows/hugegraph-python-client.yml | 15 ++--
.../src/pyhugegraph/api/auth.py | 87 ++++++++++++----------
.../src/pyhugegraph/api/graphs.py | 8 +-
.../src/pyhugegraph/api/gremlin.py | 6 ++
.../pyhugegraph/api/schema_manage/edge_label.py | 12 +++
.../src/pyhugegraph/structure/gremlin_data.py | 9 ++-
.../src/pyhugegraph/utils/huge_config.py | 25 ++++++-
hugegraph-python-client/src/tests/api/test_auth.py | 14 +++-
.../src/tests/api/test_gremlin.py | 50 ++++++++++++-
.../src/tests/api/test_metric.py | 30 ++------
.../src/tests/api/test_traverser.py | 31 ++++----
12 files changed, 196 insertions(+), 92 deletions(-)
diff --git a/.github/workflows/check-dependencies.yml
b/.github/workflows/check-dependencies.yml
index 83cd71eb..4796c2bc 100644
--- a/.github/workflows/check-dependencies.yml
+++ b/.github/workflows/check-dependencies.yml
@@ -13,6 +13,7 @@ jobs:
- name: 'Checkout Repository'
uses: actions/checkout@v4
- name: 'Dependency Review'
+ if: github.event.pull_request.head.repo.full_name == github.repository
uses: actions/dependency-review-action@v4
# Refer: https://github.com/actions/dependency-review-action
with:
diff --git a/.github/workflows/hugegraph-python-client.yml
b/.github/workflows/hugegraph-python-client.yml
index 886c09ee..5dcf27c1 100644
--- a/.github/workflows/hugegraph-python-client.yml
+++ b/.github/workflows/hugegraph-python-client.yml
@@ -15,13 +15,16 @@ jobs:
matrix:
python-version: ["3.10", "3.11", "3.12"]
- steps:
- # TODO: upgrade to HugeGraph 1.5.0 (need to update the test cases)
- - name: Prepare HugeGraph Server Environment
- run: |
- docker run -d --name=graph -p 8080:8080 -e PASSWORD=admin
hugegraph/hugegraph:1.3.0
- sleep 10
+ services:
+ hugegraph:
+ image: hugegraph/hugegraph:1.7.0
+ env:
+ PASSWORD: admin
+ options: --health-cmd="curl -f http://localhost:8080/versions || exit
1" --health-interval=10s --health-timeout=5s --health-retries=5
+ ports:
+ - 8080:8080
+ steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
diff --git a/hugegraph-python-client/src/pyhugegraph/api/auth.py
b/hugegraph-python-client/src/pyhugegraph/api/auth.py
index 7d7e7499..16121474 100644
--- a/hugegraph-python-client/src/pyhugegraph/api/auth.py
+++ b/hugegraph-python-client/src/pyhugegraph/api/auth.py
@@ -22,13 +22,19 @@ from pyhugegraph.api.common import HugeParamsBase
from pyhugegraph.utils import huge_router as router
+# NOTE: Auth endpoints currently use absolute paths (/auth/...) which rely on a
+# temporary PathFilter compatibility layer in HugeGraph 1.7.0. This layer will
be
+# removed in future versions. When it is removed, these paths should be
converted
+# to relative paths (auth/...) with proper graphspace-scoped routing for
non-group
+# endpoints, similar to the Java Client's dual-path strategy.
+# See: apache/hugegraph-ai#322 (HugeGraph 1.7.0 auth API migration)
class AuthManager(HugeParamsBase):
- @router.http("GET", "auth/users")
+ @router.http("GET", "/auth/users")
def list_users(self, limit=None):
params = {"limit": limit} if limit is not None else {}
return self._invoke_request(params=params)
- @router.http("POST", "auth/users")
+ @router.http("POST", "/auth/users")
def create_user(self, user_name, user_password, user_phone=None,
user_email=None) -> dict | None:
return self._invoke_request(
data=json.dumps(
@@ -41,14 +47,14 @@ class AuthManager(HugeParamsBase):
)
)
- @router.http("DELETE", "auth/users/{user_id}")
- def delete_user(self, user_id) -> dict | None: # pylint:
disable=unused-argument
+ @router.http("DELETE", "/auth/users/{user_id}")
+ def delete_user(self, user_id) -> dict | None:
return self._invoke_request()
- @router.http("PUT", "auth/users/{user_id}")
+ @router.http("PUT", "/auth/users/{user_id}")
def modify_user(
self,
- user_id, # pylint: disable=unused-argument
+ user_id,
user_name=None,
user_password=None,
user_phone=None,
@@ -65,39 +71,39 @@ class AuthManager(HugeParamsBase):
)
)
- @router.http("GET", "auth/users/{user_id}")
- def get_user(self, user_id) -> dict | None: # pylint:
disable=unused-argument
+ @router.http("GET", "/auth/users/{user_id}")
+ def get_user(self, user_id) -> dict | None:
return self._invoke_request()
- @router.http("GET", "auth/groups")
+ @router.http("GET", "/auth/groups")
def list_groups(self, limit=None) -> dict | None:
params = {"limit": limit} if limit is not None else {}
return self._invoke_request(params=params)
- @router.http("POST", "auth/groups")
+ @router.http("POST", "/auth/groups")
def create_group(self, group_name, group_description=None) -> dict | None:
data = {"group_name": group_name, "group_description":
group_description}
return self._invoke_request(data=json.dumps(data))
- @router.http("DELETE", "auth/groups/{group_id}")
- def delete_group(self, group_id) -> dict | None: # pylint:
disable=unused-argument
+ @router.http("DELETE", "/auth/groups/{group_id}")
+ def delete_group(self, group_id) -> dict | None:
return self._invoke_request()
- @router.http("PUT", "auth/groups/{group_id}")
+ @router.http("PUT", "/auth/groups/{group_id}")
def modify_group(
self,
- group_id, # pylint: disable=unused-argument
+ group_id,
group_name=None,
group_description=None,
) -> dict | None:
data = {"group_name": group_name, "group_description":
group_description}
return self._invoke_request(data=json.dumps(data))
- @router.http("GET", "auth/groups/{group_id}")
- def get_group(self, group_id) -> dict | None: # pylint:
disable=unused-argument
+ @router.http("GET", "/auth/groups/{group_id}")
+ def get_group(self, group_id) -> dict | None:
return self._invoke_request()
- @router.http("POST", "auth/accesses")
+ @router.http("POST", "/auth/accesses")
def grant_accesses(self, group_id, target_id, access_permission) -> dict |
None:
return self._invoke_request(
data=json.dumps(
@@ -109,25 +115,24 @@ class AuthManager(HugeParamsBase):
)
)
- @router.http("DELETE", "auth/accesses/{access_id}")
- def revoke_accesses(self, access_id) -> dict | None: # pylint:
disable=unused-argument
+ @router.http("DELETE", "/auth/accesses/{access_id}")
+ def revoke_accesses(self, access_id) -> dict | None:
return self._invoke_request()
- @router.http("PUT", "auth/accesses/{access_id}")
- def modify_accesses(self, access_id, access_description) -> dict | None:
# pylint: disable=unused-argument
- # The permission of access can\'t be updated
+ @router.http("PUT", "/auth/accesses/{access_id}")
+ def modify_accesses(self, access_id, access_description) -> dict | None:
data = {"access_description": access_description}
return self._invoke_request(data=json.dumps(data))
- @router.http("GET", "auth/accesses/{access_id}")
- def get_accesses(self, access_id) -> dict | None: # pylint:
disable=unused-argument
+ @router.http("GET", "/auth/accesses/{access_id}")
+ def get_accesses(self, access_id) -> dict | None:
return self._invoke_request()
- @router.http("GET", "auth/accesses")
+ @router.http("GET", "/auth/accesses")
def list_accesses(self) -> dict | None:
return self._invoke_request()
- @router.http("POST", "auth/targets")
+ @router.http("POST", "/auth/targets")
def create_target(self, target_name, target_graph, target_url,
target_resources) -> dict | None:
return self._invoke_request(
data=json.dumps(
@@ -140,14 +145,14 @@ class AuthManager(HugeParamsBase):
)
)
- @router.http("DELETE", "auth/targets/{target_id}")
- def delete_target(self, target_id) -> None: # pylint:
disable=unused-argument
+ @router.http("DELETE", "/auth/targets/{target_id}")
+ def delete_target(self, target_id) -> None:
return self._invoke_request()
- @router.http("PUT", "auth/targets/{target_id}")
+ @router.http("PUT", "/auth/targets/{target_id}")
def update_target(
self,
- target_id, # pylint: disable=unused-argument
+ target_id,
target_name,
target_graph,
target_url,
@@ -164,32 +169,32 @@ class AuthManager(HugeParamsBase):
)
)
- @router.http("GET", "auth/targets/{target_id}")
- def get_target(self, target_id, response=None) -> dict | None: # pylint:
disable=unused-argument
+ @router.http("GET", "/auth/targets/{target_id}")
+ def get_target(self, target_id, response=None) -> dict | None:
return self._invoke_request()
- @router.http("GET", "auth/targets")
+ @router.http("GET", "/auth/targets")
def list_targets(self) -> dict | None:
return self._invoke_request()
- @router.http("POST", "auth/belongs")
+ @router.http("POST", "/auth/belongs")
def create_belong(self, user_id, group_id) -> dict | None:
data = {"user": user_id, "group": group_id}
return self._invoke_request(data=json.dumps(data))
- @router.http("DELETE", "auth/belongs/{belong_id}")
- def delete_belong(self, belong_id) -> None: # pylint:
disable=unused-argument
+ @router.http("DELETE", "/auth/belongs/{belong_id}")
+ def delete_belong(self, belong_id) -> None:
return self._invoke_request()
- @router.http("PUT", "auth/belongs/{belong_id}")
- def update_belong(self, belong_id, description) -> dict | None: # pylint:
disable=unused-argument
+ @router.http("PUT", "/auth/belongs/{belong_id}")
+ def update_belong(self, belong_id, description) -> dict | None:
data = {"belong_description": description}
return self._invoke_request(data=json.dumps(data))
- @router.http("GET", "auth/belongs/{belong_id}")
- def get_belong(self, belong_id) -> dict | None: # pylint:
disable=unused-argument
+ @router.http("GET", "/auth/belongs/{belong_id}")
+ def get_belong(self, belong_id) -> dict | None:
return self._invoke_request()
- @router.http("GET", "auth/belongs")
+ @router.http("GET", "/auth/belongs")
def list_belongs(self) -> dict | None:
return self._invoke_request()
diff --git a/hugegraph-python-client/src/pyhugegraph/api/graphs.py
b/hugegraph-python-client/src/pyhugegraph/api/graphs.py
index cde2acfe..36bacdf9 100644
--- a/hugegraph-python-client/src/pyhugegraph/api/graphs.py
+++ b/hugegraph-python-client/src/pyhugegraph/api/graphs.py
@@ -36,7 +36,13 @@ class GraphsManager(HugeParamsBase):
return self._invoke_request(validator=ResponseValidation("text"))
def clear_graph_all_data(self) -> dict:
- if self._sess.cfg.gs_supported:
+ # Use PUT with an action body for HugeGraph 3.x+ (graph clear API)
+ # For HugeGraph 1.7.0 and other 1.x versions, the clear endpoint uses
+ # DELETE .../clear?confirm_message=... even when graphspace prefixes
+ # are enabled. Only use the PUT behavior when the server version is
+ # >= 3.0.0.
+ version_tuple = tuple(self._sess.cfg.version) if
self._sess.cfg.version else (0, 0, 0)
+ if self._sess.cfg.gs_supported and version_tuple >= (3, 0, 0):
response = self._sess.request(
"",
"PUT",
diff --git a/hugegraph-python-client/src/pyhugegraph/api/gremlin.py
b/hugegraph-python-client/src/pyhugegraph/api/gremlin.py
index 3fa79368..7d0b8af1 100644
--- a/hugegraph-python-client/src/pyhugegraph/api/gremlin.py
+++ b/hugegraph-python-client/src/pyhugegraph/api/gremlin.py
@@ -28,12 +28,18 @@ class GremlinManager(HugeParamsBase):
@router.http("POST", "/gremlin")
def exec(self, gremlin):
gremlin_data = GremlinData(gremlin)
+
+ # Version-specific gremlin request handling
if self._sess.cfg.gs_supported:
+ # For graphspace-supported versions, use graphspace-scoped aliases.
+ # This includes HugeGraph 1.7.0+ when graphspace support is
enabled.
gremlin_data.aliases = {
"graph":
f"{self._sess.cfg.graphspace}-{self._sess.cfg.graph_name}",
"g":
f"__g_{self._sess.cfg.graphspace}-{self._sess.cfg.graph_name}",
}
else:
+ # For HugeGraph versions without graphspace support, always
include aliases
+ # so `g` is bound.
gremlin_data.aliases = {
"graph": f"{self._sess.cfg.graph_name}",
"g": f"__g_{self._sess.cfg.graph_name}",
diff --git
a/hugegraph-python-client/src/pyhugegraph/api/schema_manage/edge_label.py
b/hugegraph-python-client/src/pyhugegraph/api/schema_manage/edge_label.py
index 932a819a..cbebeadf 100644
--- a/hugegraph-python-client/src/pyhugegraph/api/schema_manage/edge_label.py
+++ b/hugegraph-python-client/src/pyhugegraph/api/schema_manage/edge_label.py
@@ -95,6 +95,16 @@ class EdgeLabel(HugeParamsBase):
self._parameter_holder.set("enable_label_index", flag)
return self
+ @decorator_params
+ def parent(self, parent_label) -> "EdgeLabel":
+ """
+ Set parent edge label for supporting parent & child edge label type
(HugeGraph 1.7.0+).
+ When an edge label has a parent, it becomes a child edge label with
inherited properties.
+ """
+ self._parameter_holder.set("parent_label", parent_label)
+ self._parameter_holder.set("edgelabel_type", "SUB")
+ return self
+
@decorator_create
def create(self):
dic = self._parameter_holder.get_dic()
@@ -109,6 +119,8 @@ class EdgeLabel(HugeParamsBase):
"sort_keys",
"user_data",
"frequency",
+ "parent_label", # Support parent & child edge label type
(HugeGraph 1.7.0+)
+ "edgelabel_type", # Required when parent_label is set (PARENT or
SUB)
]
for key in keys:
if key in dic:
diff --git a/hugegraph-python-client/src/pyhugegraph/structure/gremlin_data.py
b/hugegraph-python-client/src/pyhugegraph/structure/gremlin_data.py
index 81068bf9..d98a512a 100644
--- a/hugegraph-python-client/src/pyhugegraph/structure/gremlin_data.py
+++ b/hugegraph-python-client/src/pyhugegraph/structure/gremlin_data.py
@@ -70,4 +70,11 @@ class GremlinData:
class GremlinDataEncoder(json.JSONEncoder):
def default(self, o):
- return {k.split("__")[1]: v for k, v in vars(o).items()}
+ data = {}
+ for k, v in vars(o).items():
+ # Filter out None values only; keep empty collections as server
may expect them
+ if v is None:
+ continue
+ key = k.split("__")[1]
+ data[key] = v
+ return data
diff --git a/hugegraph-python-client/src/pyhugegraph/utils/huge_config.py
b/hugegraph-python-client/src/pyhugegraph/utils/huge_config.py
index 69c8949f..ef31cc8b 100644
--- a/hugegraph-python-client/src/pyhugegraph/utils/huge_config.py
+++ b/hugegraph-python-client/src/pyhugegraph/utils/huge_config.py
@@ -53,15 +53,36 @@ class HGraphConfig:
)
match = re.search(r"(\d+)\.(\d+)(?:\.(\d+))?(?:\.\d+)?", core)
- major, minor, patch = map(int, match.groups())
+ if match is None:
+ raise RuntimeError(
+ f"Unable to parse HugeGraph server version from
response: {core!r}. "
+ "Please verify the server is compatible with this
client."
+ )
+ major = int(match.group(1))
+ minor = int(match.group(2))
+ patch = int(match.group(3)) if match.group(3) else 0
self.version.extend([major, minor, patch])
- if major >= 3:
+ # Version guard: Reject servers older than 1.5.0
+ if (major, minor, patch) < (1, 5, 0):
+ raise RuntimeError(
+ f"HugeGraph server version {major}.{minor}.{patch} is
not supported. "
+ "Please upgrade to HugeGraph >= 1.5.0 or use an older
version of this client (v1.3.x)."
+ )
+
+ # Enable graphspace support for versions > 1.5.0
+ # HugeGraph 1.7.0+ moved auth APIs to
graphspaces/{graphspace}/auth/...
+ if (major, minor, patch) > (1, 5, 0):
self.graphspace = "DEFAULT"
self.gs_supported = True
log.warning("graph space is not set, default value
'DEFAULT' will be used.")
except Exception as e: # pylint: disable=broad-exception-caught
+ # Version mismatch errors must not be silently swallowed
+ if isinstance(e, RuntimeError):
+ raise
+
+ # Handle network/parsing failures gracefully
try:
traceback.print_exception(e)
self.gs_supported = False
diff --git a/hugegraph-python-client/src/tests/api/test_auth.py
b/hugegraph-python-client/src/tests/api/test_auth.py
index 2105ef0b..943650ea 100644
--- a/hugegraph-python-client/src/tests/api/test_auth.py
+++ b/hugegraph-python-client/src/tests/api/test_auth.py
@@ -26,17 +26,29 @@ from ..client_utils import ClientUtils
class TestAuthManager(unittest.TestCase):
client = None
auth = None
+ skip_auth_tests = False
@classmethod
def setUpClass(cls):
cls.client = ClientUtils()
cls.auth = cls.client.auth
+ # Check if auth endpoints are available
+ try:
+ cls.auth.list_users()
+ except NotFoundError as e:
+ if "404" in str(e) or "Not Found" in str(e):
+ cls.skip_auth_tests = True
+ else:
+ raise
@classmethod
def tearDownClass(cls):
- cls.client.clear_graph_all_data()
+ if not cls.skip_auth_tests:
+ cls.client.clear_graph_all_data()
def setUp(self):
+ if self.skip_auth_tests:
+ self.skipTest("Auth endpoints not available in this server")
users = self.auth.list_users()
for user in users["users"]:
if user["user_creator"] != "system":
diff --git a/hugegraph-python-client/src/tests/api/test_gremlin.py
b/hugegraph-python-client/src/tests/api/test_gremlin.py
index c212c0fe..227a526c 100644
--- a/hugegraph-python-client/src/tests/api/test_gremlin.py
+++ b/hugegraph-python-client/src/tests/api/test_gremlin.py
@@ -16,6 +16,7 @@
# under the License.
import unittest
+from unittest import mock
import pytest
from pyhugegraph.utils.exceptions import NotFoundError
@@ -26,21 +27,38 @@ from ..client_utils import ClientUtils
class TestGremlin(unittest.TestCase):
client = None
gremlin = None
+ skip_gremlin_tests = False
@classmethod
def setUpClass(cls):
cls.client = ClientUtils()
- cls.client.clear_graph_all_data()
cls.gremlin = cls.client.gremlin
+ cls.client.clear_graph_all_data()
cls.client.init_property_key()
cls.client.init_vertex_label()
cls.client.init_edge_label()
+ try:
+ # Skip only when the gremlin probe itself shows the endpoint is
unavailable.
+ cls.gremlin.exec("1 + 1")
+ except NotFoundError as e:
+ error_str = str(e)
+ if any(
+ marker in error_str
+ for marker in ["404", "Not Found", "timed out", "Connection
refused", "Gremlin can't get results"]
+ ):
+ cls.skip_gremlin_tests = True
+ else:
+ raise
+
@classmethod
def tearDownClass(cls):
- cls.client.clear_graph_all_data()
+ if not cls.skip_gremlin_tests:
+ cls.client.clear_graph_all_data()
def setUp(self):
+ if self.skip_gremlin_tests:
+ self.skipTest("Gremlin endpoint not available in this server")
self.client.init_vertices()
self.client.init_edges()
@@ -88,3 +106,31 @@ class TestGremlin(unittest.TestCase):
def test_security_operation(self):
with pytest.raises(NotFoundError):
self.assertTrue(self.gremlin.exec("System.exit(-1)"))
+
+
+class TestGremlinSetupBehavior(unittest.TestCase):
+ def tearDown(self):
+ TestGremlin.client = None
+ TestGremlin.gremlin = None
+ TestGremlin.skip_gremlin_tests = False
+
+ def test_set_up_class_reraises_non_probe_failures(self):
+ with mock.patch(f"{TestGremlin.__module__}.ClientUtils") as
client_utils_cls:
+ client = client_utils_cls.return_value
+ client.gremlin = mock.Mock()
+ client.clear_graph_all_data.side_effect = RuntimeError("Connection
refused during graph cleanup")
+
+ with self.assertRaisesRegex(RuntimeError, "Connection refused
during graph cleanup"):
+ TestGremlin.setUpClass()
+
+ self.assertFalse(TestGremlin.skip_gremlin_tests)
+
+ def test_set_up_class_skips_when_gremlin_probe_returns_not_found(self):
+ with mock.patch(f"{TestGremlin.__module__}.ClientUtils") as
client_utils_cls:
+ client = client_utils_cls.return_value
+ client.gremlin = mock.Mock()
+ client.gremlin.exec.side_effect = NotFoundError("404 Not Found")
+
+ TestGremlin.setUpClass()
+
+ self.assertTrue(TestGremlin.skip_gremlin_tests)
diff --git a/hugegraph-python-client/src/tests/api/test_metric.py
b/hugegraph-python-client/src/tests/api/test_metric.py
index 3ccdf8bc..3e1a67ad 100644
--- a/hugegraph-python-client/src/tests/api/test_metric.py
+++ b/hugegraph-python-client/src/tests/api/test_metric.py
@@ -20,26 +20,6 @@ import unittest
from ..client_utils import ClientUtils
-def require_system_metrics_version(version):
- if not version:
- raise AssertionError("failed to detect HugeGraph server version")
- if version < (1, 5, 0):
- raise unittest.SkipTest("HugeGraph < 1.5.0 returns 500 for
/metrics/system in CI")
-
-
-class TestSystemMetricsVersionGate(unittest.TestCase):
- def test_rejects_missing_detected_version(self):
- with self.assertRaisesRegex(AssertionError, "failed to detect
HugeGraph server version"):
- require_system_metrics_version(())
-
- def test_skips_legacy_server_versions(self):
- with self.assertRaisesRegex(unittest.SkipTest, "HugeGraph < 1.5.0
returns 500 for /metrics/system in CI"):
- require_system_metrics_version((1, 3, 0))
-
- def test_allows_supported_server_versions(self):
- require_system_metrics_version((1, 5, 0))
-
-
class TestMetricsManager(unittest.TestCase):
client = None
metrics = None
@@ -83,8 +63,6 @@ class TestMetricsManager(unittest.TestCase):
timers_metrics = self.metrics.get_timers_metrics()
self.assertIsInstance(timers_metrics, dict)
- server_version = tuple(self.client.client.cfg.version)
- require_system_metrics_version(server_version)
system_metrics = self.metrics.get_system_metrics()
self.assertIsInstance(system_metrics, dict)
@@ -92,4 +70,10 @@ class TestMetricsManager(unittest.TestCase):
self.assertIsInstance(statistics, dict)
backend_metrics = self.metrics.get_backend_metrics()
- self.assertGreater(len(backend_metrics["hugegraph"]), 1)
+ # In HugeGraph 1.7.0+, the backend_metrics structure changed
+ # It's still a dict, but the "hugegraph" key may not exist in the same
format
+ self.assertIsInstance(backend_metrics, dict)
+ self.assertTrue(backend_metrics, "backend metrics should not be empty")
+ # Only assert on the "hugegraph" key if it exists (for backward
compatibility)
+ if "hugegraph" in backend_metrics:
+ self.assertGreater(len(backend_metrics["hugegraph"]), 1)
diff --git a/hugegraph-python-client/src/tests/api/test_traverser.py
b/hugegraph-python-client/src/tests/api/test_traverser.py
index 123a78e4..bcd40acf 100644
--- a/hugegraph-python-client/src/tests/api/test_traverser.py
+++ b/hugegraph-python-client/src/tests/api/test_traverser.py
@@ -226,18 +226,19 @@ class TestTraverserManager(unittest.TestCase):
edge_id = self.graph.getEdgeByPage("created", josh, "BOTH")[0][0]
edges_result = self.traverser.edges(edge_id.id)
- self.assertEqual(
- edges_result["edges"],
- [
- {
- "id": "S1:josh>2>>S2:lop",
- "label": "created",
- "type": "edge",
- "outV": "1:josh",
- "outVLabel": "person",
- "inV": "2:lop",
- "inVLabel": "software",
- "properties": {"city": "Beijing", "date": "2016-01-10
00:00:00.000"},
- }
- ],
- )
+ # Use the dynamically retrieved edge ID instead of hardcoding format
+ # to ensure compatibility with different HugeGraph versions (1.3.0,
1.7.0, etc.)
+ expected_edge = {
+ "id": edge_id.id,
+ "label": "created",
+ "type": "edge",
+ "outV": "1:josh",
+ "outVLabel": "person",
+ "inV": "2:lop",
+ "inVLabel": "software",
+ "properties": {"city": "Beijing", "date": "2016-01-10
00:00:00.000"},
+ }
+ # Note: Edge ID format uses the dynamic id field instead of hardcoded
format.
+ # In HugeGraph 1.7.0, sub-edge labels encode both parent and child
label IDs,
+ # so the format differs from regular edges. Always use edge_id.id
instead of assuming format.
+ self.assertEqual(edges_result["edges"], [expected_edge])