Skip to content

Commit a63a69b

Browse files
authored
Merge pull request #392 from neo4j/support-gds-2.0
Support GDS 2.0 API
2 parents 8c36c09 + bc16da9 commit a63a69b

6 files changed

Lines changed: 76 additions & 25 deletions

File tree

.github/workflows/gds-integration-tests.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jobs:
2020
fail-fast: false
2121
matrix:
2222
os: [ubuntu-latest]
23+
gds-version: ["1.22", "2.0.0a1"]
2324
defaults:
2425
run:
2526
working-directory: python-wrapper
@@ -33,11 +34,12 @@ jobs:
3334
with:
3435
python-version: "3.11"
3536
enable-cache: true
36-
- run: uv sync --group dev --extra pandas --extra neo4j --extra gds
37+
- name: Install just
38+
uses: extractions/setup-just@v2
3739

3840
- name: Run tests
3941
env:
4042
AURA_API_CLIENT_ID: 4V1HYCYEeoU4dSxThKnBeLvE2U4hSphx
4143
AURA_API_CLIENT_SECRET: ${{ secrets.AURA_API_CLIENT_SECRET }}
4244
AURA_API_PROJECT_ID: 3f8df5e7-4800-4d4f-ad1d-2d044dfd587c
43-
run: uv run pytest tests/ --include-neo4j-and-gds
45+
run: just py-ci-test-gds "${{ matrix.gds-version }}"

justfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ py-test:
2525
cd python-wrapper && uv sync --all-extras --group dev
2626
cd python-wrapper && uv run --group dev pytest
2727

28+
# install a specific GDS client version and run the GDS integration tests (used by CI)
29+
# example: just py-ci-test-gds 2.0.0a1
30+
py-ci-test-gds gds_version:
31+
#!/usr/bin/env bash
32+
set -e
33+
cd {{py_dir}}
34+
uv sync --group dev --extra pandas --extra neo4j --extra gds
35+
uv pip install "graphdatascience=={{gds_version}}"
36+
uv run pytest tests/ --include-neo4j-and-gds
37+
2838
py-test-gds:
2939
#!/usr/bin/env bash
3040
set -e

python-wrapper/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ requires-python = ">=3.10"
4343

4444
[project.optional-dependencies]
4545
pandas = ["pandas>=2, <3", "pandas-stubs>=2, <3"]
46-
gds = ["graphdatascience>=1.22, <2"]
46+
gds = ["graphdatascience>=1.22, <3"]
4747
neo4j = ["neo4j"]
4848
snowflake = ["snowflake-snowpark-python>=1, <2"]
4949

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from __future__ import annotations
2+
3+
import importlib
4+
import re
5+
from typing import Any
6+
7+
from graphdatascience.version import __version__ as _gds_version
8+
9+
10+
def _parse_major(version: str) -> int:
11+
match = re.match(r"\s*(\d+)", version)
12+
return int(match.group(1)) if match else 0
13+
14+
15+
IS_GDS_2: bool = _parse_major(_gds_version) >= 2
16+
17+
if IS_GDS_2:
18+
GdsGraph: Any = importlib.import_module("graphdatascience.graph").Graph
19+
_GRAPH_TYPES: tuple[type, ...] = (GdsGraph,)
20+
else:
21+
GdsGraph = importlib.import_module("graphdatascience.graph.v2").GraphV2
22+
_GRAPH_TYPES = (GdsGraph, importlib.import_module("graphdatascience").Graph)
23+
24+
25+
def _check_graph_type(G: Any) -> None:
26+
"""Raise ``TypeError`` unless ``G`` is a graph object accepted by the installed client."""
27+
if not isinstance(G, _GRAPH_TYPES):
28+
accepted = " or ".join(t.__name__ for t in _GRAPH_TYPES)
29+
raise TypeError(f"`G` must be a GDS graph object ({accepted}), but got {type(G).__name__}")
30+
31+
32+
def _catalog(gds: Any) -> Any:
33+
"""Return the graph catalog endpoints for either client version."""
34+
return gds.graph if IS_GDS_2 else gds.v2.graph
35+
36+
37+
def _degree_centrality(gds: Any) -> Any:
38+
"""Return the degree centrality endpoints for either client version."""
39+
return gds.degree_centrality if IS_GDS_2 else gds.v2.degree_centrality

