This formats to export to an excel file directly with a few enhancements. ready to run on any file with Dynamo player to output the Keynote format directly. We could also add a CSV output to save the TXT file directly to save a few steps
# =====================================================================================
# Dynamo CPython3 - Recover Revit KeynoteTable (key-based tree) into Excel-safe 2D data.
#
# Purpose:
# - Extract the internal KeynoteTable entries (Key, Text, Parent).
# - Add a fixed TOP header row and a second-level header row (from IN[1] or filename/project).
# - Re-parent all original root entries under the second-level header key.
# - Output a rectangular (Excel-safe) List-of-Lists plus a safe sheet name and status string.
#
# Inputs:
# IN[0] = include headers (bool), default True
# IN[1] = second-level header key override (string), optional
# IN[2] = (optional future use, unused here)
#
# Outputs:
# OUT[0] = Excel-safe 2D list (rectangular)
# OUT[1] = safe sheet name (<= 31 chars, no invalid characters)
# OUT[2] = status string
#
# Output row format (3 columns only, per keynote file needs):
# [Key ID] [TAB] [Keynote text] [TAB] [Parent]
# =====================================================================================
# -----------------------------------------
# Imports (declarations)
# -----------------------------------------
import clr # .NET bridge used by Dynamo Python
import re # Regular expressions for sheet name sanitizing
# -----------------------------------------
# Revit/Dynamo references (declarations)
# -----------------------------------------
clr.AddReference("RevitServices") # Dynamo Revit services assembly
from RevitServices.Persistence import DocumentManager # Provides access to the active Revit document
clr.AddReference("RevitAPI") # Revit API assembly
from Autodesk.Revit.DB import ( # Import needed Revit API classes
KeynoteTable, # Access keynote table stored/loaded in the model session
BuiltInParameter # Used to read project number/name fallback
)
# -----------------------------------------
# Active document (declaration)
# -----------------------------------------
doc = DocumentManager.Instance.CurrentDBDocument # Current model open in Revit
# -----------------------------------------
# Inputs (declarations)
# -----------------------------------------
include_headers = IN[0] if len(IN) > 0 else True # IN[0] header row toggle, default True
header_key_in = IN[1] if len(IN) > 1 else "" # IN[1] optional second-level header key
# -----------------------------------------
# Constants (declarations)
# -----------------------------------------
TOP_KEY = "00 00 00-DIVISION-00" # Fixed top entry key
TOP_TEXT = "USER NOTES SECTION" # Fixed top entry text
TOP_PARENT = "" # Fixed top entry parent is blank
SECOND_TEXT = "User extracted keynotes" # Second-level header text (under TOP_KEY)
# -----------------------------------------
# Helper functions (def declarations)
# -----------------------------------------
def s(x):
"""
Safe string conversion.
- Converts None to empty string.
- Avoids exceptions if str() fails.
"""
try:
return "" if x is None else str(x)
except:
return ""
def clean_text(val):
"""
Normalizes text:
- Replace tabs/newlines with spaces
- Collapse whitespace
- Trim edges
"""
t = s(val) # Convert safely to string
t = t.replace("\t", " ") # Remove TABs (important for TSV output safety)
t = t.replace("\r", " ") # Remove CR
t = t.replace("\n", " ") # Remove LF
t = " ".join(t.split()).strip() # Collapse whitespace and trim
return t
def try_get(obj, prop_name):
"""
Safely read a property from a .NET object if it exists.
"""
try: # Attempt property access
if hasattr(obj, prop_name): # If the property exists...
return getattr(obj, prop_name) # ...return its value
except: # If anything fails...
pass # ...ignore and return None
return None # Property not found or not readable
def get_entry_text(entry):
"""
KeynoteEntry/KeyBasedTreeEntry text accessor with fallbacks.
Different builds/wrappers may expose different property names:
- KeynoteText
- Text
- Name
"""
v = try_get(entry, "KeynoteText") # Try canonical property name
if v is None: # If not available...
v = try_get(entry, "Text") # ...try alternate
if v is None: # If still not available...
v = try_get(entry, "Name") # ...try last resort
return clean_text(v) # Return cleaned text
def derive_header_key():
"""
If IN[1] is blank, derive the second-level header key from:
1) RVT file name stem (preferred)
2) Project Number + Project Name
3) "Project" fallback
"""
# 1) filename stem (control structure: try/if)
try:
p = doc.PathName # Full file path
if p: # If doc has a saved path...
fn = p.replace("\\", "/").split("/")[-1] # Get file name only
if fn.lower().endswith(".rvt"): # If it ends with .rvt...
fn = fn[:-4] # ...strip extension
fn = clean_text(fn) # Clean
if fn: # If non-empty...
return fn # ...return filename stem
except:
pass # Ignore and continue to project info fallback
# 2) project info fallback (control structure: try/if)
pn = "" # Project number accumulator
nm = "" # Project name accumulator
try:
pi = doc.ProjectInformation # ProjectInformation element
if pi: # If available...
try:
p1 = pi.get_Parameter(BuiltInParameter.PROJECT_NUMBER)
if p1 and p1.HasValue:
pn = clean_text(p1.AsString() or "")
except:
pass
try:
p2 = pi.get_Parameter(BuiltInParameter.PROJECT_NAME)
if p2 and p2.HasValue:
nm = clean_text(p2.AsString() or "")
except:
pass
except:
pass
combo = (pn + " " + nm).strip() # Combine and trim
if combo: # If non-empty...
return combo # ...use it
# 3) final fallback
return "Project"
def make_safe_sheet_name(name):
"""
Excel sheet name rules:
- Max length 31
- Cannot contain: : \ / ? * [ ]
- Cannot be blank
"""
n = clean_text(name) # Normalize input
if not n: # If blank...
n = "Keynotes" # ...fallback name
n = re.sub(r"[:\\\/\?\*\[\]]", " ", n) # Replace invalid chars with spaces
n = " ".join(n.split()).strip() # Collapse whitespace
if not n: # If still blank...
n = "Keynotes" # ...fallback
if len(n) > 31: # If too long...
n = n[:31] # ...truncate
return n
def rectangularize(table):
"""
Ensure output is a strict 2D rectangular list:
- All rows have identical column counts
- Pads with "" where needed
This prevents Excel interop failures when row lengths vary.
"""
if not table: # If nothing to process...
return [] # ...return empty list
maxc = 0 # Max column count (declaration)
for r in table: # Control structure: for
try:
if len(r) > maxc: # If this row is longer...
maxc = len(r) # ...update max
except:
if maxc < 1:
maxc = 1
out = [] # Output table accumulator
for r in table: # Control structure: for
try:
rr = list(r) # Convert row to list
except:
rr = [s(r)] # If row isn't iterable, wrap as one cell
while len(rr) < maxc: # Control structure: while
rr.append("") # Pad with blanks until rectangular
if len(rr) > maxc: # Control structure: if
rr = rr[:maxc] # Truncate if somehow longer than max
out.append(rr) # Append normalized row
return out # Return rectangular table
# -----------------------------------------
# Determine second-level header key (control structure: if)
# -----------------------------------------
header_key = clean_text(header_key_in) # Normalize IN[1]
if not header_key: # If IN[1] not provided...
header_key = derive_header_key() # ...derive from filename/project
# -----------------------------------------
# Determine Excel sheet name (declaration)
# -----------------------------------------
sheet_name = make_safe_sheet_name(header_key) # Use header key as sheet name (sanitized)
# -----------------------------------------
# Build output rows (declarations)
# -----------------------------------------
rows = [] # Output accumulator (list-of-lists)
# -----------------------------------------
# Optional Excel headers row (control structure: if)
# -----------------------------------------
if include_headers:
rows.append(["Key ID", "Keynote text", "Parent"]) # Header row for Excel export node
# -----------------------------------------
# Always add required top entry (fixed) (statement)
# -----------------------------------------
rows.append([TOP_KEY, TOP_TEXT, TOP_PARENT]) # Top/root line
# -----------------------------------------
# Always add required second-level header entry (statement)
# -----------------------------------------
rows.append([header_key, SECOND_TEXT, TOP_KEY]) # Links under the fixed top node
# -----------------------------------------
# Acquire KeynoteTable (declarations + try/except)
# -----------------------------------------
kt = None # KeynoteTable variable declaration
try:
kt = KeynoteTable.GetKeynoteTable(doc) # Get the document keynote table
except:
kt = None # Fail gracefully
# -----------------------------------------
# If no keynote table is available (control structure: if/else)
# -----------------------------------------
if not kt:
rows.append(["", "ERROR: Could not get KeynoteTable from document.", header_key]) # Error row
OUT = (rectangularize(rows), sheet_name, "ERROR: No KeynoteTable") # Output tuple
else:
# -------------------------------------
# Pull key-based tree entries (declarations + try/except)
# -------------------------------------
entries = None # Entries variable declaration
try:
entries = kt.GetKeyBasedTreeEntries() # Retrieve the full key-based keynote tree
except:
entries = None # Fail gracefully
# -------------------------------------
# If entries are missing (control structure: if/else)
# -------------------------------------
if not entries:
rows.append(["", "ERROR: No KeyBasedTreeEntries available.", header_key]) # Error row
OUT = (rectangularize(rows), sheet_name, "ERROR: No KeyBasedTreeEntries") # Output tuple
else:
# ---------------------------------
# Build dictionaries for traversal (declarations)
# ---------------------------------
data = {} # key -> (text, parentKey)
children = {} # parentKey -> [childKey...]
# ---------------------------------
# Enumerate .NET entries (control structure: while)
# ---------------------------------
it = entries.GetEnumerator() # Get .NET enumerator
while it.MoveNext(): # While enumerator advances...
e = it.Current # Current entry
key = clean_text(try_get(e, "Key")) # Entry key
parent = clean_text(try_get(e, "ParentKey")) # Entry parent key
text = get_entry_text(e) # Entry text
if not key and not text: # Control structure: if
continue # Skip empty rows
if key: # Control structure: if
data[key] = (text, parent) # Store key metadata
if parent not in children: # Control structure: if
children[parent] = [] # Initialize child list for parent
children[parent].append(key) # Add key under its parent
# ---------------------------------
# Depth-first emission in keynote order (declarations)
# ---------------------------------
visited = set() # Track emitted keys to prevent duplicates/loops
def emit_key(k):
"""
Emit row in required 3-column format:
[Key ID, Keynote text, Parent]
Root-level entries (parent == "") are re-parented under header_key.
"""
if k in visited: # Control structure: if
return # Prevent loops
visited.add(k) # Mark as emitted
text, parent = data.get(k, ("", "")) # Get metadata
# Root-level (no parent) gets re-parented to header_key
if not parent: # Control structure: if/else
parent_out = header_key
else:
parent_out = parent
rows.append([k, text or "", parent_out]) # Append required 3-column output row
for ck in children.get(k, []): # Control structure: for
emit_key(ck) # Recurse into children
# ---------------------------------
# Emit from original roots (parent == "") (control structure: for)
# ---------------------------------
for root_key in children.get("", []):
emit_key(root_key)
# ---------------------------------
# Emit orphans not reached from roots (control structure: for/if)
# ---------------------------------
for k in data.keys():
if k not in visited:
text, parent = data.get(k, ("", ""))
if not parent:
parent_out = header_key
else:
parent_out = parent
rows.append([k, text or "", parent_out])
# ---------------------------------
# Final output (declarations)
# ---------------------------------
rows2d = rectangularize(rows) # Ensure Excel-safe rectangle
status = "OK: {} rows".format( # Build status string
max(0, len(rows2d) - (1 if include_headers else 0))
)
OUT = (rows2d, sheet_name, status) # Output tuple for Dynamo wiring
Keynote-Extract+Internal-keynote-tree.dyn (36.6 KB) (Tested in Revit 24)