feat(viz-platform): implement DMC 2.x component registry (#171)
- 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>
This commit is contained in:
262
mcp-servers/viz-platform/scripts/generate-dmc-registry.py
Normal file
262
mcp-servers/viz-platform/scripts/generate-dmc-registry.py
Normal file
@@ -0,0 +1,262 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user