Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,4 @@ tmp/
temp/
*.tmp
.aider*
.serena/
29 changes: 20 additions & 9 deletions core/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,22 @@


class FinOpsCLI:
"""Main CLI with plugin discovery."""
"""Main CLI coordinator that discovers and manages vendor plugins.

Automatically discovers vendor plugins via setuptools entry points,
with fallback to direct imports for development environments.
"""

def __init__(self):
self.vendors: Dict[str, Type[VendorCommands]] = {}
self._discover_vendors()

def _discover_vendors(self):
"""Discover vendor plugins via entry points."""
"""Discover vendor plugins via entry points with development fallback."""
vendors_found = False

try:
# Phase 2: Automatic discovery via entry points
# Primary method: Auto-discovery via setuptools entry points
import pkg_resources

entry_points = list(pkg_resources.iter_entry_points('open_finops.vendors'))
Expand All @@ -34,21 +38,24 @@ def _discover_vendors(self):
print(f"⚠ Failed to load vendor plugin {entry_point.name}: {e}")

except ImportError:
pass # pkg_resources not available
# pkg_resources not available (rare case)
pass

# Fallback: Manual discovery for development mode
# Development fallback: Direct import when entry points not set up
if not vendors_found:
try:
from vendors.aws.cli import AWSCommands
self.vendors['aws'] = AWSCommands
except ImportError:
pass # AWS not installed
# AWS vendor not available in this installation
pass

def run(self):
"""Run the CLI."""
"""Parse arguments and execute the appropriate vendor command."""
parser = self._create_parser()
args = parser.parse_args()

# Show help if no command specified
if not args.command:
parser.print_help()
sys.exit(0)
Expand All @@ -59,8 +66,12 @@ def run(self):
vendor_instance = vendor_class()
vendor_instance.execute(args)
else:
# Handle unknown vendor with helpful error message
print(f"Vendor '{args.command}' not available")
print(f"Available vendors: {', '.join(self.vendors.keys())}")
if self.vendors:
print(f"Available vendors: {', '.join(self.vendors.keys())}")
else:
print("No vendor plugins found. Check your installation.")
sys.exit(1)

def _create_parser(self):
Expand All @@ -75,7 +86,7 @@ def _create_parser(self):

subparsers = parser.add_subparsers(dest='command', help='Available commands')

# Let each vendor add its subcommands
# Register subcommands from each discovered vendor plugin
for name, vendor_class in self.vendors.items():
vendor_instance = vendor_class()
vendor_instance.add_subparser(subparsers)
Expand Down
110 changes: 49 additions & 61 deletions vendors/aws/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,47 +11,59 @@
from .manifest import ManifestLocator


def aws_import_cur(args):
"""Import AWS Cost and Usage Reports."""

# Load configuration
def _load_and_validate_config(args, required_aws_fields=True):
"""Load configuration, merge CLI args, and validate AWS config if needed."""
config_path = Path(args.config) if args.config else Path('config.toml')
config = Config.load(config_path)

# Merge CLI arguments into configuration
# Merge CLI arguments into configuration (only non-None values)
cli_args = {
'bucket': args.bucket,
'prefix': args.prefix,
'export_name': args.export_name,
'cur_version': args.cur_version,
'export_format': args.export_format,
'start_date': args.start_date,
'end_date': args.end_date,
'reset': args.reset,
'table_strategy': args.table_strategy
'bucket': getattr(args, 'bucket', None),
'prefix': getattr(args, 'prefix', None),
'export_name': getattr(args, 'export_name', None),
'cur_version': getattr(args, 'cur_version', None),
'export_format': getattr(args, 'export_format', None),
'start_date': getattr(args, 'start_date', None),
'end_date': getattr(args, 'end_date', None),
'reset': getattr(args, 'reset', None),
'table_strategy': getattr(args, 'table_strategy', None)
}
cli_args = {k: v for k, v in cli_args.items() if v is not None}
config.merge_cli_args(cli_args)

# Override database backend if specified via CLI
if hasattr(args, 'destination') and args.destination != 'duckdb':
# CLI override for destination
config.database.backend = args.destination

# Remove None values
cli_args = {k: v for k, v in cli_args.items() if v is not None}

# Merge with config
config.merge_cli_args(cli_args)
# Validate AWS configuration if required
if required_aws_fields:
try:
config.validate_aws_config()
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
print("\nRequired parameters can be set via:")
print(" - config.toml file")
print(" - Environment variables (OPEN_FINOPS_AWS_*)")
print(" - Command-line flags")
sys.exit(1)

return config


def _get_aws_credentials(config):
"""Extract AWS credentials dictionary from config."""
return {
'access_key_id': config.aws.access_key_id,
'secret_access_key': config.aws.secret_access_key,
'region': config.aws.region
}


def aws_import_cur(args):
"""Import AWS Cost and Usage Reports."""

# Validate we have required fields
try:
config.validate_aws_config()
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
print("\nRequired parameters can be set via:")
print(" - config.toml file")
print(" - Environment variables (OPEN_FINOPS_AWS_*)")
print(" - Command-line flags")
sys.exit(1)
# Load and validate configuration
config = _load_and_validate_config(args)

# Show configuration
print("\nAWS CUR Import Configuration:")
Expand Down Expand Up @@ -87,33 +99,11 @@ def aws_import_cur(args):
def aws_list_manifests(args):
"""List available billing periods in S3."""

# Load configuration
config_path = Path(args.config) if args.config else Path('config.toml')
config = Config.load(config_path)

# Merge CLI arguments for bucket/prefix/export_name if provided
cli_args = {
'bucket': args.bucket,
'prefix': args.prefix,
'export_name': args.export_name,
'cur_version': args.cur_version
}
cli_args = {k: v for k, v in cli_args.items() if v is not None}
config.merge_cli_args(cli_args)

# Validate we have required fields
try:
config.validate_aws_config()
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
# Load and validate configuration
config = _load_and_validate_config(args)

# Get AWS credentials
aws_creds = {
'access_key_id': config.aws.access_key_id,
'secret_access_key': config.aws.secret_access_key,
'region': config.aws.region
}
aws_creds = _get_aws_credentials(config)

# Initialize manifest locator
locator = ManifestLocator(
Expand Down Expand Up @@ -148,9 +138,8 @@ def aws_list_manifests(args):
def aws_show_state(args):
"""Show load state and version history."""

# Load configuration
config_path = Path(args.config) if args.config else Path('config.toml')
config = Config.load(config_path)
# Load configuration (don't require all AWS fields, just export_name)
config = _load_and_validate_config(args, required_aws_fields=False)

# Override export name if provided
if args.export_name:
Expand Down Expand Up @@ -229,9 +218,8 @@ def aws_show_state(args):
def aws_list_exports(args):
"""List all available exports and their tables."""

# Load configuration
config_path = Path(args.config) if args.config else Path('config.toml')
config = Config.load(config_path)
# Load configuration (don't require AWS fields for this command)
config = _load_and_validate_config(args, required_aws_fields=False)

# Set up data directory path
if config.project and config.project.data_dir:
Expand Down
Loading