Has anyone come up with a better way to ID/Find the local DC and resolve paths for e.g. exporting parameters, reports, etc? Worked this one through with GPT- but is there a better way?
/Dynamo/py-resolve-export-path-from-dc-clsid.py
Encoding: utf-8
PURPOSE
Resolve a reliable export file path for the current Revit model.
OUTPUT CONTRACT
OUT[0] = final target file path
OUT[1] = one-item list containing a multi-line plain-text details/export block
RESOLUTION ORDER
1. If IN[1] contains an override path, resolve special tokens and environment variables.
- If valid, use it immediately.
- If invalid, stop and use Desktop fallback.
2. If no valid override is supplied, search HKCR\CLSID for the Desktop Connector shell item.
3. Read the Desktop Connector local root from:
CLSID\Instance\InitPropertyBag\TargetFolderPath
4. If that fails, stop and use Desktop fallback.
5. Check whether the current document is workshared.
6. Search under the Desktop Connector local root for the actual current model file.
7. If found, use that model’s folder and strip the Revit extension from the matched file name.
8. If any step fails, stop and use Desktop fallback.
INPUTS
IN[0] = desired export extension, such as “.txt”, “.csv”, “.params.txt”, or “log”
If null, empty, or missing, “.txt” is used.
IN[1] = optional override path
Supported examples:
- C:\Temp
- C:\Temp\MyFolder
- %desktop%
- %desktop%\Exports
- %userprofile%\Documents
- %downloads%
- %localappdata%\Temp
USAGE
- Feed IN[0] with the file extension you want to append to the model base name.
- Feed IN[1] with an optional target folder or tokenized path.
- Use OUT[0] as the file path for writing.
- Use OUT[1][0] when you want a single copy/paste diagnostic text block.
------------------------------------------------------------
IMPORTS
------------------------------------------------------------
Load .NET interop support required by Dynamo Python.
import clr
Load Python modules used for path handling and regular expressions.
import os
import re
------------------------------------------------------------
REVIT / DYNAMO REFERENCES
------------------------------------------------------------
Add access to Dynamo’s current Revit document manager.
clr.AddReference(“RevitServices”)
from RevitServices.Persistence import DocumentManager
Add access to Revit API model path utilities.
clr.AddReference(“RevitAPI”)
from Autodesk.Revit.DB import ModelPathUtils
Add access to Windows environment and special folders.
clr.AddReference(“System”)
from System import Environment
Add access to the Windows registry for Desktop Connector lookup.
clr.AddReference(“mscorlib”)
from Microsoft.Win32 import Registry
------------------------------------------------------------
CURRENT DOCUMENT DECLARATION
------------------------------------------------------------
Get the currently open Revit document from Dynamo.
doc = DocumentManager.Instance.CurrentDBDocument
------------------------------------------------------------
STRING HELPERS
------------------------------------------------------------
Safely convert any value to a string.
Returns the default when the value is None or conversion fails.
def _safe_str(value, default=“”):
try:
# Use the provided default when the input is missing.
if value is None:
return default
# Convert the value to text.
return str(value)
except:
# Fall back to the default when conversion fails.
return default
Normalize the requested extension.
Ensures the extension starts with a period and defaults to .txt when blank.
def _normalize_extension(value, default_ext=“.txt”):
# Convert the input to a trimmed string.
ext = _safe_str(value, “”).strip()
# Use the default extension when nothing was supplied.
if not ext:
return default_ext
# Prefix the extension with a period when the caller omitted it.
if not ext.startswith("."):
ext = "." + ext
# Return the normalized extension.
return ext
Remove a trailing Revit file extension from a file or title.
Supported: .rvt, .rfa, .rte, .rft
def _strip_revit_extension(name):
# Normalize the input text.
s = _safe_str(name, “”).strip()
# Remove a supported Revit extension only when it appears at the end.
return re.sub(r"(?i)\.(rvt|rfa|rte|rft)$", "", s).strip()
Remove invalid Windows filename characters and ensure a non-empty result.
def _sanitize_filename(name, fallback=“Model”):
# Normalize the incoming name to a trimmed string.
s = _safe_str(name, fallback).strip()
# Use the fallback when the name is empty.
if not s:
s = fallback
# Replace characters that are invalid in Windows file names.
s = re.sub(r'[<>:"/\\\\|?*]+', "_", s)
# Remove trailing spaces and periods, which are problematic in Windows paths.
s = s.strip(" .")
# Return the safe file name, or the fallback if sanitizing emptied it.
return s or fallback
------------------------------------------------------------
FOLDER AND FILE HELPERS
------------------------------------------------------------
Return the current user’s Desktop folder path.
def _get_desktop_folder():
return Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory)
Check whether a folder exists.
Returns True only for existing directories.
def _folder_exists(path_value):
try:
# Validate the input and confirm the path is a directory.
return bool(path_value) and os.path.isdir(path_value)
except:
# Return False when the input cannot be evaluated safely.
return False
Check whether a file exists.
Returns True only for existing files.
def _file_exists(path_value):
try:
# Validate the input and confirm the path is a file.
return bool(path_value) and os.path.isfile(path_value)
except:
# Return False when the input cannot be evaluated safely.
return False
------------------------------------------------------------
PATH TOKEN HELPERS
------------------------------------------------------------
Return a map of supported shell-like folder tokens.
The tokens are resolved case-insensitively.
def _get_special_folder_map():
return {
“%desktop%”: Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
“%desktopdirectory%”: Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory),
“%documents%”: Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
“%mydocuments%”: Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
“%personal%”: Environment.GetFolderPath(Environment.SpecialFolder.Personal),
“%favorites%”: Environment.GetFolderPath(Environment.SpecialFolder.Favorites),
“%programfiles%”: Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
“%programfilesx86%”: Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
“%appdata%”: Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
“%localappdata%”: Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
“%commonappdata%”: Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
“%userprofile%”: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
“%mypictures%”: Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
“%mypicturesfolder%”: Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
“%templates%”: Environment.GetFolderPath(Environment.SpecialFolder.Templates),
“%startup%”: Environment.GetFolderPath(Environment.SpecialFolder.Startup),
“%commonstartup%”: Environment.GetFolderPath(Environment.SpecialFolder.CommonStartup),
“%sendto%”: Environment.GetFolderPath(Environment.SpecialFolder.SendTo),
“%startmenu%”: Environment.GetFolderPath(Environment.SpecialFolder.StartMenu),
“%commonstartmenu%”: Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
“%programs%”: Environment.GetFolderPath(Environment.SpecialFolder.Programs),
“%commonprograms%”: Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms),
“%temp%”: Environment.GetEnvironmentVariable(“TEMP”) or “”,
“%tmp%”: Environment.GetEnvironmentVariable(“TMP”) or “”,
“%home%”: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
}
Best-effort resolution for the user’s Downloads folder.
def _try_get_downloads_folder():
try:
# Build the default Downloads folder path under the user’s profile.
user_profile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
candidate = os.path.join(user_profile, “Downloads”)
# Return the folder only if it already exists.
if _folder_exists(candidate):
return candidate
except:
pass
# Return blank when the Downloads folder cannot be confirmed.
return ""
Expand supported custom tokens, shell-like tokens, and normal environment variables.
Examples:
- %desktop%
- %documents%
- %downloads%
- %userprofile%
- %LOCALAPPDATA%
def _expand_special_tokens(path_value):
# Normalize the incoming path text.
raw = _safe_str(path_value, “”).strip()
# Stop immediately when no path was supplied.
if not raw:
return ""
# Start with the raw input.
resolved = raw
# Build the supported token map.
token_map = _get_special_folder_map()
# Add Downloads when it can be confirmed.
downloads = _try_get_downloads_folder()
if downloads:
token_map["%downloads%"] = downloads
# Replace all supported custom tokens.
# A lambda is used so Windows backslashes are inserted literally.
for token, token_value in token_map.items():
if token_value:
resolved = re.sub(re.escape(token), lambda m: token_value, resolved, flags=re.IGNORECASE)
# Expand any remaining standard Windows environment variables.
resolved = Environment.ExpandEnvironmentVariables(resolved)
# Expand Python-side environment and user-home conventions for completeness.
resolved = os.path.expandvars(resolved)
resolved = os.path.expanduser(resolved)
# Return a normalized path.
return os.path.normpath(resolved)
Resolve the override input to a usable folder path.
Supports folder paths, file paths, and tokenized paths.
def _resolve_override_folder(override_value):
# Resolve tokens and environment variables first.
resolved = _expand_special_tokens(override_value)
# Stop early when no override was supplied.
if not resolved:
return {
"provided": False,
"input": _safe_str(override_value, ""),
"resolved": "",
"valid": False,
"folder": ""
}
# Use the path directly when it is an existing folder.
if _folder_exists(resolved):
return {
"provided": True,
"input": _safe_str(override_value, ""),
"resolved": resolved,
"valid": True,
"folder": resolved
}
# Use the parent folder when the resolved path is an existing file.
if _file_exists(resolved):
return {
"provided": True,
"input": _safe_str(override_value, ""),
"resolved": resolved,
"valid": True,
"folder": os.path.dirname(resolved)
}
# Use the parent folder when the resolved path appears to be a future file path
# and the parent folder already exists.
parent = os.path.dirname(resolved)
if parent and _folder_exists(parent):
return {
"provided": True,
"input": _safe_str(override_value, ""),
"resolved": resolved,
"valid": True,
"folder": parent
}
# Mark the override as invalid when no usable folder could be confirmed.
return {
"provided": True,
"input": _safe_str(override_value, ""),
"resolved": resolved,
"valid": False,
"folder": ""
}
------------------------------------------------------------
REGISTRY HELPERS
------------------------------------------------------------
Open a registry subkey and return None if it does not exist.
def _open_subkey(root_key, subkey_path):
try:
return root_key.OpenSubKey(subkey_path)
except:
return None
Read the default unnamed value from a registry key.
def _read_default_value(reg_key):
try:
return _safe_str(reg_key.GetValue(None), “”)
except:
return “”
Search HKCR\CLSID for the shell namespace entry whose default value is Autodesk Docs.
def _find_desktop_connector_clsid():
# Open the CLSID root under HKCR.
clsid_root = _open_subkey(Registry.ClassesRoot, r"CLSID")
# Stop if HKCR\CLSID is not accessible.
if clsid_root is None:
return ""
try:
# Enumerate all CLSID child keys.
for subkey_name in clsid_root.GetSubKeyNames():
# Build the full child key path.
child_path = r"CLSID\{0}".format(subkey_name)
# Open the CLSID child key.
child_key = _open_subkey(Registry.ClassesRoot, child_path)
# Skip any key that cannot be opened.
if child_key is None:
continue
# Read the display name stored as the default value.
display_name = _read_default_value(child_key).strip()
# Return the CLSID when the display name matches Autodesk Docs.
if display_name.lower() == "autodesk docs":
return subkey_name
except:
# Return blank when CLSID enumeration fails.
return ""
# Return blank when no matching CLSID is found.
return ""
Read the Desktop Connector local root from the CLSID InitPropertyBag TargetFolderPath.
def _get_dc_root_from_clsid(clsid_value):
# Stop when the CLSID is missing.
if not clsid_value:
return “”
# Build the registry path to the InitPropertyBag key.
subkey_path = r"CLSID\{0}\Instance\InitPropertyBag".format(clsid_value)
# Open the registry key.
reg_key = _open_subkey(Registry.ClassesRoot, subkey_path)
# Stop when the key cannot be opened.
if reg_key is None:
return ""
try:
# Read the TargetFolderPath value.
target_folder_path = _safe_str(reg_key.GetValue("TargetFolderPath"), "").strip()
# Return the normalized folder path only when it exists.
if target_folder_path and _folder_exists(target_folder_path):
return os.path.normpath(target_folder_path)
except:
# Return blank when the registry value cannot be read safely.
return ""
# Return blank when the folder path is not usable.
return ""
------------------------------------------------------------
DOCUMENT HELPERS
------------------------------------------------------------
Build likely file names for the current document based on its title.
def _get_current_document_name_candidates(document):
# Get the current document title and strip any visible Revit extension.
title = _safe_str(document.Title, “”).strip()
base_name = _sanitize_filename(_strip_revit_extension(title), “Model”)
# Determine likely file extensions from the visible title.
lower_title = title.lower()
exts = []
if lower_title.endswith(".rvt"):
exts = [".rvt"]
elif lower_title.endswith(".rfa"):
exts = [".rfa"]
elif lower_title.endswith(".rte"):
exts = [".rte"]
elif lower_title.endswith(".rft"):
exts = [".rft"]
else:
exts = [".rvt", ".rfa", ".rte", ".rft"]
# Return the safe base name and all likely current file names.
return {
"base_name": base_name,
"file_names": [base_name + ext for ext in exts]
}
Return whether the current document is workshared.
def _is_workshared(document):
try:
return bool(document.IsWorkshared)
except:
return False
Return the best available user-visible Revit path for ACC / BIM 360 / local reference.
def _get_acc_bim360_reference_path(document):
try:
# Prefer the user-visible central path for workshared models.
if document.IsWorkshared:
model_path = document.GetWorksharingCentralModelPath()
if model_path is not None:
visible = _safe_str(ModelPathUtils.ConvertModelPathToUserVisiblePath(model_path), “”)
if visible:
return visible
except:
pass
try:
# Fall back to the cloud model path for cloud-hosted models.
if document.IsModelInCloud:
model_path = document.GetCloudModelPath()
if model_path is not None:
visible = _safe_str(ModelPathUtils.ConvertModelPathToUserVisiblePath(model_path), "")
if visible:
return visible
except:
pass
try:
# Last fallback is the document's own path name.
return _safe_str(document.PathName, "")
except:
return ""
------------------------------------------------------------
MODEL SEARCH HELPERS
------------------------------------------------------------
Search recursively for the actual model file under the Desktop Connector root.
def _search_for_model_under_root(root_folder, target_file_names, max_hits=10, max_dirs=15000):
# Return no matches when the root folder is invalid.
if not _folder_exists(root_folder):
return
# Use a lowercase lookup set for case-insensitive file matching.
target_names = set([name.lower() for name in target_file_names])
# Collect matched file paths.
hits = []
# Track how many directories have been visited to avoid runaway searches.
visited = 0
try:
# Walk the Desktop Connector tree recursively.
for current_root, dirnames, filenames in os.walk(root_folder):
# Count each visited folder.
visited += 1
# Stop when the traversal cap is reached.
if visited > max_dirs:
break
# Inspect every file in the current folder.
for file_name in filenames:
# Match only exact file names, case-insensitively.
if file_name.lower() in target_names:
# Build the full path for the candidate model file.
full_path = os.path.join(current_root, file_name)
# Keep only real files.
if _file_exists(full_path):
hits.append(os.path.normpath(full_path))
# Stop early once enough matches have been found.
if len(hits) >= max_hits:
return hits
except:
# Return any matches found before the traversal failed.
return hits
# Return all matches found.
return hits
------------------------------------------------------------
DETAILS / EXPORT HELPERS
------------------------------------------------------------
Convert the result dictionary into a stable plain-text export block.
def _to_export_lines(result_dict):
# Define the export order for readability and consistency.
ordered_keys = [
“Source”,
“IsWorkshared”,
“DesktopFolder”,
“TargetFolder”,
“BaseName”,
“Extension”,
“FileName”,
“FullPath”,
“AccBim360Path”,
“DcClsid”,
“DcRoot”,
“MatchedModelPath”,
“OverrideInput”,
“OverrideResolved”,
“FailReason”,
]
# Start the output block with a title and separator.
lines = []
lines.append("EXPORT_PATH_RESOLUTION")
lines.append("----------------------------------------")
# Add each ordered key/value pair as a label line.
for key in ordered_keys:
value = _safe_str(result_dict.get(key, ""), "")
lines.append("{0}: {1}".format(key, value))
# Return the final line list.
return lines
Convert the result dictionary into a single copy/paste text block.
def _to_export_text(result_dict):
# Join the export lines into one multi-line string.
return “\n”.join(_to_export_lines(result_dict))
------------------------------------------------------------
FAILOVER BUILDER
------------------------------------------------------------
Build the Desktop fallback result when any earlier step fails.
def _build_desktop_failover(document, extension_value, fail_reason, override_input=“”, override_resolved=“”):
# Determine the current user’s Desktop folder.
desktop = _get_desktop_folder()
# Derive the model base name from the current document title.
doc_info = _get_current_document_name_candidates(document)
base_name = doc_info["base_name"]
# Normalize the requested export extension.
extension_value = _normalize_extension(extension_value, ".txt")
# Build the fallback file name and full path.
file_name = base_name + extension_value
full_path = os.path.join(desktop, file_name)
# Get the best available ACC / BIM 360 reference path for diagnostics.
acc_bim360_path = _get_acc_bim360_reference_path(document)
# Return the structured fallback result.
return {
"Source": "DesktopFallback",
"IsWorkshared": _is_workshared(document),
"DesktopFolder": desktop,
"TargetFolder": desktop,
"BaseName": base_name,
"Extension": extension_value,
"FileName": file_name,
"FullPath": full_path,
"AccBim360Path": acc_bim360_path,
"DcClsid": "",
"DcRoot": "",
"MatchedModelPath": "",
"OverrideInput": override_input,
"OverrideResolved": override_resolved,
"FailReason": fail_reason
}
------------------------------------------------------------
MAIN RESOLUTION
------------------------------------------------------------
Resolve the final export path using the requested stop-on-failure logic.
def resolve_export_path(document, requested_extension, override_path):
# Normalize the requested export extension first.
extension_value = _normalize_extension(requested_extension, “.txt”)
# Capture the best available ACC / BIM 360 reference path early for reporting.
acc_bim360_path = _get_acc_bim360_reference_path(document)
# --------------------------------------------------------
# STEP 1: OVERRIDE PATH
# --------------------------------------------------------
# Resolve the override path when one was provided.
override_info = _resolve_override_folder(override_path)
# Use the override immediately when it resolves successfully.
if override_info["provided"] and override_info["valid"]:
# Build the file name from the current model base name and requested extension.
doc_info = _get_current_document_name_candidates(document)
base_name = doc_info["base_name"]
file_name = base_name + extension_value
full_path = os.path.join(override_info["folder"], file_name)
# Return the successful override result.
return {
"Source": "Override",
"IsWorkshared": _is_workshared(document),
"DesktopFolder": _get_desktop_folder(),
"TargetFolder": override_info["folder"],
"BaseName": base_name,
"Extension": extension_value,
"FileName": file_name,
"FullPath": full_path,
"AccBim360Path": acc_bim360_path,
"DcClsid": "",
"DcRoot": "",
"MatchedModelPath": "",
"OverrideInput": override_info["input"],
"OverrideResolved": override_info["resolved"],
"FailReason": ""
}
# Stop and use failover when the caller supplied an override but it is invalid.
if override_info["provided"] and not override_info["valid"]:
return _build_desktop_failover(
document,
extension_value,
"Override path was supplied but could not be resolved to a valid existing folder.",
override_info["input"],
override_info["resolved"]
)
# --------------------------------------------------------
# STEP 2: FIND DESKTOP CONNECTOR CLSID
# --------------------------------------------------------
# Search HKCR\CLSID for the Autodesk Docs namespace entry.
dc_clsid = _find_desktop_connector_clsid()
# Stop and use failover if the Desktop Connector CLSID cannot be found.
if not dc_clsid:
return _build_desktop_failover(
document,
extension_value,
"Desktop Connector CLSID was not found under HKCR\\CLSID.",
override_info["input"],
override_info["resolved"]
)
# --------------------------------------------------------
# STEP 3: READ DESKTOP CONNECTOR ROOT
# --------------------------------------------------------
# Read the Desktop Connector local root from the CLSID InitPropertyBag.
dc_root = _get_dc_root_from_clsid(dc_clsid)
# Stop and use failover when the Desktop Connector local root cannot be read.
if not dc_root:
return _build_desktop_failover(
document,
extension_value,
"Desktop Connector TargetFolderPath could not be read from the CLSID registry key.",
override_info["input"],
override_info["resolved"]
)
# --------------------------------------------------------
# STEP 4: CHECK WORKSHARING STATE
# --------------------------------------------------------
# Determine whether the current document is workshared.
workshared = _is_workshared(document)
# Stop and use failover when the current model is not workshared.
if not workshared:
return _build_desktop_failover(
document,
extension_value,
"Current document is not workshared.",
override_info["input"],
override_info["resolved"]
)
# --------------------------------------------------------
# STEP 5: FIND THE ACTUAL MODEL FILE UNDER DC ROOT
# --------------------------------------------------------
# Build likely current model file names from the document title.
doc_info = _get_current_document_name_candidates(document)
# Search for the actual current model file under the Desktop Connector root.
matches = _search_for_model_under_root(dc_root, doc_info["file_names"])
# Stop and use failover when no matching model file is found.
if not matches:
return _build_desktop_failover(
document,
extension_value,
"No matching model file was found under the Desktop Connector local root.",
override_info["input"],
override_info["resolved"]
)
# --------------------------------------------------------
# STEP 6: BUILD SUCCESS RESULT
# --------------------------------------------------------
# Use the first matching model file.
matched_model_path = matches[0]
# Use the matched model folder as the export target folder.
target_folder = os.path.dirname(matched_model_path)
# Strip the model file extension to derive the final base name.
base_name = _sanitize_filename(
_strip_revit_extension(os.path.basename(matched_model_path)),
doc_info["base_name"]
)
# Build the export file name and full export path.
file_name = base_name + extension_value
full_path = os.path.join(target_folder, file_name)
# Return the successful Desktop Connector result.
return {
"Source": "DesktopConnector",
"IsWorkshared": workshared,
"DesktopFolder": _get_desktop_folder(),
"TargetFolder": target_folder,
"BaseName": base_name,
"Extension": extension_value,
"FileName": file_name,
"FullPath": full_path,
"AccBim360Path": acc_bim360_path,
"DcClsid": dc_clsid,
"DcRoot": dc_root,
"MatchedModelPath": matched_model_path,
"OverrideInput": override_info["input"],
"OverrideResolved": override_info["resolved"],
"FailReason": ""
}
------------------------------------------------------------
EXECUTION
------------------------------------------------------------
Read the desired export extension from Dynamo input.
requested_extension = IN[0] if len(IN) > 0 else None
Read the optional override path from Dynamo input.
override_path = IN[1] if len(IN) > 1 else None
Resolve the final export path using the stop-on-failure flow.
result = resolve_export_path(doc, requested_extension, override_path)
Build the plain-text details block for copy/paste or diagnostics.
details_text = _to_export_text(result)
------------------------------------------------------------
OUTPUT
------------------------------------------------------------
OUT[0] = final target file path
OUT[1] = one-item list containing the multi-line details text
OUT = [result[“FullPath”], [details_text]]