Skip to content

Commit f8901d7

Browse files
committed
fix: remove MERGE workarounds, tighten assertions, bind Jupyter localhost
- graph.py _create_edge(): remove if/else split around SET r += $props — SET r += {} is supported since coordinode-rs v0.3.12 - base.py upsert_relations(): same simplification - test_sdk.py: assert schema_mode == 3 (FLEXIBLE now enforced by server, remove fallback for older servers that returned 0) - client.py _build_property_definitions(): add isinstance(raw_type, str) guard and .strip() before .lower() — consistent with schema_mode normalization - demo/docker-compose.yml: bind Jupyter to 127.0.0.1:38888 (explicit localhost-only, matches documented intent) - demo/notebooks 01-03: add IN_COLAB gate explanation comment (same as nb00)
1 parent 678ee5f commit f8901d7

8 files changed

Lines changed: 26 additions & 42 deletions

File tree

coordinode/coordinode/client.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,10 +427,14 @@ def _build_property_definitions(
427427
name = p.get("name")
428428
if not isinstance(name, str) or not name:
429429
raise ValueError(f"Property at index {idx} must have a non-empty 'name' key; got {p!r}")
430-
type_str = str(p.get("type", "string")).lower()
430+
raw_type = p.get("type", "string")
431+
if "type" in p and not isinstance(raw_type, str):
432+
raise ValueError(f"Property {name!r} must use a string value for 'type'; got {raw_type!r}")
433+
type_str = str(raw_type).strip().lower()
431434
if type_str not in type_map:
432435
raise ValueError(
433-
f"Unknown property type {type_str!r} for property {name!r}. Expected one of: {sorted(type_map)}"
436+
f"Unknown property type {type_str!r} for property {name!r}. "
437+
f"Expected 'type' to be one of: {sorted(type_map)}"
434438
)
435439
required = p.get("required", False)
436440
unique = p.get("unique", False)

demo/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ services:
2323
dockerfile: Dockerfile.jupyter
2424
container_name: demo-jupyter
2525
ports:
26-
- "38888:8888" # Jupyter Lab
26+
- "127.0.0.1:38888:8888" # Jupyter Lab (localhost-only)
2727
volumes:
2828
- ./notebooks:/home/jovyan/work
2929
- ../:/sdk # mount SDK source so notebooks can pip install -e

demo/notebooks/01_llama_index_property_graph.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
"id": "b2c3d4e5-0001-0000-0000-000000000003",
4040
"metadata": {},
4141
"outputs": [],
42-
"source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"llama-index-graph-stores-coordinode\",\n \"llama-index-core\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"SDK installed\")"
42+
"source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"llama-index-graph-stores-coordinode\",\n \"llama-index-core\",\n \"nest_asyncio\",\n ],\n check=True,\n timeout=300,\n)\n\nimport nest_asyncio\n\nnest_asyncio.apply()\n\nprint(\"SDK installed\")"
4343
},
4444
{
4545
"cell_type": "markdown",

demo/notebooks/02_langchain_graph_chain.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"id": "c3d4e5f6-0002-0000-0000-000000000003",
3737
"metadata": {},
3838
"outputs": [],
39-
"source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")"
39+
"source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")"
4040
},
4141
{
4242
"cell_type": "markdown",

demo/notebooks/03_langgraph_agent.ipynb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
"id": "d4e5f6a7-0003-0000-0000-000000000003",
3838
"metadata": {},
3939
"outputs": [],
40-
"source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")"
40+
"source": "import os, sys, subprocess\n\nIN_COLAB = \"google.colab\" in sys.modules\n\n# Install coordinode-embedded in Colab only (requires Rust build).\nif IN_COLAB:\n # Install Rust toolchain via rustup (https://rustup.rs).\n # Colab's apt packages ship rustc ≤1.75, which cannot build coordinode-embedded\n # (requires Rust ≥1.80 for maturin/pyo3). apt-get is not a viable alternative here.\n # Download the installer to a temp file and execute it explicitly — this avoids\n # piping remote content directly into a shell while maintaining HTTPS/TLS security\n # through Python's default ssl context (cert-verified, TLS 1.2+).\n # SHA256 pinning of rustup-init is intentionally omitted: rustup.rs does not\n # publish a stable per-release checksum for sh.rustup.rs itself (only for\n # platform-specific rustup-init binaries), and pinning a hash here would break\n # silently on every rustup release. The HTTPS/TLS verification + temp-file\n # execution (not piped to shell) is the rustup team's recommended trust model.\n # No additional env-var gate (e.g. COORDINODE_ENABLE_RUSTUP) is needed:\n # the `IN_COLAB` check above already ensures this block never runs outside\n # Colab sessions, so there is no risk of unintentional execution in local\n # or server environments.\n import ssl as _ssl, tempfile as _tmp, urllib.request as _ur\n\n _ctx = _ssl.create_default_context()\n with _tmp.NamedTemporaryFile(mode=\"wb\", suffix=\".sh\", delete=False) as _f:\n with _ur.urlopen(\"https://sh.rustup.rs\", context=_ctx, timeout=30) as _r:\n _f.write(_r.read())\n _rustup_path = _f.name\n try:\n subprocess.run([\"/bin/sh\", _rustup_path, \"-s\", \"--\", \"-y\", \"-q\"], check=True, timeout=300)\n finally:\n os.unlink(_rustup_path)\n # Add cargo to PATH so maturin/pip can find it.\n _cargo_bin = os.path.expanduser(\"~/.cargo/bin\")\n os.environ[\"PATH\"] = f\"{_cargo_bin}{os.pathsep}{os.environ.get('PATH', '')}\"\n subprocess.run([sys.executable, \"-m\", \"pip\", \"install\", \"-q\", \"maturin\"], check=True, timeout=300)\n subprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"git+https://github.com/structured-world/coordinode-python.git@b23c833#subdirectory=coordinode-embedded\",\n ],\n check=True,\n timeout=600,\n )\n\nsubprocess.run(\n [\n sys.executable,\n \"-m\",\n \"pip\",\n \"install\",\n \"-q\",\n \"coordinode\",\n \"langchain-coordinode\",\n \"langchain-community\",\n \"langchain-openai\",\n \"langgraph\",\n ],\n check=True,\n timeout=300,\n)\n\nprint(\"SDK installed\")"
4141
},
4242
{
4343
"cell_type": "markdown",

langchain-coordinode/langchain_coordinode/graph.py

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -210,29 +210,17 @@ def _upsert_node(self, node: Any) -> None:
210210
)
211211

