runbooks 1.0.0__py3-none-any.whl → 1.0.2__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.
Files changed (99) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/cfat/WEIGHT_CONFIG_README.md +368 -0
  3. runbooks/cfat/app.ts +27 -19
  4. runbooks/cfat/assessment/runner.py +6 -5
  5. runbooks/cfat/tests/test_weight_configuration.ts +449 -0
  6. runbooks/cfat/weight_config.ts +574 -0
  7. runbooks/cloudops/models.py +20 -14
  8. runbooks/common/__init__.py +26 -9
  9. runbooks/common/aws_pricing.py +1070 -105
  10. runbooks/common/aws_pricing_api.py +276 -44
  11. runbooks/common/date_utils.py +115 -0
  12. runbooks/common/dry_run_examples.py +587 -0
  13. runbooks/common/dry_run_framework.py +520 -0
  14. runbooks/common/enhanced_exception_handler.py +10 -7
  15. runbooks/common/mcp_cost_explorer_integration.py +5 -4
  16. runbooks/common/memory_optimization.py +533 -0
  17. runbooks/common/performance_optimization_engine.py +1153 -0
  18. runbooks/common/profile_utils.py +86 -118
  19. runbooks/common/rich_utils.py +3 -3
  20. runbooks/common/sre_performance_suite.py +574 -0
  21. runbooks/finops/business_case_config.py +314 -0
  22. runbooks/finops/cost_processor.py +19 -4
  23. runbooks/finops/dashboard_runner.py +47 -28
  24. runbooks/finops/ebs_cost_optimizer.py +1 -1
  25. runbooks/finops/ebs_optimizer.py +56 -9
  26. runbooks/finops/embedded_mcp_validator.py +642 -36
  27. runbooks/finops/enhanced_trend_visualization.py +7 -2
  28. runbooks/finops/executive_export.py +789 -0
  29. runbooks/finops/finops_dashboard.py +6 -5
  30. runbooks/finops/finops_scenarios.py +34 -27
  31. runbooks/finops/iam_guidance.py +6 -1
  32. runbooks/finops/nat_gateway_optimizer.py +46 -27
  33. runbooks/finops/notebook_utils.py +1 -1
  34. runbooks/finops/schemas.py +73 -58
  35. runbooks/finops/single_dashboard.py +20 -4
  36. runbooks/finops/tests/test_integration.py +3 -1
  37. runbooks/finops/vpc_cleanup_exporter.py +2 -1
  38. runbooks/finops/vpc_cleanup_optimizer.py +22 -29
  39. runbooks/inventory/core/collector.py +51 -28
  40. runbooks/inventory/discovery.md +197 -247
  41. runbooks/inventory/inventory_modules.py +2 -2
  42. runbooks/inventory/list_ec2_instances.py +3 -3
  43. runbooks/inventory/models/account.py +5 -3
  44. runbooks/inventory/models/inventory.py +1 -1
  45. runbooks/inventory/models/resource.py +5 -3
  46. runbooks/inventory/organizations_discovery.py +102 -13
  47. runbooks/inventory/unified_validation_engine.py +2 -15
  48. runbooks/main.py +255 -92
  49. runbooks/operate/base.py +9 -6
  50. runbooks/operate/deployment_framework.py +5 -4
  51. runbooks/operate/deployment_validator.py +6 -5
  52. runbooks/operate/mcp_integration.py +6 -5
  53. runbooks/operate/networking_cost_heatmap.py +17 -13
  54. runbooks/operate/vpc_operations.py +82 -13
  55. runbooks/remediation/base.py +3 -1
  56. runbooks/remediation/commons.py +5 -5
  57. runbooks/remediation/commvault_ec2_analysis.py +66 -18
  58. runbooks/remediation/config/accounts_example.json +31 -0
  59. runbooks/remediation/multi_account.py +120 -7
  60. runbooks/remediation/remediation_cli.py +710 -0
  61. runbooks/remediation/universal_account_discovery.py +377 -0
  62. runbooks/remediation/workspaces_list.py +2 -2
  63. runbooks/security/compliance_automation_engine.py +99 -20
  64. runbooks/security/config/__init__.py +24 -0
  65. runbooks/security/config/compliance_config.py +255 -0
  66. runbooks/security/config/compliance_weights_example.json +22 -0
  67. runbooks/security/config_template_generator.py +500 -0
  68. runbooks/security/security_cli.py +377 -0
  69. runbooks/validation/cli.py +8 -7
  70. runbooks/validation/comprehensive_2way_validator.py +26 -15
  71. runbooks/validation/mcp_validator.py +62 -8
  72. runbooks/vpc/config.py +49 -15
  73. runbooks/vpc/cross_account_session.py +5 -1
  74. runbooks/vpc/heatmap_engine.py +438 -59
  75. runbooks/vpc/mcp_no_eni_validator.py +115 -36
  76. runbooks/vpc/performance_optimized_analyzer.py +546 -0
  77. runbooks/vpc/runbooks_adapter.py +33 -12
  78. runbooks/vpc/tests/conftest.py +4 -2
  79. runbooks/vpc/tests/test_cost_engine.py +3 -1
  80. {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/METADATA +1 -1
  81. {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/RECORD +85 -79
  82. runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
  83. runbooks/finops/runbooks.security.report_generator.log +0 -0
  84. runbooks/finops/runbooks.security.run_script.log +0 -0
  85. runbooks/finops/runbooks.security.security_export.log +0 -0
  86. runbooks/finops/tests/results_test_finops_dashboard.xml +0 -1
  87. runbooks/inventory/artifacts/scale-optimize-status.txt +0 -12
  88. runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
  89. runbooks/inventory/runbooks.security.report_generator.log +0 -0
  90. runbooks/inventory/runbooks.security.run_script.log +0 -0
  91. runbooks/inventory/runbooks.security.security_export.log +0 -0
  92. runbooks/vpc/runbooks.inventory.organizations_discovery.log +0 -0
  93. runbooks/vpc/runbooks.security.report_generator.log +0 -0
  94. runbooks/vpc/runbooks.security.run_script.log +0 -0
  95. runbooks/vpc/runbooks.security.security_export.log +0 -0
  96. {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/WHEEL +0 -0
  97. {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/entry_points.txt +0 -0
  98. {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/licenses/LICENSE +0 -0
  99. {runbooks-1.0.0.dist-info → runbooks-1.0.2.dist-info}/top_level.txt +0 -0
@@ -22,7 +22,8 @@ AWS_AVAILABLE = True
22
22
 
23
23
  def get_aws_profiles() -> List[str]:
24
24
  """Stub implementation - use dashboard_runner.py instead."""
25
- return ["default", "ams-admin-Billing-ReadOnlyAccess-909135376185"]
25
+ import os
26
+ return ["default", os.getenv("BILLING_PROFILE", "default-billing-profile")]
26
27
 
27
28
 
28
29
  def get_account_id(profile: str = "default") -> str:
@@ -46,10 +47,10 @@ class FinOpsConfig:
46
47
  include_budget_data: bool = True
47
48
  include_resource_analysis: bool = True
48
49
 
49
- # Legacy compatibility properties with environment variable support
50
- billing_profile: str = "ams-admin-Billing-ReadOnlyAccess-909135376185"
51
- management_profile: str = "ams-admin-ReadOnlyAccess-909135376185"
52
- operational_profile: str = "ams-centralised-ops-ReadOnlyAccess-335083429030"
50
+ # Legacy compatibility properties with universal environment support
51
+ billing_profile: str = field(default_factory=lambda: os.getenv("BILLING_PROFILE", "default-billing-profile"))
52
+ management_profile: str = field(default_factory=lambda: os.getenv("MANAGEMENT_PROFILE", "default-management-profile"))
53
+ operational_profile: str = field(default_factory=lambda: os.getenv("CENTRALISED_OPS_PROFILE", "default-ops-profile"))
53
54
 
54
55
  # Additional expected attributes from tests
55
56
  time_range_days: int = 30
@@ -1,18 +1,18 @@
1
1
  """
2
- FinOps Business Scenarios - Manager Priority Cost Optimization Framework
2
+ FinOps Business Scenarios - Dynamic Business Case Framework
3
3
 
4
- Strategic Achievement: $132,720+ annual savings (380-757% above targets)
5
- - FinOps-24: WorkSpaces cleanup ($13,020 annual, 104% of target)
6
- - FinOps-23: RDS snapshots optimization ($119,700 annual, 498% of target)
7
- - FinOps-25: Commvault EC2 investigation framework (methodology established)
4
+ Strategic Achievement: Enterprise business case management with configurable scenarios
5
+ - Dynamic scenario configuration with environment variable overrides
6
+ - Business-focused naming conventions replacing hardcoded JIRA references
7
+ - Scalable template system for unlimited business case expansion
8
8
 
9
9
  This module provides business-oriented wrapper functions for executive presentations
10
10
  calling proven technical implementations from src/runbooks/remediation/ modules.
11
11
 
12
12
  Strategic Alignment:
13
- - "Do one thing and do it well": Business wrappers focusing on executive insights
14
- - "Move Fast, But Not So Fast We Crash": Proven technical implementations underneath
15
- - Enterprise FAANG SDLC: Evidence-based cost optimization with audit trails
13
+ - "Do one thing and do it well": Dynamic configuration management with enterprise templates
14
+ - "Move Fast, But Not So Fast We Crash": Proven technical implementations with configurable business cases
15
+ - Enterprise FAANG SDLC: Evidence-based cost optimization with reusable template framework
16
16
  """
17
17
 
18
18
  import asyncio
@@ -30,6 +30,10 @@ from ..common.rich_utils import (
30
30
  )
31
31
  from ..remediation import workspaces_list, rds_snapshot_list
32
32
  from . import commvault_ec2_analysis
33
+ from .business_case_config import (
34
+ get_business_case_config, get_scenario_display_name, get_scenario_savings_range,
35
+ format_business_achievement, migrate_legacy_scenario_reference
36
+ )
33
37
 
34
38
  logger = logging.getLogger(__name__)
35
39
 
@@ -55,9 +59,9 @@ def create_business_scenarios_validated(profile_name: Optional[str] = None) -> D
55
59
  commvault_data = scenarios_analyzer._get_real_commvault_data()
56
60
 
57
61
  scenarios = {
58
- 'FinOps-24_WorkSpaces': workspaces_data,
59
- 'FinOps-23_RDS_Snapshots': rds_data,
60
- 'FinOps-25_Commvault': commvault_data,
62
+ 'workspaces': workspaces_data,
63
+ 'rds_snapshots': rds_data,
64
+ 'backup_investigation': commvault_data,
61
65
  'metadata': {
62
66
  'generated_at': datetime.now().isoformat(),
63
67
  'data_source': 'Real AWS APIs via runbooks',
@@ -70,29 +74,32 @@ def create_business_scenarios_validated(profile_name: Optional[str] = None) -> D
70
74
 
71
75
  except Exception as e:
72
76
  logger.error(f"Error creating validated scenarios: {e}")
73
- # Return fallback business scenarios with manager's validated achievements
77
+ # Return fallback business scenarios using dynamic configuration
78
+ config = get_business_case_config()
79
+ workspaces_scenario = config.get_scenario('workspaces')
80
+ rds_scenario = config.get_scenario('rds-snapshots')
81
+ backup_scenario = config.get_scenario('backup-investigation')
82
+
74
83
  return {
75
- 'FinOps-24_WorkSpaces': {
76
- 'title': 'WorkSpaces Cleanup - Zero Usage Detection',
77
- 'validated_savings': 13020,
78
- 'achievement_rate': 104,
79
- 'risk_level': 'Low'
84
+ 'workspaces': {
85
+ 'title': workspaces_scenario.display_name if workspaces_scenario else 'WorkSpaces Resource Optimization',
86
+ 'savings_range': workspaces_scenario.savings_range_display if workspaces_scenario else '$12K-15K/year',
87
+ 'risk_level': workspaces_scenario.risk_level if workspaces_scenario else 'Low'
80
88
  },
81
- 'FinOps-23_RDS_Snapshots': {
82
- 'title': 'RDS Manual Snapshots Cleanup',
83
- 'validated_savings': 119700,
84
- 'achievement_rate': 498,
85
- 'risk_level': 'Medium'
89
+ 'rds_snapshots': {
90
+ 'title': rds_scenario.display_name if rds_scenario else 'RDS Storage Optimization',
91
+ 'savings_range': rds_scenario.savings_range_display if rds_scenario else '$5K-24K/year',
92
+ 'risk_level': rds_scenario.risk_level if rds_scenario else 'Medium'
86
93
  },
87
- 'FinOps-25_Commvault': {
88
- 'title': 'Commvault Account Investigation',
94
+ 'backup_investigation': {
95
+ 'title': backup_scenario.display_name if backup_scenario else 'Backup Infrastructure Analysis',
89
96
  'framework_status': 'Investigation Ready',
90
- 'risk_level': 'Medium'
97
+ 'risk_level': backup_scenario.risk_level if backup_scenario else 'Medium'
91
98
  },
92
99
  'metadata': {
93
100
  'generated_at': datetime.now().isoformat(),
94
- 'data_source': 'Manager scenarios fallback - $132,720+ validated',
95
- 'validation_method': 'Business case validation',
101
+ 'data_source': 'Dynamic business case configuration',
102
+ 'validation_method': 'Template-based business scenarios',
96
103
  'version': '0.9.5'
97
104
  }
98
105
  }
@@ -11,6 +11,8 @@ from rich.console import Console
11
11
  from rich.panel import Panel
12
12
  from rich.table import Table
13
13
 
14
+ from runbooks.common import get_aws_cli_example_period
15
+
14
16
  console = Console()
15
17
 
16
18
 
@@ -311,6 +313,9 @@ def handle_cost_explorer_error(error: Exception, profile_name: Optional[str] = N
311
313
 
312
314
  def _display_single_account_cost_explorer_guidance(error: Exception, profile_name: Optional[str] = None):
313
315
  """Display context-aware guidance for single account Cost Explorer limitations."""
316
+
317
+ # Get dynamic date period for CLI examples
318
+ start_date, end_date = get_aws_cli_example_period()
314
319
 
315
320
  # Main explanation panel
316
321
  explanation_panel = Panel(
@@ -354,7 +359,7 @@ def _display_single_account_cost_explorer_guidance(error: Exception, profile_nam
354
359
  f"[green]✅ Recommended Solutions:[/green]\n\n"
355
360
  f"{solution_commands}\n\n"
356
361
  f"[bold]🎯 Quick Test Commands:[/bold]\n"
357
- f"• Test billing access: `aws ce get-cost-and-usage --time-period Start=2024-01-01,End=2024-01-02 --granularity MONTHLY --metrics UnblendedCost --profile your-billing-profile`\n"
362
+ f"• Test billing access: `aws ce get-cost-and-usage --time-period Start={start_date},End={end_date} --granularity MONTHLY --metrics UnblendedCost --profile your-billing-profile`\n"
358
363
  f"• List available profiles: `aws configure list-profiles`\n"
359
364
  f"• Check current identity: `aws sts get-caller-identity --profile {profile_name or 'your-profile'}`\n\n"
360
365
  f"[bold]💡 Alternative Approach:[/bold]\n"
@@ -143,7 +143,7 @@ class NATGatewayOptimizer:
143
143
  # NAT Gateway pricing - using dynamic pricing engine
144
144
  # Base monthly cost calculation (will be applied per region)
145
145
  self._base_monthly_cost_us_east_1 = get_service_monthly_cost("nat_gateway", "us-east-1")
146
- self.nat_gateway_data_processing_cost = 0.045 # $0.045/GB (data transfer pricing)
146
+ self.nat_gateway_data_processing_cost = get_service_monthly_cost("data_transfer", "us-east-1") # Dynamic data transfer pricing
147
147
 
148
148
  # Enterprise thresholds for optimization recommendations
149
149
  self.low_usage_threshold_connections = 10 # Active connections per day
@@ -155,9 +155,9 @@ class NATGatewayOptimizer:
155
155
  try:
156
156
  return get_service_monthly_cost("nat_gateway", region)
157
157
  except Exception:
158
- # Fallback to regional cost calculation
158
+ # Fallback to regional cost calculation using dynamic pricing
159
159
  from ..common.aws_pricing import calculate_regional_cost
160
- return calculate_regional_cost(self._base_monthly_cost_us_east_1, region)
160
+ return calculate_regional_cost(self._base_monthly_cost_us_east_1, region, "nat_gateway", self.profile_name)
161
161
 
162
162
  async def analyze_nat_gateways(self, dry_run: bool = True) -> NATGatewayOptimizerResults:
163
163
  """
@@ -778,39 +778,58 @@ class EnhancedVPCCostOptimizer:
778
778
  self.cost_model = self._initialize_dynamic_cost_model()
779
779
 
780
780
  def _initialize_dynamic_cost_model(self) -> Dict[str, float]:
781
- """Initialize dynamic cost model using AWS pricing engine."""
781
+ """Initialize dynamic cost model using AWS pricing engine with universal compatibility."""
782
782
  try:
783
783
  # Get base pricing for us-east-1, then apply regional multipliers as needed
784
784
  base_region = "us-east-1"
785
785
 
786
786
  return {
787
- "nat_gateway_monthly": get_service_monthly_cost("nat_gateway", base_region),
788
- "nat_gateway_data_processing": get_service_monthly_cost("data_transfer", base_region),
789
- "transit_gateway_monthly": get_service_monthly_cost("transit_gateway", base_region),
790
- "vpc_endpoint_monthly": get_service_monthly_cost("vpc_endpoint", base_region),
791
- "data_transfer_regional": get_service_monthly_cost("data_transfer", base_region),
792
- "data_transfer_internet": get_service_monthly_cost("data_transfer", base_region) * 4.5, # Internet is ~4.5x higher
787
+ "nat_gateway_monthly": get_service_monthly_cost("nat_gateway", base_region, self.profile),
788
+ "nat_gateway_data_processing": get_service_monthly_cost("data_transfer", base_region, self.profile),
789
+ "transit_gateway_monthly": get_service_monthly_cost("transit_gateway", base_region, self.profile),
790
+ "vpc_endpoint_monthly": get_service_monthly_cost("vpc_endpoint", base_region, self.profile),
791
+ "vpc_endpoint_interface_hourly": get_service_monthly_cost("vpc_endpoint_interface", base_region, self.profile) / (24 * 30),
792
+ "transit_gateway_attachment_hourly": get_service_monthly_cost("transit_gateway_attachment", base_region, self.profile) / (24 * 30),
793
+ "data_transfer_regional": get_service_monthly_cost("data_transfer", base_region, self.profile),
794
+ "data_transfer_internet": get_service_monthly_cost("data_transfer", base_region, self.profile) * 4.5, # Internet is ~4.5x higher
793
795
  }
794
796
  except Exception as e:
795
797
  print_warning(f"Dynamic pricing initialization failed: {e}")
796
- # Fallback to regional cost calculation
797
- from ..common.aws_pricing import calculate_regional_cost
798
- base_costs = {
799
- "nat_gateway_hourly": 0.045,
800
- "nat_gateway_data_processing": 0.045, # per GB
801
- "transit_gateway_monthly": 36.50,
802
- "transit_gateway_attachment_hourly": 0.05,
803
- "vpc_endpoint_interface_hourly": 0.01,
804
- "data_transfer_regional": 0.01, # per GB within region
805
- "data_transfer_cross_region": 0.02, # per GB cross-region
806
- "data_transfer_internet": 0.09 # per GB to internet
807
- }
798
+ print_warning("Attempting AWS Pricing API fallback with universal profile support")
799
+ # Enhanced fallback with AWS Pricing API integration
800
+ from ..common.aws_pricing import get_aws_pricing_engine, AWSOfficialPricingEngine
808
801
 
809
- # Apply regional multipliers to fallback costs
810
- return {
811
- key: calculate_regional_cost(value, "us-east-1")
812
- for key, value in base_costs.items()
813
- }
802
+ try:
803
+ # Use AWS Pricing API with profile support for universal compatibility
804
+ pricing_engine = get_aws_pricing_engine(profile=self.profile, enable_fallback=True)
805
+
806
+ # Get actual AWS pricing instead of hardcoded values
807
+ nat_gateway_pricing = pricing_engine.get_nat_gateway_pricing("us-east-1")
808
+ transit_gateway_pricing = pricing_engine.get_transit_gateway_pricing("us-east-1")
809
+ vpc_endpoint_pricing = pricing_engine.get_vpc_endpoint_pricing("us-east-1")
810
+ data_transfer_pricing = pricing_engine.get_data_transfer_pricing("us-east-1", "internet")
811
+
812
+ return {
813
+ "nat_gateway_monthly": nat_gateway_pricing.monthly_cost,
814
+ "nat_gateway_data_processing": data_transfer_pricing.cost_per_gb,
815
+ "transit_gateway_monthly": transit_gateway_pricing.monthly_cost,
816
+ "transit_gateway_attachment_hourly": transit_gateway_pricing.attachment_hourly_cost,
817
+ "vpc_endpoint_interface_hourly": vpc_endpoint_pricing.interface_hourly_cost,
818
+ "data_transfer_regional": data_transfer_pricing.cost_per_gb * 0.1, # Regional is ~10% of internet cost
819
+ "data_transfer_cross_region": data_transfer_pricing.cost_per_gb * 0.2, # Cross-region is ~20% of internet cost
820
+ "data_transfer_internet": data_transfer_pricing.cost_per_gb
821
+ }
822
+
823
+ except Exception as pricing_error:
824
+ print_error(f"ENTERPRISE COMPLIANCE VIOLATION: Cannot determine pricing without AWS API access: {pricing_error}")
825
+ print_warning("Universal compatibility requires dynamic pricing - hardcoded values not permitted")
826
+
827
+ # Return error state instead of hardcoded values to maintain enterprise compliance
828
+ raise RuntimeError(
829
+ "Universal compatibility mode requires dynamic AWS pricing API access. "
830
+ "Please ensure your AWS profile has pricing:GetProducts permissions or configure "
831
+ "appropriate billing/management profile access."
832
+ )
814
833
 
815
834
  async def analyze_comprehensive_vpc_costs(self, profile: Optional[str] = None,
816
835
  regions: Optional[List[str]] = None) -> Dict[str, Any]:
@@ -591,6 +591,6 @@ if __name__ == '__main__':
591
591
  config=config,
592
592
  optimization_focus=OptimizationCategory.COST_OPTIMIZATION
593
593
  )
594
- print(f"Dashboard created with {len(result.export_files)} export files")
594
+ console.print(f"Dashboard created with {len(result.export_files)} export files")
595
595
 
596
596
  asyncio.run(test_dashboard())
@@ -26,7 +26,7 @@ from typing import Dict, List, Optional, Union, Any, Literal
26
26
  from enum import Enum
27
27
  import re
28
28
 
29
- from pydantic import BaseModel, Field, validator, root_validator, ConfigDict
29
+ from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict
30
30
  from pydantic.types import UUID4, PositiveFloat, NonNegativeFloat
31
31
 
32
32
 
@@ -106,7 +106,8 @@ class CostBreakdown(BaseSchema):
106
106
  percentage_of_total: float = Field(..., ge=0, le=100)
107
107
  resource_count: int = Field(..., ge=0)
108
108
 
109
- @validator('service_name')
109
+ @field_validator('service_name')
110
+ @classmethod
110
111
  def validate_service_name(cls, v):
111
112
  """Validate AWS service names."""
112
113
  # Common AWS service patterns
@@ -130,14 +131,15 @@ class CostBreakdown(BaseSchema):
130
131
 
131
132
  return v.strip()
132
133
 
133
- @validator('annual_cost')
134
- def validate_annual_cost_consistency(cls, v, values):
134
+ @field_validator('annual_cost')
135
+ @classmethod
136
+ def validate_annual_cost_consistency(cls, v, info):
135
137
  """Ensure annual cost is approximately 12x monthly cost."""
136
- if 'monthly_cost' in values:
137
- expected_annual = values['monthly_cost'] * 12
138
+ if 'monthly_cost' in info.data:
139
+ expected_annual = info.data['monthly_cost'] * 12
138
140
  # Allow 1% tolerance for rounding differences
139
141
  if abs(v - expected_annual) > (expected_annual * 0.01):
140
- raise ValueError(f'Annual cost {v} should be approximately 12x monthly cost {values["monthly_cost"]}')
142
+ raise ValueError(f'Annual cost {v} should be approximately 12x monthly cost {info.data["monthly_cost"]}')
141
143
  return v
142
144
 
143
145
 
@@ -169,7 +171,8 @@ class OptimizationScenario(BaseSchema):
169
171
  validation_timestamp: Optional[datetime] = Field(None)
170
172
  mcp_variance_percent: Optional[float] = Field(None, ge=0, le=100)
171
173
 
172
- @validator('scenario_name')
174
+ @field_validator('scenario_name')
175
+ @classmethod
173
176
  def validate_scenario_name(cls, v):
174
177
  """Validate scenario naming conventions."""
175
178
  # Ensure professional naming
@@ -177,27 +180,30 @@ class OptimizationScenario(BaseSchema):
177
180
  raise ValueError('Scenario name must start with capital letter and contain only letters, numbers, spaces, hyphens, and parentheses')
178
181
  return v.strip()
179
182
 
180
- @validator('annual_savings')
181
- def validate_annual_savings_consistency(cls, v, values):
183
+ @field_validator('annual_savings')
184
+ @classmethod
185
+ def validate_annual_savings_consistency(cls, v, info):
182
186
  """Ensure annual savings consistency with monthly savings."""
183
- if 'monthly_savings' in values:
184
- expected_annual = values['monthly_savings'] * 12
187
+ if 'monthly_savings' in info.data:
188
+ expected_annual = info.data['monthly_savings'] * 12
185
189
  if abs(v - expected_annual) > (expected_annual * 0.01): # 1% tolerance
186
- raise ValueError(f'Annual savings {v} should be approximately 12x monthly savings {values["monthly_savings"]}')
190
+ raise ValueError(f'Annual savings {v} should be approximately 12x monthly savings {info.data["monthly_savings"]}')
187
191
  return v
188
192
 
189
- @validator('payback_period_months')
190
- def calculate_payback_period(cls, v, values):
193
+ @field_validator('payback_period_months')
194
+ @classmethod
195
+ def calculate_payback_period(cls, v, info):
191
196
  """Calculate payback period if not provided."""
192
- if v is None and 'implementation_cost' in values and 'monthly_savings' in values:
193
- impl_cost = values['implementation_cost']
194
- monthly_savings = values['monthly_savings']
197
+ if v is None and 'implementation_cost' in info.data and 'monthly_savings' in info.data:
198
+ impl_cost = info.data['implementation_cost']
199
+ monthly_savings = info.data['monthly_savings']
195
200
  if monthly_savings > 0:
196
201
  calculated_payback = impl_cost / monthly_savings
197
202
  return round(calculated_payback, 1)
198
203
  return v
199
204
 
200
- @validator('affected_services')
205
+ @field_validator('affected_services')
206
+ @classmethod
201
207
  def validate_aws_services(cls, v):
202
208
  """Validate AWS service names in affected services."""
203
209
  common_services = {
@@ -215,7 +221,8 @@ class OptimizationScenario(BaseSchema):
215
221
 
216
222
  return v
217
223
 
218
- @validator('affected_accounts')
224
+ @field_validator('affected_accounts')
225
+ @classmethod
219
226
  def validate_account_ids(cls, v):
220
227
  """Validate AWS account ID format."""
221
228
  account_pattern = r'^\d{12}$|^[\w\-\.]{1,50}$' # 12-digit ID or account name
@@ -270,47 +277,50 @@ class CostOptimizationResult(BaseSchema):
270
277
  default=[ExportFormat.JSON, ExportFormat.CSV, ExportFormat.PDF]
271
278
  )
272
279
 
273
- @validator('total_potential_annual_savings')
274
- def validate_annual_consistency(cls, v, values):
280
+ @field_validator('total_potential_annual_savings')
281
+ @classmethod
282
+ def validate_annual_consistency(cls, v, info):
275
283
  """Validate annual savings consistency."""
276
- if 'total_potential_monthly_savings' in values:
277
- expected = values['total_potential_monthly_savings'] * 12
284
+ if 'total_potential_monthly_savings' in info.data:
285
+ expected = info.data['total_potential_monthly_savings'] * 12
278
286
  if abs(v - expected) > (expected * 0.01):
279
287
  raise ValueError('Annual savings must be approximately 12x monthly savings')
280
288
  return v
281
289
 
282
- @validator('savings_percentage')
283
- def calculate_savings_percentage(cls, v, values):
290
+ @field_validator('savings_percentage')
291
+ @classmethod
292
+ def calculate_savings_percentage(cls, v, info):
284
293
  """Validate or calculate savings percentage."""
285
- if 'current_monthly_spend' in values and 'total_potential_monthly_savings' in values:
286
- current_spend = values['current_monthly_spend']
294
+ if 'current_monthly_spend' in info.data and 'total_potential_monthly_savings' in info.data:
295
+ current_spend = info.data['current_monthly_spend']
287
296
  if current_spend > 0:
288
- calculated = (values['total_potential_monthly_savings'] / current_spend) * 100
297
+ calculated = (info.data['total_potential_monthly_savings'] / current_spend) * 100
289
298
  if abs(v - calculated) > 0.1: # 0.1% tolerance
290
299
  raise ValueError(f'Savings percentage {v}% inconsistent with calculated {calculated:.1f}%')
291
300
  return v
292
301
 
293
- @validator('total_scenarios')
294
- def validate_scenario_count(cls, v, values):
302
+ @field_validator('total_scenarios')
303
+ @classmethod
304
+ def validate_scenario_count(cls, v, info):
295
305
  """Ensure scenario count matches actual scenarios."""
296
- if 'optimization_scenarios' in values:
297
- actual_count = len(values['optimization_scenarios'])
306
+ if 'optimization_scenarios' in info.data:
307
+ actual_count = len(info.data['optimization_scenarios'])
298
308
  if v != actual_count:
299
309
  raise ValueError(f'Total scenarios {v} does not match actual scenarios count {actual_count}')
300
310
  return v
301
311
 
302
- @root_validator
303
- def validate_complexity_distribution(cls, values):
312
+ @model_validator(mode='after')
313
+ def validate_complexity_distribution(self):
304
314
  """Validate complexity scenario counts."""
305
- scenarios = values.get('optimization_scenarios', [])
315
+ scenarios = self.optimization_scenarios or []
306
316
  if scenarios:
307
317
  low_count = sum(1 for s in scenarios if s.complexity == ComplexityLevel.LOW)
308
318
  medium_count = sum(1 for s in scenarios if s.complexity == ComplexityLevel.MEDIUM)
309
319
  high_count = sum(1 for s in scenarios if s.complexity == ComplexityLevel.HIGH)
310
320
 
311
- expected_low = values.get('low_complexity_scenarios', 0)
312
- expected_medium = values.get('medium_complexity_scenarios', 0)
313
- expected_high = values.get('high_complexity_scenarios', 0)
321
+ expected_low = self.low_complexity_scenarios or 0
322
+ expected_medium = self.medium_complexity_scenarios or 0
323
+ expected_high = self.high_complexity_scenarios or 0
314
324
 
315
325
  if (low_count != expected_low or
316
326
  medium_count != expected_medium or
@@ -320,7 +330,7 @@ class CostOptimizationResult(BaseSchema):
320
330
  f'actual L:{low_count} M:{medium_count} H:{high_count}'
321
331
  )
322
332
 
323
- return values
333
+ return self
324
334
 
325
335
 
326
336
  # Business Interface Schemas
@@ -350,7 +360,8 @@ class ExecutiveSummary(BaseSchema):
350
360
  data_validation_status: ValidationStatus = Field(...)
351
361
  last_validated: datetime = Field(...)
352
362
 
353
- @validator('roi_percentage')
363
+ @field_validator('roi_percentage')
364
+ @classmethod
354
365
  def validate_reasonable_roi(cls, v):
355
366
  """Ensure ROI is reasonable for executive presentation."""
356
367
  if v > 1000: # 1000% ROI
@@ -377,12 +388,13 @@ class MCPValidationResult(BaseSchema):
377
388
  mcp_source: str = Field(..., min_length=1)
378
389
  response_time_seconds: Optional[PositiveFloat] = Field(None, le=300) # 5 minute timeout
379
390
 
380
- @validator('variance_percent')
381
- def calculate_variance_percent(cls, v, values):
391
+ @field_validator('variance_percent')
392
+ @classmethod
393
+ def calculate_variance_percent(cls, v, info):
382
394
  """Calculate and validate variance percentage."""
383
- if 'notebook_value' in values and 'mcp_value' in values:
384
- notebook_val = values['notebook_value']
385
- mcp_val = values['mcp_value']
395
+ if 'notebook_value' in info.data and 'mcp_value' in info.data:
396
+ notebook_val = info.data['notebook_value']
397
+ mcp_val = info.data['mcp_value']
386
398
 
387
399
  if notebook_val > 0:
388
400
  calculated = abs((notebook_val - mcp_val) / notebook_val) * 100
@@ -441,22 +453,24 @@ class ComprehensiveTestSuite(BaseSchema):
441
453
  meets_production_criteria: bool = Field(...)
442
454
  quality_score: float = Field(..., ge=0, le=100)
443
455
 
444
- @validator('passed_tests')
445
- def validate_test_counts(cls, v, values):
456
+ @field_validator('passed_tests')
457
+ @classmethod
458
+ def validate_test_counts(cls, v, info):
446
459
  """Ensure test counts are consistent."""
447
- if 'failed_tests' in values and 'skipped_tests' in values and 'total_tests' in values:
448
- calculated_total = v + values['failed_tests'] + values['skipped_tests']
449
- if calculated_total != values['total_tests']:
450
- raise ValueError(f'Test counts inconsistent: {calculated_total} ≠ {values["total_tests"]}')
460
+ if 'failed_tests' in info.data and 'skipped_tests' in info.data and 'total_tests' in info.data:
461
+ calculated_total = v + info.data['failed_tests'] + info.data['skipped_tests']
462
+ if calculated_total != info.data['total_tests']:
463
+ raise ValueError(f'Test counts inconsistent: {calculated_total} ≠ {info.data["total_tests"]}')
451
464
  return v
452
465
 
453
- @validator('pass_rate_percent')
454
- def calculate_pass_rate(cls, v, values):
466
+ @field_validator('pass_rate_percent')
467
+ @classmethod
468
+ def calculate_pass_rate(cls, v, info):
455
469
  """Calculate and validate pass rate."""
456
- if 'passed_tests' in values and 'total_tests' in values:
457
- total = values['total_tests']
470
+ if 'passed_tests' in info.data and 'total_tests' in info.data:
471
+ total = info.data['total_tests']
458
472
  if total > 0:
459
- calculated = (values['passed_tests'] / total) * 100
473
+ calculated = (info.data['passed_tests'] / total) * 100
460
474
  if abs(v - calculated) > 0.01:
461
475
  raise ValueError(f'Pass rate {v}% inconsistent with calculated {calculated:.2f}%')
462
476
  return v
@@ -480,7 +494,8 @@ class ExportMetadata(BaseSchema):
480
494
  export_validated: bool = Field(...)
481
495
  validation_errors: List[str] = Field(default_factory=list)
482
496
 
483
- @validator('file_path')
497
+ @field_validator('file_path')
498
+ @classmethod
484
499
  def validate_file_path(cls, v):
485
500
  """Validate file path format."""
486
501
  # Basic path validation
@@ -346,18 +346,34 @@ class SingleAccountDashboard:
346
346
  # Integrate quarterly data into trend data structure
347
347
  corrected_trend_data["quarterly_costs_by_service"] = quarterly_costs
348
348
 
349
- # Log the trend analysis context for transparency
349
+ # Enhanced trend analysis context with MCP validation awareness
350
350
  if "period_metadata" in corrected_trend_data:
351
351
  metadata = corrected_trend_data["period_metadata"]
352
352
  current_days = metadata.get("current_days", 0)
353
353
  previous_days = metadata.get("previous_days", 0)
354
+ days_difference = metadata.get("days_difference", abs(current_days - previous_days))
354
355
  reliability = metadata.get("trend_reliability", "unknown")
356
+ alignment_strategy = metadata.get("period_alignment_strategy", "standard")
355
357
 
358
+ # ENHANCED LOGIC: Reduce warnings when using intelligent period alignment
356
359
  if metadata.get("is_partial_comparison", False):
357
- print_warning(f"Partial period comparison detected: {current_days} vs {previous_days} days")
358
- print_info(f"Trend reliability: {reliability}")
360
+ if alignment_strategy == "equal_days":
361
+ # Equal-day comparison reduces the severity of partial period concerns
362
+ print_info(f"🔄 Enhanced period alignment: {current_days} vs {previous_days} days (equal-day strategy)")
363
+ if reliability in ["high", "medium_with_validation_support"]:
364
+ print_success(f"✅ Trend reliability: {reliability} (enhanced alignment)")
365
+ else:
366
+ print_info(f"Trend reliability: {reliability}")
367
+ else:
368
+ # Standard partial period warning for traditional comparisons
369
+ print_warning(f"⚠️ Partial period comparison: {current_days} vs {previous_days} days")
370
+ print_info(f"Trend reliability: {reliability}")
371
+
372
+ # Add context for very small differences
373
+ if days_difference <= 5:
374
+ print_info(f"💡 Small period difference ({days_difference} days) - trends should be reliable")
359
375
  else:
360
- print_success(f"Equal period comparison: {current_days} vs {previous_days} days")
376
+ print_success(f"Equal period comparison: {current_days} vs {previous_days} days")
361
377
 
362
378
  return corrected_trend_data
363
379
 
@@ -34,6 +34,8 @@ try:
34
34
  except ImportError:
35
35
  # Define mock_costexplorer as a no-op decorator for compatibility
36
36
  def mock_costexplorer(func):
37
+ # Dynamic test period for consistent test data
38
+ test_period = get_test_date_period(30)
37
39
  def wrapper(*args, **kwargs):
38
40
  return func(*args, **kwargs)
39
41
 
@@ -128,7 +130,7 @@ class TestAWSIntegrationWithMoto:
128
130
  mock_cost_data = {
129
131
  "ResultsByTime": [
130
132
  {
131
- "TimePeriod": {"Start": "2024-01-01", "End": "2024-01-31"},
133
+ "TimePeriod": {"Start": test_period["Start"], "End": test_period["End"]},
132
134
  "Total": {"UnblendedCost": {"Amount": "50000.00", "Unit": "USD"}},
133
135
  "Groups": [
134
136
  {"Keys": ["EC2-Instance"], "Metrics": {"UnblendedCost": {"Amount": "20000.00", "Unit": "USD"}}},
@@ -16,6 +16,7 @@ from datetime import datetime
16
16
  from typing import Any, Dict, List
17
17
 
18
18
  from .markdown_exporter import MarkdownExporter
19
+ from runbooks.common.rich_utils import console
19
20
 
20
21
 
21
22
  def _format_tags_for_display(tags_dict: Dict[str, str]) -> str:
@@ -76,7 +77,7 @@ def export_vpc_cleanup_results(vpc_result: Any, export_formats: List[str], outpu
76
77
  )
77
78
  results['markdown'] = markdown_filename
78
79
  except Exception as e:
79
- print(f"Warning: Markdown export failed: {e}")
80
+ console.print(f"[yellow]Warning: Markdown export failed: {e}[/yellow]")
80
81
  results['markdown'] = None
81
82
 
82
83
  # Real implementations for other formats