{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# WeakNode and Nested WeakNode (Interactive)\n", "\n", "This notebook shows how to build `Document → Section → Page` hierarchies with `WeakNode`,\n", "how to inspect the graph state after each operation, and how to delete nodes with different strategies.\n", "\n", "It includes an **interactive visual graph editor** powered by Cytoscape.js: the graph updates in real time\n", "as you add or delete nodes, with color-coded node types and a hierarchical layout." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import importlib.util\n", "\n", "# Check if the package is installed\n", "package_to_check = 'cvcdocdb'\n", "spec = importlib.util.find_spec(package_to_check)\n", "\n", "if spec is None:\n", " print(f'⚠️ {package_to_check} is not installed. Installing...')\n", " %pip install -q --upgrade cvcdocdb-tools\n", " print(\"✅ Installation complete. Kernel may need a restart.\")\n", "else:\n", " print(f'✅ {package_to_check} is already installed. Skipping installation.')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Overview\n", "\n", "The notebook is organised as a step-by-step walkthrough followed by an **interactive visual editor**:\n", "\n", "1. **Control panel** — add documents, sections, pages, or delete selected nodes\n", "2. **Visual graph** — real-time Cytoscape.js rendering with color-coded node types\n", "3. **State inspector** — detailed text output after each operation\n", "\n", "**Node colors:** 🟢 Document (strong) · 🔵 Section (weak) · 🟠 Page (nested weak)\n", "\n", "**Interactions:** Scroll to zoom · Drag to pan · Drag nodes to rearrange" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "import tempfile\n", "import uuid\n", "from IPython.display import display, clear_output, Javascript, HTML\n", "import ipywidgets as widgets\n", "\n", "try:\n", " from cvcdocdb import NetworkXGraph, Node, WeakNode\n", "except ImportError:\n", " from cvcdocdb import NetworkXGraph\n", " from cvcdocdb.base import Node, WeakNode\n", "\n", "persistence_path = os.path.join(tempfile.gettempdir(), f\"cvcdocdb_tutorial_weaknodes_{uuid.uuid4().hex}.pkl\")\n", "graph = NetworkXGraph(persistence_path=persistence_path)\n", "registry = {}\n", "\n", "# ── Color palette for node types ──────────────────────────────────\n", "NODE_COLORS = {\n", " \"Document\": \"#4CAF50\",\n", " \"Section\": \"#2196F3\",\n", " \"Page\": \"#FF9800\",\n", "}\n", "\n", "NODE_BORDERS = {\n", " \"Document\": \"#2E7D32\",\n", " \"Section\": \"#1565C0\",\n", " \"Page\": \"#E65100\",\n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ── Core graph operations ─────────────────────────────────────────\n", "\n", "def key_for_document(doc_id):\n", " return f\"doc:{doc_id}\"\n", "\n", "def key_for_section(doc_id, section_id):\n", " return f\"section:{doc_id}:{section_id}\"\n", "\n", "def key_for_page(doc_id, section_id, page_id):\n", " return f\"page:{doc_id}:{section_id}:{page_id}\"\n", "\n", "def show_state(title=\"State\"):\n", " print(f\"\\n=== {title} ===\")\n", " print(\"nodes:\", graph.get_nodes())\n", " print(\"edges:\", graph.get_edges())\n", " debug = graph.debug()\n", " print(\"debug nodes:\", debug.get(\"nodes\", []))\n", " print(\"debug edges:\", debug.get(\"edges\", []))\n", "\n", "def add_document(doc_id):\n", " k = key_for_document(doc_id)\n", " node = Node(pk={\"doc\": doc_id}, main_label=\"Document\")\n", " graph.insertNode(node, replace=True)\n", " registry[k] = node\n", " return k\n", "\n", "def add_section(doc_id, section_id):\n", " parent_key = key_for_document(doc_id)\n", " if parent_key not in registry:\n", " add_document(doc_id)\n", " parent = registry[parent_key]\n", " k = key_for_section(doc_id, section_id)\n", " node = WeakNode(parent=parent, pk={\"section\": section_id}, main_label=\"Section\")\n", " graph.insertNode(node, insert_parent=True, replace=True)\n", " registry[k] = node\n", " return k\n", "\n", "def add_page(doc_id, section_id, page_id):\n", " section_key = key_for_section(doc_id, section_id)\n", " if section_key not in registry:\n", " add_section(doc_id, section_id)\n", " parent = registry[section_key]\n", " k = key_for_page(doc_id, section_id, page_id)\n", " node = WeakNode(parent=parent, pk={\"page\": page_id}, main_label=\"Page\")\n", " graph.insertNode(node, insert_parent=True, replace=True)\n", " registry[k] = node\n", " return k\n", "\n", "def delete_key(k, detach=True, propagation=False, on_delete=\"cascade\"):\n", " node = registry.get(k)\n", " if node is None:\n", " raise KeyError(f\"Unknown key: {k}\")\n", " graph.deleteNode(node, detach=detach, propagation=propagation, on_delete=on_delete)\n", " registry.pop(k, None)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ── Graph → Cytoscape conversion helpers ───────────────────────────\n", "\n", "def _node_label(key, node):\n", " \"\"\"Generate a human-readable label for display.\"\"\"\n", " base = node.main_label.split(':')[0] if ':' in node.main_label else node.main_label\n", " pk_parts = []\n", " if hasattr(node, '_pk') and node._pk:\n", " for k, v in sorted(node._pk.items()):\n", " pk_parts.append(f\"{k}={v}\")\n", " return f\"{base}({', '.join(pk_parts)})\" if pk_parts else base\n", "\n", "def _node_color(main_label):\n", " \"\"\"Map node main label to a color.\"\"\"\n", " base = main_label.split(':')[0] if ':' in main_label else main_label\n", " return NODE_COLORS.get(base, '#888888')\n", "\n", "def _node_border(main_label):\n", " \"\"\"Map node main label to a border color.\"\"\"\n", " base = main_label.split(':')[0] if ':' in main_label else main_label\n", " return NODE_BORDERS.get(base, '#555555')\n", "\n", "def _is_weak_node(node):\n", " \"\"\"Check if a node is a WeakNode.\"\"\"\n", " return isinstance(node, WeakNode)\n", "\n", "def update_graph_visualization():\n", " \"\"\"Extract nodes and edges from the graph and push to the Cytoscape.js visualization.\"\"\"\n", " nodes = []\n", " edges = []\n", " \n", " # Collect nodes from registry\n", " for key, node in registry.items():\n", " nodes.append({\n", " \"id\": key,\n", " \"label\": _node_label(key, node),\n", " \"color\": _node_color(node.main_label),\n", " \"border\": _node_border(node.main_label),\n", " \"weak\": _is_weak_node(node),\n", " })\n", " \n", " # Collect edges from the graph\n", " try:\n", " all_edges = graph.get_edges()\n", " for edge in all_edges:\n", " src = getattr(edge, 'source', None)\n", " tgt = getattr(edge, 'target', None)\n", " if src and tgt:\n", " src_key = getattr(src, '_key', None) or str(src)\n", " tgt_key = getattr(tgt, '_key', None) or str(tgt)\n", " \n", " is_weak_edge = False\n", " if hasattr(edge, '_metadata') and edge._metadata:\n", " is_weak_edge = edge._metadata.get('_propagate', False)\n", " \n", " edges.append({\n", " \"id\": f\"e_{src_key}_{tgt_key}\",\n", " \"source\": src_key,\n", " \"target\": tgt_key,\n", " \"label\": \"cascade\" if is_weak_edge else \"\",\n", " \"weak\": is_weak_edge,\n", " })\n", " except Exception:\n", " pass\n", " \n", " # Push to Cytoscape.js\n", " js_code = f\"updateGraph({nodes}, {edges});\"\n", " display(Javascript(js_code))\n", "\n", "print(\"Graph initialized at:\", persistence_path)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Step-by-step walkthrough\n", "\n", "Build a hierarchy and watch the visual graph update below:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "add_document(\"DOC-001\")\n", "update_graph_visualization()\n", "show_state(\"After adding Document\")\n", "\n", "add_section(\"DOC-001\", 1)\n", "update_graph_visualization()\n", "show_state(\"After adding Section as WeakNode\")\n", "\n", "add_page(\"DOC-001\", 1, 1)\n", "update_graph_visualization()\n", "show_state(\"After adding Page as WeakNode of WeakNode\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "delete_key(\"section:DOC-001:1\", detach=True, propagation=False, on_delete=\"cascade\")\n", "update_graph_visualization()\n", "show_state(\"After deleting Section with ON DELETE CASCADE\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Interactive visual editor\n", "\n", "Below is the **interactive graph visualization** (Cytoscape.js). Use the control panel to add or delete nodes, and watch the graph update in real time.\n", "\n", "**How to interact:**\n", "- **Scroll** to zoom in/out\n", "- **Drag** the background to pan\n", "- **Drag** individual nodes to rearrange\n", "- **Hover** over nodes to see their details\n", "- **Dashed blue edges** indicate `WeakRelation` links (cascade delete)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ── Cytoscape.js visualization ────────────────────────────────────\n", "\n", "CYTOSCAPE_HTML = \"\"\"\n", "
\n", "\n", "
\n", "\n", "\n", "\n", "\"\"\"\n", "\n", "display(HTML(CYTOSCAPE_HTML))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ── Control panel ─────────────────────────────────────────────────\n", "\n", "doc_w = widgets.Text(value=\"DOC-002\", description=\"doc:\", layout=widgets.Layout(width='150px'))\n", "section_w = widgets.IntText(value=1, description=\"sec:\", layout=widgets.Layout(width='70px'))\n", "page_w = widgets.IntText(value=1, description=\"page:\", layout=widgets.Layout(width='70px'))\n", "\n", "delete_dropdown = widgets.Dropdown(options=[], description=\"delete:\", layout=widgets.Layout(width='250px'))\n", "detach_w = widgets.Checkbox(value=True, description=\"detach\", layout=widgets.Layout(width='90px'))\n", "prop_w = widgets.Checkbox(value=False, description=\"propagate\", layout=widgets.Layout(width='100px'))\n", "on_delete_w = widgets.Dropdown(\n", " options=[\"cascade\", \"restrict\", \"set_null\"], \n", " value=\"cascade\", \n", " description=\"on_delete:\",\n", " layout=widgets.Layout(width='150px')\n", ")\n", "out = widgets.Output(layout=widgets.Layout(max_height='180px', overflow='auto'))\n", "\n", "def refresh_delete_options(*_):\n", " opts = sorted(registry.keys())\n", " delete_dropdown.options = opts if opts else [\"(none)\"]\n", "\n", "def act_add_doc(_):\n", " with out:\n", " clear_output(wait=True)\n", " k = add_document(doc_w.value)\n", " print(f\"✓ Added: {k}\")\n", " refresh_delete_options()\n", " update_graph_visualization()\n", "\n", "def act_add_section(_):\n", " with out:\n", " clear_output(wait=True)\n", " k = add_section(doc_w.value, section_w.value)\n", " print(f\"✓ Added: {k}\")\n", " refresh_delete_options()\n", " update_graph_visualization()\n", "\n", "def act_add_page(_):\n", " with out:\n", " clear_output(wait=True)\n", " k = add_page(doc_w.value, section_w.value, page_w.value)\n", " print(f\"✓ Added: {k}\")\n", " refresh_delete_options()\n", " update_graph_visualization()\n", "\n", "def act_delete(_):\n", " with out:\n", " clear_output(wait=True)\n", " k = delete_dropdown.value\n", " if not k or k == \"(none)\":\n", " print(\"No node selected for deletion\")\n", " return\n", " try:\n", " delete_key(k, detach=detach_w.value, propagation=prop_w.value, on_delete=on_delete_w.value)\n", " print(f\"✓ Deleted: {k}\")\n", " except Exception as err:\n", " print(f\"✗ Delete error: {err}\")\n", " refresh_delete_options()\n", " update_graph_visualization()\n", "\n", "btn_doc = widgets.Button(description=\"+ Document\", button_style=\"success\", tooltip=\"Add a new Document\")\n", "btn_sec = widgets.Button(description=\"+ Section\", button_style=\"info\", tooltip=\"Add a Section under the current doc\")\n", "btn_page = widgets.Button(description=\"+ Page\", button_style=\"warning\", tooltip=\"Add a Page under the current section\")\n", "btn_del = widgets.Button(description=\"Delete\", button_style=\"danger\", tooltip=\"Delete the selected node\")\n", "\n", "btn_doc.on_click(act_add_doc)\n", "btn_sec.on_click(act_add_section)\n", "btn_page.on_click(act_add_page)\n", "btn_del.on_click(act_delete)\n", "\n", "refresh_delete_options()\n", "\n", "display(widgets.VBox([\n", " widgets.HBox([doc_w, section_w, page_w]),\n", " widgets.HBox([btn_doc, btn_sec, btn_page]),\n", " widgets.HBox([delete_dropdown, detach_w, prop_w, on_delete_w, btn_del]),\n", " out,\n", "]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Cleanup\n", "\n", "Close the graph and remove temporary files:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "graph.close()\n", "try:\n", " os.remove(persistence_path)\n", "except FileNotFoundError:\n", " pass\n", "print(\"Closed graph and removed temp persistence file\")" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "version": "3.11.0" } }, "nbformat": 4, "nbformat_minor": 4 }