__main__.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. #!/usr/bin/env python3
  2. """
  3. Main entry point for the Boilerplates CLI application.
  4. This file serves as the primary executable when running the CLI.
  5. """
  6. from __future__ import annotations
  7. import importlib
  8. import logging
  9. import pkgutil
  10. import sys
  11. from pathlib import Path
  12. from typing import Optional
  13. from typer import Typer, Context, Option
  14. from rich.console import Console
  15. import cli.modules
  16. from cli.core.registry import registry
  17. # Using standard Python exceptions instead of custom ones
  18. # Version is automatically updated by CI/CD on release
  19. __version__ = "0.0.2"
  20. app = Typer(
  21. 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]",
  22. add_completion=True,
  23. rich_markup_mode="rich",
  24. )
  25. console = Console()
  26. def setup_logging(log_level: str = "WARNING") -> None:
  27. """Configure the logging system with the specified log level.
  28. Args:
  29. log_level: The logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
  30. Raises:
  31. ValueError: If the log level is invalid
  32. RuntimeError: If logging configuration fails
  33. """
  34. numeric_level = getattr(logging, log_level.upper(), None)
  35. if not isinstance(numeric_level, int):
  36. raise ValueError(
  37. f"Invalid log level '{log_level}'. Valid levels: DEBUG, INFO, WARNING, ERROR, CRITICAL"
  38. )
  39. try:
  40. logging.basicConfig(
  41. level=numeric_level,
  42. format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
  43. datefmt='%Y-%m-%d %H:%M:%S'
  44. )
  45. logger = logging.getLogger(__name__)
  46. logger.setLevel(numeric_level)
  47. except Exception as e:
  48. raise RuntimeError(f"Failed to configure logging: {e}")
  49. @app.callback(invoke_without_command=True)
  50. def main(
  51. ctx: Context,
  52. version: Optional[bool] = Option(
  53. None,
  54. "--version",
  55. "-v",
  56. help="Show the application version and exit.",
  57. is_flag=True,
  58. callback=lambda v: console.print(f"boilerplates version {__version__}") or sys.exit(0) if v else None,
  59. is_eager=True,
  60. ),
  61. log_level: Optional[str] = Option(
  62. None,
  63. "--log-level",
  64. help="Set the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). If omitted, logging is disabled."
  65. )
  66. ) -> None:
  67. """CLI tool for managing infrastructure boilerplates."""
  68. # Disable logging by default; only enable when user provides --log-level
  69. if log_level:
  70. # Re-enable logging and configure
  71. logging.disable(logging.NOTSET)
  72. setup_logging(log_level)
  73. else:
  74. # Silence all logging (including third-party) unless user explicitly requests it
  75. logging.disable(logging.CRITICAL)
  76. # Store log level in context for potential use by other commands
  77. ctx.ensure_object(dict)
  78. ctx.obj['log_level'] = log_level
  79. # If no subcommand is provided, show help and friendly intro
  80. if ctx.invoked_subcommand is None:
  81. console.print(ctx.get_help())
  82. sys.exit(0)
  83. def init_app() -> None:
  84. """Initialize the application by discovering and registering modules.
  85. Raises:
  86. ImportError: If critical module import operations fail
  87. RuntimeError: If application initialization fails
  88. """
  89. logger = logging.getLogger(__name__)
  90. failed_imports = []
  91. failed_registrations = []
  92. try:
  93. # Auto-discover and import all modules
  94. modules_path = Path(cli.modules.__file__).parent
  95. logger.debug(f"Discovering modules in {modules_path}")
  96. for finder, name, ispkg in pkgutil.iter_modules([str(modules_path)]):
  97. if not ispkg and not name.startswith('_') and name != 'base':
  98. try:
  99. logger.debug(f"Importing module: {name}")
  100. importlib.import_module(f"cli.modules.{name}")
  101. except ImportError as e:
  102. error_info = f"Import failed for '{name}': {str(e)}"
  103. failed_imports.append(error_info)
  104. logger.warning(error_info)
  105. except Exception as e:
  106. error_info = f"Unexpected error importing '{name}': {str(e)}"
  107. failed_imports.append(error_info)
  108. logger.error(error_info)
  109. # Register modules with app lazily
  110. module_classes = list(registry.iter_module_classes())
  111. logger.debug(f"Registering {len(module_classes)} discovered modules")
  112. for name, module_cls in module_classes:
  113. try:
  114. logger.debug(f"Registering module class: {module_cls.__name__}")
  115. module_cls.register_cli(app)
  116. except Exception as e:
  117. error_info = f"Registration failed for '{module_cls.__name__}': {str(e)}"
  118. failed_registrations.append(error_info)
  119. # Log warning but don't raise exception for individual module failures
  120. logger.warning(error_info)
  121. console.print(f"[yellow]Warning:[/yellow] {error_info}")
  122. # If we have no modules registered at all, that's a critical error
  123. if not module_classes and not failed_imports:
  124. raise RuntimeError("No modules found to register")
  125. # Log summary
  126. successful_modules = len(module_classes) - len(failed_registrations)
  127. logger.info(f"Application initialized: {successful_modules} modules registered successfully")
  128. if failed_imports:
  129. logger.info(f"Module import failures: {len(failed_imports)}")
  130. if failed_registrations:
  131. logger.info(f"Module registration failures: {len(failed_registrations)}")
  132. except Exception as e:
  133. error_details = []
  134. if failed_imports:
  135. error_details.extend(["Import failures:"] + [f" - {err}" for err in failed_imports])
  136. if failed_registrations:
  137. error_details.extend(["Registration failures:"] + [f" - {err}" for err in failed_registrations])
  138. details = "\n".join(error_details) if error_details else str(e)
  139. raise RuntimeError(f"Application initialization failed: {details}")
  140. def run() -> None:
  141. """Run the CLI application."""
  142. try:
  143. init_app()
  144. app()
  145. except (ValueError, RuntimeError) as e:
  146. # Handle configuration and initialization errors cleanly
  147. console.print(f"[bold red]Error:[/bold red] {e}")
  148. sys.exit(1)
  149. except ImportError as e:
  150. # Handle module import errors with detailed info
  151. console.print(f"[bold red]Module Import Error:[/bold red] {e}")
  152. sys.exit(1)
  153. except KeyboardInterrupt:
  154. # Handle Ctrl+C gracefully
  155. console.print("\n[yellow]Operation cancelled by user[/yellow]")
  156. sys.exit(130)
  157. except Exception as e:
  158. # Handle unexpected errors - show simplified message
  159. console.print(f"[bold red]Unexpected error:[/bold red] {e}")
  160. console.print("[dim]Use --log-level DEBUG for more details[/dim]")
  161. sys.exit(1)
  162. if __name__ == "__main__":
  163. run()