Skip to main content
Glama
gh_file_ops.py5.59 kB
""" Grasshopper File Operations Module Read, parse, and modify .gh/.ghx Grasshopper definition files .ghx files are XML-based and can be parsed directly .gh files are gzip-compressed .ghx files """ import gzip import json import xml.etree.ElementTree as ET from pathlib import Path from typing import Optional, Any from dataclasses import dataclass, asdict @dataclass class GHComponent: """Represents a component in a Grasshopper definition""" guid: str name: str nickname: str category: str subcategory: str position_x: float position_y: float instance_guid: str @dataclass class GHWire: """Represents a wire connection between components""" source_component: str source_param: str target_component: str target_param: str @dataclass class GHDefinition: """Represents a parsed Grasshopper definition""" file_path: str components: list[GHComponent] wires: list[GHWire] metadata: dict def read_gh_file(file_path: str) -> str: """Read a .gh or .ghx file and return XML content""" path = Path(file_path) if not path.exists(): raise FileNotFoundError(f"File not found: {file_path}") suffix = path.suffix.lower() if suffix == ".ghx": with open(path, "r", encoding="utf-8") as f: return f.read() elif suffix == ".gh": with gzip.open(path, "rt", encoding="utf-8") as f: return f.read() else: raise ValueError(f"Unsupported file type: {suffix}") def parse_gh_definition(file_path: str) -> GHDefinition: """Parse a Grasshopper definition file""" xml_content = read_gh_file(file_path) root = ET.fromstring(xml_content) components = [] wires = [] metadata = {} # Extract document metadata doc_header = root.find(".//DocumentHeader") if doc_header is not None: metadata["document_id"] = doc_header.get("DocumentID", "") # Parse components (Objects in GH XML) for obj in root.findall(".//Object"): try: name = obj.get("Name", "Unknown") guid = obj.get("Id", "") # Get component container container = obj.find("Container") if container is not None: nickname = container.get("NickName", name) # Get position from Attributes attrs = container.find("Attributes") pos_x, pos_y = 0.0, 0.0 if attrs is not None: bounds = attrs.get("Bounds", "") if bounds: parts = bounds.split(",") if len(parts) >= 2: pos_x = float(parts[0]) pos_y = float(parts[1]) components.append(GHComponent( guid=guid, name=name, nickname=nickname, category="", subcategory="", position_x=pos_x, position_y=pos_y, instance_guid=container.get("InstanceGuid", "") )) except Exception: continue # Parse wires/connections for wire in root.findall(".//Wire"): try: source = wire.get("Source", "") target = wire.get("Target", "") if source and target: # Format is typically "ComponentGuid/ParamIndex" src_parts = source.split("/") tgt_parts = target.split("/") wires.append(GHWire( source_component=src_parts[0] if src_parts else "", source_param=src_parts[1] if len(src_parts) > 1 else "0", target_component=tgt_parts[0] if tgt_parts else "", target_param=tgt_parts[1] if len(tgt_parts) > 1 else "0" )) except Exception: continue return GHDefinition( file_path=str(file_path), components=components, wires=wires, metadata=metadata ) def get_definition_summary(file_path: str) -> dict: """Get a summary of a Grasshopper definition""" try: definition = parse_gh_definition(file_path) # Count components by type component_counts = {} for comp in definition.components: name = comp.name component_counts[name] = component_counts.get(name, 0) + 1 return { "file": file_path, "total_components": len(definition.components), "total_wires": len(definition.wires), "component_types": component_counts, "metadata": definition.metadata } except Exception as e: return { "file": file_path, "error": str(e) } def list_components(file_path: str) -> list[dict]: """List all components in a definition""" definition = parse_gh_definition(file_path) return [asdict(c) for c in definition.components] def get_xml_content(file_path: str) -> str: """Get raw XML content of a GH file""" return read_gh_file(file_path) def find_components_by_name(file_path: str, name_pattern: str) -> list[dict]: """Find components matching a name pattern""" definition = parse_gh_definition(file_path) pattern_lower = name_pattern.lower() matches = [] for comp in definition.components: if pattern_lower in comp.name.lower() or pattern_lower in comp.nickname.lower(): matches.append(asdict(comp)) return matches

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/dongwoosuk/grasshopper-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server