| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- """
- Base classes and utilities for CLI modules and commands.
- Provides common functionality and patterns for all modules.
- """
- import logging
- from abc import ABC, abstractmethod
- from pathlib import Path
- from typing import Optional, Set, Dict, Any, List, Tuple
- from rich.console import Console
- import typer
- from .config import ConfigManager
- from .helpers import find_boilerplates
- from . import template, values, render
- class BaseModule(ABC):
- """Abstract base class for all CLI modules with shared commands."""
-
- def __init__(self, name: str, icon: str = "", description: str = ""):
- self.name = name
- self.icon = icon
- self.description = description
- self.console = Console()
- self.logger = logging.getLogger(f"boilerplates.module.{name}")
-
- @property
- def template_paths(self) -> List[str]:
- """Return list of valid template file paths/patterns for this module.
- Override this in modules that support template generation."""
- return []
-
- @property
- def library_path(self) -> Optional[Path]:
- """Return the path to the template library for this module.
- Override this in modules that support template generation."""
- return None
-
- @property
- def variable_handler_class(self) -> Any:
- """Return the variable handler class for this module."""
- return None
-
- def get_valid_variables(self) -> Set[str]:
- """Get the set of valid variable names for this module."""
- if self.variable_handler_class:
- handler = self.variable_handler_class()
- return set(handler._declared.keys())
- return set()
-
- def process_template_content(self, content: str) -> str:
- """Process template content before rendering. Override if needed."""
- return content
-
- def get_template_syntax(self) -> str:
- """Return the syntax highlighting to use for this template type."""
- return "yaml"
-
- def get_app(self) -> typer.Typer:
- """
- Create and return the Typer app with shared commands.
- Subclasses can override this to add module-specific commands.
- """
- app = typer.Typer(
- name=self.name,
- help=f"{self.icon} {self.description}",
- rich_markup_mode="rich"
- )
-
- # Add shared config commands
- self._add_config_commands(app)
-
- # Add module-specific commands
- self._add_module_commands(app)
-
- return app
-
- def _add_config_commands(self, app: typer.Typer) -> None:
- """
- Add shared configuration commands to the app.
- These commands are available for all modules.
- """
- config_app = typer.Typer(name="config", help="Manage module configuration")
- app.add_typer(config_app, name="config")
-
- @config_app.command("set", help="Set a configuration value")
- def set_config(
- key: str = typer.Argument(..., help="Configuration key"),
- value: str = typer.Argument(..., help="Configuration value")
- ):
- """Set a configuration value for this module."""
- # Validate that the key is a valid variable for this module
- valid_vars = self.get_valid_variables()
- if valid_vars and key not in valid_vars:
- self.console.print(f"[red]✗[/red] Invalid config key '{key}'. Valid keys are: {', '.join(sorted(valid_vars))}")
- raise typer.Exit(code=1)
-
- config_manager = ConfigManager(self.name)
- try:
- # Try to parse as JSON for complex values
- import json
- try:
- parsed_value = json.loads(value)
- except json.JSONDecodeError:
- parsed_value = value
-
- config_manager.set(key, parsed_value)
- self.console.print(f"[green]✓[/green] Set {self.name} config '{key}' = {parsed_value}")
- except Exception as e:
- self.console.print(f"[red]✗[/red] Failed to set config: {e}")
-
- @config_app.command("get", help="Get a configuration value")
- def get_config(
- key: str = typer.Argument(..., help="Configuration key"),
- default: Optional[str] = typer.Option(None, "--default", "-d", help="Default value if key not found")
- ):
- """Get a configuration value for this module."""
- config_manager = ConfigManager(self.name)
- value = config_manager.get(key, default)
- if value is None:
- self.console.print(f"[yellow]⚠[/yellow] Config key '{key}' not found")
- return
-
- import json
- if isinstance(value, (dict, list)):
- self.console.print(json.dumps(value, indent=2))
- else:
- self.console.print(f"{key}: {value}")
-
- @config_app.command("list", help="List all configuration values")
- def list_config():
- """List all configuration values for this module."""
- config_manager = ConfigManager(self.name)
- config = config_manager.list_all()
- if not config:
- self.console.print(f"[yellow]No configuration found for {self.name}[/yellow]")
- return
-
- from rich.table import Table
- table = Table(title=f"⚙️ {self.name.title()} Configuration", title_style="bold blue")
- table.add_column("Key", style="cyan", no_wrap=True)
- table.add_column("Value", style="green")
-
- import json
- for key, value in config.items():
- if isinstance(value, (dict, list)):
- value_str = json.dumps(value, indent=2)
- else:
- value_str = str(value)
- table.add_row(key, value_str)
-
- self.console.print(table)
-
- @config_app.command("delete", help="Delete a configuration value")
- def delete_config(key: str = typer.Argument(..., help="Configuration key")):
- """Delete a configuration value for this module."""
- config_manager = ConfigManager(self.name)
- if config_manager.delete(key):
- self.console.print(f"[green]✓[/green] Deleted config key '{key}'")
- else:
- self.console.print(f"[yellow]⚠[/yellow] Config key '{key}' not found")
-
- @config_app.command("variables", help="List valid configuration variables for this module")
- def list_variables():
- """List all valid configuration variables for this module."""
- valid_vars = self.get_valid_variables()
- if not valid_vars:
- self.console.print(f"[yellow]No variables defined for {self.name} module yet.[/yellow]")
- return
-
- from rich.table import Table
- table = Table(title=f"🔧 Valid {self.name.title()} Variables", title_style="bold blue")
- table.add_column("Variable Name", style="cyan", no_wrap=True)
- table.add_column("Set", style="magenta")
- table.add_column("Type", style="green")
- table.add_column("Description", style="dim")
-
- # Get detailed variable information
- if hasattr(self, '_get_variable_details'):
- var_details = self._get_variable_details()
- for var_name in sorted(valid_vars):
- if var_name in var_details:
- detail = var_details[var_name]
- table.add_row(
- var_name,
- detail.get('set', 'unknown'),
- detail.get('type', 'str'),
- detail.get('display_name', '')
- )
- else:
- table.add_row(var_name, 'unknown', 'str', '')
- else:
- for var_name in sorted(valid_vars):
- table.add_row(var_name, 'unknown', 'str', '')
-
- self.console.print(table)
-
- def _add_module_commands(self, app: typer.Typer) -> None:
- """Add module-specific commands to the app."""
- # Only add generate command if module supports templates
- if self.library_path is not None and self.template_paths:
- self._add_generate_command(app)
- self._add_custom_commands(app)
-
- def _add_custom_commands(self, app: typer.Typer) -> None:
- """Override this method in subclasses to add module-specific commands."""
- pass
-
- def _add_generate_command(self, app: typer.Typer) -> None:
- """Add the generate command to the app."""
-
- @app.command("generate", help="Generate from a template and write to --out")
- def generate(
- name: str,
- out: Optional[Path] = typer.Option(None, "--out", "-o",
- help="Output path to write rendered template (prints to stdout when omitted)"),
- values_file: Optional[Path] = typer.Option(None, "--values-file", "-f",
- help="Load values from YAML/JSON file"),
- values: Optional[List[str]] = typer.Option(None, "--values",
- help="Set values (format: key=value)")
- ):
- """Generate output from a template with optional value overrides."""
- # Find and validate template
- bps = find_boilerplates(self.library_path, self.template_paths)
- bp = next((b for b in bps if b.file_path.parent.name.lower() == name.lower()), None)
- if not bp:
- self.console.print(f"[red]Template '{name}' not found.[/red]")
- raise typer.Exit(code=1)
-
- # Get variable handler if module provides one
- var_handler = None
- if self.variable_handler_class:
- var_handler = self.variable_handler_class()
-
- # Clean and process template content
- content = self.process_template_content(bp.content)
- cleaned_content = template.clean_template_content(content)
-
- # Find variables if handler exists
- used_vars = set()
- if var_handler:
- _, used_vars = var_handler.determine_variable_sets(cleaned_content)
-
- if not used_vars:
- rendered = content
- else:
- # Validate template syntax
- is_valid, error = template.validate_template(cleaned_content, bp.file_path)
- if not is_valid:
- self.console.print(f"[red]{error}[/red]")
- raise typer.Exit(code=2)
-
- # Extract defaults and metadata if handler exists
- template_defaults = {}
- if var_handler:
- template_defaults = var_handler.extract_template_defaults(cleaned_content)
- try:
- meta_overrides = var_handler.extract_variable_meta_overrides(content)
- for var_name, overrides in meta_overrides.items():
- if var_name in var_handler._declared and isinstance(overrides, dict):
- existing = var_handler._declared[var_name][1]
- existing.update(overrides)
- except Exception:
- pass
-
- # Get subscript keys and load values from all sources
- used_subscripts = set()
- if var_handler:
- used_subscripts = var_handler.find_used_subscript_keys(content)
-
- # Load and merge values from all sources
- try:
- merged_values = values.load_and_merge_values(
- values_file=values_file,
- cli_values=values,
- config_values=ConfigManager(self.name).list_all(),
- defaults=template_defaults
- )
- except Exception as e:
- self.console.print(f"[red]{str(e)}[/red]")
- raise typer.Exit(code=1)
-
- # Collect final values and render template
- values_dict = {}
- if var_handler:
- values_dict = var_handler.collect_values(
- used_vars,
- merged_values,
- used_subscripts
- )
- else:
- values_dict = merged_values
-
- success, rendered, error = template.render_template(
- cleaned_content,
- values_dict
- )
-
- if not success:
- self.console.print(f"[red]{error}[/red]")
- raise typer.Exit(code=2)
-
- # Output the rendered content
- output_handler = render.RenderOutput(self.console)
- output_handler.output_rendered_content(
- rendered,
- out,
- self.get_template_syntax(),
- bp.name
- )
|