From ce0cb5984b7340ff83c4be9fcb52b9ae9fbfe2ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florentin=20D=C3=B6rre?= Date: Fri, 5 Jun 2026 15:20:12 +0200 Subject: [PATCH 1/6] Add support for AGA --- changelog.md | 7 +- examples/gds-example.ipynb | 5031 +----------------------- examples/neo4j-example.ipynb | 24 +- justfile | 16 +- python-wrapper/pyproject.toml | 18 +- python-wrapper/src/neo4j_viz/gds.py | 84 +- python-wrapper/tests/conftest.py | 61 +- python-wrapper/tests/gds_helper.py | 47 +- python-wrapper/tests/test_gds.py | 19 +- python-wrapper/tests/test_notebooks.py | 4 + python-wrapper/uv.lock | 171 +- 11 files changed, 386 insertions(+), 5096 deletions(-) diff --git a/changelog.md b/changelog.md index 2b09d1ea..500e1c18 100644 --- a/changelog.md +++ b/changelog.md @@ -6,12 +6,9 @@ ## Bug fixes -- Fixed a bug in displaying the `Download`, `Selection` and `Layout` buttons, which was introduced in 1.2.0. - ## Improvements -- Support `neo4j.EagerResult` in the `from_neo4j` integration which is the default return type by `neo4j.Driver.execute_query()`. -- Detect light/dark theme changes and adapt rendering unless theme was explicitly set. Before the theme would only be checked on the first render. - +* Support Aura Graph Analytics +* Support `gds.v2` endpoints ## Other changes diff --git a/examples/gds-example.ipynb b/examples/gds-example.ipynb index 29b1387c..661f6ffd 100644 --- a/examples/gds-example.ipynb +++ b/examples/gds-example.ipynb @@ -2,6 +2,7 @@ "cells": [ { "cell_type": "markdown", + "id": "e613852e", "metadata": {}, "source": [ "# Visualizing Neo4j Graph Data Science (GDS) Graphs" @@ -10,54 +11,112 @@ { "cell_type": "code", "execution_count": null, + "id": "7149a3b4", "metadata": {}, "outputs": [], "source": [ "%pip install graphdatascience\n", - "%pip install matplotlib" + "%pip install matplotlib\n", + "%pip install python-dotenv" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6a8c792", + "metadata": {}, + "outputs": [], + "source": [ + "import dotenv\n", + "\n", + "dotenv.load_dotenv(\"../local.env\", override=True)" ] }, { "cell_type": "markdown", + "id": "533c652a", "metadata": {}, "source": [ - "## Setup GDS graph" + "## Setup GDS graph\n", + "\n", + "To use GDS, you can either use GDS as a plugin or Aura Graph Analytics.\n", + "In the following, you can choose:\n", + "\n", + " * Provide Aura API credentials and and use Aura Graph Analytics.\n", + " * Use Neo4j + GDS Plugin.\n", + "\n", + "For more information, see the [GDS documentation](https://neo4j.com/docs/graph-data-science/current/installation/)." ] }, { "cell_type": "code", "execution_count": null, + "id": "3d98bfe7", "metadata": {}, "outputs": [], "source": [ "import os\n", "\n", + "from graphdatascience.session import (\n", + " GdsSessions,\n", + " DbmsConnectionInfo,\n", + " AuraAPICredentials,\n", + " SessionMemory,\n", + ")\n", "from graphdatascience import GraphDataScience\n", "\n", "# Get Neo4j DB URI, credentials and name from environment if applicable\n", - "NEO4J_URI = os.environ.get(\"NEO4J_URI\", \"bolt://localhost:7687\")\n", - "NEO4J_AUTH = (\"neo4j\", None)\n", - "NEO4J_DB = os.environ.get(\"NEO4J_DB\", \"neo4j\")\n", - "if os.environ.get(\"NEO4J_USER\") and os.environ.get(\"NEO4J_PASSWORD\"):\n", - " NEO4J_AUTH = (\n", - " os.environ.get(\"NEO4J_USER\"),\n", - " os.environ.get(\"NEO4J_PASSWORD\"),\n", + "db_connection = DbmsConnectionInfo(\n", + " aura_instance_id=os.environ.get(\"AURA_INSTANCEID\"),\n", + " username=os.environ.get(\"NEO4J_USERNAME\", \"neo4j\"),\n", + " password=os.environ.get(\"NEO4J_PASSWORD\"),\n", + " uri=os.environ[\"NEO4J_URI\"],\n", + ")\n", + "\n", + "session_name = \"neo4j-viz-gds-example\"\n", + "if os.environ.get(\"AURA_API_CLIENT_ID\"):\n", + " # Use Aura Graph Analytics\n", + " sessions = GdsSessions(\n", + " api_credentials=AuraAPICredentials(\n", + " client_id=os.environ[\"AURA_API_CLIENT_ID\"],\n", + " client_secret=os.environ[\"AURA_API_CLIENT_SECRET\"],\n", + " project_id=os.environ.get(\"AURA_API_PROJECT_ID\"),\n", + " )\n", + " )\n", + " gds = sessions.get_or_create(\n", + " session_name=session_name,\n", + " memory=SessionMemory.m_2GB,\n", + " db_connection=db_connection,\n", " )\n", - "gds = GraphDataScience(NEO4J_URI, auth=NEO4J_AUTH, database=NEO4J_DB)" + "else:\n", + " # Use GDS Plugin\n", + " sessions = None\n", + " gds = GraphDataScience(\n", + " endpoint=db_connection.get_uri(),\n", + " auth=(db_connection.username, db_connection.password),\n", + " )" ] }, { "cell_type": "code", "execution_count": null, + "id": "051e4229", "metadata": {}, "outputs": [], "source": [ "G = gds.graph.load_cora(graph_name=\"cora\")" ] }, + { + "cell_type": "markdown", + "id": "f2dff187", + "metadata": {}, + "source": [] + }, { "cell_type": "code", "execution_count": null, + "id": "618e4529", "metadata": {}, "outputs": [], "source": [ @@ -71,6 +130,7 @@ }, { "cell_type": "markdown", + "id": "2a7ccb13", "metadata": {}, "source": [ "## Visualization" @@ -79,1648 +139,32 @@ { "cell_type": "code", "execution_count": null, + "id": "9d29891b", "metadata": {}, "outputs": [], "source": [ "from neo4j_viz.gds import from_gds\n", "\n", - "VG = from_gds(gds, G, max_node_count=500)\n", - "str(VG)" + "VG = from_gds(\n", + " gds,\n", + " G,\n", + " max_node_count=100,\n", + ")" ] }, { "cell_type": "code", - "execution_count": 6, - "metadata": { - "tags": [ - "preserve-output" - ] - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - " \n", - " \n", - " \n", - " neo4j-viz\n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - " \n", - "
\n", - " \n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "id": "00929fe9", + "metadata": {}, + "outputs": [], "source": [ - "VG.render(theme=\"auto\")" + "VG.render()" ] }, { "cell_type": "markdown", + "id": "5246cda2", "metadata": {}, "source": [ "### Changing captions\n", @@ -1732,6 +176,7 @@ { "cell_type": "code", "execution_count": null, + "id": "449956ba", "metadata": {}, "outputs": [], "source": [ @@ -1742,9 +187,8 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "id": "d3289f24", + "metadata": {}, "outputs": [], "source": [ "VG.render()" @@ -1752,6 +196,7 @@ }, { "cell_type": "markdown", + "id": "7eb5e6a2", "metadata": {}, "source": [ "## Sizing the nodes\n", @@ -1762,16 +207,17 @@ { "cell_type": "code", "execution_count": null, + "id": "80801ca0", "metadata": {}, "outputs": [], "source": [ "VG.resize_nodes(property=\"pagerank\")\n", - "VG.color_nodes(property=\"componentId\")\n", "VG.render()" ] }, { "cell_type": "markdown", + "id": "9f0e22eb", "metadata": {}, "source": [ "### Coloring" @@ -1779,6 +225,7 @@ }, { "cell_type": "markdown", + "id": "e2eda873", "metadata": {}, "source": [ "There are two main ways of coloring the nodes of a graph:\n", @@ -1792,17 +239,17 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "id": "b1c33ce3", + "metadata": {}, "outputs": [], "source": [ - "VG.color_nodes(property=\"componentId\")\n", + "VG.color_nodes(property=\"subject\")\n", "VG.render()" ] }, { "cell_type": "markdown", + "id": "9e3d9b21", "metadata": {}, "source": [ "Now, let us color by our continuous node field \"size\" that we computed above with PageRank, again using the default colors.\n", @@ -1813,9 +260,8 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [] - }, + "id": "a25c21cc", + "metadata": {}, "outputs": [], "source": [ "from neo4j_viz.colors import ColorSpace\n", @@ -1826,6 +272,7 @@ }, { "cell_type": "markdown", + "id": "170d3457", "metadata": {}, "source": [ "#### Custom coloring\n", @@ -1837,6 +284,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d0fbf37e", "metadata": {}, "outputs": [], "source": [ @@ -1846,6 +294,7 @@ { "cell_type": "code", "execution_count": null, + "id": "c5027692", "metadata": {}, "outputs": [], "source": [ @@ -1863,1637 +312,17 @@ }, { "cell_type": "code", - "execution_count": 14, - "metadata": { - "tags": [ - "preserve-output" - ] - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - " \n", - " \n", - " \n", - " neo4j-viz\n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - " \n", - "
\n", - " \n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 14, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "id": "58e11be0", + "metadata": {}, + "outputs": [], "source": [ "VG.render()" ] }, { "cell_type": "markdown", + "id": "397221c3", "metadata": {}, "source": [ "### Render options\n", @@ -3509,1639 +338,19 @@ }, { "cell_type": "code", - "execution_count": 15, - "metadata": { - "tags": [ - "preserve-output" - ] - }, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - " \n", - " \n", - " \n", - " neo4j-viz\n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n", - " \n", - "
\n", - " \n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], + "execution_count": null, + "id": "d639feeb", + "metadata": {}, + "outputs": [], "source": [ "from neo4j_viz import Layout\n", "\n", - "VG.render(layout=Layout.CIRCULAR, initial_zoom=0.15)" + "VG.render(layout=Layout.CIRCULAR, initial_zoom=0.5)" ] }, { "cell_type": "markdown", + "id": "0120a2aa", "metadata": {}, "source": [ "## Saving the visualization" @@ -5150,6 +359,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1993a72c", "metadata": {}, "outputs": [], "source": [ @@ -5164,6 +374,7 @@ }, { "cell_type": "markdown", + "id": "f2bc05f4", "metadata": {}, "source": [ "## Cleanup\n", @@ -5174,15 +385,23 @@ { "cell_type": "code", "execution_count": null, - "metadata": { - "tags": [ - "teardown" - ] - }, + "id": "aa3c4f06", + "metadata": {}, "outputs": [], "source": [ "gds.graph.drop(\"cora\")" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2af45ef", + "metadata": {}, + "outputs": [], + "source": [ + "if sessions:\n", + " sessions.delete(session_name=session_name)" + ] } ], "metadata": { @@ -5191,5 +410,5 @@ } }, "nbformat": 4, - "nbformat_minor": 4 + "nbformat_minor": 5 } diff --git a/examples/neo4j-example.ipynb b/examples/neo4j-example.ipynb index 0fb13102..e2149fa1 100644 --- a/examples/neo4j-example.ipynb +++ b/examples/neo4j-example.ipynb @@ -24,7 +24,8 @@ "outputs": [], "source": [ "%pip install neo4j\n", - "%pip install neo4j-viz" + "%pip install neo4j-viz\n", + "%pip install python-dotenv" ] }, { @@ -43,12 +44,13 @@ "outputs": [], "source": [ "import os\n", + "import dotenv\n", + "\n", + "dotenv.load_dotenv(\"../local.env\", override=True)\n", "\n", - "URI = os.environ.get(\"NEO4J_URI\", \"bolt://localhost:7687\")\n", "\n", - "auth = None\n", - "if os.environ.get(\"NEO4J_USER\") and os.environ.get(\"NEO4J_PASSWORD\"):\n", - " auth = (os.environ.get(\"NEO4J_USER\"), os.environ.get(\"NEO4J_PASSWORD\"))" + "URI = os.environ.get(\"NEO4J_URI\", \"bolt://localhost:7687\")\n", + "auth = (os.environ.get(\"NEO4J_USERNAME\"), os.environ.get(\"NEO4J_PASSWORD\"))" ] }, { @@ -1775,12 +1777,12 @@ "

Expected window.__NEO4J_VIZ_DATA__ to be set.

\n", "

This page should be generated by neo4j_viz's render() method.

\n", " \n", - " `,new Error(\"window.__NEO4J_VIZ_DATA__ is not defined\");const ypr={get(t){return x3[t]},on(){},off(){},set(){},save_changes(){}},_3=document.getElementById(\"neo4j-viz-26eaf825ab79\");if(!_3)throw new Error(\"Container element #neo4j-viz-26eaf825ab79 not found\");_3.style.width=x3.width??\"100%\";_3.style.height=x3.height??\"100vh\";mpr.render({model:ypr,el:_3});\n", + " `,new Error(\"window.__NEO4J_VIZ_DATA__ is not defined\");const ypr={get(t){return x3[t]},on(){},off(){},set(){},save_changes(){}},_3=document.getElementById(\"neo4j-viz-03e8377c9452\");if(!_3)throw new Error(\"Container element #neo4j-viz-03e8377c9452 not found\");_3.style.width=x3.width??\"100%\";_3.style.height=x3.height??\"100vh\";mpr.render({model:ypr,el:_3});\n", " \n", - " \n", + " \n", "\n", " \n", - "
\n", + "
\n", " \n", "\n" ], @@ -3421,12 +3423,12 @@ "

Expected window.__NEO4J_VIZ_DATA__ to be set.

\n", "

This page should be generated by neo4j_viz's render() method.

\n", " \n", - " `,new Error(\"window.__NEO4J_VIZ_DATA__ is not defined\");const ypr={get(t){return x3[t]},on(){},off(){},set(){},save_changes(){}},_3=document.getElementById(\"neo4j-viz-77c0906029ee\");if(!_3)throw new Error(\"Container element #neo4j-viz-77c0906029ee not found\");_3.style.width=x3.width??\"100%\";_3.style.height=x3.height??\"100vh\";mpr.render({model:ypr,el:_3});\n", + " `,new Error(\"window.__NEO4J_VIZ_DATA__ is not defined\");const ypr={get(t){return x3[t]},on(){},off(){},set(){},save_changes(){}},_3=document.getElementById(\"neo4j-viz-bcdfb11b9d7f\");if(!_3)throw new Error(\"Container element #neo4j-viz-bcdfb11b9d7f not found\");_3.style.width=x3.width??\"100%\";_3.style.height=x3.height??\"100vh\";mpr.render({model:ypr,el:_3});\n", " \n", - " \n", + " \n", "\n", " \n", - "
\n", + "
\n", " \n", "\n" ], diff --git a/justfile b/justfile index 9f5048a6..f876c3eb 100644 --- a/justfile +++ b/justfile @@ -18,13 +18,25 @@ py-test-gds: trap "cd $ENV_DIR && docker compose down" EXIT cd $ENV_DIR && docker compose up -d cd - + cd python-wrapper && \ NEO4J_URI=bolt://localhost:7687 \ - NEO4J_USER=neo4j \ + NEO4J_USERNAME=neo4j \ NEO4J_PASSWORD=password \ NEO4J_DB=neo4j \ - cd python-wrapper && uv run --group dev --extra gds pytest tests --include-neo4j-and-gds + uv run --group dev --extra gds pytest tests --include-neo4j-and-gds cd .. + +# this expects the local compose setup to be running. +py-test-gds-sessions filter="": + #!/usr/bin/env bash + cd python-wrapper && \ + GDS_SESSION_URI=bolt://localhost:7688 \ + NEO4J_URI=bolt://localhost:7687 \ + NEO4J_USERNAME=neo4j \ + NEO4J_PASSWORD=password \ + uv run --group dev --extra gds pytest tests --include-neo4j-and-gds {{ if filter != "" { "-k '" + filter + "'" } else { "" } }} + local-neo4j-setup: #!/usr/bin/env bash set -e diff --git a/python-wrapper/pyproject.toml b/python-wrapper/pyproject.toml index a2b60e88..66b5e4e4 100644 --- a/python-wrapper/pyproject.toml +++ b/python-wrapper/pyproject.toml @@ -42,7 +42,7 @@ requires-python = ">=3.10" [project.optional-dependencies] pandas = ["pandas>=2, <3", "pandas-stubs>=2, <3"] -gds = ["graphdatascience>=1, <2"] +gds = ["graphdatascience>=1.22, <2"] neo4j = ["neo4j"] snowflake = ["snowflake-snowpark-python>=1, <2"] @@ -60,7 +60,8 @@ dev = [ "streamlit==1.57.0", "matplotlib>=3.9.4", "jupyterlab>=4.5.7", - "anywidget[dev]" + "anywidget[dev]", + "python-dotenv" ] docs = [ "sphinx==8.1.3", @@ -76,9 +77,9 @@ notebook = [ "palettable>=3.3.3", "matplotlib>=3.9.4", "snowflake-snowpark-python==1.50.0", - "python-dotenv", "requests", "marimo", + "python-dotenv" ] [project.urls] @@ -113,7 +114,10 @@ markers = [ ] filterwarnings = [ "error", - "ignore:Jupyter is migrating its paths to use standard platformdirs:DeprecationWarning" + "ignore:Jupyter is migrating its paths to use standard platformdirs:DeprecationWarning", + # snowflake vendors an older `requests` whose dependency check rejects the chardet + # version pulled in transitively by the notebook group. Harmless; ignore it. + "ignore:.*doesn't match a supported version:snowflake.connector.vendored.requests.exceptions.RequestsDependencyWarning" ] [tool.ruff] @@ -174,9 +178,3 @@ exclude = [ ] plugins = ['pydantic.mypy'] untyped_calls_exclude=["nbconvert"] - -[tool.marimo.runtime] -output_max_bytes = 20_000_000 -# -#[tool.marimo.server] -#follow_symlink = true diff --git a/python-wrapper/src/neo4j_viz/gds.py b/python-wrapper/src/neo4j_viz/gds.py index e6a399ac..78e37bae 100644 --- a/python-wrapper/src/neo4j_viz/gds.py +++ b/python-wrapper/src/neo4j_viz/gds.py @@ -2,11 +2,13 @@ import warnings from itertools import chain -from typing import Optional, cast +from typing import Collection, Optional from uuid import uuid4 import pandas as pd from graphdatascience import Graph, GraphDataScience +from graphdatascience.graph.v2 import GraphV2 +from graphdatascience.session import AuraGraphDataScience from neo4j_viz.colors import NEO4J_COLORS_DISCRETE, ColorSpace @@ -15,39 +17,43 @@ def _fetch_node_dfs( - gds: GraphDataScience, - G: Graph, + gds: GraphDataScience | AuraGraphDataScience, + G: GraphV2, node_properties_by_label: dict[str, list[str]], - node_labels: list[str], + node_labels: Collection[str], additional_db_node_properties: list[str], ) -> dict[str, pd.DataFrame]: return { - lbl: gds.graph.nodeProperties.stream( + lbl: gds.v2.graph.node_properties.stream( G, node_properties=node_properties_by_label[lbl], node_labels=[lbl], - separate_property_columns=True, db_node_properties=additional_db_node_properties, ) for lbl in node_labels } -def _fetch_rel_dfs(gds: GraphDataScience, G: Graph) -> list[pd.DataFrame]: - rel_types = G.relationship_types() - - rel_props = {rel_type: G.relationship_properties(rel_type) for rel_type in rel_types} +def _fetch_rel_dfs(gds: GraphDataScience | AuraGraphDataScience, G: GraphV2) -> list[pd.DataFrame]: + rel_props = G.relationship_properties() rel_dfs: list[pd.DataFrame] = [] + # Have to call per stream per relationship type as there was a bug in GDS < 2.21 for rel_type, props in rel_props.items(): - assert isinstance(props, list) - if len(props) > 0: - rel_df = gds.graph.relationshipProperties.stream( - G, relationship_types=rel_type, relationship_properties=list(props), separate_property_columns=True + rel_df = gds.v2.graph.relationships.stream( + G, relationship_types=[rel_type], relationship_properties=list(props) + ) + + # there was a bug in the v2 endpoints in GDS (1.22) where for dataframe would have the incorrect shape + if "propertyValue" and "relationshipProperty" in rel_df.keys(): + rel_df = rel_df.pivot( + index=["sourceNodeId", "targetNodeId", "relationshipType"], + columns="relationshipProperty", + values="propertyValue", ) - else: - rel_df = gds.graph.relationships.stream(G, relationship_types=[rel_type]) + rel_df = rel_df.reset_index() + rel_df.columns.name = None rel_dfs.append(rel_df) @@ -55,8 +61,8 @@ def _fetch_rel_dfs(gds: GraphDataScience, G: Graph) -> list[pd.DataFrame]: def from_gds( - gds: GraphDataScience, - G: Graph, + gds: GraphDataScience | AuraGraphDataScience, + G: Graph | GraphV2, node_properties: Optional[list[str]] = None, db_node_properties: Optional[list[str]] = None, max_node_count: int = 10_000, @@ -76,9 +82,9 @@ def from_gds( Parameters ---------- - gds : GraphDataScience - GraphDataScience object. - G : Graph + gds + GraphDataScience object. AuraGraphDataScience object if using Aura Graph Analytics. + G Graph object. node_properties : list[str], optional Additional properties to include in the visualization node, by default None which means that all node @@ -91,37 +97,41 @@ def from_gds( """ if db_node_properties is None: db_node_properties = [] + if isinstance(G, Graph): + G_v2 = gds.v2.graph.get(G.name()) + else: + G_v2 = G - node_properties_from_gds = G.node_properties() - assert isinstance(node_properties_from_gds, pd.Series) - actual_node_properties: dict[str, list[str]] = cast(dict[str, list[str]], node_properties_from_gds.to_dict()) - all_actual_node_properties = list(chain.from_iterable(actual_node_properties.values())) + gds_properties_per_label = G_v2.node_properties() + all_gds_properties = list(chain.from_iterable(gds_properties_per_label.values())) node_properties_by_label_sets: dict[str, set[str]] = dict() if node_properties is None: - node_properties_by_label_sets = {k: set(v) for k, v in actual_node_properties.items()} + node_properties_by_label_sets = {k: set(v) for k, v in gds_properties_per_label.items()} else: for prop in node_properties: - if prop not in all_actual_node_properties: + if prop not in all_gds_properties: raise ValueError(f"There is no node property '{prop}' in graph '{G.name()}'") - for label, props in actual_node_properties.items(): + for label, props in gds_properties_per_label.items(): node_properties_by_label_sets[label] = { - prop for prop in actual_node_properties[label] if prop in node_properties + prop for prop in gds_properties_per_label[label] if prop in node_properties } node_properties_by_label = {k: list(v) for k, v in node_properties_by_label_sets.items()} - node_count = G.node_count() + node_count = G_v2.node_count() if node_count > max_node_count: warnings.warn( - f"The '{G.name()}' projection's node count ({G.node_count()}) exceeds `max_node_count` ({max_node_count}), so subsampling will be applied. Increase `max_node_count` if needed" + f"The '{G_v2.name()}' projection's node count ({G_v2.node_count()}) exceeds `max_node_count` ({max_node_count}), so subsampling will be applied. Increase `max_node_count` if needed" ) sampling_ratio = float(max_node_count) / node_count sample_name = f"neo4j-viz_sample_{uuid4()}" - G_fetched, _ = gds.graph.sample.rwr(sample_name, G, samplingRatio=sampling_ratio, nodeLabelStratification=True) + G_fetched, _ = gds.v2.graph.sample.rwr( + G_v2, sample_name, sampling_ratio=sampling_ratio, node_label_stratification=True + ) else: - G_fetched = G + G_fetched = G_v2 property_name = None try: @@ -129,12 +139,12 @@ def from_gds( # as a temporary property to ensure that we have at least one property for each label to fetch if sum([len(props) == 0 for props in node_properties_by_label.values()]) > 0: property_name = f"neo4j-viz_property_{uuid4()}" - gds.degree.mutate(G_fetched, mutateProperty=property_name) + gds.v2.degree_centrality.mutate(G_fetched, mutate_property=property_name) for props in node_properties_by_label.values(): props.append(property_name) node_dfs = _fetch_node_dfs( - gds, G_fetched, node_properties_by_label, G_fetched.node_labels(), db_node_properties + gds, G_fetched, node_properties_by_label, node_properties_by_label.keys(), db_node_properties ) if property_name is not None: for df in node_dfs.values(): @@ -145,7 +155,7 @@ def from_gds( if G_fetched.name() != G.name(): G_fetched.drop() elif property_name is not None: - gds.graph.nodeProperties.drop(G_fetched, node_properties=[property_name]) + gds.v2.graph.node_properties.drop(G_fetched, node_properties=[property_name]) for df in node_dfs.values(): if property_name is not None and property_name in df.columns: @@ -154,7 +164,7 @@ def from_gds( node_props_df = pd.concat(node_dfs.values(), ignore_index=True, axis=0).drop_duplicates(subset=["nodeId"]) for lbl, df in node_dfs.items(): - if "labels" in all_actual_node_properties: + if "labels" in all_gds_properties: df.rename(columns={"labels": "__labels"}, inplace=True) df["labels"] = lbl diff --git a/python-wrapper/tests/conftest.py b/python-wrapper/tests/conftest.py index 40f7f4e8..5bdf7758 100644 --- a/python-wrapper/tests/conftest.py +++ b/python-wrapper/tests/conftest.py @@ -1,4 +1,5 @@ import os +import random from typing import Any, Generator import pytest @@ -31,56 +32,76 @@ def pytest_collection_modifyitems(config: Any, items: Any) -> None: @pytest.fixture(scope="package") -def aura_ds_instance() -> Generator[Any, None, None]: +def aura_db_instance() -> Generator[Any, None, None]: + if os.environ.get("NEO4J_URI", ""): + print(f"Skipping Aura DB setup since NEO4J_URI is set to {os.environ['NEO4J_URI']}") + yield None + return + if os.environ.get("AURA_API_CLIENT_ID", None) is None: yield None return - from tests.gds_helper import aura_api, create_aurads_instance + from tests.gds_helper import aura_api, create_auradb_instance api = aura_api() - id, dbms_connection_info = create_aurads_instance(api) + dbms_connection_info = create_auradb_instance(api) # setting as environment variables to run notebooks with this connection os.environ["NEO4J_URI"] = dbms_connection_info.get_uri() assert isinstance(dbms_connection_info.username, str) - os.environ["NEO4J_USER"] = dbms_connection_info.username + os.environ["NEO4J_USERNAME"] = dbms_connection_info.username assert isinstance(dbms_connection_info.password, str) os.environ["NEO4J_PASSWORD"] = dbms_connection_info.password + old_instance = os.environ.get("AURA_INSTANCEID", "") + if dbms_connection_info.aura_instance_id: + os.environ["AURA_INSTANCEID"] = dbms_connection_info.aura_instance_id + yield dbms_connection_info # Clear Neo4j_URI after test (rerun should create a new instance) - os.environ["NEO4J_URI"] = "" - api.delete_instance(id) + os.environ["AURA_INSTANCEID"] = old_instance + assert dbms_connection_info.aura_instance_id is not None + api.delete_instance(dbms_connection_info.aura_instance_id) @pytest.fixture(scope="package") -def gds(aura_ds_instance: Any) -> Generator[Any, None, None]: - from graphdatascience import GraphDataScience +def gds(aura_db_instance: Any) -> Generator[Any, None, None]: + from graphdatascience.session import SessionMemory - from tests.gds_helper import connect_to_plugin_gds + from tests.gds_helper import connect_to_local_gds_session, connect_to_plugin_gds, gds_sessions - if aura_ds_instance: - yield GraphDataScience( - endpoint=aura_ds_instance.uri, - auth=(aura_ds_instance.username, aura_ds_instance.password), - aura_ds=True, - database="neo4j", + if aura_db_instance: + sessions = gds_sessions() + + gds = sessions.get_or_create( + f"neo4j-viz-ci-{os.environ.get('GITHUB_RUN_ID', random.randint(0, 10**6))}", + memory=SessionMemory.m_2GB, + db_connection=aura_db_instance, ) + + yield gds + gds.delete() else: - NEO4J_URI = os.environ.get("NEO4J_URI", "neo4j://localhost:7687") - gds = connect_to_plugin_gds(NEO4J_URI) + neo4j_uri = os.environ["NEO4J_URI"] + neo4j_auth = (os.environ.get("NEO4J_USERNAME", "neo4j"), os.environ.get("NEO4J_PASSWORD", "password")) + + session_uri = os.environ.get("GDS_SESSION_URI") + if session_uri: + gds = connect_to_local_gds_session(session_uri, neo4j_uri, neo4j_auth) + else: + gds = connect_to_plugin_gds(neo4j_uri, neo4j_auth) # type: ignore yield gds gds.close() @pytest.fixture(scope="package") -def neo4j_driver(aura_ds_instance: Any) -> Generator[Any, None, None]: +def neo4j_driver(aura_db_instance: Any) -> Generator[Any, None, None]: import neo4j - if aura_ds_instance: + if aura_db_instance: driver = neo4j.GraphDatabase.driver( - aura_ds_instance.uri, auth=(aura_ds_instance.username, aura_ds_instance.password) + aura_db_instance.uri, auth=(aura_db_instance.username, aura_db_instance.password) ) else: NEO4J_URI = os.environ.get("NEO4J_URI", "neo4j://localhost:7687") diff --git a/python-wrapper/tests/gds_helper.py b/python-wrapper/tests/gds_helper.py index e5a0d3dc..5d7a6775 100644 --- a/python-wrapper/tests/gds_helper.py +++ b/python-wrapper/tests/gds_helper.py @@ -1,9 +1,10 @@ import os import re -from graphdatascience import GraphDataScience +from graphdatascience import GdsSessions, GraphDataScience +from graphdatascience.arrow_client.arrow_authentication import UsernamePasswordAuthentication from graphdatascience.semantic_version.semantic_version import SemanticVersion -from graphdatascience.session import DbmsConnectionInfo, SessionMemory +from graphdatascience.session import AuraAPICredentials, AuraGraphDataScience, DbmsConnectionInfo, SessionMemory from graphdatascience.session.aura_api import AuraApi from graphdatascience.session.aura_api_responses import InstanceCreateDetails from graphdatascience.version import __version__ @@ -26,12 +27,20 @@ def parse_version(version: str) -> SemanticVersion: GDS_VERSION = parse_version(__version__) -def connect_to_plugin_gds(uri: str) -> GraphDataScience: - NEO4J_AUTH = ("neo4j", "password") - if os.environ.get("NEO4J_USER"): - NEO4J_AUTH = (os.environ.get("NEO4J_USER", "DUMMY"), os.environ.get("NEO4J_PASSWORD", "neo4j")) +def connect_to_plugin_gds(uri: str, auth: tuple[str, str]) -> GraphDataScience: + return GraphDataScience(endpoint=uri, auth=auth, database="neo4j") - return GraphDataScience(endpoint=uri, auth=NEO4J_AUTH, database="neo4j") + +def connect_to_local_gds_session(session_uri: str, db_uri: str, db_auth: tuple[str, str]) -> AuraGraphDataScience: + session_bolt_connection_info = DbmsConnectionInfo(uri=session_uri, username="neo4j", password="password") + db_connection_info = DbmsConnectionInfo(uri=db_uri, username=db_auth[0], password=db_auth[1]) + + return AuraGraphDataScience.create( + session_bolt_connection_info=session_bolt_connection_info, + arrow_authentication=UsernamePasswordAuthentication("neo4j", "password"), + session_lifecycle_manager=None, # type: ignore + db_endpoint=db_connection_info, + ) def aura_api() -> AuraApi: @@ -49,21 +58,29 @@ def aura_api() -> AuraApi: ) -def create_aurads_instance(api: AuraApi) -> tuple[str, DbmsConnectionInfo]: - # Switch to Sessions once they can be created without a DB +def gds_sessions() -> GdsSessions: + return GdsSessions( + api_credentials=AuraAPICredentials( + client_id=os.environ["AURA_API_CLIENT_ID"], + client_secret=os.environ["AURA_API_CLIENT_SECRET"], + project_id=os.environ.get("AURA_API_TENANT_ID"), + ) + ) + + +def create_auradb_instance(api: AuraApi) -> DbmsConnectionInfo: instance_details: InstanceCreateDetails = api.create_instance( - name="ci-neo4j-viz-session", - memory=SessionMemory.m_8GB.value, + name="ci-neo4j-viz-db", + memory=SessionMemory.m_2GB.value, cloud_provider="gcp", region="europe-west1", + type="enterprise-db", ) wait_result = api.wait_for_instance_running(instance_id=instance_details.id) if wait_result.error: raise Exception(f"Error while waiting for instance to be running: {wait_result.error}") - return instance_details.id, DbmsConnectionInfo( - uri=wait_result.connection_url, - username="neo4j", - password=instance_details.password, + return DbmsConnectionInfo( + username="neo4j", password=instance_details.password, aura_instance_id=instance_details.id ) diff --git a/python-wrapper/tests/test_gds.py b/python-wrapper/tests/test_gds.py index fb078aad..ef9c4d8a 100644 --- a/python-wrapper/tests/test_gds.py +++ b/python-wrapper/tests/test_gds.py @@ -20,12 +20,23 @@ def db_setup(gds: Any) -> Generator[None, None, None]: gds.run_cypher("MATCH (n:_CI_A|_CI_B) DETACH DELETE n") +def project_graph(gds: Any) -> Any: + from graphdatascience import GraphDataScience + from graphdatascience.session import AuraGraphDataScience + + if isinstance(gds, GraphDataScience): + return gds.v2.graph.project("g2", "*", "*") + elif isinstance(gds, AuraGraphDataScience): + return gds.v2.graph.project("g2", "MATCH (n)–->(m) RETURN gds.graph.project.remote(n, m)") + raise Exception(f"Unsupported GDS type {type(gds)}") + + @pytest.mark.filterwarnings("ignore::DeprecationWarning") @pytest.mark.requires_neo4j_and_gds def test_from_gds_integration_all_db_properties(gds: Any, db_setup: None) -> None: from neo4j_viz.gds import from_gds - with gds.graph.project("g2", ["_CI_A", "_CI_B"], "*") as G: + with project_graph(gds) as G: VG = from_gds(gds, G, db_node_properties=["name"]) assert len(VG.nodes) == 2 @@ -55,7 +66,7 @@ def test_from_gds_integration_all_properties(gds: Any) -> None: } ) - with gds.graph.construct("flo", nodes, rels) as G: + with gds.v2.graph.construct("flo", nodes, rels) as G: VG = from_gds(gds, G) assert len(VG.nodes) == 3 @@ -106,7 +117,7 @@ def test_from_gds_integration_all_properties(gds: Any) -> None: def test_from_gds_sample(gds: Any) -> None: from neo4j_viz.gds import from_gds - with gds.graph.generate("hello", node_count=11_000, average_degree=1) as G: + with gds.v2.graph.generate("hello", node_count=11_000, average_degree=1) as G: with pytest.warns( UserWarning, match=re.escape( @@ -159,7 +170,7 @@ def test_from_gds_hetero(gds: Any) -> None: } ) - with gds.graph.construct("flo", [A_nodes, B_nodes], [X_rels, Y_rels]) as G: + with gds.v2.graph.construct("flo", [A_nodes, B_nodes], [X_rels, Y_rels]) as G: VG = from_gds( gds, G, diff --git a/python-wrapper/tests/test_notebooks.py b/python-wrapper/tests/test_notebooks.py index d3b7c60f..939a01e1 100644 --- a/python-wrapper/tests/test_notebooks.py +++ b/python-wrapper/tests/test_notebooks.py @@ -1,3 +1,4 @@ +import os import pathlib import signal import sys @@ -6,6 +7,7 @@ import nbformat import pytest +from dotenv import load_dotenv from nbclient.exceptions import CellExecutionError from nbconvert.preprocessors.execute import ExecutePreprocessor @@ -117,6 +119,8 @@ def run_notebooks(filter_func: Callable[[str], bool]) -> None: def test_neo4j(gds: Any) -> None: neo4j_notebooks = ["neo4j-example.ipynb", "gds-example.ipynb"] + load_dotenv(os.environ.get("ENV_FILE")) + def filter_func(notebook: str) -> bool: return notebook in neo4j_notebooks diff --git a/python-wrapper/uv.lock b/python-wrapper/uv.lock index 39871d69..32ee284a 100644 --- a/python-wrapper/uv.lock +++ b/python-wrapper/uv.lock @@ -193,15 +193,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, ] -[[package]] -name = "async-generator" -version = "1.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/b6/6fa6b3b598a03cba5e80f829e0dadbb49d7645f523d209b2fb7ea0bbb02a/async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144", size = 29870, upload-time = "2018-08-01T03:36:21.69Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/52/39d20e03abd0ac9159c162ec24b93fbcaa111e8400308f2465432495ca2b/async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", size = 18857, upload-time = "2018-08-01T03:36:20.029Z" }, -] - [[package]] name = "async-lru" version = "2.3.0" @@ -1102,7 +1093,7 @@ wheels = [ [[package]] name = "graphdatascience" -version = "1.21" +version = "1.22" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "multimethod" }, @@ -1118,9 +1109,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c2/e9/a49127cc728c7ea2acaa2aad44f06f291aabe0a5a700f980ffdf88f853da/graphdatascience-1.21.tar.gz", hash = "sha256:5a9f16be010eee69d027c5b1ea76bba7029fad8426646c603f137cc9841e3934", size = 1746004, upload-time = "2026-04-16T11:49:01.974Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/9f/290f299f770ef9aad73b0c49e5683a6f7455cbc16b1b0a5546109a6f0fd9/graphdatascience-1.22.tar.gz", hash = "sha256:00bfad65dcaccd3d40f1f4e23661125dc7400ecffdb5a04c34512fc9a287f5a9", size = 1781480, upload-time = "2026-06-04T11:57:24.877Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/67/a2a944d6d6c0baea1b30017435d2b04ce0b766328c277fd90b7ee36b7bbc/graphdatascience-1.21-py3-none-any.whl", hash = "sha256:4813c3fa6eef5d469a7d344ac37b8992461ed067ee30b6b11bd17c3d5c471592", size = 2016416, upload-time = "2026-04-16T11:48:59.86Z" }, + { url = "https://files.pythonhosted.org/packages/2f/4c/a55ff1ad641414456be2b2bc3d57a4d2bb86c5dadca378c0c83503bddbb4/graphdatascience-1.22-py3-none-any.whl", hash = "sha256:e2dfbd206daa61c39d00653d420edc63f409674d1449d6d0253794e1807b7604", size = 2101782, upload-time = "2026-06-04T11:57:23.036Z" }, ] [[package]] @@ -1158,6 +1149,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/b9/be66eb0decd730d89b9c94f930e4b8d87787b05724bb84af98bfd825f72c/httptools-0.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bf3b6f807c8541503cecfbb8a8dffb385640d0d96102f3d112aa8740f9b7c826", size = 208805, upload-time = "2026-05-25T22:16:50.434Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f7/b4d41eaae2869d31356bc4bbf546f44fae83ff298af0a043ca0625b06773/httptools-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da684f2e1aa2ee9bdcb083f3f3a68c5956750b375bc5df864d3a5f0c42a40b77", size = 113527, upload-time = "2026-05-25T22:16:51.672Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e4/77487e14fc7be47180fd0eb4267c7486d0cc59b74031839a3daf8650136b/httptools-0.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6f21e2a3b0067bbe7f67e34cfd16276af556e5e52f4c7503be0cb5f90e905e4", size = 450035, upload-time = "2026-05-25T22:16:53.313Z" }, + { url = "https://files.pythonhosted.org/packages/da/72/5a8f787e323f56fbd86c32a4be92a86776e4cfe8b4317db999f452028362/httptools-0.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea897f0c729581ebf72131a438a7932d9b14efef72d75ada966700cac3caaeb", size = 451101, upload-time = "2026-05-25T22:16:54.696Z" }, + { url = "https://files.pythonhosted.org/packages/ed/41/b44a25560955197674b6744cb903664300e239235a5eaa69df0890d87054/httptools-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0d726cc107fceb7d45f978483b4b70dd8caa836f5914d3434bb18628eb73813", size = 436140, upload-time = "2026-05-25T22:16:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/74/b0/054aac84c03d7e097bf4c605fb7e74eec3d65c0276adf64ee97f3a103ff5/httptools-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9878eb2785ba5eb70631ad269b37976f73d647955e26c91d490eb8a4edfda4ba", size = 437041, upload-time = "2026-05-25T22:16:57.716Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e8/86b85bbc0ac7892232f1a99ab96a9aa71936984fa06adfc0afc83ca7789e/httptools-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:b205e5f5523fa039679da0dfe5a10132b2a4abeae6a86fdd1ddc035f7f836557", size = 90454, upload-time = "2026-05-25T22:16:58.871Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" }, + { url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" }, + { url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" }, + { url = "https://files.pythonhosted.org/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07", size = 90991, upload-time = "2026-05-25T22:17:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -1191,18 +1232,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, ] -[[package]] -name = "importlib-metadata" -version = "9.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -2469,7 +2498,7 @@ wheels = [ [[package]] name = "neo4j-viz" -version = "1.4.0" +version = "1.5.0" source = { editable = "." } dependencies = [ { name = "anywidget" }, @@ -2507,6 +2536,7 @@ dev = [ { name = "palettable" }, { name = "pytest" }, { name = "pytest-mock" }, + { name = "python-dotenv" }, { name = "ruff" }, { name = "selenium" }, { name = "streamlit" }, @@ -2534,7 +2564,7 @@ notebook = [ requires-dist = [ { name = "anywidget", specifier = ">=0.9,<1" }, { name = "enum-tools", specifier = "==0.13.0" }, - { name = "graphdatascience", marker = "extra == 'gds'", specifier = ">=1,<2" }, + { name = "graphdatascience", marker = "extra == 'gds'", specifier = ">=1.22,<2" }, { name = "ipython", specifier = ">=7,<10" }, { name = "neo4j", marker = "extra == 'neo4j'" }, { name = "pandas", marker = "extra == 'pandas'", specifier = ">=2,<3" }, @@ -2557,9 +2587,10 @@ dev = [ { name = "palettable", specifier = "==3.3.3" }, { name = "pytest", specifier = "==9.0.3" }, { name = "pytest-mock", specifier = "==3.15.1" }, + { name = "python-dotenv" }, { name = "ruff", specifier = "==0.15.12" }, - { name = "selenium", specifier = "==4.40.0" }, - { name = "streamlit", specifier = "==1.56.0" }, + { name = "selenium", specifier = "==4.43.0" }, + { name = "streamlit", specifier = "==1.57.0" }, ] docs = [ { name = "enum-tools", extras = ["sphinx"] }, @@ -2577,7 +2608,7 @@ notebook = [ { name = "pykernel", specifier = ">=0.1.6" }, { name = "python-dotenv" }, { name = "requests" }, - { name = "snowflake-snowpark-python", specifier = "==1.42.0" }, + { name = "snowflake-snowpark-python", specifier = "==1.50.0" }, ] [[package]] @@ -3497,6 +3528,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/27/be/0631a861af4d1c875f096c07d34e9a63639560a717130e7a87cbc82b7e3f/python_json_logger-4.1.0-py3-none-any.whl", hash = "sha256:132994765cf75bf44554be9aa49b06ef2345d23661a96720262716438141b6b2", size = 15021, upload-time = "2026-03-29T04:39:55.266Z" }, ] +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + [[package]] name = "pytz" version = "2026.2" @@ -3967,22 +4007,19 @@ wheels = [ [[package]] name = "selenium" -version = "4.40.0" +version = "4.43.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "trio" }, - { name = "trio-typing" }, { name = "trio-websocket" }, - { name = "types-certifi" }, - { name = "types-urllib3" }, { name = "typing-extensions" }, { name = "urllib3", extra = ["socks"] }, { name = "websocket-client" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/ef/a5727fa7b33d20d296322adf851b76072d8d3513e1b151969d3228437faf/selenium-4.40.0.tar.gz", hash = "sha256:a88f5905d88ad0b84991c2386ea39e2bbde6d6c334be38df5842318ba98eaa8c", size = 930444, upload-time = "2026-01-18T23:12:31.565Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/6a/fe950b498a3c570ab538ad1c2b60f18863eecf077a865eea4459f3fa78a9/selenium-4.43.0.tar.gz", hash = "sha256:bada5c08a989f812728a4b5bea884d8e91894e939a441cc3a025201ce718581e", size = 967747, upload-time = "2026-04-10T06:47:03.149Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/74/eb9d6540aca1911106fa0877b8e9ef24171bc18857937a6b0ffe0586c623/selenium-4.40.0-py3-none-any.whl", hash = "sha256:c8823fc02e2c771d9ad9a0cf899cee7de1a57a6697e3d0b91f67566129f2b729", size = 9608184, upload-time = "2026-01-18T23:12:29.435Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/0c55fbb0275fc368676ea50514ce7d7839d799a8b3ff8425f380186c7626/selenium-4.43.0-py3-none-any.whl", hash = "sha256:4f97639055dcfa9eadf8ccf549ba7b0e49c655d4e2bde19b9a44e916b754e769", size = 9573091, upload-time = "2026-04-10T06:47:01.134Z" }, ] [[package]] @@ -4088,7 +4125,7 @@ wheels = [ [[package]] name = "snowflake-snowpark-python" -version = "1.42.0" +version = "1.50.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cloudpickle" }, @@ -4101,9 +4138,9 @@ dependencies = [ { name = "tzlocal" }, { name = "wheel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/78/e3/b70799997481185cdad44b0786c7597764935d78d71632b4735fb05d63a1/snowflake_snowpark_python-1.42.0.tar.gz", hash = "sha256:e994b3860c816d1b5fdf0c6272f8d9e41505e470140b063ff9418d234fd8cc00", size = 1781749, upload-time = "2025-10-28T18:10:52.418Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/f9/c7e4d0f4dceb6cf1b88ed984948af5471ecc19514a7e519da7276226429f/snowflake_snowpark_python-1.50.0.tar.gz", hash = "sha256:8af823326c2681333bf59ad3d6152b07098b7926165667a7fdcebd5adb53642f", size = 1760444, upload-time = "2026-04-23T20:33:56.522Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/ea/a3f1ff82aa144fd072f4be440ed636f4c298a7ee7a278e68709cf2753da5/snowflake_snowpark_python-1.42.0-py3-none-any.whl", hash = "sha256:fd92a3633b79573bb481b6e85a1434842758637dc6a30b32b9c5ce2824f4296d", size = 1825602, upload-time = "2025-10-28T18:10:48.778Z" }, + { url = "https://files.pythonhosted.org/packages/b3/11/0fbeb214832e5f9a6c8123e8de3e94610cab9c9a94eb1f011580f717f682/snowflake_snowpark_python-1.50.0-py3-none-any.whl", hash = "sha256:1a0140dba9a4d44910a052110494069fc9142577ef5db67d58abe6996f420a11", size = 1851309, upload-time = "2026-04-23T20:33:54.654Z" }, ] [[package]] @@ -4360,14 +4397,17 @@ wheels = [ [[package]] name = "streamlit" -version = "1.56.0" +version = "1.57.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "altair" }, + { name = "anyio" }, { name = "blinker" }, { name = "cachetools" }, { name = "click" }, { name = "gitpython" }, + { name = "httptools" }, + { name = "itsdangerous" }, { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "packaging" }, @@ -4376,16 +4416,19 @@ dependencies = [ { name = "protobuf" }, { name = "pyarrow" }, { name = "pydeck" }, + { name = "python-multipart" }, { name = "requests" }, + { name = "starlette" }, { name = "tenacity" }, { name = "toml" }, - { name = "tornado" }, { name = "typing-extensions" }, + { name = "uvicorn" }, { name = "watchdog", marker = "sys_platform != 'darwin'" }, + { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/03/85/7c669b3a1336d34ef39fa9760fbd343185f3b15db2ad0838fd78423d1c7f/streamlit-1.56.0.tar.gz", hash = "sha256:1176acfa89ae1318b79078e8efe689a9d02e8d58e325c00fc0e55fa2f3fe8d6a", size = 8559239, upload-time = "2026-03-31T22:29:38.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5d/f8/b2daf7a5f8ae15527daf94406e771bb6075e958a01c3dde9eba79dc3c9a3/streamlit-1.57.0.tar.gz", hash = "sha256:0b028d305c1a1a757071b2c9504966787602842fc8af6e873795ca58d2b4d12f", size = 8678859, upload-time = "2026-04-28T22:13:32.238Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e4/91/cb6f13a89e376ef179309d74f37a70ea0041d5e4b5ba5c4836dbf6e020ad/streamlit-1.56.0-py3-none-any.whl", hash = "sha256:8677a335734a30a51bc57ad0ec910e365d95f7c456fc02c60032927cd0729dc5", size = 9052089, upload-time = "2026-03-31T22:29:36.342Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1a/3ca2293d8552bacea3e67e9600d2d1df7df4a325059769ad83d91c279595/streamlit-1.57.0-py3-none-any.whl", hash = "sha256:0d1d41972aeade5637dbb0e7f0eefa5312272f85304923d240a1b1f0475249c8", size = 9194216, upload-time = "2026-04-28T22:13:29.624Z" }, ] [[package]] @@ -4569,23 +4612,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/93/dab25dc87ac48da0fe0f6419e07d0bfd98799bed4e05e7b9e0f85a1a4b4b/trio-0.33.0-py3-none-any.whl", hash = "sha256:3bd5d87f781d9b0192d592aef28691f8951d6c2e41b7e1da4c25cde6c180ae9b", size = 510294, upload-time = "2026-02-14T18:40:53.313Z" }, ] -[[package]] -name = "trio-typing" -version = "0.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "async-generator" }, - { name = "importlib-metadata" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "trio" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b5/74/a87aafa40ec3a37089148b859892cbe2eef08d132c816d58a60459be5337/trio-typing-0.10.0.tar.gz", hash = "sha256:065ee684296d52a8ab0e2374666301aec36ee5747ac0e7a61f230250f8907ac3", size = 38747, upload-time = "2023-12-01T02:54:55.508Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/ff/9bd795273eb14fac7f6a59d16cc8c4d0948a619a1193d375437c7f50f3eb/trio_typing-0.10.0-py3-none-any.whl", hash = "sha256:6d0e7ec9d837a2fe03591031a172533fbf4a1a95baf369edebfc51d5a49f0264", size = 42224, upload-time = "2023-12-01T02:54:54.1Z" }, -] - [[package]] name = "trio-websocket" version = "0.12.2" @@ -4601,15 +4627,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221, upload-time = "2025-02-25T05:16:57.545Z" }, ] -[[package]] -name = "types-certifi" -version = "2021.10.8.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/68/943c3aeaf14624712a0357c4a67814dba5cea36d194f5c764dad7959a00c/types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f", size = 2095, upload-time = "2022-06-09T15:19:05.244Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/63/2463d89481e811f007b0e1cd0a91e52e141b47f9de724d20db7b861dcfec/types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a", size = 2136, upload-time = "2022-06-09T15:19:03.127Z" }, -] - [[package]] name = "types-pytz" version = "2026.1.1.20260408" @@ -4619,15 +4636,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/90/12c059e6bb330a22d9cc97daf027ac7fb7f50fbf518e4d88185b4d39120e/types_pytz-2026.1.1.20260408-py3-none-any.whl", hash = "sha256:c7e4dec76221fb7d0c97b91ad8561d689bebe39b6bcb7b728387e7ffd8cde788", size = 10124, upload-time = "2026-04-08T04:28:13.353Z" }, ] -[[package]] -name = "types-urllib3" -version = "1.26.25.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/de/b9d7a68ad39092368fb21dd6194b362b98a1daeea5dcfef5e1adb5031c7e/types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f", size = 11239, upload-time = "2023-07-20T15:19:31.307Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/7b/3fc711b2efea5e85a7a0bbfe269ea944aa767bbba5ec52f9ee45d362ccf3/types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e", size = 15377, upload-time = "2023-07-20T15:19:30.379Z" }, -] - [[package]] name = "typing-extensions" version = "4.15.0" @@ -4964,12 +4972,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/c7/79/12135bdf8b9c9367b wheels = [ { url = "https://files.pythonhosted.org/packages/a4/f5/10b68b7b1544245097b2a1b8238f66f2fc6dcaeb24ba5d917f52bd2eed4f/wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584", size = 24405, upload-time = "2025-11-20T18:18:00.454Z" }, ] - -[[package]] -name = "zipp" -version = "3.23.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, -] From 4ed1b894521f5ba516171f52b55e0a944d443f35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florentin=20D=C3=B6rre?= Date: Fri, 5 Jun 2026 15:59:51 +0200 Subject: [PATCH 2/6] Fix setup for AGA --- examples/gds-example.ipynb | 34 +----------------------- examples/neo4j-example.ipynb | 2 +- python-wrapper/tests/conftest.py | 42 ++++++++++++++++-------------- python-wrapper/tests/gds_helper.py | 21 ++++++++++----- 4 files changed, 39 insertions(+), 60 deletions(-) diff --git a/examples/gds-example.ipynb b/examples/gds-example.ipynb index 661f6ffd..e9606ccb 100644 --- a/examples/gds-example.ipynb +++ b/examples/gds-example.ipynb @@ -2,7 +2,6 @@ "cells": [ { "cell_type": "markdown", - "id": "e613852e", "metadata": {}, "source": [ "# Visualizing Neo4j Graph Data Science (GDS) Graphs" @@ -11,7 +10,6 @@ { "cell_type": "code", "execution_count": null, - "id": "7149a3b4", "metadata": {}, "outputs": [], "source": [ @@ -23,18 +21,16 @@ { "cell_type": "code", "execution_count": null, - "id": "a6a8c792", "metadata": {}, "outputs": [], "source": [ "import dotenv\n", "\n", - "dotenv.load_dotenv(\"../local.env\", override=True)" + "dotenv.load_dotenv()" ] }, { "cell_type": "markdown", - "id": "533c652a", "metadata": {}, "source": [ "## Setup GDS graph\n", @@ -51,7 +47,6 @@ { "cell_type": "code", "execution_count": null, - "id": "3d98bfe7", "metadata": {}, "outputs": [], "source": [ @@ -100,7 +95,6 @@ { "cell_type": "code", "execution_count": null, - "id": "051e4229", "metadata": {}, "outputs": [], "source": [ @@ -109,14 +103,12 @@ }, { "cell_type": "markdown", - "id": "f2dff187", "metadata": {}, "source": [] }, { "cell_type": "code", "execution_count": null, - "id": "618e4529", "metadata": {}, "outputs": [], "source": [ @@ -130,7 +122,6 @@ }, { "cell_type": "markdown", - "id": "2a7ccb13", "metadata": {}, "source": [ "## Visualization" @@ -139,7 +130,6 @@ { "cell_type": "code", "execution_count": null, - "id": "9d29891b", "metadata": {}, "outputs": [], "source": [ @@ -155,7 +145,6 @@ { "cell_type": "code", "execution_count": null, - "id": "00929fe9", "metadata": {}, "outputs": [], "source": [ @@ -164,7 +153,6 @@ }, { "cell_type": "markdown", - "id": "5246cda2", "metadata": {}, "source": [ "### Changing captions\n", @@ -176,7 +164,6 @@ { "cell_type": "code", "execution_count": null, - "id": "449956ba", "metadata": {}, "outputs": [], "source": [ @@ -187,7 +174,6 @@ { "cell_type": "code", "execution_count": null, - "id": "d3289f24", "metadata": {}, "outputs": [], "source": [ @@ -196,7 +182,6 @@ }, { "cell_type": "markdown", - "id": "7eb5e6a2", "metadata": {}, "source": [ "## Sizing the nodes\n", @@ -207,7 +192,6 @@ { "cell_type": "code", "execution_count": null, - "id": "80801ca0", "metadata": {}, "outputs": [], "source": [ @@ -217,7 +201,6 @@ }, { "cell_type": "markdown", - "id": "9f0e22eb", "metadata": {}, "source": [ "### Coloring" @@ -225,7 +208,6 @@ }, { "cell_type": "markdown", - "id": "e2eda873", "metadata": {}, "source": [ "There are two main ways of coloring the nodes of a graph:\n", @@ -239,7 +221,6 @@ { "cell_type": "code", "execution_count": null, - "id": "b1c33ce3", "metadata": {}, "outputs": [], "source": [ @@ -249,7 +230,6 @@ }, { "cell_type": "markdown", - "id": "9e3d9b21", "metadata": {}, "source": [ "Now, let us color by our continuous node field \"size\" that we computed above with PageRank, again using the default colors.\n", @@ -260,7 +240,6 @@ { "cell_type": "code", "execution_count": null, - "id": "a25c21cc", "metadata": {}, "outputs": [], "source": [ @@ -272,7 +251,6 @@ }, { "cell_type": "markdown", - "id": "170d3457", "metadata": {}, "source": [ "#### Custom coloring\n", @@ -284,7 +262,6 @@ { "cell_type": "code", "execution_count": null, - "id": "d0fbf37e", "metadata": {}, "outputs": [], "source": [ @@ -294,7 +271,6 @@ { "cell_type": "code", "execution_count": null, - "id": "c5027692", "metadata": {}, "outputs": [], "source": [ @@ -313,7 +289,6 @@ { "cell_type": "code", "execution_count": null, - "id": "58e11be0", "metadata": {}, "outputs": [], "source": [ @@ -322,7 +297,6 @@ }, { "cell_type": "markdown", - "id": "397221c3", "metadata": {}, "source": [ "### Render options\n", @@ -339,7 +313,6 @@ { "cell_type": "code", "execution_count": null, - "id": "d639feeb", "metadata": {}, "outputs": [], "source": [ @@ -350,7 +323,6 @@ }, { "cell_type": "markdown", - "id": "0120a2aa", "metadata": {}, "source": [ "## Saving the visualization" @@ -359,7 +331,6 @@ { "cell_type": "code", "execution_count": null, - "id": "1993a72c", "metadata": {}, "outputs": [], "source": [ @@ -374,7 +345,6 @@ }, { "cell_type": "markdown", - "id": "f2bc05f4", "metadata": {}, "source": [ "## Cleanup\n", @@ -385,7 +355,6 @@ { "cell_type": "code", "execution_count": null, - "id": "aa3c4f06", "metadata": {}, "outputs": [], "source": [ @@ -395,7 +364,6 @@ { "cell_type": "code", "execution_count": null, - "id": "c2af45ef", "metadata": {}, "outputs": [], "source": [ diff --git a/examples/neo4j-example.ipynb b/examples/neo4j-example.ipynb index e2149fa1..2e8da54e 100644 --- a/examples/neo4j-example.ipynb +++ b/examples/neo4j-example.ipynb @@ -46,7 +46,7 @@ "import os\n", "import dotenv\n", "\n", - "dotenv.load_dotenv(\"../local.env\", override=True)\n", + "dotenv.load_dotenv()\n", "\n", "\n", "URI = os.environ.get(\"NEO4J_URI\", \"bolt://localhost:7687\")\n", diff --git a/python-wrapper/tests/conftest.py b/python-wrapper/tests/conftest.py index 5bdf7758..543b276a 100644 --- a/python-wrapper/tests/conftest.py +++ b/python-wrapper/tests/conftest.py @@ -42,27 +42,31 @@ def aura_db_instance() -> Generator[Any, None, None]: yield None return - from tests.gds_helper import aura_api, create_auradb_instance + from tests.gds_helper import aura_api, create_auradb_instance, wait_for_instance api = aura_api() - dbms_connection_info = create_auradb_instance(api) - - # setting as environment variables to run notebooks with this connection - os.environ["NEO4J_URI"] = dbms_connection_info.get_uri() - assert isinstance(dbms_connection_info.username, str) - os.environ["NEO4J_USERNAME"] = dbms_connection_info.username - assert isinstance(dbms_connection_info.password, str) - os.environ["NEO4J_PASSWORD"] = dbms_connection_info.password - old_instance = os.environ.get("AURA_INSTANCEID", "") - if dbms_connection_info.aura_instance_id: - os.environ["AURA_INSTANCEID"] = dbms_connection_info.aura_instance_id - - yield dbms_connection_info - - # Clear Neo4j_URI after test (rerun should create a new instance) - os.environ["AURA_INSTANCEID"] = old_instance - assert dbms_connection_info.aura_instance_id is not None - api.delete_instance(dbms_connection_info.aura_instance_id) + instance_details = create_auradb_instance(api) + + try: + dbms_connection_info = wait_for_instance(api, instance_details) + + # setting as environment variables to run notebooks with this connection + os.environ["NEO4J_URI"] = dbms_connection_info.get_uri() + assert isinstance(dbms_connection_info.username, str) + os.environ["NEO4J_USERNAME"] = dbms_connection_info.username + assert isinstance(dbms_connection_info.password, str) + os.environ["NEO4J_PASSWORD"] = dbms_connection_info.password + old_instance = os.environ.get("AURA_INSTANCEID", "") + if dbms_connection_info.aura_instance_id: + os.environ["AURA_INSTANCEID"] = dbms_connection_info.aura_instance_id + + yield dbms_connection_info + + # Clear Neo4j_URI after test (rerun should create a new instance) + os.environ["AURA_INSTANCEID"] = old_instance + assert dbms_connection_info.aura_instance_id is not None + finally: + api.delete_instance(instance_details.id) @pytest.fixture(scope="package") diff --git a/python-wrapper/tests/gds_helper.py b/python-wrapper/tests/gds_helper.py index 5d7a6775..8644fa26 100644 --- a/python-wrapper/tests/gds_helper.py +++ b/python-wrapper/tests/gds_helper.py @@ -1,3 +1,4 @@ +import logging import os import re @@ -48,13 +49,13 @@ def aura_api() -> AuraApi: return AuraApi( client_id=os.environ["AURA_API_CLIENT_ID"], client_secret=os.environ["AURA_API_CLIENT_SECRET"], - project_id=os.environ.get("AURA_API_TENANT_ID"), + project_id=os.environ.get("AURA_API_PROJECT_ID"), ) else: return AuraApi( client_id=os.environ["AURA_API_CLIENT_ID"], client_secret=os.environ["AURA_API_CLIENT_SECRET"], - tenant_id=os.environ.get("AURA_API_TENANT_ID"), # type: ignore + tenant_id=os.environ.get("AURA_API_PROJECT_ID"), # type: ignore ) @@ -63,24 +64,30 @@ def gds_sessions() -> GdsSessions: api_credentials=AuraAPICredentials( client_id=os.environ["AURA_API_CLIENT_ID"], client_secret=os.environ["AURA_API_CLIENT_SECRET"], - project_id=os.environ.get("AURA_API_TENANT_ID"), + project_id=os.environ.get("AURA_API_PROJECT_ID"), ) ) -def create_auradb_instance(api: AuraApi) -> DbmsConnectionInfo: +def create_auradb_instance(api: AuraApi) -> InstanceCreateDetails: + type = "enterprise-db" if os.environ.get("AURA_ENTERPRISE_PROJECT", "false").lower() == "true" else "professional-db" instance_details: InstanceCreateDetails = api.create_instance( name="ci-neo4j-viz-db", memory=SessionMemory.m_2GB.value, cloud_provider="gcp", region="europe-west1", - type="enterprise-db", + type=type, ) + logger = logging.getLogger(__name__) + logger.debug(f"Created instance with ID: {instance_details.id}") - wait_result = api.wait_for_instance_running(instance_id=instance_details.id) + return instance_details + +def wait_for_instance(api: AuraApi, instance_details: InstanceCreateDetails) -> DbmsConnectionInfo: + wait_result = api.wait_for_instance_running(instance_id=instance_details.id, max_wait_time=600) if wait_result.error: raise Exception(f"Error while waiting for instance to be running: {wait_result.error}") return DbmsConnectionInfo( - username="neo4j", password=instance_details.password, aura_instance_id=instance_details.id + username="neo4j", password=instance_details.password, aura_instance_id=instance_details.id, uri=wait_result.connection_url ) From c2e5ef7089cdea77e78ac19a58b3c5fd1b31ef8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florentin=20D=C3=B6rre?= Date: Fri, 5 Jun 2026 16:14:49 +0200 Subject: [PATCH 3/6] Fix cell ids in gds notebook --- examples/gds-example.ipynb | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/examples/gds-example.ipynb b/examples/gds-example.ipynb index e9606ccb..cd7ab333 100644 --- a/examples/gds-example.ipynb +++ b/examples/gds-example.ipynb @@ -2,6 +2,7 @@ "cells": [ { "cell_type": "markdown", + "id": "133e0a86", "metadata": {}, "source": [ "# Visualizing Neo4j Graph Data Science (GDS) Graphs" @@ -10,6 +11,7 @@ { "cell_type": "code", "execution_count": null, + "id": "eab42b4e", "metadata": {}, "outputs": [], "source": [ @@ -21,6 +23,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1dde3aae", "metadata": {}, "outputs": [], "source": [ @@ -31,6 +34,7 @@ }, { "cell_type": "markdown", + "id": "d9daded3", "metadata": {}, "source": [ "## Setup GDS graph\n", @@ -47,6 +51,7 @@ { "cell_type": "code", "execution_count": null, + "id": "9ca9a1de", "metadata": {}, "outputs": [], "source": [ @@ -95,6 +100,7 @@ { "cell_type": "code", "execution_count": null, + "id": "735411fa", "metadata": {}, "outputs": [], "source": [ @@ -103,12 +109,14 @@ }, { "cell_type": "markdown", + "id": "8cbc843a", "metadata": {}, "source": [] }, { "cell_type": "code", "execution_count": null, + "id": "35a65700", "metadata": {}, "outputs": [], "source": [ @@ -122,6 +130,7 @@ }, { "cell_type": "markdown", + "id": "cde09804", "metadata": {}, "source": [ "## Visualization" @@ -130,6 +139,7 @@ { "cell_type": "code", "execution_count": null, + "id": "cab5ffab", "metadata": {}, "outputs": [], "source": [ @@ -145,6 +155,7 @@ { "cell_type": "code", "execution_count": null, + "id": "9f37e34f", "metadata": {}, "outputs": [], "source": [ @@ -153,6 +164,7 @@ }, { "cell_type": "markdown", + "id": "47f4b968", "metadata": {}, "source": [ "### Changing captions\n", @@ -164,6 +176,7 @@ { "cell_type": "code", "execution_count": null, + "id": "ceb601eb", "metadata": {}, "outputs": [], "source": [ @@ -174,6 +187,7 @@ { "cell_type": "code", "execution_count": null, + "id": "1ad40c84", "metadata": {}, "outputs": [], "source": [ @@ -182,6 +196,7 @@ }, { "cell_type": "markdown", + "id": "a752ad45", "metadata": {}, "source": [ "## Sizing the nodes\n", @@ -192,6 +207,7 @@ { "cell_type": "code", "execution_count": null, + "id": "4e6f0c10", "metadata": {}, "outputs": [], "source": [ @@ -201,6 +217,7 @@ }, { "cell_type": "markdown", + "id": "c36a218b", "metadata": {}, "source": [ "### Coloring" @@ -208,6 +225,7 @@ }, { "cell_type": "markdown", + "id": "40665ee4", "metadata": {}, "source": [ "There are two main ways of coloring the nodes of a graph:\n", @@ -221,6 +239,7 @@ { "cell_type": "code", "execution_count": null, + "id": "25afe567", "metadata": {}, "outputs": [], "source": [ @@ -230,6 +249,7 @@ }, { "cell_type": "markdown", + "id": "dbe78a8c", "metadata": {}, "source": [ "Now, let us color by our continuous node field \"size\" that we computed above with PageRank, again using the default colors.\n", @@ -240,6 +260,7 @@ { "cell_type": "code", "execution_count": null, + "id": "931a3465", "metadata": {}, "outputs": [], "source": [ @@ -251,6 +272,7 @@ }, { "cell_type": "markdown", + "id": "07a51bda", "metadata": {}, "source": [ "#### Custom coloring\n", @@ -262,6 +284,7 @@ { "cell_type": "code", "execution_count": null, + "id": "583b37b7", "metadata": {}, "outputs": [], "source": [ @@ -271,6 +294,7 @@ { "cell_type": "code", "execution_count": null, + "id": "fa2a8d32", "metadata": {}, "outputs": [], "source": [ @@ -289,6 +313,7 @@ { "cell_type": "code", "execution_count": null, + "id": "dd7b6944", "metadata": {}, "outputs": [], "source": [ @@ -297,6 +322,7 @@ }, { "cell_type": "markdown", + "id": "e163e522", "metadata": {}, "source": [ "### Render options\n", @@ -313,6 +339,7 @@ { "cell_type": "code", "execution_count": null, + "id": "02d2e7ce", "metadata": {}, "outputs": [], "source": [ @@ -323,6 +350,7 @@ }, { "cell_type": "markdown", + "id": "c325ea54", "metadata": {}, "source": [ "## Saving the visualization" @@ -331,6 +359,7 @@ { "cell_type": "code", "execution_count": null, + "id": "d505dfb9", "metadata": {}, "outputs": [], "source": [ @@ -345,6 +374,7 @@ }, { "cell_type": "markdown", + "id": "71006712", "metadata": {}, "source": [ "## Cleanup\n", @@ -355,6 +385,7 @@ { "cell_type": "code", "execution_count": null, + "id": "b2197a5f", "metadata": {}, "outputs": [], "source": [ @@ -364,6 +395,7 @@ { "cell_type": "code", "execution_count": null, + "id": "74468c43", "metadata": {}, "outputs": [], "source": [ From cb34a6d1e957c7acc1d1e8627bb6ff03dbc06542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florentin=20D=C3=B6rre?= Date: Fri, 5 Jun 2026 16:18:09 +0200 Subject: [PATCH 4/6] Switch CI to AuraDB project --- .github/workflows/gds-integration-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gds-integration-tests.yml b/.github/workflows/gds-integration-tests.yml index 21034a7d..fb51388e 100644 --- a/.github/workflows/gds-integration-tests.yml +++ b/.github/workflows/gds-integration-tests.yml @@ -39,5 +39,5 @@ jobs: env: AURA_API_CLIENT_ID: 4V1HYCYEeoU4dSxThKnBeLvE2U4hSphx AURA_API_CLIENT_SECRET: ${{ secrets.AURA_API_CLIENT_SECRET }} - AURA_API_TENANT_ID: eee7ec28-6b1a-5286-8e3a-3362cc1c4c78 + AURA_API_TENANT_ID: 3f8df5e7-4800-4d4f-ad1d-2d044dfd587c run: uv run pytest tests/ --include-neo4j-and-gds From 127abc5b6d85a81fe47d1a37bd9c6cf054612d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florentin=20D=C3=B6rre?= Date: Fri, 5 Jun 2026 16:21:19 +0200 Subject: [PATCH 5/6] Try to fix example graph image --- python-wrapper/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python-wrapper/README.md b/python-wrapper/README.md index 64fddaca..f164c036 100644 --- a/python-wrapper/README.md +++ b/python-wrapper/README.md @@ -16,7 +16,7 @@ Alternatively, you can export the output to a file and view it in a web browser. The package wraps the [Neo4j Visualization JavaScript library (NVL)](https://neo4j.com/docs/nvl/current/). -![Example Graph](https://github.com/neo4j/python-graph-visualization/blob/main/examples/example_graph.png) +![Example Graph](https://raw.githubusercontent.com/neo4j/python-graph-visualization/main/examples/example_graph.png) ## Some notable features From ea47a6267b44c4e3ef0be0e8a59029f75f5e2ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florentin=20D=C3=B6rre?= Date: Fri, 5 Jun 2026 16:25:50 +0200 Subject: [PATCH 6/6] Format code --- python-wrapper/tests/gds_helper.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/python-wrapper/tests/gds_helper.py b/python-wrapper/tests/gds_helper.py index 8644fa26..f0a66da2 100644 --- a/python-wrapper/tests/gds_helper.py +++ b/python-wrapper/tests/gds_helper.py @@ -70,7 +70,9 @@ def gds_sessions() -> GdsSessions: def create_auradb_instance(api: AuraApi) -> InstanceCreateDetails: - type = "enterprise-db" if os.environ.get("AURA_ENTERPRISE_PROJECT", "false").lower() == "true" else "professional-db" + type = ( + "enterprise-db" if os.environ.get("AURA_ENTERPRISE_PROJECT", "false").lower() == "true" else "professional-db" + ) instance_details: InstanceCreateDetails = api.create_instance( name="ci-neo4j-viz-db", memory=SessionMemory.m_2GB.value, @@ -83,11 +85,15 @@ def create_auradb_instance(api: AuraApi) -> InstanceCreateDetails: return instance_details + def wait_for_instance(api: AuraApi, instance_details: InstanceCreateDetails) -> DbmsConnectionInfo: wait_result = api.wait_for_instance_running(instance_id=instance_details.id, max_wait_time=600) if wait_result.error: raise Exception(f"Error while waiting for instance to be running: {wait_result.error}") return DbmsConnectionInfo( - username="neo4j", password=instance_details.password, aura_instance_id=instance_details.id, uri=wait_result.connection_url + username="neo4j", + password=instance_details.password, + aura_instance_id=instance_details.id, + uri=wait_result.connection_url, )