{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Propagation Properties — Full Neo4j Demo\n", "\n", "This notebook demonstrates the complete workflow for working with\n", "propagation properties (`_propagate`, `is_weak`, `parent_relation`,\n", "`_weak_init_done`) on a real Neo4j database.\n", "\n", "**Steps:**\n", "1. Connect to Neo4j DEV and load the Game of Thrones dataset\n", "2. Generate YAML schema + Python entity classes\n", "3. Initialize propagation properties on existing data\n", "4. Demonstrate `create_group()` — atomic group creation\n", "5. Run sample queries to verify everything works\n", "\n", "## Prerequisites\n", "\n", "- A running Neo4j instance (DEV target)\n", "- Environment variables: `NEO4J_DEV_URL`, `NEO4J_DEV_USER`, `NEO4J_DEV_PASSWORD`\n", "- Default connection: `bolt://localhost:7687` / `neo4j` / `secret`\n", "\n", "## Connection configuration\n", "\n", "The notebook reads connection parameters from environment variables\n", "with sensible defaults. Set these before running:\n", "\n", "```bash\n", "export NEO4J_DEV_URL=bolt://your-host:7687\n", "export NEO4J_DEV_USER=neo4j\n", "export NEO4J_DEV_PASSWORD=your-password\n", "```" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:20.968790Z", "start_time": "2026-07-12T13:50:20.900397Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ drm already installed. Skipping.\n" ] } ], "source": [ "import importlib.util\n", "\n", "# Check if drm is installed\n", "package_to_check = 'drm'\n", "spec = importlib.util.find_spec(package_to_check)\n", "\n", "if spec is None:\n", " print(f'⚠️ {package_to_check} not installed. Installing...')\n", " %pip install -q --upgrade drm-tools\n", " print(\"✅ Installation complete.\")\n", "else:\n", " print(f'✅ {package_to_check} already installed. Skipping.')" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:22.705132Z", "start_time": "2026-07-12T13:50:20.984610Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ Loaded .env from /Users/oriol/Desenvolupament/drm-tools/.env\n", " Found 5 variables\n", "✅ Added /Users/oriol/Desenvolupament/drm-tools to sys.path\n", "✅ All imports successful.\n" ] } ], "source": [ "import os\n", "import sys\n", "from pathlib import Path\n", "\n", "# Read .env file manually (no python-dotenv dependency)\n", "def read_env(env_path):\n", " \"\"\"Read a .env file and return a dict of key=value pairs.\"\"\"\n", " env_vars = {}\n", " with open(env_path, 'r') as f:\n", " for line in f:\n", " line = line.strip()\n", " if not line or line.startswith('#'):\n", " continue\n", " if '=' in line:\n", " key, value = line.split('=', 1)\n", " env_vars[key.strip()] = value.strip()\n", " return env_vars\n", "\n", "# Find .env in repo root\n", "_env_path = Path.cwd() / \".env\"\n", "if not _env_path.exists():\n", " for parent in Path.cwd().parents:\n", " candidate = parent / \".env\"\n", " if candidate.exists():\n", " _env_path = candidate\n", " break\n", "\n", "if _env_path.exists():\n", " _env_vars = read_env(_env_path)\n", " for key, value in _env_vars.items():\n", " os.environ.setdefault(key, value)\n", " print(f\"✅ Loaded .env from {_env_path}\")\n", " print(f\" Found {len(_env_vars)} variables\")\n", "else:\n", " print(\"⚠️ .env file not found. Set NEO4J_DEV_PASSWORD environment variable manually.\")\n", "\n", "# Add drm package to path for development\n", "_repo_root = Path.cwd()\n", "if not (_repo_root / \"drm\").exists():\n", " for parent in Path.cwd().parents:\n", " if (parent / \"drm\").exists():\n", " _repo_root = parent\n", " break\n", "# Add repo root to sys.path so local drm loads before PyPI version\n", "if str(_repo_root) not in sys.path:\n", " sys.path.insert(0, str(_repo_root))\n", " print(f\"✅ Added {_repo_root} to sys.path\")\n", "\n", "import json\n", "\n", "from drm import Neo4jGraph, NetworkXGraph\n", "from drm.exemples import load_got_characters\n", "from drm.schema_gen import generate_classes\n", "\n", "\n", "def neo4j_config(default_target=\"DEV\"):\n", " \"\"\"Build Neo4j connection config from environment variables.\"\"\"\n", " target = os.environ.get(\"NEO4J_TARGET\", default_target).upper()\n", " prefix = f\"NEO4J_{target}_\"\n", " return {\n", " \"target\": target,\n", " \"url\": os.environ.get(f\"{prefix}URL\", os.environ.get(\"NEO4J_URL\", \"bolt://localhost:7687\")),\n", " \"user\": os.environ.get(f\"{prefix}USER\", os.environ.get(\"NEO4J_USER\", \"neo4j\")),\n", " \"password\": os.environ.get(f\"{prefix}PASSWORD\", os.environ.get(\"NEO4J_PASSWORD\", \"secret\")),\n", " \"database\": os.environ.get(f\"{prefix}DATABASE\", os.environ.get(\"NEO4J_DATABASE\", \"neo4j\")),\n", " }\n", "\n", "\n", "def section(title):\n", " \"\"\"Print a section header.\"\"\"\n", " print(f\"\\n{'=' * 60}\")\n", " print(f\" {title}\")\n", " print(f\"{'=' * 60}\")\n", "\n", "\n", "def sub_section(title):\n", " \"\"\"Print a subsection header.\"\"\"\n", " print(f\"\\n--- {title} ---\")\n", "\n", "\n", "# Resolve paths relative to repo root\n", "def repo_path(rel):\n", " \"\"\"Return a Path relative to the repository root.\"\"\"\n", " return _repo_root / rel\n", "\n", "print(\"✅ All imports successful.\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 1: Connect to Neo4j\n", "\n", "Connect to the DEV Neo4j instance and verify the connection." ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:23.216775Z", "start_time": "2026-07-12T13:50:22.770306Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Target: DEV\n", "URL: bolt://localhost:7687\n", "User: neo4j\n", "Database: neo4j\n", "\n", "✅ Connection verified.\n", "Nodes in database: 45\n" ] } ], "source": [ "cfg = neo4j_config()\n", "print(f\"Target: {cfg['target']}\")\n", "print(f\"URL: {cfg['url']}\")\n", "print(f\"User: {cfg['user']}\")\n", "print(f\"Database: {cfg['database']}\")\n", "\n", "graph = Neo4jGraph(\n", " url=cfg[\"url\"],\n", " user=cfg[\"user\"],\n", " password=cfg[\"password\"],\n", " database=cfg[\"database\"],\n", ")\n", "\n", "# Verify connection\n", "result = graph.query(\"RETURN 1 AS test\")\n", "if result and result[0].get(\"test\") == 1:\n", " print(\"\\n✅ Connection verified.\")\n", "else:\n", " print(\"\\n⚠️ Connection returned unexpected result.\")\n", "\n", "# Check current state\n", "count_result = graph.query(\"MATCH (n) RETURN count(n) AS total\")\n", "total = count_result[0][\"total\"] if count_result else 0\n", "print(f\"Nodes in database: {total}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 1.5: Clear existing data\n", "\n", "Delete all nodes and relationships before loading fresh GOT data.\n" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:23.428830Z", "start_time": "2026-07-12T13:50:23.227028Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ All existing nodes and relationships deleted.\n", "Nodes remaining: 0\n" ] } ], "source": [ "# Clear all existing data from the database\n", "graph.query(\"MATCH (n) DETACH DELETE n\")\n", "print(\"✅ All existing nodes and relationships deleted.\")\n", "\n", "# Verify database is empty\n", "count_result = graph.query(\"MATCH (n) RETURN count(n) AS total\")\n", "total = count_result[0][\"total\"] if count_result else 0\n", "print(f\"Nodes remaining: {total}\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 2: Load Game of Thrones dataset\n", "\n", "Load character and house data from [thronesapi.com](https://thronesapi.com).\n", "Falls back to bundled data if the API is unavailable." ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:26.842349Z", "start_time": "2026-07-12T13:50:23.436699Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Loaded: {\n", " \"characters\": 25,\n", " \"houses\": 15,\n", " \"member_of\": 25\n", "}\n", "\n", "--- Nodes by label ---\n", " Character: 25\n", " House: 15\n", "\n", "--- Relations by type ---\n", " MEMBER_OF: 25\n" ] } ], "source": [ "stats = load_got_characters(graph, limit=25)\n", "print(\"Loaded:\", json.dumps(stats, indent=2))\n", "\n", "# Show nodes by label\n", "sub_section(\"Nodes by label\")\n", "result = graph.query(\n", " \"MATCH (n) RETURN labels(n)[0] AS label, count(n) AS cnt \"\n", " \"ORDER BY cnt DESC\"\n", ")\n", "for row in result:\n", " print(f\" {row['label']}: {row['cnt']}\")\n", "\n", "# Show relations by type\n", "sub_section(\"Relations by type\")\n", "result = graph.query(\n", " \"MATCH ()-[r]->() RETURN type(r) AS rel_type, count(r) AS cnt \"\n", " \"ORDER BY cnt DESC\"\n", ")\n", "for row in result:\n", " print(f\" {row['rel_type']}: {row['cnt']}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 3: Generate YAML schema\n", "\n", "Introspect the Neo4j database and generate a YAML schema description.\n", "This captures labels, properties, relationships, and counts." ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:27.480315Z", "start_time": "2026-07-12T13:50:26.913949Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Written: /Users/oriol/Desenvolupament/drm-tools/drm/exemples/generated/got_schema.yaml\n", "Size: 820 bytes\n", "\n", "Schema preview:\n", " # Schema generated from: got\n", " # Generated at: 2026-07-12\n", " \n", " labels:\n", " Character:\n", " class_name: Character\n", " base_class: Node\n", " properties:\n", " character_id: integer\n", " name: string\n", " source: string\n", " title: string\n", " primary_key: ['name', 'character_id']\n", " count: 25\n", " House:\n", " class_name: House\n", " base_class: Node\n", " properties:\n", " house_name: string\n", " name: string\n", " ...\n" ] } ], "source": [ "output_dir = repo_path(\"drm/exemples/generated\")\n", "output_dir.mkdir(exist_ok=True)\n", "\n", "yaml_content = graph.schema_yaml(db_name=\"got\")\n", "yaml_path = output_dir / \"got_schema.yaml\"\n", "yaml_path.write_text(yaml_content)\n", "\n", "print(f\"Written: {yaml_path}\")\n", "print(f\"Size: {len(yaml_content)} bytes\")\n", "\n", "print(\"\\nSchema preview:\")\n", "for line in yaml_content.split(\"\\n\")[:20]:\n", " print(f\" {line}\")\n", "print(\" ...\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 4: Generate Python entity classes\n", "\n", "Generate Python classes from the YAML schema. This creates `Node`,\n", "`WeakNode`, `Relation`, and `WeakRelation` subclasses for each\n", "label and relationship type in the database." ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:27.733061Z", "start_time": "2026-07-12T13:50:27.539970Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Written: /Users/oriol/Desenvolupament/drm-tools/drm/exemples/generated/got_entities.py\n", "Size: 2520 bytes\n", "\n", "Generated code preview:\n", " \"\"\"Auto-generated entity classes from schema.\"\"\"\n", " \n", " from __future__ import annotations\n", " \n", " from typing import Any, Dict, List, Optional\n", " \n", " from drm.base import Node, WeakNode, Relation, WeakRelation\n", " \n", " \n", " class Character(Node):\n", " \"\"\"Auto-generated entity class..\"\"\"\n", " \n", " def __init__(self, pk: Dict[str, Any], **kwargs: Any) -> None:\n", " \"\"\"Initialize a Character node.\n", " \n", " Args:\n", " pk: Primary key dict with fields: name, character_id.\n", " **kwargs: Additional properties and attributes.\n", " \"\"\"\n", " super().__init__(pk=pk, main_label=\"Character\", **kwargs)\n", " self.character_id = kwargs.get(\"character_id\", 'integer')\n", " self.name = kwargs.get(\"name\", 'string')\n", " self.source = kwargs.get(\"source\", 'string')\n", " self.title = kwargs.get(\"title\", 'string')\n", " class House(Node):\n", " ...\n" ] } ], "source": [ "# Read the YAML file content (generate_classes expects YAML string, not file path)\n", "yaml_content = yaml_path.read_text()\n", "code = generate_classes(yaml_content)\n", "\n", "py_path = output_dir / \"got_entities.py\"\n", "py_path.write_text(code)\n", "\n", "print(f\"Written: {py_path}\")\n", "print(f\"Size: {len(code)} bytes\")\n", "\n", "print(\"\\nGenerated code preview:\")\n", "for line in code.split(\"\\n\")[:25]:\n", " print(f\" {line}\")\n", "print(\" ...\")\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": "## Step 5: Initialize propagation properties\n\nScan the entire Neo4j graph and initialize propagation properties:\n- `is_weak`: marks nodes that are children of edges with `_propagate = TRUE`\n- `_propagate`: marks edges that have cascade-delete enabled\n- `parent_relation`: stores the edge type linking parent → child\n- `_weak_init_done`: marks nodes whose propagation properties have been initialized\n\nThis uses `init_propagation()` which:\n1. Finds all edges with `_propagate = TRUE`\n2. Marks the child nodes as `is_weak = TRUE` (only for edges that have `_propagate`)\n3. Skips nodes already initialized via `_weak_init_done`\n\n**Note:** `is_weak` is only set on nodes that are direct children of edges marked with `_propagate = TRUE`. Nodes not connected via such edges will remain unmarked." }, { "cell_type": "code", "execution_count": 8, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:29.650824Z", "start_time": "2026-07-12T13:50:27.745182Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "--- Before init_propagation() ---\n", " Total nodes: 40\n", " Nodes with is_weak: 0\n", " Nodes with _weak_init_done: 0\n", "\n", "Running init_propagation()...\n", "Result: True\n", "\n", "--- After init_propagation() ---\n", " Total nodes: 40\n", " Nodes with is_weak: 0\n", " Nodes with _weak_init_done: 40\n", " Edges with _propagate: 0\n" ] } ], "source": [ "# Check state before init\n", "sub_section(\"Before init_propagation()\")\n", "result = graph.query(\n", " \"MATCH (n) RETURN count(n) AS total, \"\n", " \"count(n.is_weak) AS weak_count, \"\n", " \"count(n._weak_init_done) AS done_count\"\n", ")\n", "row = result[0] if result else {}\n", "print(f\" Total nodes: {row.get('total', 0)}\")\n", "print(f\" Nodes with is_weak: {row.get('weak_count', 0)}\")\n", "print(f\" Nodes with _weak_init_done: {row.get('done_count', 0)}\")\n", "\n", "# Run initialization\n", "print(\"\\nRunning init_propagation()...\")\n", "init_result = graph.init_propagation()\n", "print(f\"Result: {init_result}\")\n", "\n", "# Check state after init\n", "sub_section(\"After init_propagation()\")\n", "result = graph.query(\n", " \"MATCH (n) RETURN count(n) AS total, \"\n", " \"count(n.is_weak) AS weak_count, \"\n", " \"count(n._weak_init_done) AS done_count\"\n", ")\n", "row = result[0] if result else {}\n", "print(f\" Total nodes: {row.get('total', 0)}\")\n", "print(f\" Nodes with is_weak: {row.get('weak_count', 0)}\")\n", "print(f\" Nodes with _weak_init_done: {row.get('done_count', 0)}\")\n", "\n", "# Show propagation edges\n", "result = graph.query(\n", " \"MATCH ()-[r]->() WHERE r._propagate = TRUE RETURN count(r) AS c\"\n", ")\n", "prop_count = result[0][\"c\"] if result else 0\n", "print(f\" Edges with _propagate: {prop_count}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 6: Transactional group creation\n", "\n", "Use `create_group()` to atomically create a strong node with its\n", "WeakNodes and WeakRelations. The entire group is created in a single\n", "Neo4j transaction — if any step fails, everything is rolled back.\n", "\n", "We create a Document → Section → Page hierarchy." ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:29.957511Z", "start_time": "2026-07-12T13:50:29.697628Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Creating group:\n", " Strong node: Document(GOT-DEMO-001)\n", " Weak nodes: Section(intro), Section(characters)\n", " Weak nodes: Page(1) → intro, Page(1) → characters\n", "\n", "✅ Group created (strong node id: 833)\n" ] } ], "source": [ "from drm.base import Node, WeakNode\n", "\n", "# Create the strong node (Document)\n", "doc = Node(\n", " pk={\"doc_id\": \"GOT-DEMO-001\"},\n", " main_label=\"Document\",\n", " title=\"Game of Thrones — Episode Guide\",\n", " author=\"Demo Script\",\n", ")\n", "\n", "# Create weak nodes (Sections and Pages)\n", "section1 = WeakNode(\n", " parent=doc,\n", " parent_relation=\"HAS_SECTION\",\n", " pk={\"section\": \"intro\"},\n", " main_label=\"Section\",\n", " title=\"Introduction\",\n", " order=1,\n", ")\n", "\n", "section2 = WeakNode(\n", " parent=doc,\n", " parent_relation=\"HAS_SECTION\",\n", " pk={\"section\": \"characters\"},\n", " main_label=\"Section\",\n", " title=\"Main Characters\",\n", " order=2,\n", ")\n", "\n", "page1 = WeakNode(\n", " parent=section1,\n", " parent_relation=\"HAS_PAGE\",\n", " pk={\"page\": 1},\n", " main_label=\"Page\",\n", " content=\"Welcome to the Game of Thrones episode guide.\",\n", " order=1,\n", ")\n", "\n", "page2 = WeakNode(\n", " parent=section2,\n", " parent_relation=\"HAS_PAGE\",\n", " pk={\"page\": 1},\n", " main_label=\"Page\",\n", " content=\"Jon Snow, Daenerys Targaryen, Tyrion Lannister...\",\n", " order=1,\n", ")\n", "\n", "print(\"Creating group:\")\n", "print(f\" Strong node: Document(GOT-DEMO-001)\")\n", "print(f\" Weak nodes: Section(intro), Section(characters)\")\n", "print(f\" Weak nodes: Page(1) → intro, Page(1) → characters\")\n", "\n", "# Create atomically\n", "doc_id = graph.create_group(\n", " strong_node=doc,\n", " weak_nodes=[section1, section2, page1, page2],\n", ")\n", "\n", "print(f\"\\n✅ Group created (strong node id: {doc_id})\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 7: Verify the group\n", "\n", "Query the database to verify the group was created correctly and\n", "check the propagation properties." ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:30.150838Z", "start_time": "2026-07-12T13:50:30.020186Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "--- Document node ---\n", " Labels: ['Document']\n", " Title: None\n", " _weak_init_done: None\n", " Labels: ['Section']\n", " Title: None\n", " _weak_init_done: None\n", " Labels: ['Section']\n", " Title: None\n", " _weak_init_done: None\n", " Labels: ['Page']\n", " Title: None\n", " _weak_init_done: None\n", " Labels: ['Page']\n", " Title: None\n", " _weak_init_done: None\n", "\n", "--- Section nodes ---\n", " None: is_weak=None, _propagate=None\n", " None: is_weak=None, _propagate=None\n", "\n", "--- Page nodes ---\n", " Page: '...' is_weak=None, _propagate=None\n", " Page: '...' is_weak=None, _propagate=None\n" ] } ], "source": [ "# Verify Document node\n", "sub_section(\"Document node\")\n", "result = graph.query(\n", " \"MATCH (n) WHERE n.doc_id = 'GOT-DEMO-001' \"\n", " \"RETURN labels(n) AS labels, n.title, n._weak_init_done\"\n", ")\n", "for row in result:\n", " print(f\" Labels: {row['labels']}\")\n", " print(f\" Title: {row.get('title')}\")\n", " print(f\" _weak_init_done: {row.get('_weak_init_done')}\")\n", "\n", "# Verify Section nodes\n", "sub_section(\"Section nodes\")\n", "result = graph.query(\n", " \"MATCH (n:Section) WHERE n._weak_init_done IS NULL \"\n", " \"RETURN labels(n) AS labels, n.title, n.is_weak, n._propagate\"\n", ")\n", "for row in result:\n", " print(f\" {row.get('title')}: is_weak={row.get('is_weak')}, _propagate={row.get('_propagate')}\")\n", "\n", "# Verify Page nodes\n", "sub_section(\"Page nodes\")\n", "result = graph.query(\n", " \"MATCH (n:Page) WHERE n._weak_init_done IS NULL \"\n", " \"RETURN labels(n) AS labels, n.content, n.is_weak, n._propagate\"\n", ")\n", "for row in result:\n", " content = (row.get('content') or '')[:50]\n", " print(f\" Page: '{content}...' is_weak={row.get('is_weak')}, _propagate={row.get('_propagate')}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 8: Sample queries\n", "\n", "Run several queries to demonstrate the initialized graph and\n", "propagation properties." ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:30.280082Z", "start_time": "2026-07-12T13:50:30.156826Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "--- Characters by House ---\n", " Free Folk: Ygritte\n", " House Baelish: Petyr Baelish\n", " House Baratheon: Stannis Baratheon, Robert Baratheon\n", " House Clegane: The Hound\n", " House Greyjoy: Theon Greyjoy\n", " House Lanister: Tyrion Lannister, Joffrey Baratheon\n", " House Lannister: Cersei Lannister, Jamie Lannister\n", " House Seaworth: Davos Seaworth\n", " House Stark: Rob Stark, Catelyn Stark, Ned Stark, Brandon Stark, Sansa Stark\n", " ... and 2 more\n", " House Targaryen: Khal Drogo, Daenerys Targaryen\n", " House Tarly: Samwell Tarly\n", " House Tyrell: Margaery Tyrell\n", " Naathi: Missandei\n", " Tarth: Brienne of Tarth\n", " Unknown: Varys\n" ] } ], "source": [ "# Query 1: Characters by House\n", "sub_section(\"Characters by House\")\n", "result = graph.query(\n", " \"MATCH (c:Character)-[:MEMBER_OF]->(h:House) \"\n", " \"RETURN h.name AS house, collect(c.name) AS characters \"\n", " \"ORDER BY house\"\n", ")\n", "for row in result:\n", " chars = row['characters'][:5]\n", " print(f\" {row['house']}: {', '.join(chars)}\")\n", " if len(row['characters']) > 5:\n", " print(f\" ... and {len(row['characters']) - 5} more\")" ] }, { "cell_type": "code", "execution_count": 12, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:30.462348Z", "start_time": "2026-07-12T13:50:30.296470Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "--- Nodes with propagation properties ---\n", "\n", "--- Edges with _propagate ---\n", " Document -[HAS_SECTION]-> Section: 2\n", " Document -[HAS_PAGE]-> Page: 2\n" ] } ], "source": [ "# Query 2: Nodes with propagation properties\n", "sub_section(\"Nodes with propagation properties\")\n", "result = graph.query(\n", " \"MATCH (n) WHERE n.is_weak = TRUE \"\n", " \"RETURN labels(n) AS label, count(n) AS count \"\n", " \"ORDER BY count DESC\"\n", ")\n", "for row in result:\n", " print(f\" {row['label']}: {row['count']} nodes\")\n", "\n", "# Query 3: Edges with _propagate\n", "sub_section(\"Edges with _propagate\")\n", "result = graph.query(\n", " \"MATCH (a)-[r]->(b) WHERE r._propagate = TRUE \"\n", " \"RETURN labels(a)[0] AS from_label, type(r) AS rel_type, \"\n", " \"labels(b)[0] AS to_label, count(*) AS count \"\n", " \"ORDER BY count DESC\"\n", ")\n", "for row in result:\n", " print(f\" {row['from_label']} -[{row['rel_type']}]-> {row['to_label']}: {row['count']}\")" ] }, { "cell_type": "code", "execution_count": 13, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:30.665832Z", "start_time": "2026-07-12T13:50:30.492159Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "--- Document group hierarchy ---\n", " Document({'labels': ['Document'], 'properties': {'_weak_init_done': True, 'author': 'Demo Script', 'title': 'Game of Thrones — Episode Guide', 'doc_id': 'GOT-DEMO-001'}}) → Section({'labels': ['Section'], 'properties': {'section': 'intro', 'title': 'Introduction', 'doc_id': 'GOT-DEMO-001', 'order': 1}}) → -[HAS_SECTION]->\n", " Document({'labels': ['Document'], 'properties': {'_weak_init_done': True, 'author': 'Demo Script', 'title': 'Game of Thrones — Episode Guide', 'doc_id': 'GOT-DEMO-001'}}) → Page({'labels': ['Page'], 'properties': {'section': 'characters', 'page': 1, 'doc_id': 'GOT-DEMO-001', 'content': 'Jon Snow, Daenerys Targaryen, Tyrion Lannister...', 'order': 1}}) → -[HAS_PAGE]->\n", " Document({'labels': ['Document'], 'properties': {'_weak_init_done': True, 'author': 'Demo Script', 'title': 'Game of Thrones — Episode Guide', 'doc_id': 'GOT-DEMO-001'}}) → Page({'labels': ['Page'], 'properties': {'section': 'intro', 'page': 1, 'doc_id': 'GOT-DEMO-001', 'content': 'Welcome to the Game of Thrones episode guide.', 'order': 1}}) → -[HAS_PAGE]->\n", " Document({'labels': ['Document'], 'properties': {'_weak_init_done': True, 'author': 'Demo Script', 'title': 'Game of Thrones — Episode Guide', 'doc_id': 'GOT-DEMO-001'}}) → Section({'labels': ['Section'], 'properties': {'section': 'characters', 'title': 'Main Characters', 'doc_id': 'GOT-DEMO-001', 'order': 2}}) → -[HAS_SECTION]->\n" ] } ], "source": [ "# Query 4: Document group hierarchy\n", "sub_section(\"Document group hierarchy\")\n", "result = graph.query(\n", " \"MATCH path = (d:Document {doc_id: 'GOT-DEMO-001'})-[*1..3]->(n) \"\n", " \"RETURN path\"\n", ")\n", "if result:\n", " for row in result:\n", " path = row.get(\"path\")\n", " if path:\n", " # Handle both native objects (driver 4.x) and dicts (driver 6.x)\n", " if isinstance(path, dict):\n", " nodes = path.get(\"nodes\", [])\n", " rels = path.get(\"relationships\", [])\n", " else:\n", " nodes = path.nodes\n", " rels = path.relationships\n", " parts = []\n", " for node in nodes:\n", " if isinstance(node, dict):\n", " label = list(node.get(\"labels\", [\"Node\"]))[0]\n", " name = node.get(\"title\", node.get(\"name\", str(node)))\n", " else:\n", " label = list(node.labels)[0] if hasattr(node, 'labels') else 'Node'\n", " name = node.get('title', node.get('name', str(node.element_id)))\n", " parts.append(f\"{label}({name})\")\n", " for rel in rels:\n", " if isinstance(rel, dict):\n", " parts.append(f\"-[{rel.get('type', '')}]->\")\n", " else:\n", " parts.append(f\"-[{rel.type}]->\")\n", " print(f\" {' → '.join(parts)}\")" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:30.796211Z", "start_time": "2026-07-12T13:50:30.676973Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "--- Summary statistics ---\n", " Total nodes: 45\n", " Weak nodes: 0\n", " Initialized nodes: 41\n", " Total edges: 29\n", " Propagate edges: 4\n", "\n", "============================================================\n", " ✅ Demo completed successfully!\n", "============================================================\n" ] } ], "source": [ "# Query 5: Summary statistics\n", "sub_section(\"Summary statistics\")\n", "\n", "result = graph.query(\n", " \"MATCH (n) \"\n", " \"RETURN count(n) AS total_nodes, \"\n", " \"count(n.is_weak) AS weak_nodes, \"\n", " \"count(n._weak_init_done) AS initialized_nodes\"\n", ")\n", "row = result[0] if result else {}\n", "print(f\" Total nodes: {row.get('total_nodes', 0)}\")\n", "print(f\" Weak nodes: {row.get('weak_nodes', 0)}\")\n", "print(f\" Initialized nodes: {row.get('initialized_nodes', 0)}\")\n", "\n", "result = graph.query(\n", " \"MATCH ()-[r]->() \"\n", " \"RETURN count(r) AS total_edges, \"\n", " \"count(r._propagate) AS propagate_edges\"\n", ")\n", "row = result[0] if result else {}\n", "print(f\" Total edges: {row.get('total_edges', 0)}\")\n", "print(f\" Propagate edges: {row.get('propagate_edges', 0)}\")\n", "\n", "print(\"\\n\" + \"=\" * 60)\n", "print(\" ✅ Demo completed successfully!\")\n", "print(\"=\" * 60)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step 9: Get subdocuments\n", "\n", "Given a strong document node, retrieve all its subdocuments\n", "(sections, pages, etc.) connected through `_propagate` edges.\n" ] }, { "cell_type": "code", "execution_count": 15, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:31.306044Z", "start_time": "2026-07-12T13:50:30.829613Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "--- 9a: get_subdocuments for GOT-DEMO-001 ---\n", " Found 4 subdocuments:\n", " - Section: Introduction\n", " - Page: characters\n", " - Page: intro\n", " - Section: Main Characters\n" ] } ], "source": [ "# get_subdocuments: find all subdocuments of a strong node\n", "sub_section(\"9a: get_subdocuments for GOT-DEMO-001\")\n", "\n", "strong_doc = Node(pk={\"doc_id\": \"GOT-DEMO-001\"}, main_label=\"Document\")\n", "subdocuments = graph.get_subdocuments(strong_doc)\n", "\n", "print(f\" Found {len(subdocuments)} subdocuments:\")\n", "for sd in subdocuments:\n", " name = sd[\"pk\"].get(\"title\", sd[\"pk\"].get(\"section\", sd[\"pk\"].get(\"page\", \"?\")))\n", " print(f\" - {sd['label']}: {name}\")" ] }, { "cell_type": "code", "execution_count": 16, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:31.645908Z", "start_time": "2026-07-12T13:50:31.421832Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "--- Final summary ---\n", " Total nodes: 45\n", " Weak nodes: 0\n", " Initialized nodes: 41\n", " Total edges: 29\n", " Propagate edges: 4\n", "\n", "============================================================\n", " Demo completed successfully!\n", "============================================================\n" ] } ], "source": [ "# Final summary\n", "sub_section(\"Final summary\")\n", "\n", "result = graph.query(\n", " \"MATCH (n) \"\n", " \"RETURN count(n) AS total_nodes, \"\n", " \"count(n.is_weak) AS weak_nodes, \"\n", " \"count(n._weak_init_done) AS initialized_nodes\"\n", ")\n", "row = result[0] if result else {}\n", "print(f\" Total nodes: {row.get('total_nodes', 0)}\")\n", "print(f\" Weak nodes: {row.get('weak_nodes', 0)}\")\n", "print(f\" Initialized nodes: {row.get('initialized_nodes', 0)}\")\n", "\n", "result = graph.query(\n", " \"MATCH ()-[r]->() \"\n", " \"RETURN count(r) AS total_edges, \"\n", " \"count(r._propagate) AS propagate_edges\"\n", ")\n", "row = result[0] if result else {}\n", "print(f\" Total edges: {row.get('total_edges', 0)}\")\n", "print(f\" Propagate edges: {row.get('propagate_edges', 0)}\")\n", "\n", "print(\"\\n\" + \"=\" * 60)\n", "print(\" Demo completed successfully!\")\n", "print(\"=\" * 60)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Cleanup\n", "\n", "Close the Neo4j connection when done." ] }, { "cell_type": "code", "execution_count": 17, "metadata": { "ExecuteTime": { "end_time": "2026-07-12T13:50:31.727393Z", "start_time": "2026-07-12T13:50:31.664257Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ Neo4j connection closed.\n" ] } ], "source": [ "graph.close()\n", "print(\"✅ Neo4j connection closed.\")" ] } ], "metadata": { "kernelspec": { "display_name": "Python (drm-tools)", "language": "python", "name": "drm-env" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.14" } }, "nbformat": 4, "nbformat_minor": 4 }