python-wrapper/src/neo4j_viz/gds.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,29 @@
22

33
import warnings
44
from itertools import chain
5-
from typing import Collection, Optional
5+
from typing import Any, Collection, Optional
66
from uuid import uuid4
77

88
import pandas as pd
9-
from graphdatascience import Graph, GraphDataScience
10-
from graphdatascience.graph.v2 import GraphV2
9+
from graphdatascience import GraphDataScience
1110
from graphdatascience.session import AuraGraphDataScience
1211

1312
from neo4j_viz.colors import NEO4J_COLORS_DISCRETE, ColorSpace
1413

14+
from ._gds_compat import IS_GDS_2, GdsGraph, _catalog, _check_graph_type, _degree_centrality
1515
from .pandas import _from_dfs
1616
from .visualization_graph import VisualizationGraph
1717

1818

1919
def _fetch_node_dfs(
2020
gds: GraphDataScience | AuraGraphDataScience,
21-
G: GraphV2,
21+
G: Any,
2222
node_properties_by_label: dict[str, list[str]],
2323
node_labels: Collection[str],
2424
additional_db_node_properties: list[str],
2525
) -> dict[str, pd.DataFrame]:
2626
return {
27-
lbl: gds.v2.graph.node_properties.stream(
27+
lbl: _catalog(gds).node_properties.stream(
2828
G,
2929
node_properties=node_properties_by_label[lbl],
3030
node_labels=[lbl],
@@ -34,14 +34,14 @@ def _fetch_node_dfs(
3434
}
3535

3636

37-
def _fetch_rel_dfs(gds: GraphDataScience | AuraGraphDataScience, G: GraphV2) -> list[pd.DataFrame]:
37+
def _fetch_rel_dfs(gds: GraphDataScience | AuraGraphDataScience, G: Any) -> list[pd.DataFrame]:
3838
rel_props = G.relationship_properties()
3939

4040
rel_dfs: list[pd.DataFrame] = []
4141

4242
# Have to call per stream per relationship type as there was a bug in GDS < 2.21
4343
for rel_type, props in rel_props.items():
44-
rel_df = gds.v2.graph.relationships.stream(
44+
rel_df = _catalog(gds).relationships.stream(
4545
G, relationship_types=[rel_type], relationship_properties=list(props)
4646
)
4747

@@ -62,7 +62,7 @@ def _fetch_rel_dfs(gds: GraphDataScience | AuraGraphDataScience, G: GraphV2) ->
6262

6363
def from_gds(
6464
gds: GraphDataScience | AuraGraphDataScience,
65-
G: Graph | GraphV2,
65+
G: Any,
6666
node_properties: Optional[list[str]] = None,
6767
db_node_properties: Optional[list[str]] = None,
6868
max_node_count: int = 10_000,
@@ -97,8 +97,9 @@ def from_gds(
9797
"""
9898
if db_node_properties is None:
9999
db_node_properties = []
100-
if isinstance(G, Graph):
101-
G_v2 = gds.v2.graph.get(G.name())
100+
_check_graph_type(G)
101+
if not IS_GDS_2 and not isinstance(G, GdsGraph):
102+
G_v2 = _catalog(gds).get(G.name())
102103
else:
103104
G_v2 = G
104105

@@ -127,7 +128,7 @@ def from_gds(
127128
)
128129
sampling_ratio = float(max_node_count) / node_count
129130
sample_name = f"neo4j-viz_sample_{uuid4()}"
130-
G_fetched, _ = gds.v2.graph.sample.rwr(
131+
G_fetched, _ = _catalog(gds).sample.rwr(
131132
G_v2, sample_name, sampling_ratio=sampling_ratio, node_label_stratification=True
132133
)
133134
else:
@@ -139,7 +140,7 @@ def from_gds(
139140
# as a temporary property to ensure that we have at least one property for each label to fetch
140141
if sum([len(props) == 0 for props in node_properties_by_label.values()]) > 0:
141142
property_name = f"neo4j-viz_property_{uuid4()}"
142-
gds.v2.degree_centrality.mutate(G_fetched, mutate_property=property_name)
143+
_degree_centrality(gds).mutate(G_fetched, mutate_property=property_name)
143144
for props in node_properties_by_label.values():
144145
props.append(property_name)
145146

@@ -155,7 +156,7 @@ def from_gds(
155156
if G_fetched.name() != G.name():
156157
G_fetched.drop()
157158
elif property_name is not None:
158-
gds.v2.graph.node_properties.drop(G_fetched, node_properties=[property_name])
159+
_catalog(gds).node_properties.drop(G_fetched, node_properties=[property_name])
159160

160161
for df in node_dfs.values():
161162
if property_name is not None and property_name in df.columns:

python-wrapper/tests/neo4j_and_gds/test_gds.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import re
2-
from contextlib import AbstractContextManager
3-
from typing import Generator
2+
from typing import Any, Generator
43

54
import pandas as pd
65
import pytest
76
from graphdatascience import GraphDataScience
8-
from graphdatascience.graph.v2 import GraphV2
97
from graphdatascience.session import AuraGraphDataScience
108

119
from neo4j_viz import Node
10+
from neo4j_viz._gds_compat import _catalog
1211
from neo4j_viz.gds import from_gds
1312

1413

@@ -25,11 +24,11 @@ def db_setup(gds: GraphDataScience | AuraGraphDataScience) -> Generator[None, No
2524
gds.run_cypher("MATCH (n:_CI_A|_CI_B) DETACH DELETE n")
2625

2726

28-
def project_graph(gds: GraphDataScience | AuraGraphDataScience) -> AbstractContextManager[GraphV2]:
27+
def project_graph(gds: GraphDataScience | AuraGraphDataScience) -> Any:
2928
if isinstance(gds, GraphDataScience):
30-
return gds.v2.graph.project("g2", "*", "*")
29+
return _catalog(gds).project("g2", "*", "*")
3130
elif isinstance(gds, AuraGraphDataScience):
32-
return gds.v2.graph.project("g2", "MATCH (n)–->(m) RETURN gds.graph.project.remote(n, m)")
31+
return _catalog(gds).project("g2", "MATCH (n)–->(m) RETURN gds.graph.project.remote(n, m)")
3332
raise Exception(f"Unsupported GDS type {type(gds)}")
3433

3534

@@ -64,7 +63,7 @@ def test_from_gds_integration_all_properties(gds: GraphDataScience | AuraGraphDa
6463
}
6564
)
6665

67-
with gds.v2.graph.construct("flo", nodes, rels) as G:
66+
with _catalog(gds).construct("flo", nodes, rels) as G:
6867
VG = from_gds(gds, G)
6968

7069
assert len(VG.nodes) == 3
@@ -113,7 +112,7 @@ def test_from_gds_integration_all_properties(gds: GraphDataScience | AuraGraphDa
113112

114113
@pytest.mark.requires_neo4j_and_gds
115114
def test_from_gds_sample(gds: GraphDataScience | AuraGraphDataScience) -> None:
116-
with gds.v2.graph.generate("hello", node_count=11_000, average_degree=1) as G:
115+
with _catalog(gds).generate("hello", node_count=11_000, average_degree=1) as G:
117116
with pytest.warns(
118117
UserWarning,
119118
match=re.escape(
@@ -164,7 +163,7 @@ def test_from_gds_hetero(gds: GraphDataScience | AuraGraphDataScience) -> None:
164163
}
165164
)
166165

167-
with gds.v2.graph.construct("flo", [A_nodes, B_nodes], [X_rels, Y_rels]) as G:
166+
with _catalog(gds).construct("flo", [A_nodes, B_nodes], [X_rels, Y_rels]) as G:
168167
VG = from_gds(
169168
gds,
170169
G,

0 commit comments

Comments
 (0)