212212
def _create_edge(self, rel: Any) -> None:
213-
"""Upsert a relationship via MERGE (idempotent).
214-
215-
SET r += $props is skipped when props is empty because
216-
SET r += {} is not supported by all server versions.
217-
"""
213+
"""Upsert a relationship via MERGE (idempotent)."""
218214
src_label = _cypher_ident(rel.source.type or "Entity")
219215
dst_label = _cypher_ident(rel.target.type or "Entity")
220216
rel_type = _cypher_ident(rel.type)
221217
props = dict(rel.properties or {})
222-
if props:
223-
self._client.cypher(
224-
f"MATCH (src:{src_label} {{name: $src}}) "
225-
f"MATCH (dst:{dst_label} {{name: $dst}}) "
226-
f"MERGE (src)-[r:{rel_type}]->(dst) SET r += $props",
227-
params={"src": rel.source.id, "dst": rel.target.id, "props": props},
228-
)
229-
else:
230-
self._client.cypher(
231-
f"MATCH (src:{src_label} {{name: $src}}) "
232-
f"MATCH (dst:{dst_label} {{name: $dst}}) "
233-
f"MERGE (src)-[r:{rel_type}]->(dst)",
234-
params={"src": rel.source.id, "dst": rel.target.id},
235-
)
218+
self._client.cypher(
219+
f"MATCH (src:{src_label} {{name: $src}}) "
220+
f"MATCH (dst:{dst_label} {{name: $dst}}) "
221+
f"MERGE (src)-[r:{rel_type}]->(dst) SET r += $props",
222+
params={"src": rel.source.id, "dst": rel.target.id, "props": props},
223+
)
236224

237225
def _link_document_to_entities(self, doc: Any) -> None:
238226
"""Upsert a ``__Document__`` node and MERGE ``MENTIONS`` edges to all entities."""

0 commit comments

Comments
 (0)