runbooks 0.9.8__py3-none-any.whl → 0.9.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- runbooks/__init__.py +1 -1
- runbooks/common/rich_utils.py +3 -0
- runbooks/finops/markdown_exporter.py +226 -0
- runbooks/finops/optimizer.py +2 -0
- runbooks/finops/single_dashboard.py +2 -2
- runbooks/finops/vpc_cleanup_exporter.py +328 -0
- runbooks/finops/vpc_cleanup_optimizer.py +536 -35
- runbooks/main.py +315 -7
- runbooks/operate/vpc_operations.py +1 -1
- runbooks/vpc/unified_scenarios.py +3199 -0
- runbooks/vpc/vpc_cleanup_integration.py +4 -4
- {runbooks-0.9.8.dist-info → runbooks-0.9.9.dist-info}/METADATA +1 -1
- {runbooks-0.9.8.dist-info → runbooks-0.9.9.dist-info}/RECORD +17 -15
- {runbooks-0.9.8.dist-info → runbooks-0.9.9.dist-info}/WHEEL +0 -0
- {runbooks-0.9.8.dist-info → runbooks-0.9.9.dist-info}/entry_points.txt +0 -0
- {runbooks-0.9.8.dist-info → runbooks-0.9.9.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.8.dist-info → runbooks-0.9.9.dist-info}/top_level.txt +0 -0
runbooks/main.py
CHANGED
@@ -6129,7 +6129,15 @@ def finops(
|
|
6129
6129
|
profile=profile_str
|
6130
6130
|
)
|
6131
6131
|
|
6132
|
-
|
6132
|
+
# Check if we have context from VPC analyze command for filtering
|
6133
|
+
filter_options = ctx.obj.get('vpc_analyze_options', {})
|
6134
|
+
no_eni_only = filter_options.get('no_eni_only', False)
|
6135
|
+
filter_type = filter_options.get('filter', 'all')
|
6136
|
+
|
6137
|
+
vpc_result = vpc_optimizer.analyze_vpc_cleanup_opportunities(
|
6138
|
+
no_eni_only=no_eni_only,
|
6139
|
+
filter_type=filter_type
|
6140
|
+
)
|
6133
6141
|
|
6134
6142
|
# Convert to legacy format for backward compatibility
|
6135
6143
|
results = {
|
@@ -7356,34 +7364,285 @@ def vpc(ctx):
|
|
7356
7364
|
@click.option("--vpc-ids", multiple=True, help="Specific VPC IDs to analyze (space-separated)")
|
7357
7365
|
@click.option("--output-dir", default="./awso_evidence", help="Output directory for evidence")
|
7358
7366
|
@click.option("--generate-evidence", is_flag=True, default=True, help="Generate AWSO-05 evidence bundle")
|
7359
|
-
@click.
|
7360
|
-
|
7367
|
+
@click.option("--markdown", is_flag=True, help="Export markdown table")
|
7368
|
+
@click.option("--csv", is_flag=True, help="Export CSV data")
|
7369
|
+
@click.option("--json", is_flag=True, help="Export JSON data")
|
7370
|
+
@click.option("--pdf", is_flag=True, help="Export PDF report")
|
7371
|
+
@click.option("--all", is_flag=True, help="Analyze all accounts")
|
7372
|
+
@click.option("--no-eni-only", is_flag=True, help="Show only VPCs with zero ENI attachments")
|
7373
|
+
@click.option("--filter", type=click.Choice(['none', 'default', 'all']), default='all',
|
7374
|
+
help="Filter VPCs: none=no resources, default=default VPCs only, all=show all")
|
7375
|
+
@click.pass_context
|
7376
|
+
def analyze(ctx, profile, region, dry_run, vpc_ids, output_dir, generate_evidence, markdown, csv, json, pdf, all, no_eni_only, filter):
|
7361
7377
|
"""
|
7362
|
-
🔍 Comprehensive VPC analysis with AWSO-05 integration
|
7378
|
+
🔍 Comprehensive VPC analysis with AWSO-05 integration + Enhanced Export Options
|
7363
7379
|
|
7364
7380
|
Migrated from VPC module with enhanced capabilities:
|
7365
7381
|
- Complete VPC topology discovery
|
7366
7382
|
- 12-step AWSO-05 dependency analysis
|
7367
7383
|
- ENI gate validation for workload protection
|
7368
7384
|
- Evidence bundle generation for compliance
|
7385
|
+
- Enhanced filtering for safety-first cleanup
|
7386
|
+
- Multi-format exports (markdown, CSV, JSON, PDF)
|
7369
7387
|
|
7370
7388
|
Examples:
|
7371
|
-
runbooks vpc analyze --profile prod
|
7389
|
+
runbooks vpc analyze --profile prod --markdown
|
7390
|
+
runbooks vpc analyze --no-eni-only --profile prod --markdown
|
7391
|
+
runbooks vpc analyze --filter=none --profile prod --csv --json
|
7392
|
+
runbooks vpc analyze --filter=default --all --profile prod --markdown
|
7372
7393
|
runbooks vpc analyze --vpc-ids vpc-123 vpc-456 --generate-evidence
|
7373
|
-
runbooks vpc analyze --output-dir ./custom_evidence
|
7374
7394
|
"""
|
7395
|
+
# Fix profile tuple handling like other commands (lines 5567-5568 pattern)
|
7396
|
+
active_profile = profile[0] if isinstance(profile, tuple) and profile else "default"
|
7397
|
+
|
7375
7398
|
console.print("[cyan]🔍 VPC Analysis - Enhanced with VPC Module Integration[/cyan]")
|
7399
|
+
console.print(f"[dim]Using AWS profile: {active_profile}[/dim]")
|
7376
7400
|
|
7377
7401
|
try:
|
7378
7402
|
from runbooks.operate.vpc_operations import VPCOperations
|
7379
7403
|
from runbooks.inventory.vpc_analyzer import VPCAnalyzer
|
7404
|
+
from runbooks.finops.vpc_cleanup_optimizer import VPCCleanupOptimizer
|
7405
|
+
|
7406
|
+
# Store filter options in context for potential FinOps integration
|
7407
|
+
if not ctx.obj:
|
7408
|
+
ctx.obj = {}
|
7409
|
+
ctx.obj['vpc_analyze_options'] = {
|
7410
|
+
'no_eni_only': no_eni_only,
|
7411
|
+
'filter': filter
|
7412
|
+
}
|
7413
|
+
|
7414
|
+
# Check if this is integrated with FinOps VPC cleanup scenario or markdown export requested
|
7415
|
+
if ctx.obj.get('use_vpc_cleanup_optimizer', False) or markdown:
|
7416
|
+
console.print("[yellow]🔗 Using VPC Cleanup Optimizer for enhanced analysis[/yellow]")
|
7417
|
+
|
7418
|
+
# Use VPC Cleanup Optimizer for comprehensive analysis
|
7419
|
+
vpc_cleanup_optimizer = VPCCleanupOptimizer(profile=active_profile)
|
7420
|
+
vpc_result = vpc_cleanup_optimizer.analyze_vpc_cleanup_opportunities(
|
7421
|
+
no_eni_only=no_eni_only,
|
7422
|
+
filter_type=filter
|
7423
|
+
)
|
7424
|
+
|
7425
|
+
# Display enhanced results
|
7426
|
+
console.print(f"\n✅ Enhanced VPC Cleanup Analysis Complete!")
|
7427
|
+
console.print(f"📊 Total VPCs analyzed: {vpc_result.total_vpcs_analyzed}")
|
7428
|
+
console.print(f"💰 Total annual savings potential: ${vpc_result.total_annual_savings:,.2f}")
|
7429
|
+
console.print(f"🎯 MCP validation accuracy: {vpc_result.mcp_validation_accuracy:.1f}%")
|
7430
|
+
|
7431
|
+
# Export results if requested
|
7432
|
+
if any([markdown, csv, json, pdf]):
|
7433
|
+
from runbooks.finops.vpc_cleanup_exporter import export_vpc_cleanup_results
|
7434
|
+
export_formats = []
|
7435
|
+
if markdown: export_formats.append('markdown')
|
7436
|
+
if csv: export_formats.append('csv')
|
7437
|
+
if json: export_formats.append('json')
|
7438
|
+
if pdf: export_formats.append('pdf')
|
7439
|
+
|
7440
|
+
export_vpc_cleanup_results(vpc_result, export_formats, output_dir)
|
7441
|
+
console.print(f"📁 Export complete: {len(export_formats)} formats to {output_dir}")
|
7442
|
+
|
7443
|
+
return
|
7380
7444
|
|
7381
7445
|
# Initialize VPC operations with analyzer integration
|
7382
|
-
vpc_ops = VPCOperations(profile=
|
7446
|
+
vpc_ops = VPCOperations(profile=active_profile, region=region, dry_run=dry_run)
|
7383
7447
|
|
7384
7448
|
# Convert tuple to list for VPC IDs
|
7385
7449
|
vpc_id_list = list(vpc_ids) if vpc_ids else None
|
7386
7450
|
|
7451
|
+
# ENTERPRISE ENHANCEMENT: Organization-wide discovery when --all flag is used
|
7452
|
+
if all:
|
7453
|
+
console.print(f"\n🏢 Starting organization-wide VPC discovery...")
|
7454
|
+
console.print(f"📊 Discovering accounts using Organizations API with profile: {active_profile}")
|
7455
|
+
console.print(f"🎯 Target: ≥13 VPCs across all organization accounts")
|
7456
|
+
|
7457
|
+
# Import and initialize organizations discovery
|
7458
|
+
from runbooks.inventory.organizations_discovery import run_enhanced_organizations_discovery
|
7459
|
+
import asyncio
|
7460
|
+
|
7461
|
+
# CRITICAL FIX: Use proper profile mapping for Organizations discovery
|
7462
|
+
# Different profiles have different access capabilities
|
7463
|
+
from runbooks.common.profile_utils import get_profile_for_operation
|
7464
|
+
|
7465
|
+
# Map profiles based on their capabilities
|
7466
|
+
management_profile = get_profile_for_operation("management", active_profile)
|
7467
|
+
billing_profile = get_profile_for_operation("billing", active_profile)
|
7468
|
+
operational_profile = get_profile_for_operation("operational", active_profile)
|
7469
|
+
|
7470
|
+
console.print(f"🔐 Organization discovery profile mapping:")
|
7471
|
+
console.print(f" Management: {management_profile}")
|
7472
|
+
console.print(f" Billing: {billing_profile}")
|
7473
|
+
console.print(f" Operational: {operational_profile}")
|
7474
|
+
|
7475
|
+
# Run organization discovery with proper profile mapping
|
7476
|
+
org_discovery_result = asyncio.run(
|
7477
|
+
run_enhanced_organizations_discovery(
|
7478
|
+
management_profile=management_profile, # Organizations API access
|
7479
|
+
billing_profile=billing_profile, # Cost Explorer access
|
7480
|
+
operational_profile=operational_profile, # Resource operations
|
7481
|
+
single_account_profile=active_profile, # Original profile for fallback
|
7482
|
+
performance_target_seconds=45.0
|
7483
|
+
)
|
7484
|
+
)
|
7485
|
+
|
7486
|
+
if org_discovery_result.get("status") != "success":
|
7487
|
+
error_msg = org_discovery_result.get('error', 'Unknown error')
|
7488
|
+
console.print(f"[yellow]⚠️ Organization discovery failed: {error_msg}[/yellow]")
|
7489
|
+
|
7490
|
+
# FALLBACK: If organization discovery fails, analyze single account only
|
7491
|
+
console.print(f"[cyan]🔄 Fallback: Analyzing single account with profile {active_profile}[/cyan]")
|
7492
|
+
console.print(f"[cyan]ℹ️ Note: For multi-account analysis, use a profile with Organizations API access[/cyan]")
|
7493
|
+
|
7494
|
+
# Create single-account context for VPC analysis
|
7495
|
+
try:
|
7496
|
+
import boto3
|
7497
|
+
session = boto3.Session(profile_name=active_profile)
|
7498
|
+
sts_client = session.client('sts')
|
7499
|
+
identity = sts_client.get_caller_identity()
|
7500
|
+
current_account = identity.get('Account')
|
7501
|
+
|
7502
|
+
accounts = {current_account: {'name': f'Account {current_account}'}}
|
7503
|
+
account_ids = [current_account]
|
7504
|
+
|
7505
|
+
console.print(f"✅ Single account analysis: {current_account}")
|
7506
|
+
|
7507
|
+
except Exception as e:
|
7508
|
+
console.print(f"[red]❌ Failed to determine current account: {e}[/red]")
|
7509
|
+
raise click.ClickException("Cannot determine target accounts for analysis")
|
7510
|
+
|
7511
|
+
else:
|
7512
|
+
# Extract account IDs from successful discovery results
|
7513
|
+
accounts = org_discovery_result.get("accounts", {})
|
7514
|
+
account_ids = list(accounts.keys()) if accounts else []
|
7515
|
+
|
7516
|
+
console.print(f"✅ Discovered {len(account_ids)} organization accounts")
|
7517
|
+
console.print(f"🔍 Starting VPC analysis across all accounts...")
|
7518
|
+
|
7519
|
+
# Initialize VPC cleanup optimizer for multi-account analysis
|
7520
|
+
from runbooks.finops.vpc_cleanup_optimizer import VPCCleanupOptimizer
|
7521
|
+
from runbooks.common.rich_utils import create_progress_bar
|
7522
|
+
|
7523
|
+
total_vpcs_found = 0
|
7524
|
+
all_vpc_results = []
|
7525
|
+
|
7526
|
+
# Analyze VPCs across accounts using enhanced discovery
|
7527
|
+
with create_progress_bar() as progress:
|
7528
|
+
task = progress.add_task("Analyzing VPCs across accounts...", total=len(account_ids))
|
7529
|
+
|
7530
|
+
# CRITICAL FIX: Use multi-account VPC discovery instead of per-account optimization
|
7531
|
+
# Initialize enhanced VPC discovery that leverages Organizations discovery results
|
7532
|
+
from runbooks.finops.vpc_cleanup_optimizer import VPCCleanupOptimizer
|
7533
|
+
vpc_cleanup_optimizer = VPCCleanupOptimizer(profile=active_profile)
|
7534
|
+
|
7535
|
+
# Enhanced discovery: Pass account information to VPC discovery
|
7536
|
+
console.print(f"🔍 Discovering VPCs across {len(account_ids)} organization accounts...")
|
7537
|
+
|
7538
|
+
# Create enhanced multi-account VPC discovery
|
7539
|
+
try:
|
7540
|
+
# Use the organization account list for targeted discovery
|
7541
|
+
multi_account_result = vpc_cleanup_optimizer.analyze_vpc_cleanup_opportunities_multi_account(
|
7542
|
+
account_ids=account_ids,
|
7543
|
+
accounts_info=accounts, # Pass full account info from org discovery
|
7544
|
+
no_eni_only=no_eni_only,
|
7545
|
+
filter_type=filter,
|
7546
|
+
progress_callback=lambda msg: progress.update(task, description=msg)
|
7547
|
+
)
|
7548
|
+
|
7549
|
+
total_vpcs_found = multi_account_result.total_vpcs_analyzed
|
7550
|
+
all_vpc_results = [{
|
7551
|
+
'account_id': 'multi-account-analysis',
|
7552
|
+
'vpcs_found': total_vpcs_found,
|
7553
|
+
'result': multi_account_result,
|
7554
|
+
'accounts_analyzed': len(account_ids)
|
7555
|
+
}]
|
7556
|
+
|
7557
|
+
progress.update(task, advance=len(account_ids)) # Complete all accounts
|
7558
|
+
|
7559
|
+
except AttributeError:
|
7560
|
+
# Fallback: If multi-account method doesn't exist, use iterative approach
|
7561
|
+
console.print("[yellow]⚠️ Multi-account method not available, using iterative approach[/yellow]")
|
7562
|
+
|
7563
|
+
# Enhanced iterative approach with account context
|
7564
|
+
for account_id in account_ids:
|
7565
|
+
try:
|
7566
|
+
# ENHANCED: Pass account context to VPC discovery
|
7567
|
+
account_name = accounts.get(account_id, {}).get('name', 'Unknown')
|
7568
|
+
progress.update(task, description=f"Analyzing {account_name} ({account_id[:12]}...)")
|
7569
|
+
|
7570
|
+
# Use organization-aware VPC analysis (reads accessible VPCs only)
|
7571
|
+
account_result = vpc_cleanup_optimizer.analyze_vpc_cleanup_opportunities(
|
7572
|
+
no_eni_only=no_eni_only,
|
7573
|
+
filter_type=filter
|
7574
|
+
)
|
7575
|
+
|
7576
|
+
if hasattr(account_result, 'total_vpcs_analyzed'):
|
7577
|
+
account_vpcs = account_result.total_vpcs_analyzed
|
7578
|
+
total_vpcs_found += account_vpcs
|
7579
|
+
|
7580
|
+
if account_vpcs > 0: # Only include accounts with VPCs
|
7581
|
+
all_vpc_results.append({
|
7582
|
+
'account_id': account_id,
|
7583
|
+
'account_name': account_name,
|
7584
|
+
'vpcs_found': account_vpcs,
|
7585
|
+
'result': account_result
|
7586
|
+
})
|
7587
|
+
|
7588
|
+
progress.update(task, advance=1)
|
7589
|
+
|
7590
|
+
except Exception as e:
|
7591
|
+
console.print(f"[yellow]⚠️ Account {account_id}: {str(e)[:80]}[/yellow]")
|
7592
|
+
progress.update(task, advance=1)
|
7593
|
+
continue
|
7594
|
+
|
7595
|
+
except Exception as e:
|
7596
|
+
console.print(f"[red]❌ Multi-account analysis failed: {str(e)}[/red]")
|
7597
|
+
raise click.ClickException("Multi-account VPC discovery failed")
|
7598
|
+
|
7599
|
+
# Display organization-wide results
|
7600
|
+
console.print(f"\n✅ Organization-wide VPC Analysis Complete!")
|
7601
|
+
console.print(f"🏢 Accounts analyzed: {len(account_ids)}")
|
7602
|
+
console.print(f"🔗 Total VPCs discovered: {total_vpcs_found}")
|
7603
|
+
console.print(f"🎯 Target achievement: {'✅ ACHIEVED' if total_vpcs_found >= 13 else '⚠️ BELOW TARGET'} (≥13 VPCs)")
|
7604
|
+
|
7605
|
+
# Calculate total savings potential
|
7606
|
+
total_annual_savings = sum(
|
7607
|
+
result['result'].total_annual_savings
|
7608
|
+
for result in all_vpc_results
|
7609
|
+
if hasattr(result['result'], 'total_annual_savings')
|
7610
|
+
)
|
7611
|
+
console.print(f"💰 Total annual savings potential: ${total_annual_savings:,.2f}")
|
7612
|
+
|
7613
|
+
# Export organization-wide results if requested
|
7614
|
+
if any([markdown, csv, json, pdf]):
|
7615
|
+
console.print(f"\n📋 Exporting organization-wide results...")
|
7616
|
+
# Aggregate all results for export
|
7617
|
+
aggregated_results = {
|
7618
|
+
'organization_summary': {
|
7619
|
+
'total_accounts': len(account_ids),
|
7620
|
+
'total_vpcs_found': total_vpcs_found,
|
7621
|
+
'total_annual_savings': total_annual_savings,
|
7622
|
+
'analysis_timestamp': datetime.now().isoformat()
|
7623
|
+
},
|
7624
|
+
'account_results': all_vpc_results
|
7625
|
+
}
|
7626
|
+
|
7627
|
+
from runbooks.finops.vpc_cleanup_exporter import export_vpc_cleanup_results
|
7628
|
+
# Export organization-wide analysis
|
7629
|
+
export_formats = []
|
7630
|
+
if markdown: export_formats.append('markdown')
|
7631
|
+
if csv: export_formats.append('csv')
|
7632
|
+
if json: export_formats.append('json')
|
7633
|
+
if pdf: export_formats.append('pdf')
|
7634
|
+
|
7635
|
+
# Use the first successful result for export structure
|
7636
|
+
first_successful_result = next(
|
7637
|
+
(r['result'] for r in all_vpc_results if hasattr(r['result'], 'total_vpcs_analyzed')),
|
7638
|
+
None
|
7639
|
+
)
|
7640
|
+
if first_successful_result:
|
7641
|
+
export_vpc_cleanup_results(first_successful_result, export_formats, output_dir)
|
7642
|
+
console.print(f"📁 Organization-wide export complete: {len(export_formats)} formats to {output_dir}")
|
7643
|
+
|
7644
|
+
return
|
7645
|
+
|
7387
7646
|
console.print(f"\n🔍 Starting comprehensive VPC analysis...")
|
7388
7647
|
if vpc_id_list:
|
7389
7648
|
console.print(f"Analyzing specific VPCs: {', '.join(vpc_id_list)}")
|
@@ -7414,6 +7673,55 @@ def analyze(ctx, profile, region, dry_run, vpc_ids, output_dir, generate_evidenc
|
|
7414
7673
|
if results.get('evidence_files'):
|
7415
7674
|
console.print(f"📋 Evidence bundle: {len(results['evidence_files'])} files in {output_dir}")
|
7416
7675
|
|
7676
|
+
# Handle export flags - add VPC-specific export functionality
|
7677
|
+
if any([markdown, csv, json, pdf]):
|
7678
|
+
from datetime import datetime
|
7679
|
+
console.print(f"\n📋 Generating exports in {len([f for f in [markdown, csv, json, pdf] if f])} formats...")
|
7680
|
+
|
7681
|
+
# Check if we have VPC candidates from unified scenarios analysis
|
7682
|
+
vpc_candidates = results.get('vpc_candidates', [])
|
7683
|
+
# FIXED: Handle VPCCleanupResults object from VPCCleanupOptimizer
|
7684
|
+
if hasattr(results, 'cleanup_candidates'):
|
7685
|
+
vpc_candidates = results.cleanup_candidates
|
7686
|
+
elif 'vpc_result' in locals() and hasattr(vpc_result, 'cleanup_candidates'):
|
7687
|
+
vpc_candidates = vpc_result.cleanup_candidates
|
7688
|
+
|
7689
|
+
# Debug info for troubleshooting
|
7690
|
+
if markdown:
|
7691
|
+
console.print(f"[cyan]🔍 VPC candidates found: {len(vpc_candidates)} (type: {type(vpc_candidates)})[/cyan]")
|
7692
|
+
|
7693
|
+
if vpc_candidates and markdown:
|
7694
|
+
# Use enhanced MarkdownExporter for VPC cleanup table
|
7695
|
+
from runbooks.finops.markdown_exporter import MarkdownExporter
|
7696
|
+
|
7697
|
+
exporter = MarkdownExporter(output_dir=output_dir)
|
7698
|
+
markdown_file = exporter.export_vpc_analysis_to_file(
|
7699
|
+
vpc_candidates=vpc_candidates,
|
7700
|
+
filename=None, # Auto-generate filename
|
7701
|
+
output_dir=output_dir
|
7702
|
+
)
|
7703
|
+
console.print(f"📄 Markdown export: {markdown_file}")
|
7704
|
+
|
7705
|
+
# Handle other export formats (placeholders for future implementation)
|
7706
|
+
export_files = []
|
7707
|
+
if csv:
|
7708
|
+
csv_file = f"{output_dir}/vpc-analysis-{datetime.now().strftime('%Y-%m-%d')}.csv"
|
7709
|
+
export_files.append(csv_file)
|
7710
|
+
console.print(f"📊 CSV export: {csv_file} (to be implemented)")
|
7711
|
+
|
7712
|
+
if json:
|
7713
|
+
json_file = f"{output_dir}/vpc-analysis-{datetime.now().strftime('%Y-%m-%d')}.json"
|
7714
|
+
export_files.append(json_file)
|
7715
|
+
console.print(f"📋 JSON export: {json_file} (to be implemented)")
|
7716
|
+
|
7717
|
+
if pdf:
|
7718
|
+
pdf_file = f"{output_dir}/vpc-analysis-{datetime.now().strftime('%Y-%m-%d')}.pdf"
|
7719
|
+
export_files.append(pdf_file)
|
7720
|
+
console.print(f"📄 PDF export: {pdf_file} (to be implemented)")
|
7721
|
+
|
7722
|
+
if export_files or (vpc_candidates and markdown):
|
7723
|
+
console.print(f"✅ Export complete - files ready for executive review")
|
7724
|
+
|
7417
7725
|
except Exception as e:
|
7418
7726
|
console.print(f"[red]❌ VPC Analysis Error: {e}[/red]")
|
7419
7727
|
logger.error(f"VPC analysis failed: {e}")
|
@@ -91,7 +91,7 @@ class RichConsoleWrapper:
|
|
91
91
|
def print(self, *args, **kwargs):
|
92
92
|
return self.console.print(*args, **kwargs)
|
93
93
|
|
94
|
-
def print_panel(self, content, subtitle=None, title="Panel"):
|
94
|
+
def print_panel(self, content, subtitle=None, title="Panel", style=None):
|
95
95
|
"""Print a panel with content."""
|
96
96
|
panel_content = content
|
97
97
|
if subtitle:
|