- Add component_registry.py with version-locked registry loading - Create dmc_2_5.json with 39 components for DMC 2.5.1/Mantine 7 - Add generate-dmc-registry.py script for future registry generation - Update dependencies to DMC >=2.0.0 (installs 2.5.1) - Includes prop validation with typo detection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
263 lines
8.1 KiB
Python
263 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate DMC Component Registry from installed dash-mantine-components package.
|
|
|
|
This script introspects the installed DMC package and generates a JSON registry
|
|
file containing component definitions, props, types, and defaults.
|
|
|
|
Usage:
|
|
python generate-dmc-registry.py [--output registry/dmc_X_Y.json]
|
|
|
|
Requirements:
|
|
- dash-mantine-components must be installed
|
|
- Run from the mcp-servers/viz-platform directory
|
|
"""
|
|
import argparse
|
|
import inspect
|
|
import json
|
|
import sys
|
|
from datetime import date
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional, get_type_hints
|
|
|
|
|
|
def get_dmc_version() -> Optional[str]:
|
|
"""Get installed DMC version."""
|
|
try:
|
|
from importlib.metadata import version
|
|
return version('dash-mantine-components')
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def get_component_categories() -> Dict[str, List[str]]:
|
|
"""Define component categories."""
|
|
return {
|
|
"buttons": ["Button", "ActionIcon", "CopyButton", "FileButton", "UnstyledButton"],
|
|
"inputs": [
|
|
"TextInput", "PasswordInput", "NumberInput", "Textarea",
|
|
"Select", "MultiSelect", "Checkbox", "Switch", "Radio",
|
|
"Slider", "RangeSlider", "ColorInput", "ColorPicker",
|
|
"DateInput", "DatePicker", "TimeInput"
|
|
],
|
|
"navigation": ["Anchor", "Breadcrumbs", "Burger", "NavLink", "Pagination", "Stepper", "Tabs"],
|
|
"feedback": ["Alert", "Loader", "Notification", "Progress", "RingProgress", "Skeleton"],
|
|
"overlays": ["Dialog", "Drawer", "HoverCard", "Menu", "Modal", "Popover", "Tooltip"],
|
|
"typography": ["Blockquote", "Code", "Highlight", "Mark", "Text", "Title"],
|
|
"layout": [
|
|
"AppShell", "AspectRatio", "Center", "Container", "Flex",
|
|
"Grid", "Group", "Paper", "SimpleGrid", "Space", "Stack"
|
|
],
|
|
"data": [
|
|
"Accordion", "Avatar", "Badge", "Card", "Image",
|
|
"Indicator", "Kbd", "Spoiler", "Table", "ThemeIcon", "Timeline"
|
|
]
|
|
}
|
|
|
|
|
|
def extract_prop_type(prop_info: Dict[str, Any]) -> Dict[str, Any]:
|
|
"""Extract prop type information from Dash component prop."""
|
|
result = {"type": "any"}
|
|
|
|
if 'type' not in prop_info:
|
|
return result
|
|
|
|
prop_type = prop_info['type']
|
|
|
|
if isinstance(prop_type, dict):
|
|
type_name = prop_type.get('name', 'any')
|
|
|
|
# Map Dash types to JSON schema types
|
|
type_mapping = {
|
|
'string': 'string',
|
|
'number': 'number',
|
|
'bool': 'boolean',
|
|
'boolean': 'boolean',
|
|
'array': 'array',
|
|
'object': 'object',
|
|
'node': 'any',
|
|
'element': 'any',
|
|
'any': 'any',
|
|
'func': 'any',
|
|
}
|
|
|
|
result['type'] = type_mapping.get(type_name, 'any')
|
|
|
|
# Handle enums
|
|
if type_name == 'enum' and 'value' in prop_type:
|
|
values = prop_type['value']
|
|
if isinstance(values, list):
|
|
enum_values = []
|
|
for v in values:
|
|
if isinstance(v, dict) and 'value' in v:
|
|
# Remove quotes from string values
|
|
val = v['value'].strip("'\"")
|
|
enum_values.append(val)
|
|
elif isinstance(v, str):
|
|
enum_values.append(v.strip("'\""))
|
|
if enum_values:
|
|
result['enum'] = enum_values
|
|
result['type'] = 'string'
|
|
|
|
# Handle union types
|
|
elif type_name == 'union' and 'value' in prop_type:
|
|
# For unions, just mark as any for simplicity
|
|
result['type'] = 'any'
|
|
|
|
elif isinstance(prop_type, str):
|
|
result['type'] = prop_type
|
|
|
|
return result
|
|
|
|
|
|
def extract_component_props(component_class) -> Dict[str, Any]:
|
|
"""Extract props from a Dash component class."""
|
|
props = {}
|
|
|
|
# Try to get _prop_names or similar
|
|
if hasattr(component_class, '_prop_names'):
|
|
prop_names = component_class._prop_names
|
|
else:
|
|
prop_names = []
|
|
|
|
# Try to get _type attribute for prop definitions
|
|
if hasattr(component_class, '_type'):
|
|
prop_types = getattr(component_class, '_type', {})
|
|
else:
|
|
prop_types = {}
|
|
|
|
# Get default values
|
|
if hasattr(component_class, '_default_props'):
|
|
defaults = component_class._default_props
|
|
else:
|
|
defaults = {}
|
|
|
|
# Try to extract from _prop_descriptions
|
|
if hasattr(component_class, '_prop_descriptions'):
|
|
descriptions = component_class._prop_descriptions
|
|
else:
|
|
descriptions = {}
|
|
|
|
for prop_name in prop_names:
|
|
if prop_name.startswith('_'):
|
|
continue
|
|
|
|
prop_info = {}
|
|
|
|
# Get type info if available
|
|
if prop_name in prop_types:
|
|
prop_info = extract_prop_type({'type': prop_types[prop_name]})
|
|
else:
|
|
prop_info = {'type': 'any'}
|
|
|
|
# Add default if exists
|
|
if prop_name in defaults:
|
|
prop_info['default'] = defaults[prop_name]
|
|
|
|
# Add description if exists
|
|
if prop_name in descriptions:
|
|
prop_info['description'] = descriptions[prop_name]
|
|
|
|
props[prop_name] = prop_info
|
|
|
|
return props
|
|
|
|
|
|
def generate_registry() -> Dict[str, Any]:
|
|
"""Generate the component registry from installed DMC."""
|
|
try:
|
|
import dash_mantine_components as dmc
|
|
except ImportError:
|
|
print("ERROR: dash-mantine-components not installed")
|
|
print("Install with: pip install dash-mantine-components")
|
|
sys.exit(1)
|
|
|
|
version = get_dmc_version()
|
|
categories = get_component_categories()
|
|
|
|
registry = {
|
|
"version": version,
|
|
"generated": date.today().isoformat(),
|
|
"categories": categories,
|
|
"components": {}
|
|
}
|
|
|
|
# Get all components from categories
|
|
all_components = set()
|
|
for comp_list in categories.values():
|
|
all_components.update(comp_list)
|
|
|
|
# Extract props for each component
|
|
for comp_name in sorted(all_components):
|
|
if hasattr(dmc, comp_name):
|
|
comp_class = getattr(dmc, comp_name)
|
|
try:
|
|
props = extract_component_props(comp_class)
|
|
if props:
|
|
registry["components"][comp_name] = {
|
|
"description": comp_class.__doc__ or f"{comp_name} component",
|
|
"props": props
|
|
}
|
|
print(f" Extracted: {comp_name} ({len(props)} props)")
|
|
except Exception as e:
|
|
print(f" Warning: Failed to extract {comp_name}: {e}")
|
|
else:
|
|
print(f" Warning: Component not found: {comp_name}")
|
|
|
|
return registry
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Generate DMC component registry from installed package"
|
|
)
|
|
parser.add_argument(
|
|
'--output', '-o',
|
|
type=str,
|
|
help='Output file path (default: auto-generated based on version)'
|
|
)
|
|
parser.add_argument(
|
|
'--dry-run',
|
|
action='store_true',
|
|
help='Print to stdout instead of writing file'
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
print("Generating DMC Component Registry...")
|
|
print("=" * 50)
|
|
|
|
registry = generate_registry()
|
|
|
|
print("=" * 50)
|
|
print(f"Generated registry for DMC {registry['version']}")
|
|
print(f"Total components: {len(registry['components'])}")
|
|
|
|
if args.dry_run:
|
|
print(json.dumps(registry, indent=2))
|
|
return
|
|
|
|
# Determine output path
|
|
if args.output:
|
|
output_path = Path(args.output)
|
|
else:
|
|
version = registry['version']
|
|
if version:
|
|
major_minor = '_'.join(version.split('.')[:2])
|
|
output_path = Path(__file__).parent.parent / 'registry' / f'dmc_{major_minor}.json'
|
|
else:
|
|
output_path = Path(__file__).parent.parent / 'registry' / 'dmc_unknown.json'
|
|
|
|
# Create directory if needed
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Write registry
|
|
with open(output_path, 'w') as f:
|
|
json.dump(registry, indent=2, fp=f)
|
|
|
|
print(f"Registry written to: {output_path}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|