Propagation Properties — Full Neo4j Demo
This notebook demonstrates the complete workflow for working with propagation properties (_propagate, is_weak, parent_relation, _weak_init_done) on a real Neo4j database.
Steps:
Connect to Neo4j DEV and load the Game of Thrones dataset
Generate YAML schema + Python entity classes
Initialize propagation properties on existing data
Demonstrate
create_group()— atomic group creationRun sample queries to verify everything works
Prerequisites
A running Neo4j instance (DEV target)
Environment variables:
NEO4J_DEV_URL,NEO4J_DEV_USER,NEO4J_DEV_PASSWORDDefault connection:
bolt://localhost:7687/neo4j/secret
Connection configuration
The notebook reads connection parameters from environment variables with sensible defaults. Set these before running:
export NEO4J_DEV_URL=bolt://your-host:7687
export NEO4J_DEV_USER=neo4j
export NEO4J_DEV_PASSWORD=your-password
[1]:
import importlib.util
# Check if cvcdocdb is installed
package_to_check = 'cvcdocdb'
spec = importlib.util.find_spec(package_to_check)
if spec is None:
print(f'⚠️ {package_to_check} not installed. Installing...')
%pip install -q --upgrade cvcdocdb-tools
print("✅ Installation complete.")
else:
print(f'✅ {package_to_check} already installed. Skipping.')
✅ cvcdocdb already installed. Skipping.
[2]:
import os
import sys
from pathlib import Path
# Read .env file manually (no python-dotenv dependency)
def read_env(env_path):
"""Read a .env file and return a dict of key=value pairs."""
env_vars = {}
with open(env_path, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
if '=' in line:
key, value = line.split('=', 1)
env_vars[key.strip()] = value.strip()
return env_vars
# Find .env in repo root
_env_path = Path.cwd() / ".env"
if not _env_path.exists():
for parent in Path.cwd().parents:
candidate = parent / ".env"
if candidate.exists():
_env_path = candidate
break
if _env_path.exists():
_env_vars = read_env(_env_path)
for key, value in _env_vars.items():
os.environ.setdefault(key, value)
print(f"✅ Loaded .env from {_env_path}")
print(f" Found {len(_env_vars)} variables")
else:
print("⚠️ .env file not found. Set NEO4J_DEV_PASSWORD environment variable manually.")
# Add cvcdocdb package to path for development
_repo_root = Path.cwd()
if not (_repo_root / "cvcdocdb").exists():
for parent in Path.cwd().parents:
if (parent / "cvcdocdb").exists():
_repo_root = parent
break
_cvcdocdb_path = str(_repo_root / "cvcdocdb")
if _cvcdocdb_path not in sys.path:
sys.path.insert(0, _cvcdocdb_path)
print(f"✅ Added {_cvcdocdb_path} to sys.path")
import json
from cvcdocdb import Neo4jGraph, NetworkXGraph
from cvcdocdb.exemples import load_got_characters
from cvcdocdb.schema_gen import generate_classes
def neo4j_config(default_target="DEV"):
"""Build Neo4j connection config from environment variables."""
target = os.environ.get("NEO4J_TARGET", default_target).upper()
prefix = f"NEO4J_{target}_"
return {
"target": target,
"url": os.environ.get(f"{prefix}URL", os.environ.get("NEO4J_URL", "bolt://localhost:7687")),
"user": os.environ.get(f"{prefix}USER", os.environ.get("NEO4J_USER", "neo4j")),
"password": os.environ.get(f"{prefix}PASSWORD", os.environ.get("NEO4J_PASSWORD", "secret")),
"database": os.environ.get(f"{prefix}DATABASE", os.environ.get("NEO4J_DATABASE", "neo4j")),
}
def section(title):
"""Print a section header."""
print(f"\n{'=' * 60}")
print(f" {title}")
print(f"{'=' * 60}")
def sub_section(title):
"""Print a subsection header."""
print(f"\n--- {title} ---")
# Resolve paths relative to repo root
def repo_path(rel):
"""Return a Path relative to the repository root."""
return _repo_root / rel
print("✅ All imports successful.")
✅ Loaded .env from /Users/oriol/Desenvolupament/cvcdocdb-tools/.env
Found 5 variables
✅ Added /Users/oriol/Desenvolupament/cvcdocdb-tools/cvcdocdb to sys.path
✅ All imports successful.
Step 1: Connect to Neo4j
Connect to the DEV Neo4j instance and verify the connection.
[3]:
cfg = neo4j_config()
print(f"Target: {cfg['target']}")
print(f"URL: {cfg['url']}")
print(f"User: {cfg['user']}")
print(f"Database: {cfg['database']}")
graph = Neo4jGraph(
url=cfg["url"],
user=cfg["user"],
password=cfg["password"],
database=cfg["database"],
)
# Verify connection
result = graph.query("RETURN 1 AS test")
if result and result[0].get("test") == 1:
print("\n✅ Connection verified.")
else:
print("\n⚠️ Connection returned unexpected result.")
# Check current state
count_result = graph.query("MATCH (n) RETURN count(n) AS total")
total = count_result[0]["total"] if count_result else 0
print(f"Nodes in database: {total}")
Target: DEV
URL: bolt://localhost:7687
User: neo4j
Database: neo4j
✅ Connection verified.
Nodes in database: 57
Step 2: Load Game of Thrones dataset
Load character and house data from thronesapi.com. Falls back to bundled data if the API is unavailable.
[4]:
stats = load_got_characters(graph, limit=25)
print("Loaded:", json.dumps(stats, indent=2))
# Show nodes by label
sub_section("Nodes by label")
result = graph.query(
"MATCH (n) RETURN labels(n)[0] AS label, count(n) AS cnt "
"ORDER BY cnt DESC"
)
for row in result:
print(f" {row['label']}: {row['cnt']}")
# Show relations by type
sub_section("Relations by type")
result = graph.query(
"MATCH ()-[r]->() RETURN type(r) AS rel_type, count(r) AS cnt "
"ORDER BY cnt DESC"
)
for row in result:
print(f" {row['rel_type']}: {row['cnt']}")
Loaded: {
"characters": 25,
"houses": 15,
"member_of": 25
}
--- Nodes by label ---
Character: 25
House: 15
Valor: 5
IndividuPadro: 3
LlocPadro: 2
NodeA: 1
NodeB: 1
TestNode: 1
TransientNode: 1
TEST: 1
Document: 1
Page: 1
--- Relations by type ---
MEMBER_OF: 25
COGNOM1: 3
NOM: 3
CONNECTS_TO: 1
HAS_PAGE: 1
Step 3: Generate YAML schema
Introspect the Neo4j database and generate a YAML schema description. This captures labels, properties, relationships, and counts.
[5]:
output_dir = repo_path("cvcdocdb/exemples/generated")
output_dir.mkdir(exist_ok=True)
yaml_content = graph.schema_yaml(db_name="got")
yaml_path = output_dir / "got_schema.yaml"
yaml_path.write_text(yaml_content)
print(f"Written: {yaml_path}")
print(f"Size: {len(yaml_content)} bytes")
print("\nSchema preview:")
for line in yaml_content.split("\n")[:20]:
print(f" {line}")
print(" ...")
Written: /Users/oriol/Desenvolupament/cvcdocdb-tools/cvcdocdb/exemples/generated/got_schema.yaml
Size: 3845 bytes
Schema preview:
# Schema generated from: got
# Generated at: 2026-07-12
labels:
Character:
class_name: Character
base_class: Node
properties:
character_id: integer
name: string
source: string
title: string
primary_key: ['name', 'character_id']
count: 25
Document:
class_name: Document
base_class: Node
properties:
_weak_init_done: boolean
doc_id: string
...
Step 4: Generate Python entity classes
Generate Python classes from the YAML schema. This creates Node, WeakNode, Relation, and WeakRelation subclasses for each label and relationship type in the database.
[6]:
# Read the YAML file content (generate_classes expects YAML string, not file path)
yaml_content = yaml_path.read_text()
code = generate_classes(yaml_content)
py_path = output_dir / "got_entities.py"
py_path.write_text(code)
print(f"Written: {py_path}")
print(f"Size: {len(code)} bytes")
print("\nGenerated code preview:")
for line in code.split("\n")[:25]:
print(f" {line}")
print(" ...")
Written: /Users/oriol/Desenvolupament/cvcdocdb-tools/cvcdocdb/exemples/generated/got_entities.py
Size: 11259 bytes
Generated code preview:
"""Auto-generated entity classes from schema."""
from __future__ import annotations
from typing import Any, Dict, List, Optional
from cvcdocdb.base import Node, WeakNode, Relation, WeakRelation
class Character(Node):
"""Auto-generated entity class.."""
def __init__(self, pk: Dict[str, Any], **kwargs: Any) -> None:
"""Initialize a Character node.
Args:
pk: Primary key dict with fields: name, character_id.
**kwargs: Additional properties and attributes.
"""
super().__init__(pk=pk, main_label="Character", **kwargs)
self.character_id = kwargs.get("character_id", 'integer')
self.name = kwargs.get("name", 'string')
self.source = kwargs.get("source", 'string')
self.title = kwargs.get("title", 'string')
class Document(Node):
...
Step 5: Initialize propagation properties
Scan the entire Neo4j graph and initialize propagation properties:
is_weak: marks nodes that are children of_propagateedges_propagate: marks nodes that have cascade-delete enabledparent_relation: stores the edge type linking parent → child_weak_init_done: marks strong nodes whose weak children are initialized
This uses init_propagation() which:
Finds all edges with
_propagate = TRUEMarks the child nodes as
is_weak = TRUESkips nodes already marked via
_weak_init_done
[7]:
# Check state before init
sub_section("Before init_propagation()")
result = graph.query(
"MATCH (n) RETURN count(n) AS total, "
"count(n.is_weak) AS weak_count, "
"count(n._weak_init_done) AS done_count"
)
row = result[0] if result else {}
print(f" Total nodes: {row.get('total', 0)}")
print(f" Nodes with is_weak: {row.get('weak_count', 0)}")
print(f" Nodes with _weak_init_done: {row.get('done_count', 0)}")
# Run initialization
print("\nRunning init_propagation()...")
init_result = graph.init_propagation()
print(f"Result: {init_result}")
# Check state after init
sub_section("After init_propagation()")
result = graph.query(
"MATCH (n) RETURN count(n) AS total, "
"count(n.is_weak) AS weak_count, "
"count(n._weak_init_done) AS done_count"
)
row = result[0] if result else {}
print(f" Total nodes: {row.get('total', 0)}")
print(f" Nodes with is_weak: {row.get('weak_count', 0)}")
print(f" Nodes with _weak_init_done: {row.get('done_count', 0)}")
# Show propagation edges
result = graph.query(
"MATCH ()-[r]->() WHERE r._propagate = TRUE RETURN count(r) AS c"
)
prop_count = result[0]["c"] if result else 0
print(f" Edges with _propagate: {prop_count}")
--- Before init_propagation() ---
Total nodes: 57
Nodes with is_weak: 1
Nodes with _weak_init_done: 17
Running init_propagation()...
Result: True
--- After init_propagation() ---
Total nodes: 57
Nodes with is_weak: 1
Nodes with _weak_init_done: 57
Edges with _propagate: 1
Step 6: Transactional group creation
Use create_group() to atomically create a strong node with its WeakNodes and WeakRelations. The entire group is created in a single Neo4j transaction — if any step fails, everything is rolled back.
We create a Document → Section → Page hierarchy.
[8]:
from cvcdocdb.base import Node, WeakNode
# Create the strong node (Document)
doc = Node(
pk={"doc_id": "GOT-DEMO-001"},
main_label="Document",
title="Game of Thrones — Episode Guide",
author="Demo Script",
)
# Create weak nodes (Sections and Pages)
section1 = WeakNode(
parent=doc,
parent_relation="HAS_SECTION",
pk={"section": "intro"},
main_label="Section",
title="Introduction",
order=1,
)
section2 = WeakNode(
parent=doc,
parent_relation="HAS_SECTION",
pk={"section": "characters"},
main_label="Section",
title="Main Characters",
order=2,
)
page1 = WeakNode(
parent=section1,
parent_relation="HAS_PAGE",
pk={"page": 1},
main_label="Page",
content="Welcome to the Game of Thrones episode guide.",
order=1,
)
page2 = WeakNode(
parent=section2,
parent_relation="HAS_PAGE",
pk={"page": 1},
main_label="Page",
content="Jon Snow, Daenerys Targaryen, Tyrion Lannister...",
order=1,
)
print("Creating group:")
print(f" Strong node: Document(GOT-DEMO-001)")
print(f" Weak nodes: Section(intro), Section(characters)")
print(f" Weak nodes: Page(1) → intro, Page(1) → characters")
# Create atomically
doc_id = graph.create_group(
strong_node=doc,
weak_nodes=[section1, section2, page1, page2],
)
print(f"\n✅ Group created (strong node id: {doc_id})")
Creating group:
Strong node: Document(GOT-DEMO-001)
Weak nodes: Section(intro), Section(characters)
Weak nodes: Page(1) → intro, Page(1) → characters
---------------------------------------------------------------------------
AttributeError Traceback (most recent call last)
Cell In[8], line 54
50 print(f" Weak nodes: Section(intro), Section(characters)")
51 print(f" Weak nodes: Page(1) → intro, Page(1) → characters")
52
53 # Create atomically
---> 54 doc_id = graph.create_group(
55 strong_node=doc,
56 weak_nodes=[section1, section2, page1, page2],
57 )
File ~/opt/anaconda3/envs/cvcdocdb-env/lib/python3.11/site-packages/cvcdocdb/neo4j_graph.py:700, in Neo4jGraph.create_group(self, strong_node, weak_nodes, weak_relations, **kwargs)
697 return result
699 try:
--> 700 return session.write_transaction(_create_group_tx)
701 except ConstraintError as err:
702 raise RuntimeError("Duplicate key: " + err.message) from err
AttributeError: 'Session' object has no attribute 'write_transaction'
Step 7: Verify the group
Query the database to verify the group was created correctly and check the propagation properties.
[ ]:
# Verify Document node
sub_section("Document node")
result = graph.query(
"MATCH (n) WHERE n.doc_id = 'GOT-DEMO-001' "
"RETURN labels(n) AS labels, n.title, n._weak_init_done"
)
for row in result:
print(f" Labels: {row['labels']}")
print(f" Title: {row['title']}")
print(f" _weak_init_done: {row.get('_weak_init_done')}")
# Verify Section nodes
sub_section("Section nodes")
result = graph.query(
"MATCH (n:Section) WHERE n._weak_init_done IS NULL "
"RETURN labels(n) AS labels, n.title, n.is_weak, n._propagate"
)
for row in result:
print(f" {row['title']}: is_weak={row.get('is_weak')}, _propagate={row.get('_propagate')}")
# Verify Page nodes
sub_section("Page nodes")
result = graph.query(
"MATCH (n:Page) WHERE n._weak_init_done IS NULL "
"RETURN labels(n) AS labels, n.content, n.is_weak, n._propagate"
)
for row in result:
content = (row['content'] or '')[:50]
print(f" Page: '{content}...' is_weak={row.get('is_weak')}, _propagate={row.get('_propagate')}")
Step 8: Sample queries
Run several queries to demonstrate the initialized graph and propagation properties.
[ ]:
# Query 1: Characters by House
sub_section("Characters by House")
result = graph.query(
"MATCH (c:Character)-[:MEMBER_OF]->(h:House) "
"RETURN h.name AS house, collect(c.name) AS characters "
"ORDER BY house"
)
for row in result:
chars = row['characters'][:5]
print(f" {row['house']}: {', '.join(chars)}")
if len(row['characters']) > 5:
print(f" ... and {len(row['characters']) - 5} more")
[ ]:
# Query 2: Nodes with propagation properties
sub_section("Nodes with propagation properties")
result = graph.query(
"MATCH (n) WHERE n.is_weak = TRUE "
"RETURN labels(n) AS label, count(n) AS count "
"ORDER BY count DESC"
)
for row in result:
print(f" {row['label']}: {row['count']} nodes")
# Query 3: Edges with _propagate
sub_section("Edges with _propagate")
result = graph.query(
"MATCH (a)-[r]->(b) WHERE r._propagate = TRUE "
"RETURN type(a) AS from_label, type(r) AS rel_type, "
"type(b) AS to_label, count(*) AS count "
"ORDER BY count DESC"
)
for row in result:
print(f" {row['from_label']} -[{row['rel_type']}]-> {row['to_label']}: {row['count']}")
[ ]:
# Query 4: Document group hierarchy
sub_section("Document group hierarchy")
result = graph.query(
"MATCH path = (d:Document {doc_id: 'GOT-DEMO-001'})-[*1..3]->(n) "
"RETURN path"
)
if result:
for row in result:
path = row.get("path")
if path:
nodes = path.nodes
rels = path.relationships
parts = []
for i, node in enumerate(nodes):
label = list(node.labels)[0] if hasattr(node, 'labels') else 'Node'
props = dict(node)
name = props.get('title', props.get('name', str(node.element_id)))
parts.append(f"{label}({name})")
for rel in rels:
parts.append(f"-[{rel.type}]->")
print(f" {' → '.join(parts)}")
[ ]:
# Query 5: Summary statistics
sub_section("Summary statistics")
result = graph.query(
"MATCH (n) "
"RETURN count(n) AS total_nodes, "
"count(n.is_weak) AS weak_nodes, "
"count(n._weak_init_done) AS initialized_nodes"
)
row = result[0] if result else {}
print(f" Total nodes: {row.get('total_nodes', 0)}")
print(f" Weak nodes: {row.get('weak_nodes', 0)}")
print(f" Initialized nodes: {row.get('initialized_nodes', 0)}")
result = graph.query(
"MATCH ()-[r]->() "
"RETURN count(r) AS total_edges, "
"count(r._propagate) AS propagate_edges"
)
row = result[0] if result else {}
print(f" Total edges: {row.get('total_edges', 0)}")
print(f" Propagate edges: {row.get('propagate_edges', 0)}")
print("\n" + "=" * 60)
print(" ✅ Demo completed successfully!")
print("=" * 60)
Cleanup
Close the Neo4j connection when done.
[ ]:
graph.close()
print("✅ Neo4j connection closed.")