335 lines
10 KiB
Python
335 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Inventory loader for Bubble Tea examples.
|
|
Loads and parses CONTEXTUAL-INVENTORY.md from charm-examples-inventory.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
from typing import Dict, List, Optional, Tuple
|
|
from pathlib import Path
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class InventoryLoadError(Exception):
|
|
"""Raised when inventory cannot be loaded."""
|
|
pass
|
|
|
|
|
|
class Example:
|
|
"""Represents a single Bubble Tea example."""
|
|
|
|
def __init__(self, name: str, file_path: str, capability: str):
|
|
self.name = name
|
|
self.file_path = file_path
|
|
self.capability = capability
|
|
self.key_patterns: List[str] = []
|
|
self.components: List[str] = []
|
|
self.use_cases: List[str] = []
|
|
|
|
def __repr__(self):
|
|
return f"Example({self.name}, {self.capability})"
|
|
|
|
|
|
class Inventory:
|
|
"""Bubble Tea examples inventory."""
|
|
|
|
def __init__(self, base_path: str):
|
|
self.base_path = base_path
|
|
self.examples: Dict[str, Example] = {}
|
|
self.capabilities: Dict[str, List[Example]] = {}
|
|
self.components: Dict[str, List[Example]] = {}
|
|
|
|
def add_example(self, example: Example):
|
|
"""Add example to inventory."""
|
|
self.examples[example.name] = example
|
|
|
|
# Index by capability
|
|
if example.capability not in self.capabilities:
|
|
self.capabilities[example.capability] = []
|
|
self.capabilities[example.capability].append(example)
|
|
|
|
# Index by components
|
|
for component in example.components:
|
|
if component not in self.components:
|
|
self.components[component] = []
|
|
self.components[component].append(example)
|
|
|
|
def search_by_keyword(self, keyword: str) -> List[Example]:
|
|
"""Search examples by keyword in name or patterns."""
|
|
keyword_lower = keyword.lower()
|
|
results = []
|
|
|
|
for example in self.examples.values():
|
|
if keyword_lower in example.name.lower():
|
|
results.append(example)
|
|
continue
|
|
|
|
for pattern in example.key_patterns:
|
|
if keyword_lower in pattern.lower():
|
|
results.append(example)
|
|
break
|
|
|
|
return results
|
|
|
|
def get_by_capability(self, capability: str) -> List[Example]:
|
|
"""Get all examples for a capability."""
|
|
return self.capabilities.get(capability, [])
|
|
|
|
def get_by_component(self, component: str) -> List[Example]:
|
|
"""Get all examples using a component."""
|
|
return self.components.get(component, [])
|
|
|
|
|
|
def load_inventory(inventory_path: Optional[str] = None) -> Inventory:
|
|
"""
|
|
Load Bubble Tea examples inventory from CONTEXTUAL-INVENTORY.md.
|
|
|
|
Args:
|
|
inventory_path: Path to charm-examples-inventory directory
|
|
If None, tries to find it automatically
|
|
|
|
Returns:
|
|
Loaded Inventory object
|
|
|
|
Raises:
|
|
InventoryLoadError: If inventory cannot be loaded
|
|
|
|
Example:
|
|
>>> inv = load_inventory("/path/to/charm-examples-inventory")
|
|
>>> examples = inv.search_by_keyword("progress")
|
|
"""
|
|
if inventory_path is None:
|
|
inventory_path = _find_inventory_path()
|
|
|
|
inventory_file = Path(inventory_path) / "bubbletea" / "examples" / "CONTEXTUAL-INVENTORY.md"
|
|
|
|
if not inventory_file.exists():
|
|
raise InventoryLoadError(
|
|
f"Inventory file not found: {inventory_file}\n"
|
|
f"Expected at: {inventory_path}/bubbletea/examples/CONTEXTUAL-INVENTORY.md"
|
|
)
|
|
|
|
logger.info(f"Loading inventory from: {inventory_file}")
|
|
|
|
with open(inventory_file, 'r') as f:
|
|
content = f.read()
|
|
|
|
inventory = parse_inventory_markdown(content, str(inventory_path))
|
|
|
|
logger.info(f"Loaded {len(inventory.examples)} examples")
|
|
logger.info(f"Categories: {len(inventory.capabilities)}")
|
|
|
|
return inventory
|
|
|
|
|
|
def parse_inventory_markdown(content: str, base_path: str) -> Inventory:
|
|
"""
|
|
Parse CONTEXTUAL-INVENTORY.md markdown content.
|
|
|
|
Args:
|
|
content: Markdown content
|
|
base_path: Base path for example files
|
|
|
|
Returns:
|
|
Inventory object with parsed examples
|
|
"""
|
|
inventory = Inventory(base_path)
|
|
|
|
# Parse quick reference table
|
|
table_matches = re.finditer(
|
|
r'\|\s*(.+?)\s*\|\s*`(.+?)`\s*\|',
|
|
content
|
|
)
|
|
|
|
need_to_file = {}
|
|
for match in table_matches:
|
|
need = match.group(1).strip()
|
|
file_path = match.group(2).strip()
|
|
need_to_file[need] = file_path
|
|
|
|
# Parse detailed sections (## Examples by Capability)
|
|
capability_pattern = r'### (.+?)\n\n\*\*Use (.+?) when you need:\*\*(.+?)(?=\n\n\*\*|### |\Z)'
|
|
|
|
capability_sections = re.finditer(capability_pattern, content, re.DOTALL)
|
|
|
|
for section in capability_sections:
|
|
capability = section.group(1).strip()
|
|
example_name = section.group(2).strip()
|
|
description = section.group(3).strip()
|
|
|
|
# Extract file path and key patterns
|
|
file_match = re.search(r'\*\*File\*\*: `(.+?)`', description)
|
|
patterns_match = re.search(r'\*\*Key patterns\*\*: (.+?)(?=\n|$)', description)
|
|
|
|
if file_match:
|
|
file_path = file_match.group(1).strip()
|
|
example = Example(example_name, file_path, capability)
|
|
|
|
if patterns_match:
|
|
patterns_text = patterns_match.group(1).strip()
|
|
example.key_patterns = [p.strip() for p in patterns_text.split(',')]
|
|
|
|
# Extract components from file name and patterns
|
|
example.components = _extract_components(example_name, example.key_patterns)
|
|
|
|
inventory.add_example(example)
|
|
|
|
return inventory
|
|
|
|
|
|
def _extract_components(name: str, patterns: List[str]) -> List[str]:
|
|
"""Extract component names from example name and patterns."""
|
|
components = []
|
|
|
|
# Common component keywords
|
|
component_keywords = [
|
|
'textinput', 'textarea', 'viewport', 'table', 'list', 'pager',
|
|
'paginator', 'spinner', 'progress', 'timer', 'stopwatch',
|
|
'filepicker', 'help', 'tabs', 'autocomplete'
|
|
]
|
|
|
|
name_lower = name.lower()
|
|
for keyword in component_keywords:
|
|
if keyword in name_lower:
|
|
components.append(keyword)
|
|
|
|
for pattern in patterns:
|
|
pattern_lower = pattern.lower()
|
|
for keyword in component_keywords:
|
|
if keyword in pattern_lower and keyword not in components:
|
|
components.append(keyword)
|
|
|
|
return components
|
|
|
|
|
|
def _find_inventory_path() -> str:
|
|
"""
|
|
Try to find charm-examples-inventory automatically.
|
|
|
|
Searches in common locations:
|
|
- ./charm-examples-inventory
|
|
- ../charm-examples-inventory
|
|
- ~/charmtuitemplate/vinw/charm-examples-inventory
|
|
|
|
Returns:
|
|
Path to inventory directory
|
|
|
|
Raises:
|
|
InventoryLoadError: If not found
|
|
"""
|
|
search_paths = [
|
|
Path.cwd() / "charm-examples-inventory",
|
|
Path.cwd().parent / "charm-examples-inventory",
|
|
Path.home() / "charmtuitemplate" / "vinw" / "charm-examples-inventory"
|
|
]
|
|
|
|
for path in search_paths:
|
|
if (path / "bubbletea" / "examples" / "CONTEXTUAL-INVENTORY.md").exists():
|
|
logger.info(f"Found inventory at: {path}")
|
|
return str(path)
|
|
|
|
raise InventoryLoadError(
|
|
"Could not find charm-examples-inventory automatically.\n"
|
|
f"Searched: {[str(p) for p in search_paths]}\n"
|
|
"Please provide inventory_path parameter."
|
|
)
|
|
|
|
|
|
def build_capability_index(inventory: Inventory) -> Dict[str, List[str]]:
|
|
"""
|
|
Build index of capabilities to example names.
|
|
|
|
Args:
|
|
inventory: Loaded inventory
|
|
|
|
Returns:
|
|
Dict mapping capability names to example names
|
|
"""
|
|
index = {}
|
|
for capability, examples in inventory.capabilities.items():
|
|
index[capability] = [ex.name for ex in examples]
|
|
return index
|
|
|
|
|
|
def build_component_index(inventory: Inventory) -> Dict[str, List[str]]:
|
|
"""
|
|
Build index of components to example names.
|
|
|
|
Args:
|
|
inventory: Loaded inventory
|
|
|
|
Returns:
|
|
Dict mapping component names to example names
|
|
"""
|
|
index = {}
|
|
for component, examples in inventory.components.items():
|
|
index[component] = [ex.name for ex in examples]
|
|
return index
|
|
|
|
|
|
def get_example_details(inventory: Inventory, example_name: str) -> Optional[Example]:
|
|
"""
|
|
Get detailed information about a specific example.
|
|
|
|
Args:
|
|
inventory: Loaded inventory
|
|
example_name: Name of example to look up
|
|
|
|
Returns:
|
|
Example object or None if not found
|
|
"""
|
|
return inventory.examples.get(example_name)
|
|
|
|
|
|
def main():
|
|
"""Test inventory loader."""
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
print("Testing Inventory Loader\n" + "=" * 50)
|
|
|
|
try:
|
|
# Load inventory
|
|
print("\n1. Loading inventory...")
|
|
inventory = load_inventory()
|
|
print(f"✓ Loaded {len(inventory.examples)} examples")
|
|
print(f"✓ {len(inventory.capabilities)} capability categories")
|
|
|
|
# Test search
|
|
print("\n2. Testing keyword search...")
|
|
results = inventory.search_by_keyword("progress")
|
|
print(f"✓ Found {len(results)} examples for 'progress':")
|
|
for ex in results[:3]:
|
|
print(f" - {ex.name} ({ex.capability})")
|
|
|
|
# Test capability lookup
|
|
print("\n3. Testing capability lookup...")
|
|
cap_examples = inventory.get_by_capability("Installation and Progress Tracking")
|
|
print(f"✓ Found {len(cap_examples)} installation examples")
|
|
|
|
# Test component lookup
|
|
print("\n4. Testing component lookup...")
|
|
comp_examples = inventory.get_by_component("spinner")
|
|
print(f"✓ Found {len(comp_examples)} examples using 'spinner'")
|
|
|
|
# Test indices
|
|
print("\n5. Building indices...")
|
|
cap_index = build_capability_index(inventory)
|
|
comp_index = build_component_index(inventory)
|
|
print(f"✓ Capability index: {len(cap_index)} categories")
|
|
print(f"✓ Component index: {len(comp_index)} components")
|
|
|
|
print("\n✅ All tests passed!")
|
|
|
|
except InventoryLoadError as e:
|
|
print(f"\n❌ Error loading inventory: {e}")
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
exit(main())
|