| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- #!/usr/bin/env python3
- """
- Main entry point for the Boilerplates CLI application.
- This file serves as the primary executable when running the CLI.
- """
- from __future__ import annotations
- import importlib
- import logging
- import pkgutil
- import sys
- from pathlib import Path
- from typing import Optional
- from typer import Typer, Option
- from rich.console import Console
- import cli.modules
- from cli.core.registry import registry
- from cli.core import repo
- from cli import __version__
- # Using standard Python exceptions instead of custom ones
- app = Typer(
- help="CLI tool for managing infrastructure boilerplates.\n\n[dim]Easily generate, customize, and deploy templates for Docker Compose, Terraform, Kubernetes, and more.\n\n [white]Made with 💜 by [bold]Christian Lempa[/bold]",
- add_completion=True,
- rich_markup_mode="rich",
- )
- console = Console()
- def setup_logging(log_level: str = "WARNING") -> None:
- """Configure the logging system with the specified log level.
- Args:
- log_level: The logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
- Raises:
- ValueError: If the log level is invalid
- RuntimeError: If logging configuration fails
- """
- numeric_level = getattr(logging, log_level.upper(), None)
- if not isinstance(numeric_level, int):
- raise ValueError(
- f"Invalid log level '{log_level}'. Valid levels: DEBUG, INFO, WARNING, ERROR, CRITICAL"
- )
- try:
- logging.basicConfig(
- level=numeric_level,
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
- datefmt="%Y-%m-%d %H:%M:%S",
- )
- logger = logging.getLogger(__name__)
- logger.setLevel(numeric_level)
- except Exception as e:
- raise RuntimeError(f"Failed to configure logging: {e}")
- @app.callback(invoke_without_command=True)
- def main(
- version: Optional[bool] = Option(
- None,
- "--version",
- "-v",
- help="Show the application version and exit.",
- is_flag=True,
- callback=lambda v: console.print(f"boilerplates version {__version__}")
- or sys.exit(0)
- if v
- else None,
- is_eager=True,
- ),
- log_level: Optional[str] = Option(
- None,
- "--log-level",
- help="Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). If omitted, logging is disabled.",
- ),
- ) -> None:
- """CLI tool for managing infrastructure boilerplates."""
- # Disable logging by default; only enable when user provides --log-level
- if log_level:
- # Re-enable logging and configure
- logging.disable(logging.NOTSET)
- setup_logging(log_level)
- else:
- # Silence all logging (including third-party) unless user explicitly requests it
- logging.disable(logging.CRITICAL)
- # Get context without type annotation (compatible with all Typer versions)
- import click
- ctx = click.get_current_context()
- # Store log level in context for potential use by other commands
- ctx.ensure_object(dict)
- ctx.obj["log_level"] = log_level
- # If no subcommand is provided, show help and friendly intro
- if ctx.invoked_subcommand is None:
- console.print(ctx.get_help())
- sys.exit(0)
- def init_app() -> None:
- """Initialize the application by discovering and registering modules.
- Raises:
- ImportError: If critical module import operations fail
- RuntimeError: If application initialization fails
- """
- logger = logging.getLogger(__name__)
- failed_imports = []
- failed_registrations = []
- try:
- # Auto-discover and import all modules
- modules_path = Path(cli.modules.__file__).parent
- logger.debug(f"Discovering modules in {modules_path}")
- for finder, name, ispkg in pkgutil.iter_modules([str(modules_path)]):
- # Import both module files and packages (for multi-schema modules)
- if not name.startswith("_") and name != "base":
- try:
- logger.debug(
- f"Importing module: {name} ({'package' if ispkg else 'file'})"
- )
- importlib.import_module(f"cli.modules.{name}")
- except ImportError as e:
- error_info = f"Import failed for '{name}': {str(e)}"
- failed_imports.append(error_info)
- logger.warning(error_info)
- except Exception as e:
- error_info = f"Unexpected error importing '{name}': {str(e)}"
- failed_imports.append(error_info)
- logger.error(error_info)
- # Register core repo command
- try:
- logger.debug("Registering repo command")
- repo.register_cli(app)
- except Exception as e:
- error_info = f"Repo command registration failed: {str(e)}"
- failed_registrations.append(error_info)
- logger.warning(error_info)
- # Register template-based modules with app
- module_classes = list(registry.iter_module_classes())
- logger.debug(f"Registering {len(module_classes)} template-based modules")
- for name, module_cls in module_classes:
- try:
- logger.debug(f"Registering module class: {module_cls.__name__}")
- module_cls.register_cli(app)
- except Exception as e:
- error_info = (
- f"Registration failed for '{module_cls.__name__}': {str(e)}"
- )
- failed_registrations.append(error_info)
- # Log warning but don't raise exception for individual module failures
- logger.warning(error_info)
- console.print(f"[yellow]Warning:[/yellow] {error_info}")
- # If we have no modules registered at all, that's a critical error
- if not module_classes and not failed_imports:
- raise RuntimeError("No modules found to register")
- # Log summary
- successful_modules = len(module_classes) - len(failed_registrations)
- logger.info(
- f"Application initialized: {successful_modules} modules registered successfully"
- )
- if failed_imports:
- logger.info(f"Module import failures: {len(failed_imports)}")
- if failed_registrations:
- logger.info(f"Module registration failures: {len(failed_registrations)}")
- except Exception as e:
- error_details = []
- if failed_imports:
- error_details.extend(
- ["Import failures:"] + [f" - {err}" for err in failed_imports]
- )
- if failed_registrations:
- error_details.extend(
- ["Registration failures:"]
- + [f" - {err}" for err in failed_registrations]
- )
- details = "\n".join(error_details) if error_details else str(e)
- raise RuntimeError(f"Application initialization failed: {details}")
- def run() -> None:
- """Run the CLI application."""
- try:
- init_app()
- app()
- except (ValueError, RuntimeError) as e:
- # Handle configuration and initialization errors cleanly
- console.print(f"[bold red]Error:[/bold red] {e}")
- sys.exit(1)
- except ImportError as e:
- # Handle module import errors with detailed info
- console.print(f"[bold red]Module Import Error:[/bold red] {e}")
- sys.exit(1)
- except KeyboardInterrupt:
- # Handle Ctrl+C gracefully
- console.print("\n[yellow]Operation cancelled by user[/yellow]")
- sys.exit(130)
- except Exception as e:
- # Handle unexpected errors - show simplified message
- console.print(f"[bold red]Unexpected error:[/bold red] {e}")
- console.print("[dim]Use --log-level DEBUG for more details[/dim]")
- sys.exit(1)
- if __name__ == "__main__":
- run()
|