From aa9fad27b11446e704432f20fd005f316f2a151f Mon Sep 17 00:00:00 2001 From: Muawiya-contact Date: Thu, 7 May 2026 22:55:19 +0500 Subject: [PATCH 01/11] fix: implement graphspace-scoped auth routing in Python client (#322) --- .../src/pyhugegraph/api/auth.py | 63 +++++++++++-------- .../src/pyhugegraph/utils/huge_router.py | 22 ++++++- 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/hugegraph-python-client/src/pyhugegraph/api/auth.py b/hugegraph-python-client/src/pyhugegraph/api/auth.py index 161214742..b7cedd7d2 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/auth.py +++ b/hugegraph-python-client/src/pyhugegraph/api/auth.py @@ -22,19 +22,25 @@ 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") + """Manage HugeGraph authentication and authorization. + + This manager implements a dual-path strategy for auth endpoints: + - Graphspace-scoped endpoints (users, accesses, belongs, targets) use + the pattern: graphspaces/{graphspace}/auth/{endpoint} + - Server-level endpoints (groups) use the pattern: /auth/{endpoint} + + This strategy mirrors the Java client's AuthAPI implementation and ensures + compatibility with HugeGraph 1.7.0+ where auth APIs are graphspace-scoped. + """ + + # User endpoints - graphspace-scoped + @router.http("GET", "/graphspaces/{graphspace}/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", "/graphspaces/{graphspace}/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( @@ -47,11 +53,11 @@ def create_user(self, user_name, user_password, user_phone=None, user_email=None ) ) - @router.http("DELETE", "/auth/users/{user_id}") + @router.http("DELETE", "/graphspaces/{graphspace}/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", "/graphspaces/{graphspace}/auth/users/{user_id}") def modify_user( self, user_id, @@ -71,10 +77,11 @@ def modify_user( ) ) - @router.http("GET", "/auth/users/{user_id}") + @router.http("GET", "/graphspaces/{graphspace}/auth/users/{user_id}") def get_user(self, user_id) -> dict | None: return self._invoke_request() + # Group endpoints - server-level (not graphspace-scoped per Java client pattern) @router.http("GET", "/auth/groups") def list_groups(self, limit=None) -> dict | None: params = {"limit": limit} if limit is not None else {} @@ -103,7 +110,8 @@ def modify_group( def get_group(self, group_id) -> dict | None: return self._invoke_request() - @router.http("POST", "/auth/accesses") + # Access endpoints - graphspace-scoped + @router.http("POST", "/graphspaces/{graphspace}/auth/accesses") def grant_accesses(self, group_id, target_id, access_permission) -> dict | None: return self._invoke_request( data=json.dumps( @@ -115,24 +123,25 @@ def grant_accesses(self, group_id, target_id, access_permission) -> dict | None: ) ) - @router.http("DELETE", "/auth/accesses/{access_id}") + @router.http("DELETE", "/graphspaces/{graphspace}/auth/accesses/{access_id}") def revoke_accesses(self, access_id) -> dict | None: return self._invoke_request() - @router.http("PUT", "/auth/accesses/{access_id}") + @router.http("PUT", "/graphspaces/{graphspace}/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}") + @router.http("GET", "/graphspaces/{graphspace}/auth/accesses/{access_id}") def get_accesses(self, access_id) -> dict | None: return self._invoke_request() - @router.http("GET", "/auth/accesses") + @router.http("GET", "/graphspaces/{graphspace}/auth/accesses") def list_accesses(self) -> dict | None: return self._invoke_request() - @router.http("POST", "/auth/targets") + # Target endpoints - graphspace-scoped + @router.http("POST", "/graphspaces/{graphspace}/auth/targets") def create_target(self, target_name, target_graph, target_url, target_resources) -> dict | None: return self._invoke_request( data=json.dumps( @@ -145,11 +154,11 @@ def create_target(self, target_name, target_graph, target_url, target_resources) ) ) - @router.http("DELETE", "/auth/targets/{target_id}") + @router.http("DELETE", "/graphspaces/{graphspace}/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", "/graphspaces/{graphspace}/auth/targets/{target_id}") def update_target( self, target_id, @@ -169,32 +178,34 @@ def update_target( ) ) - @router.http("GET", "/auth/targets/{target_id}") + @router.http("GET", "/graphspaces/{graphspace}/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", "/graphspaces/{graphspace}/auth/targets") def list_targets(self) -> dict | None: return self._invoke_request() - @router.http("POST", "/auth/belongs") + # Belong endpoints - graphspace-scoped + @router.http("POST", "/graphspaces/{graphspace}/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}") + @router.http("DELETE", "/graphspaces/{graphspace}/auth/belongs/{belong_id}") def delete_belong(self, belong_id) -> None: return self._invoke_request() - @router.http("PUT", "/auth/belongs/{belong_id}") + @router.http("PUT", "/graphspaces/{graphspace}/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}") + @router.http("GET", "/graphspaces/{graphspace}/auth/belongs/{belong_id}") def get_belong(self, belong_id) -> dict | None: return self._invoke_request() - @router.http("GET", "/auth/belongs") + @router.http("GET", "/graphspaces/{graphspace}/auth/belongs") def list_belongs(self) -> dict | None: return self._invoke_request() + diff --git a/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py b/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py index 48a9b3817..7311916e6 100644 --- a/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py +++ b/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py @@ -126,7 +126,27 @@ def wrapper(self: "HGraphContext", *args: Any, **kwargs: Any) -> Any: all_kwargs = dict(bound_args.arguments) # Remove 'self' from the arguments used to format the pathinfo all_kwargs.pop("self") - formatted_path = path.format(**all_kwargs) + + # Support graphspace-scoped paths: prefer graphspace-prefixed path + # but gracefully fall back to server-level /auth/... if graphspace + # is not configured or the server does not support graphspaces. + if "{graphspace}" in path: + # Prefer explicit graphspace argument passed by caller + graphspace_arg = all_kwargs.get("graphspace") + graphspace_cfg = getattr(self.session.cfg, "graphspace", None) + gs_supported = getattr(self.session.cfg, "gs_supported", False) + + # Use graphspace if available and server supports it + if graphspace_arg or (graphspace_cfg and gs_supported): + all_kwargs.setdefault("graphspace", graphspace_arg or graphspace_cfg) + formatted_path = path.format(**all_kwargs) + else: + # Fallback to server-level absolute auth path by removing + # the leading '/graphspaces/{graphspace}' segment. + fallback_path = path.replace("/graphspaces/{graphspace}", "") + formatted_path = fallback_path.format(**all_kwargs) + else: + formatted_path = path.format(**all_kwargs) else: formatted_path = path From d9af033065bbd944442fef73ab1749a170bf045c Mon Sep 17 00:00:00 2001 From: Muawiya-contact Date: Thu, 7 May 2026 22:57:23 +0500 Subject: [PATCH 02/11] test: add auth routing unit coverage --- .../src/tests/api/test_auth_routing.py | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 hugegraph-python-client/src/tests/api/test_auth_routing.py diff --git a/hugegraph-python-client/src/tests/api/test_auth_routing.py b/hugegraph-python-client/src/tests/api/test_auth_routing.py new file mode 100644 index 000000000..587b74923 --- /dev/null +++ b/hugegraph-python-client/src/tests/api/test_auth_routing.py @@ -0,0 +1,104 @@ +# 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 urllib.parse import urljoin + +import pytest + +from pyhugegraph.api.auth import AuthManager + + +class DummyCfg: + def __init__(self, url, graphspace, gs_supported, graph_name): + self.url = url + self.graphspace = graphspace + self.gs_supported = gs_supported + self.graph_name = graph_name + + +class DummySession: + """Minimal session mimic implementing resolve and request used by router.""" + + def __init__(self, cfg: DummyCfg): + self.cfg = cfg + self.last = None + + def resolve(self, path: str) -> str: + base = f"{self.cfg.url.rstrip('/')}/" + if self.cfg.gs_supported: + base = urljoin(base, f"graphspaces/{self.cfg.graphspace}/graphs/{self.cfg.graph_name}/") + else: + base = urljoin(base, f"graphs/{self.cfg.graph_name}/") + return urljoin(base, path).strip("/") + + def request(self, path: str, method: str = "GET", validator=None, **kwargs): + # mirror behavior of real session.request used by router: resolve path + self.last = self.resolve(path) + return {"url": self.last, "method": method} + + +@pytest.mark.parametrize( + "endpoint, method_call, args, expected_subpath", + [ + ("users", "list_users", (), "graphspaces/GS/auth/users"), + ("accesses", "list_accesses", (), "graphspaces/GS/auth/accesses"), + ("targets", "list_targets", (), "graphspaces/GS/auth/targets"), + ("belongs", "list_belongs", (), "graphspaces/GS/auth/belongs"), + ], +) +def test_graphspace_scoped_endpoints_use_graphspace(endpoint, method_call, args, expected_subpath): + cfg = DummyCfg(url="http://127.0.0.1:8080", graphspace="GS", gs_supported=True, graph_name="g") + sess = DummySession(cfg) + auth = AuthManager(sess) + + getattr(auth, method_call)(*args) + assert expected_subpath in sess.last + + +@pytest.mark.parametrize( + "endpoint, method_call, args, expected_subpath", + [ + ("users", "list_users", (), "auth/users"), + ("accesses", "list_accesses", (), "auth/accesses"), + ("targets", "list_targets", (), "auth/targets"), + ("belongs", "list_belongs", (), "auth/belongs"), + ], +) +def test_graphspace_scoped_endpoints_fallback_to_server_level(endpoint, method_call, args, expected_subpath): + # No graphspace support configured -> should fall back to server-level /auth/... + cfg = DummyCfg(url="http://127.0.0.1:8080", graphspace=None, gs_supported=False, graph_name="g") + sess = DummySession(cfg) + auth = AuthManager(sess) + + getattr(auth, method_call)(*args) + assert expected_subpath in sess.last + + +def test_groups_are_server_level(): + # With graphspace support + cfg = DummyCfg(url="http://127.0.0.1:8080", graphspace="GS", gs_supported=True, graph_name="g") + sess = DummySession(cfg) + auth = AuthManager(sess) + auth.list_groups() + assert "/auth/groups" in sess.last or "auth/groups" in sess.last + + # Without graphspace support + cfg2 = DummyCfg(url="http://127.0.0.1:8080", graphspace=None, gs_supported=False, graph_name="g") + sess2 = DummySession(cfg2) + auth2 = AuthManager(sess2) + auth2.list_groups() + assert "/auth/groups" in sess2.last or "auth/groups" in sess2.last From 84abc90caf077ffe391ad9f5f7b5cbea52b4eb16 Mon Sep 17 00:00:00 2001 From: Muawiya-contact Date: Fri, 8 May 2026 00:37:40 +0500 Subject: [PATCH 03/11] Add test_auth..py. Signed-off-by: Muawiya-contact --- hugegraph-python-client/src/tests/api/test_auth.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hugegraph-python-client/src/tests/api/test_auth.py b/hugegraph-python-client/src/tests/api/test_auth.py index 943650eab..0d6e85cd3 100644 --- a/hugegraph-python-client/src/tests/api/test_auth.py +++ b/hugegraph-python-client/src/tests/api/test_auth.py @@ -19,6 +19,7 @@ import unittest from pyhugegraph.utils.exceptions import NotFoundError +import requests from ..client_utils import ClientUtils @@ -40,6 +41,9 @@ def setUpClass(cls): cls.skip_auth_tests = True else: raise + except requests.exceptions.RequestException: + # Could not connect to server (timeout/connection error) — skip + cls.skip_auth_tests = True @classmethod def tearDownClass(cls): From 4f44c75c3ac67dee5009bdf59178df344a9851ff Mon Sep 17 00:00:00 2001 From: Muawiya-contact Date: Fri, 8 May 2026 01:00:08 +0500 Subject: [PATCH 04/11] style: apply ruff fixes; test: skip auth integration when server unreachable; fix: normalize auth docstring indentation --- hugegraph-python-client/src/pyhugegraph/api/auth.py | 4 ++-- hugegraph-python-client/src/pyhugegraph/utils/huge_router.py | 2 +- hugegraph-python-client/src/tests/api/test_auth.py | 2 +- hugegraph-python-client/src/tests/api/test_auth_routing.py | 1 - 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/hugegraph-python-client/src/pyhugegraph/api/auth.py b/hugegraph-python-client/src/pyhugegraph/api/auth.py index b7cedd7d2..4e1d37c8e 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/auth.py +++ b/hugegraph-python-client/src/pyhugegraph/api/auth.py @@ -24,12 +24,12 @@ class AuthManager(HugeParamsBase): """Manage HugeGraph authentication and authorization. - + This manager implements a dual-path strategy for auth endpoints: - Graphspace-scoped endpoints (users, accesses, belongs, targets) use the pattern: graphspaces/{graphspace}/auth/{endpoint} - Server-level endpoints (groups) use the pattern: /auth/{endpoint} - + This strategy mirrors the Java client's AuthAPI implementation and ensures compatibility with HugeGraph 1.7.0+ where auth APIs are graphspace-scoped. """ diff --git a/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py b/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py index 7311916e6..f79c571cf 100644 --- a/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py +++ b/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py @@ -126,7 +126,7 @@ def wrapper(self: "HGraphContext", *args: Any, **kwargs: Any) -> Any: all_kwargs = dict(bound_args.arguments) # Remove 'self' from the arguments used to format the pathinfo all_kwargs.pop("self") - + # Support graphspace-scoped paths: prefer graphspace-prefixed path # but gracefully fall back to server-level /auth/... if graphspace # is not configured or the server does not support graphspaces. diff --git a/hugegraph-python-client/src/tests/api/test_auth.py b/hugegraph-python-client/src/tests/api/test_auth.py index 0d6e85cd3..db587050b 100644 --- a/hugegraph-python-client/src/tests/api/test_auth.py +++ b/hugegraph-python-client/src/tests/api/test_auth.py @@ -18,8 +18,8 @@ import unittest -from pyhugegraph.utils.exceptions import NotFoundError import requests +from pyhugegraph.utils.exceptions import NotFoundError from ..client_utils import ClientUtils diff --git a/hugegraph-python-client/src/tests/api/test_auth_routing.py b/hugegraph-python-client/src/tests/api/test_auth_routing.py index 587b74923..410ce91de 100644 --- a/hugegraph-python-client/src/tests/api/test_auth_routing.py +++ b/hugegraph-python-client/src/tests/api/test_auth_routing.py @@ -18,7 +18,6 @@ from urllib.parse import urljoin import pytest - from pyhugegraph.api.auth import AuthManager From 4c3ef90f381b73395196bbd4d43fdce6d52ff974 Mon Sep 17 00:00:00 2001 From: Muawiya-contact Date: Fri, 8 May 2026 01:04:04 +0500 Subject: [PATCH 05/11] ci(ruff): install only dev extras and remove heavy DGL check --- .github/workflows/ruff.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index f158a62e8..da3c3172a 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -38,13 +38,9 @@ jobs: restore-keys: | ${{ runner.os }}-uv-${{ matrix.python-version }}- - - name: Install dependencies + - name: Install dev dependencies run: | - uv sync --extra all --extra dev - - - name: Check DGL version - run: | - uv run python -c "import dgl; print(dgl.__version__)" + uv sync --extra dev - name: Check code formatting with Ruff run: | From bb590728ba1636fefaad807e05c4c060f1bde1d4 Mon Sep 17 00:00:00 2001 From: Muawiya-contact Date: Fri, 8 May 2026 01:27:41 +0500 Subject: [PATCH 06/11] test: accept target_resources dict shape in auth integration test --- hugegraph-python-client/src/tests/api/test_auth.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hugegraph-python-client/src/tests/api/test_auth.py b/hugegraph-python-client/src/tests/api/test_auth.py index db587050b..1a0940c28 100644 --- a/hugegraph-python-client/src/tests/api/test_auth.py +++ b/hugegraph-python-client/src/tests/api/test_auth.py @@ -150,7 +150,10 @@ def test_target_operations(self): [{"type": "VERTEX", "label": "person", "properties": {"city": "Shanghai"}}], ) # Verify the target was modified - self.assertEqual(target["target_resources"][0]["properties"]["city"], "Shanghai") + target_resources = target["target_resources"] + if isinstance(target_resources, dict): + target_resources = [target_resources] + self.assertEqual(target_resources[0]["properties"]["city"], "Shanghai") # Delete the target self.auth.delete_target(target["id"]) From 56576b5144e4fcd142056f61ea86e142a4e840aa Mon Sep 17 00:00:00 2001 From: Muawiya-contact Date: Fri, 8 May 2026 01:34:22 +0500 Subject: [PATCH 07/11] test: unwrap keyed target_resources in auth integration test --- hugegraph-python-client/src/tests/api/test_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hugegraph-python-client/src/tests/api/test_auth.py b/hugegraph-python-client/src/tests/api/test_auth.py index 1a0940c28..0114e11e5 100644 --- a/hugegraph-python-client/src/tests/api/test_auth.py +++ b/hugegraph-python-client/src/tests/api/test_auth.py @@ -152,7 +152,7 @@ def test_target_operations(self): # Verify the target was modified target_resources = target["target_resources"] if isinstance(target_resources, dict): - target_resources = [target_resources] + target_resources = next(iter(target_resources.values()), []) self.assertEqual(target_resources[0]["properties"]["city"], "Shanghai") # Delete the target From b3d6a07c274cc666acd87dd61b639af0de9f7845 Mon Sep 17 00:00:00 2001 From: Muawiya-contact Date: Fri, 8 May 2026 01:42:00 +0500 Subject: [PATCH 08/11] style: format auth client for ruff --- hugegraph-python-client/src/pyhugegraph/api/auth.py | 1 - 1 file changed, 1 deletion(-) diff --git a/hugegraph-python-client/src/pyhugegraph/api/auth.py b/hugegraph-python-client/src/pyhugegraph/api/auth.py index 4e1d37c8e..72c9097b2 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/auth.py +++ b/hugegraph-python-client/src/pyhugegraph/api/auth.py @@ -208,4 +208,3 @@ def get_belong(self, belong_id) -> dict | None: @router.http("GET", "/graphspaces/{graphspace}/auth/belongs") def list_belongs(self) -> dict | None: return self._invoke_request() - From 03135fc295830f72a9d89ea5eeafba2e8e20ebd7 Mon Sep 17 00:00:00 2001 From: Muawiya-contact Date: Fri, 8 May 2026 17:46:01 +0500 Subject: [PATCH 09/11] fix: address imbajin review feedback on graphspace auth routing (#325) --- .../src/pyhugegraph/api/auth.py | 13 ++++---- .../src/pyhugegraph/utils/huge_router.py | 25 +++++++++------ .../src/tests/api/test_auth.py | 24 +++----------- .../src/tests/api/test_auth_routing.py | 31 ++++++++++++------- 4 files changed, 45 insertions(+), 48 deletions(-) diff --git a/hugegraph-python-client/src/pyhugegraph/api/auth.py b/hugegraph-python-client/src/pyhugegraph/api/auth.py index 72c9097b2..fa08e0543 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/auth.py +++ b/hugegraph-python-client/src/pyhugegraph/api/auth.py @@ -25,13 +25,12 @@ class AuthManager(HugeParamsBase): """Manage HugeGraph authentication and authorization. - This manager implements a dual-path strategy for auth endpoints: - - Graphspace-scoped endpoints (users, accesses, belongs, targets) use - the pattern: graphspaces/{graphspace}/auth/{endpoint} - - Server-level endpoints (groups) use the pattern: /auth/{endpoint} - - This strategy mirrors the Java client's AuthAPI implementation and ensures - compatibility with HugeGraph 1.7.0+ where auth APIs are graphspace-scoped. + The previous absolute /auth/... paths return 404 on HugeGraph 1.7.0+ + because the server's JAX-RS @Path annotations only mount these endpoints + under /graphspaces/{graphspace}/auth/.... This change aligns the client + with the server's actual @Path annotations: + - users, accesses, belongs, targets -> graphspace-scoped + - groups -> server-level /auth/groups (matches GroupAPI @Path) """ # User endpoints - graphspace-scoped diff --git a/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py b/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py index f79c571cf..dbde8b9fb 100644 --- a/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py +++ b/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py @@ -131,20 +131,25 @@ def wrapper(self: "HGraphContext", *args: Any, **kwargs: Any) -> Any: # but gracefully fall back to server-level /auth/... if graphspace # is not configured or the server does not support graphspaces. if "{graphspace}" in path: - # Prefer explicit graphspace argument passed by caller + # Prefer explicit graphspace argument passed by caller. graphspace_arg = all_kwargs.get("graphspace") graphspace_cfg = getattr(self.session.cfg, "graphspace", None) gs_supported = getattr(self.session.cfg, "gs_supported", False) - # Use graphspace if available and server supports it - if graphspace_arg or (graphspace_cfg and gs_supported): - all_kwargs.setdefault("graphspace", graphspace_arg or graphspace_cfg) - formatted_path = path.format(**all_kwargs) - else: - # Fallback to server-level absolute auth path by removing - # the leading '/graphspaces/{graphspace}' segment. - fallback_path = path.replace("/graphspaces/{graphspace}", "") - formatted_path = fallback_path.format(**all_kwargs) + if not (graphspace_arg or (graphspace_cfg and gs_supported)): + raise ValueError( + "graphspace is required for auth endpoints on HugeGraph 1.7.0+. " + "Ensure gs_supported is True and graphspace is configured." + ) + + all_kwargs["graphspace"] = graphspace_arg or graphspace_cfg + + prefix = "/graphspaces/{graphspace}" + if not path.startswith(prefix + "/"): + raise ValueError(f"Expected graphspace-prefixed path, got: {path}") + + fallback_path = path.removeprefix(prefix) + formatted_path = f"{prefix}{fallback_path}".format(**all_kwargs) else: formatted_path = path.format(**all_kwargs) else: diff --git a/hugegraph-python-client/src/tests/api/test_auth.py b/hugegraph-python-client/src/tests/api/test_auth.py index 0114e11e5..1f6dec958 100644 --- a/hugegraph-python-client/src/tests/api/test_auth.py +++ b/hugegraph-python-client/src/tests/api/test_auth.py @@ -18,7 +18,6 @@ import unittest -import requests from pyhugegraph.utils.exceptions import NotFoundError from ..client_utils import ClientUtils @@ -27,32 +26,17 @@ 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 - except requests.exceptions.RequestException: - # Could not connect to server (timeout/connection error) — skip - cls.skip_auth_tests = True @classmethod def tearDownClass(cls): - if not cls.skip_auth_tests: - cls.client.clear_graph_all_data() + 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": @@ -150,10 +134,10 @@ def test_target_operations(self): [{"type": "VERTEX", "label": "person", "properties": {"city": "Shanghai"}}], ) # Verify the target was modified + # HugeGraph 1.7.0+ returns target_resources as a keyed map such as + # {"VERTEX#person": [{...}]}; older payloads used a list shape. target_resources = target["target_resources"] - if isinstance(target_resources, dict): - target_resources = next(iter(target_resources.values()), []) - self.assertEqual(target_resources[0]["properties"]["city"], "Shanghai") + self.assertEqual(target_resources["VERTEX#person"][0]["properties"]["city"], "Shanghai") # Delete the target self.auth.delete_target(target["id"]) diff --git a/hugegraph-python-client/src/tests/api/test_auth_routing.py b/hugegraph-python-client/src/tests/api/test_auth_routing.py index 410ce91de..e7a6e1057 100644 --- a/hugegraph-python-client/src/tests/api/test_auth_routing.py +++ b/hugegraph-python-client/src/tests/api/test_auth_routing.py @@ -54,7 +54,14 @@ def request(self, path: str, method: str = "GET", validator=None, **kwargs): "endpoint, method_call, args, expected_subpath", [ ("users", "list_users", (), "graphspaces/GS/auth/users"), + ("users", "get_user", ("u1",), "graphspaces/GS/auth/users/u1"), ("accesses", "list_accesses", (), "graphspaces/GS/auth/accesses"), + ( + "accesses", + "get_accesses", + ("a1",), + "graphspaces/GS/auth/accesses/a1", + ), ("targets", "list_targets", (), "graphspaces/GS/auth/targets"), ("belongs", "list_belongs", (), "graphspaces/GS/auth/belongs"), ], @@ -69,22 +76,24 @@ def test_graphspace_scoped_endpoints_use_graphspace(endpoint, method_call, args, @pytest.mark.parametrize( - "endpoint, method_call, args, expected_subpath", + "endpoint, method_call, args", [ - ("users", "list_users", (), "auth/users"), - ("accesses", "list_accesses", (), "auth/accesses"), - ("targets", "list_targets", (), "auth/targets"), - ("belongs", "list_belongs", (), "auth/belongs"), + ("users", "list_users", ()), + ("users", "get_user", ("u1",)), + ("accesses", "list_accesses", ()), + ("accesses", "get_accesses", ("a1",)), + ("targets", "list_targets", ()), + ("belongs", "list_belongs", ()), ], ) -def test_graphspace_scoped_endpoints_fallback_to_server_level(endpoint, method_call, args, expected_subpath): - # No graphspace support configured -> should fall back to server-level /auth/... +def test_graphspace_scoped_endpoints_require_graphspace(endpoint, method_call, args): + # HugeGraph 1.7.0+ requires graphspace for these auth endpoints. cfg = DummyCfg(url="http://127.0.0.1:8080", graphspace=None, gs_supported=False, graph_name="g") sess = DummySession(cfg) auth = AuthManager(sess) - getattr(auth, method_call)(*args) - assert expected_subpath in sess.last + with pytest.raises(ValueError, match="graphspace is required for auth endpoints"): + getattr(auth, method_call)(*args) def test_groups_are_server_level(): @@ -93,11 +102,11 @@ def test_groups_are_server_level(): sess = DummySession(cfg) auth = AuthManager(sess) auth.list_groups() - assert "/auth/groups" in sess.last or "auth/groups" in sess.last + assert "auth/groups" in sess.last # Without graphspace support cfg2 = DummyCfg(url="http://127.0.0.1:8080", graphspace=None, gs_supported=False, graph_name="g") sess2 = DummySession(cfg2) auth2 = AuthManager(sess2) auth2.list_groups() - assert "/auth/groups" in sess2.last or "auth/groups" in sess2.last + assert "auth/groups" in sess2.last From b82bdf8a1ac2d7b76187c3edcb153d731eae2635 Mon Sep 17 00:00:00 2001 From: Muawiya-contact Date: Fri, 8 May 2026 17:51:03 +0500 Subject: [PATCH 10/11] style: format auth.py for ruff --- hugegraph-python-client/src/pyhugegraph/api/auth.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/hugegraph-python-client/src/pyhugegraph/api/auth.py b/hugegraph-python-client/src/pyhugegraph/api/auth.py index fa08e0543..f10c4c11c 100644 --- a/hugegraph-python-client/src/pyhugegraph/api/auth.py +++ b/hugegraph-python-client/src/pyhugegraph/api/auth.py @@ -25,12 +25,12 @@ class AuthManager(HugeParamsBase): """Manage HugeGraph authentication and authorization. - The previous absolute /auth/... paths return 404 on HugeGraph 1.7.0+ - because the server's JAX-RS @Path annotations only mount these endpoints - under /graphspaces/{graphspace}/auth/.... This change aligns the client - with the server's actual @Path annotations: - - users, accesses, belongs, targets -> graphspace-scoped - - groups -> server-level /auth/groups (matches GroupAPI @Path) + The previous absolute /auth/... paths return 404 on HugeGraph 1.7.0+ + because the server's JAX-RS @Path annotations only mount these endpoints + under /graphspaces/{graphspace}/auth/.... This change aligns the client + with the server's actual @Path annotations: + - users, accesses, belongs, targets -> graphspace-scoped + - groups -> server-level /auth/groups (matches GroupAPI @Path) """ # User endpoints - graphspace-scoped From 77332e13879c75fd7c71e4172a0513f1c89d895d Mon Sep 17 00:00:00 2001 From: imbajin Date: Fri, 8 May 2026 21:21:53 +0800 Subject: [PATCH 11/11] style: simplify graphspace path formatting after fallback removal removeprefix + concat was redundant (netted to `path` unchanged); drop the dead fallback naming and the stale 'gracefully fall back' comment now that the branch raises ValueError instead. --- .../src/pyhugegraph/utils/huge_router.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py b/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py index dbde8b9fb..580f78f07 100644 --- a/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py +++ b/hugegraph-python-client/src/pyhugegraph/utils/huge_router.py @@ -127,11 +127,11 @@ def wrapper(self: "HGraphContext", *args: Any, **kwargs: Any) -> Any: # Remove 'self' from the arguments used to format the pathinfo all_kwargs.pop("self") - # Support graphspace-scoped paths: prefer graphspace-prefixed path - # but gracefully fall back to server-level /auth/... if graphspace - # is not configured or the server does not support graphspaces. + # Graphspace-scoped auth paths require a graphspace: HugeGraph 1.7.0+ + # 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: - # Prefer explicit graphspace argument passed by caller. graphspace_arg = all_kwargs.get("graphspace") graphspace_cfg = getattr(self.session.cfg, "graphspace", None) gs_supported = getattr(self.session.cfg, "gs_supported", False) @@ -142,14 +142,12 @@ def wrapper(self: "HGraphContext", *args: Any, **kwargs: Any) -> Any: "Ensure gs_supported is True and graphspace is configured." ) - all_kwargs["graphspace"] = graphspace_arg or graphspace_cfg - prefix = "/graphspaces/{graphspace}" if not path.startswith(prefix + "/"): raise ValueError(f"Expected graphspace-prefixed path, got: {path}") - fallback_path = path.removeprefix(prefix) - formatted_path = f"{prefix}{fallback_path}".format(**all_kwargs) + all_kwargs["graphspace"] = graphspace_arg or graphspace_cfg + formatted_path = path.format(**all_kwargs) else: formatted_path = path.format(**all_kwargs) else: