runbooks 1.1.3__py3-none-any.whl → 1.1.5__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 (247) hide show
  1. runbooks/__init__.py +31 -2
  2. runbooks/__init___optimized.py +18 -4
  3. runbooks/_platform/__init__.py +1 -5
  4. runbooks/_platform/core/runbooks_wrapper.py +141 -138
  5. runbooks/aws2/accuracy_validator.py +812 -0
  6. runbooks/base.py +7 -0
  7. runbooks/cfat/WEIGHT_CONFIG_README.md +1 -1
  8. runbooks/cfat/assessment/compliance.py +8 -8
  9. runbooks/cfat/assessment/runner.py +1 -0
  10. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  11. runbooks/cfat/models.py +6 -2
  12. runbooks/cfat/tests/__init__.py +6 -1
  13. runbooks/cli/__init__.py +13 -0
  14. runbooks/cli/commands/cfat.py +274 -0
  15. runbooks/cli/commands/finops.py +1164 -0
  16. runbooks/cli/commands/inventory.py +379 -0
  17. runbooks/cli/commands/operate.py +239 -0
  18. runbooks/cli/commands/security.py +248 -0
  19. runbooks/cli/commands/validation.py +825 -0
  20. runbooks/cli/commands/vpc.py +310 -0
  21. runbooks/cli/registry.py +107 -0
  22. runbooks/cloudops/__init__.py +23 -30
  23. runbooks/cloudops/base.py +96 -107
  24. runbooks/cloudops/cost_optimizer.py +549 -547
  25. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  26. runbooks/cloudops/interfaces.py +226 -227
  27. runbooks/cloudops/lifecycle_manager.py +5 -4
  28. runbooks/cloudops/mcp_cost_validation.py +252 -235
  29. runbooks/cloudops/models.py +78 -53
  30. runbooks/cloudops/monitoring_automation.py +5 -4
  31. runbooks/cloudops/notebook_framework.py +179 -215
  32. runbooks/cloudops/security_enforcer.py +125 -159
  33. runbooks/common/accuracy_validator.py +11 -0
  34. runbooks/common/aws_pricing.py +349 -326
  35. runbooks/common/aws_pricing_api.py +211 -212
  36. runbooks/common/aws_profile_manager.py +341 -0
  37. runbooks/common/aws_utils.py +75 -80
  38. runbooks/common/business_logic.py +127 -105
  39. runbooks/common/cli_decorators.py +36 -60
  40. runbooks/common/comprehensive_cost_explorer_integration.py +456 -464
  41. runbooks/common/cross_account_manager.py +198 -205
  42. runbooks/common/date_utils.py +27 -39
  43. runbooks/common/decorators.py +235 -0
  44. runbooks/common/dry_run_examples.py +173 -208
  45. runbooks/common/dry_run_framework.py +157 -155
  46. runbooks/common/enhanced_exception_handler.py +15 -4
  47. runbooks/common/enhanced_logging_example.py +50 -64
  48. runbooks/common/enhanced_logging_integration_example.py +65 -37
  49. runbooks/common/env_utils.py +16 -16
  50. runbooks/common/error_handling.py +40 -38
  51. runbooks/common/lazy_loader.py +41 -23
  52. runbooks/common/logging_integration_helper.py +79 -86
  53. runbooks/common/mcp_cost_explorer_integration.py +478 -495
  54. runbooks/common/mcp_integration.py +63 -74
  55. runbooks/common/memory_optimization.py +140 -118
  56. runbooks/common/module_cli_base.py +37 -58
  57. runbooks/common/organizations_client.py +176 -194
  58. runbooks/common/patterns.py +204 -0
  59. runbooks/common/performance_monitoring.py +67 -71
  60. runbooks/common/performance_optimization_engine.py +283 -274
  61. runbooks/common/profile_utils.py +248 -39
  62. runbooks/common/rich_utils.py +643 -92
  63. runbooks/common/sre_performance_suite.py +177 -186
  64. runbooks/enterprise/__init__.py +1 -1
  65. runbooks/enterprise/logging.py +144 -106
  66. runbooks/enterprise/security.py +187 -204
  67. runbooks/enterprise/validation.py +43 -56
  68. runbooks/finops/__init__.py +29 -33
  69. runbooks/finops/account_resolver.py +1 -1
  70. runbooks/finops/advanced_optimization_engine.py +980 -0
  71. runbooks/finops/automation_core.py +268 -231
  72. runbooks/finops/business_case_config.py +184 -179
  73. runbooks/finops/cli.py +660 -139
  74. runbooks/finops/commvault_ec2_analysis.py +157 -164
  75. runbooks/finops/compute_cost_optimizer.py +336 -320
  76. runbooks/finops/config.py +20 -20
  77. runbooks/finops/cost_optimizer.py +488 -622
  78. runbooks/finops/cost_processor.py +332 -214
  79. runbooks/finops/dashboard_runner.py +1006 -172
  80. runbooks/finops/ebs_cost_optimizer.py +991 -657
  81. runbooks/finops/elastic_ip_optimizer.py +317 -257
  82. runbooks/finops/enhanced_mcp_integration.py +340 -0
  83. runbooks/finops/enhanced_progress.py +40 -37
  84. runbooks/finops/enhanced_trend_visualization.py +3 -2
  85. runbooks/finops/enterprise_wrappers.py +230 -292
  86. runbooks/finops/executive_export.py +203 -160
  87. runbooks/finops/helpers.py +130 -288
  88. runbooks/finops/iam_guidance.py +1 -1
  89. runbooks/finops/infrastructure/__init__.py +80 -0
  90. runbooks/finops/infrastructure/commands.py +506 -0
  91. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  92. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  93. runbooks/finops/markdown_exporter.py +338 -175
  94. runbooks/finops/mcp_validator.py +1952 -0
  95. runbooks/finops/nat_gateway_optimizer.py +1513 -482
  96. runbooks/finops/network_cost_optimizer.py +657 -587
  97. runbooks/finops/notebook_utils.py +226 -188
  98. runbooks/finops/optimization_engine.py +1136 -0
  99. runbooks/finops/optimizer.py +25 -29
  100. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  101. runbooks/finops/reservation_optimizer.py +427 -363
  102. runbooks/finops/scenario_cli_integration.py +77 -78
  103. runbooks/finops/scenarios.py +1278 -439
  104. runbooks/finops/schemas.py +218 -182
  105. runbooks/finops/snapshot_manager.py +2289 -0
  106. runbooks/finops/tests/test_finops_dashboard.py +3 -3
  107. runbooks/finops/tests/test_reference_images_validation.py +2 -2
  108. runbooks/finops/tests/test_single_account_features.py +17 -17
  109. runbooks/finops/tests/validate_test_suite.py +1 -1
  110. runbooks/finops/types.py +3 -3
  111. runbooks/finops/validation_framework.py +263 -269
  112. runbooks/finops/vpc_cleanup_exporter.py +191 -146
  113. runbooks/finops/vpc_cleanup_optimizer.py +593 -575
  114. runbooks/finops/workspaces_analyzer.py +171 -182
  115. runbooks/hitl/enhanced_workflow_engine.py +1 -1
  116. runbooks/integration/__init__.py +89 -0
  117. runbooks/integration/mcp_integration.py +1920 -0
  118. runbooks/inventory/CLAUDE.md +816 -0
  119. runbooks/inventory/README.md +3 -3
  120. runbooks/inventory/Tests/common_test_data.py +30 -30
  121. runbooks/inventory/__init__.py +2 -2
  122. runbooks/inventory/cloud_foundations_integration.py +144 -149
  123. runbooks/inventory/collectors/aws_comprehensive.py +28 -11
  124. runbooks/inventory/collectors/aws_networking.py +111 -101
  125. runbooks/inventory/collectors/base.py +4 -0
  126. runbooks/inventory/core/collector.py +495 -313
  127. runbooks/inventory/discovery.md +2 -2
  128. runbooks/inventory/drift_detection_cli.py +69 -96
  129. runbooks/inventory/find_ec2_security_groups.py +1 -1
  130. runbooks/inventory/inventory_mcp_cli.py +48 -46
  131. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  132. runbooks/inventory/mcp_inventory_validator.py +549 -465
  133. runbooks/inventory/mcp_vpc_validator.py +359 -442
  134. runbooks/inventory/organizations_discovery.py +56 -52
  135. runbooks/inventory/rich_inventory_display.py +33 -32
  136. runbooks/inventory/unified_validation_engine.py +278 -251
  137. runbooks/inventory/vpc_analyzer.py +733 -696
  138. runbooks/inventory/vpc_architecture_validator.py +293 -348
  139. runbooks/inventory/vpc_dependency_analyzer.py +382 -378
  140. runbooks/inventory/vpc_flow_analyzer.py +3 -3
  141. runbooks/main.py +152 -9147
  142. runbooks/main_final.py +91 -60
  143. runbooks/main_minimal.py +22 -10
  144. runbooks/main_optimized.py +131 -100
  145. runbooks/main_ultra_minimal.py +7 -2
  146. runbooks/mcp/__init__.py +36 -0
  147. runbooks/mcp/integration.py +679 -0
  148. runbooks/metrics/dora_metrics_engine.py +2 -2
  149. runbooks/monitoring/performance_monitor.py +9 -4
  150. runbooks/operate/dynamodb_operations.py +3 -1
  151. runbooks/operate/ec2_operations.py +145 -137
  152. runbooks/operate/iam_operations.py +146 -152
  153. runbooks/operate/mcp_integration.py +1 -1
  154. runbooks/operate/networking_cost_heatmap.py +33 -10
  155. runbooks/operate/privatelink_operations.py +1 -1
  156. runbooks/operate/rds_operations.py +223 -254
  157. runbooks/operate/s3_operations.py +107 -118
  158. runbooks/operate/vpc_endpoints.py +1 -1
  159. runbooks/operate/vpc_operations.py +648 -618
  160. runbooks/remediation/base.py +1 -1
  161. runbooks/remediation/commons.py +10 -7
  162. runbooks/remediation/commvault_ec2_analysis.py +71 -67
  163. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  164. runbooks/remediation/multi_account.py +24 -21
  165. runbooks/remediation/rds_snapshot_list.py +91 -65
  166. runbooks/remediation/remediation_cli.py +92 -146
  167. runbooks/remediation/universal_account_discovery.py +83 -79
  168. runbooks/remediation/workspaces_list.py +49 -44
  169. runbooks/security/__init__.py +19 -0
  170. runbooks/security/assessment_runner.py +1150 -0
  171. runbooks/security/baseline_checker.py +812 -0
  172. runbooks/security/cloudops_automation_security_validator.py +509 -535
  173. runbooks/security/compliance_automation_engine.py +17 -17
  174. runbooks/security/config/__init__.py +2 -2
  175. runbooks/security/config/compliance_config.py +50 -50
  176. runbooks/security/config_template_generator.py +63 -76
  177. runbooks/security/enterprise_security_framework.py +1 -1
  178. runbooks/security/executive_security_dashboard.py +519 -508
  179. runbooks/security/integration_test_enterprise_security.py +5 -3
  180. runbooks/security/multi_account_security_controls.py +959 -1210
  181. runbooks/security/real_time_security_monitor.py +422 -444
  182. runbooks/security/run_script.py +1 -1
  183. runbooks/security/security_baseline_tester.py +1 -1
  184. runbooks/security/security_cli.py +143 -112
  185. runbooks/security/test_2way_validation.py +439 -0
  186. runbooks/security/two_way_validation_framework.py +852 -0
  187. runbooks/sre/mcp_reliability_engine.py +6 -6
  188. runbooks/sre/production_monitoring_framework.py +167 -177
  189. runbooks/tdd/__init__.py +15 -0
  190. runbooks/tdd/cli.py +1071 -0
  191. runbooks/utils/__init__.py +14 -17
  192. runbooks/utils/logger.py +7 -2
  193. runbooks/utils/version_validator.py +51 -48
  194. runbooks/validation/__init__.py +6 -6
  195. runbooks/validation/cli.py +9 -3
  196. runbooks/validation/comprehensive_2way_validator.py +754 -708
  197. runbooks/validation/mcp_validator.py +906 -228
  198. runbooks/validation/terraform_citations_validator.py +104 -115
  199. runbooks/validation/terraform_drift_detector.py +447 -451
  200. runbooks/vpc/README.md +617 -0
  201. runbooks/vpc/__init__.py +8 -1
  202. runbooks/vpc/analyzer.py +577 -0
  203. runbooks/vpc/cleanup_wrapper.py +476 -413
  204. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  205. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  206. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  207. runbooks/vpc/config.py +92 -97
  208. runbooks/vpc/cost_engine.py +411 -148
  209. runbooks/vpc/cost_explorer_integration.py +553 -0
  210. runbooks/vpc/cross_account_session.py +101 -106
  211. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  212. runbooks/vpc/eni_gate_validator.py +961 -0
  213. runbooks/vpc/heatmap_engine.py +190 -162
  214. runbooks/vpc/mcp_no_eni_validator.py +681 -640
  215. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  216. runbooks/vpc/networking_wrapper.py +15 -8
  217. runbooks/vpc/pdca_remediation_planner.py +528 -0
  218. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  219. runbooks/vpc/runbooks_adapter.py +1167 -241
  220. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  221. runbooks/vpc/test_data_loader.py +358 -0
  222. runbooks/vpc/tests/conftest.py +314 -4
  223. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  224. runbooks/vpc/tests/test_cost_engine.py +0 -2
  225. runbooks/vpc/topology_generator.py +326 -0
  226. runbooks/vpc/unified_scenarios.py +1302 -1129
  227. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  228. runbooks-1.1.5.dist-info/METADATA +328 -0
  229. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/RECORD +233 -200
  230. runbooks/finops/README.md +0 -414
  231. runbooks/finops/accuracy_cross_validator.py +0 -647
  232. runbooks/finops/business_cases.py +0 -950
  233. runbooks/finops/dashboard_router.py +0 -922
  234. runbooks/finops/ebs_optimizer.py +0 -956
  235. runbooks/finops/embedded_mcp_validator.py +0 -1629
  236. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  237. runbooks/finops/finops_dashboard.py +0 -584
  238. runbooks/finops/finops_scenarios.py +0 -1218
  239. runbooks/finops/legacy_migration.py +0 -730
  240. runbooks/finops/multi_dashboard.py +0 -1519
  241. runbooks/finops/single_dashboard.py +0 -1113
  242. runbooks/finops/unlimited_scenarios.py +0 -393
  243. runbooks-1.1.3.dist-info/METADATA +0 -799
  244. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
  245. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
  246. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
  247. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -14,7 +14,7 @@ Features:
14
14
  - Thread-safe operations with concurrent access support
15
15
 
16
16
  Author: CloudOps Runbooks Team
17
- Version: 0.9.1
17
+ Version: latest version
18
18
  """
19
19
 
20
20
  import asyncio
@@ -40,21 +40,23 @@ from runbooks.common.rich_utils import (
40
40
 
41
41
  # Global Organizations cache shared across all instances and modules
42
42
  _GLOBAL_ORGS_CACHE = {
43
- 'data': None,
44
- 'accounts': None,
45
- 'organizational_units': None,
46
- 'timestamp': None,
47
- 'ttl_minutes': 30
43
+ "data": None,
44
+ "accounts": None,
45
+ "organizational_units": None,
46
+ "timestamp": None,
47
+ "ttl_minutes": 30,
48
48
  }
49
49
 
50
50
  # Thread lock for cache operations
51
51
  import threading
52
+
52
53
  _cache_lock = threading.Lock()
53
54
 
54
55
 
55
56
  @dataclass
56
57
  class OrganizationAccount:
57
58
  """Standard organization account representation across all modules"""
59
+
58
60
  account_id: str
59
61
  name: str
60
62
  email: str
@@ -77,6 +79,7 @@ class OrganizationAccount:
77
79
  @dataclass
78
80
  class OrganizationalUnit:
79
81
  """Standard organizational unit representation"""
82
+
80
83
  ou_id: str
81
84
  name: str
82
85
  parent_id: Optional[str] = None
@@ -93,20 +96,15 @@ class OrganizationalUnit:
93
96
  class UnifiedOrganizationsClient:
94
97
  """
95
98
  Unified Organizations API client consolidating patterns from all modules.
96
-
99
+
97
100
  This client provides a single interface for Organizations API operations
98
101
  with global caching, error handling, and performance optimization.
99
102
  """
100
103
 
101
- def __init__(
102
- self,
103
- management_profile: Optional[str] = None,
104
- cache_ttl_minutes: int = 30,
105
- max_workers: int = 50
106
- ):
104
+ def __init__(self, management_profile: Optional[str] = None, cache_ttl_minutes: int = 30, max_workers: int = 50):
107
105
  """
108
106
  Initialize unified Organizations client.
109
-
107
+
110
108
  Args:
111
109
  management_profile: AWS profile with Organizations access
112
110
  cache_ttl_minutes: Cache TTL in minutes (default: 30)
@@ -115,18 +113,18 @@ class UnifiedOrganizationsClient:
115
113
  self.management_profile = management_profile
116
114
  self.cache_ttl_minutes = cache_ttl_minutes
117
115
  self.max_workers = max_workers
118
-
116
+
119
117
  # Initialize session
120
118
  self.session = None
121
119
  self.client = None
122
-
120
+
123
121
  # Performance metrics
124
122
  self.metrics = {
125
- 'api_calls_made': 0,
126
- 'cache_hits': 0,
127
- 'cache_misses': 0,
128
- 'errors_encountered': 0,
129
- 'last_refresh': None,
123
+ "api_calls_made": 0,
124
+ "cache_hits": 0,
125
+ "cache_misses": 0,
126
+ "errors_encountered": 0,
127
+ "last_refresh": None,
130
128
  }
131
129
 
132
130
  def _initialize_client(self) -> bool:
@@ -138,28 +136,28 @@ class UnifiedOrganizationsClient:
138
136
  # Use profile resolution from existing patterns
139
137
  profile = get_profile_for_operation("management", None)
140
138
  self.session = boto3.Session(profile_name=profile)
141
-
139
+
142
140
  # Organizations is a global service - always use us-east-1
143
- self.client = self.session.client('organizations', region_name='us-east-1')
144
-
141
+ self.client = self.session.client("organizations", region_name="us-east-1")
142
+
145
143
  # Test connectivity
146
144
  self.client.describe_organization()
147
145
  return True
148
-
146
+
149
147
  except ClientError as e:
150
- error_code = e.response.get('Error', {}).get('Code', '')
151
- if error_code == 'AccessDeniedException':
148
+ error_code = e.response.get("Error", {}).get("Code", "")
149
+ if error_code == "AccessDeniedException":
152
150
  print_warning(f"Organizations access denied for profile '{self.management_profile}'")
153
- elif error_code == 'AWSOrganizationsNotInUseException':
151
+ elif error_code == "AWSOrganizationsNotInUseException":
154
152
  print_info("AWS Organizations not enabled for this account")
155
153
  else:
156
154
  print_warning(f"Organizations API error: {error_code}")
157
155
  return False
158
-
156
+
159
157
  except NoCredentialsError:
160
158
  print_warning("AWS credentials not available for Organizations API")
161
159
  return False
162
-
160
+
163
161
  except Exception as e:
164
162
  print_error(f"Failed to initialize Organizations client: {e}")
165
163
  return False
@@ -167,39 +165,37 @@ class UnifiedOrganizationsClient:
167
165
  def _is_cache_valid(self) -> bool:
168
166
  """Check if global cache is still valid"""
169
167
  with _cache_lock:
170
- if not _GLOBAL_ORGS_CACHE['timestamp']:
168
+ if not _GLOBAL_ORGS_CACHE["timestamp"]:
171
169
  return False
172
-
173
- cache_age_minutes = (
174
- datetime.now(timezone.utc) - _GLOBAL_ORGS_CACHE['timestamp']
175
- ).total_seconds() / 60
176
-
170
+
171
+ cache_age_minutes = (datetime.now(timezone.utc) - _GLOBAL_ORGS_CACHE["timestamp"]).total_seconds() / 60
172
+
177
173
  return cache_age_minutes < self.cache_ttl_minutes
178
174
 
179
175
  def _get_cached_data(self, data_type: str) -> Optional[any]:
180
176
  """Get specific cached data type"""
181
177
  if self._is_cache_valid():
182
178
  with _cache_lock:
183
- self.metrics['cache_hits'] += 1
184
- if data_type == 'accounts':
185
- return _GLOBAL_ORGS_CACHE.get('accounts')
186
- elif data_type == 'organizational_units':
187
- return _GLOBAL_ORGS_CACHE.get('organizational_units')
188
- elif data_type == 'complete':
189
- return _GLOBAL_ORGS_CACHE.get('data')
190
-
191
- self.metrics['cache_misses'] += 1
179
+ self.metrics["cache_hits"] += 1
180
+ if data_type == "accounts":
181
+ return _GLOBAL_ORGS_CACHE.get("accounts")
182
+ elif data_type == "organizational_units":
183
+ return _GLOBAL_ORGS_CACHE.get("organizational_units")
184
+ elif data_type == "complete":
185
+ return _GLOBAL_ORGS_CACHE.get("data")
186
+
187
+ self.metrics["cache_misses"] += 1
192
188
  return None
193
189
 
194
190
  def _set_cached_data(self, accounts: List[OrganizationAccount], ous: List[OrganizationalUnit], complete_data: Dict):
195
191
  """Set cached data with thread safety"""
196
192
  with _cache_lock:
197
- _GLOBAL_ORGS_CACHE['accounts'] = accounts
198
- _GLOBAL_ORGS_CACHE['organizational_units'] = ous
199
- _GLOBAL_ORGS_CACHE['data'] = complete_data
200
- _GLOBAL_ORGS_CACHE['timestamp'] = datetime.now(timezone.utc)
201
- self.metrics['last_refresh'] = datetime.now(timezone.utc)
202
-
193
+ _GLOBAL_ORGS_CACHE["accounts"] = accounts
194
+ _GLOBAL_ORGS_CACHE["organizational_units"] = ous
195
+ _GLOBAL_ORGS_CACHE["data"] = complete_data
196
+ _GLOBAL_ORGS_CACHE["timestamp"] = datetime.now(timezone.utc)
197
+ self.metrics["last_refresh"] = datetime.now(timezone.utc)
198
+
203
199
  accounts_count = len(accounts) if accounts else 0
204
200
  ous_count = len(ous) if ous else 0
205
201
  print_success(f"✅ Organizations cache updated: {accounts_count} accounts, {ous_count} OUs")
@@ -207,15 +203,15 @@ class UnifiedOrganizationsClient:
207
203
  async def get_organization_accounts(self, include_tags: bool = False) -> List[OrganizationAccount]:
208
204
  """
209
205
  Get all organization accounts with caching support.
210
-
206
+
211
207
  Args:
212
208
  include_tags: Whether to include account tags (slower but more comprehensive)
213
-
209
+
214
210
  Returns:
215
211
  List of OrganizationAccount objects
216
212
  """
217
213
  # Check cache first
218
- cached_accounts = self._get_cached_data('accounts')
214
+ cached_accounts = self._get_cached_data("accounts")
219
215
  if cached_accounts:
220
216
  print_info(f"🚀 Using cached account data ({len(cached_accounts)} accounts)")
221
217
  return cached_accounts
@@ -233,36 +229,32 @@ class UnifiedOrganizationsClient:
233
229
  task = progress.add_task("Discovering accounts...", total=None)
234
230
 
235
231
  # Get accounts using paginator for large organizations
236
- paginator = self.client.get_paginator('list_accounts')
237
-
232
+ paginator = self.client.get_paginator("list_accounts")
233
+
238
234
  for page in paginator.paginate():
239
- for account_data in page['Accounts']:
235
+ for account_data in page["Accounts"]:
240
236
  account = OrganizationAccount(
241
- account_id=account_data['Id'],
242
- name=account_data['Name'],
243
- email=account_data['Email'],
244
- status=account_data['Status'],
245
- joined_method=account_data['JoinedMethod'],
246
- joined_timestamp=account_data['JoinedTimestamp'],
237
+ account_id=account_data["Id"],
238
+ name=account_data["Name"],
239
+ email=account_data["Email"],
240
+ status=account_data["Status"],
241
+ joined_method=account_data["JoinedMethod"],
242
+ joined_timestamp=account_data["JoinedTimestamp"],
247
243
  )
248
244
 
249
245
  # Get account tags if requested
250
246
  if include_tags:
251
247
  try:
252
- tags_response = self.client.list_tags_for_resource(
253
- ResourceId=account.account_id
254
- )
255
- account.tags = {
256
- tag['Key']: tag['Value'] for tag in tags_response['Tags']
257
- }
258
- self.metrics['api_calls_made'] += 1
248
+ tags_response = self.client.list_tags_for_resource(ResourceId=account.account_id)
249
+ account.tags = {tag["Key"]: tag["Value"] for tag in tags_response["Tags"]}
250
+ self.metrics["api_calls_made"] += 1
259
251
  except ClientError:
260
252
  # Tags may not be accessible for all accounts
261
253
  account.tags = {}
262
254
 
263
255
  accounts.append(account)
264
-
265
- self.metrics['api_calls_made'] += 1
256
+
257
+ self.metrics["api_calls_made"] += 1
266
258
  progress.update(task, description=f"Found {len(accounts)} accounts...")
267
259
 
268
260
  # Map accounts to OUs
@@ -272,19 +264,19 @@ class UnifiedOrganizationsClient:
272
264
  return accounts
273
265
 
274
266
  except Exception as e:
275
- self.metrics['errors_encountered'] += 1
267
+ self.metrics["errors_encountered"] += 1
276
268
  print_error(f"Failed to discover organization accounts: {e}")
277
269
  return []
278
270
 
279
271
  async def get_organizational_units(self) -> List[OrganizationalUnit]:
280
272
  """
281
273
  Get all organizational units with caching support.
282
-
274
+
283
275
  Returns:
284
276
  List of OrganizationalUnit objects
285
277
  """
286
278
  # Check cache first
287
- cached_ous = self._get_cached_data('organizational_units')
279
+ cached_ous = self._get_cached_data("organizational_units")
288
280
  if cached_ous:
289
281
  print_info(f"🚀 Using cached OU data ({len(cached_ous)} OUs)")
290
282
  return cached_ous
@@ -300,47 +292,43 @@ class UnifiedOrganizationsClient:
300
292
  try:
301
293
  # Get root OU
302
294
  roots_response = self.client.list_roots()
303
- if not roots_response.get('Roots'):
295
+ if not roots_response.get("Roots"):
304
296
  print_warning("No root organizational units found")
305
297
  return []
306
298
 
307
- root_id = roots_response['Roots'][0]['Id']
308
- self.metrics['api_calls_made'] += 1
299
+ root_id = roots_response["Roots"][0]["Id"]
300
+ self.metrics["api_calls_made"] += 1
309
301
 
310
302
  # Recursively discover all OUs
311
303
  await self._discover_ou_recursive(root_id, all_ous)
312
-
304
+
313
305
  print_success(f"✅ Discovered {len(all_ous)} organizational units")
314
306
  return all_ous
315
307
 
316
308
  except Exception as e:
317
- self.metrics['errors_encountered'] += 1
309
+ self.metrics["errors_encountered"] += 1
318
310
  print_error(f"Failed to discover organizational units: {e}")
319
311
  return []
320
312
 
321
313
  async def _discover_ou_recursive(self, parent_id: str, ou_list: List[OrganizationalUnit]):
322
314
  """Recursively discover organizational units"""
323
315
  try:
324
- paginator = self.client.get_paginator('list_organizational_units_for_parent')
325
-
316
+ paginator = self.client.get_paginator("list_organizational_units_for_parent")
317
+
326
318
  for page in paginator.paginate(ParentId=parent_id):
327
- for ou_data in page['OrganizationalUnits']:
328
- ou = OrganizationalUnit(
329
- ou_id=ou_data['Id'],
330
- name=ou_data['Name'],
331
- parent_id=parent_id
332
- )
333
-
319
+ for ou_data in page["OrganizationalUnits"]:
320
+ ou = OrganizationalUnit(ou_id=ou_data["Id"], name=ou_data["Name"], parent_id=parent_id)
321
+
334
322
  ou_list.append(ou)
335
-
323
+
336
324
  # Recursively discover child OUs
337
325
  await self._discover_ou_recursive(ou.ou_id, ou_list)
338
-
339
- self.metrics['api_calls_made'] += 1
326
+
327
+ self.metrics["api_calls_made"] += 1
340
328
 
341
329
  except ClientError as e:
342
330
  print_warning(f"Failed to discover OU children for {parent_id}: {e}")
343
- self.metrics['errors_encountered'] += 1
331
+ self.metrics["errors_encountered"] += 1
344
332
 
345
333
  async def _map_accounts_to_ous(self, accounts: List[OrganizationAccount]):
346
334
  """Map accounts to their organizational units"""
@@ -348,182 +336,180 @@ class UnifiedOrganizationsClient:
348
336
  return
349
337
 
350
338
  print_info("🗺️ Mapping accounts to organizational units...")
351
-
339
+
352
340
  with create_progress_bar() as progress:
353
341
  task = progress.add_task("Mapping accounts to OUs...", total=len(accounts))
354
-
342
+
355
343
  for account in accounts:
356
344
  try:
357
345
  parents_response = self.client.list_parents(ChildId=account.account_id)
358
-
359
- if parents_response['Parents']:
360
- parent = parents_response['Parents'][0]
361
- account.parent_id = parent['Id']
362
-
346
+
347
+ if parents_response["Parents"]:
348
+ parent = parents_response["Parents"][0]
349
+ account.parent_id = parent["Id"]
350
+
363
351
  # Get OU name if parent is an OU
364
- if parent['Type'] == 'ORGANIZATIONAL_UNIT':
352
+ if parent["Type"] == "ORGANIZATIONAL_UNIT":
365
353
  try:
366
354
  ou_response = self.client.describe_organizational_unit(
367
- OrganizationalUnitId=parent['Id']
355
+ OrganizationalUnitId=parent["Id"]
368
356
  )
369
- account.organizational_unit = ou_response['OrganizationalUnit']['Name']
370
- self.metrics['api_calls_made'] += 1
357
+ account.organizational_unit = ou_response["OrganizationalUnit"]["Name"]
358
+ self.metrics["api_calls_made"] += 1
371
359
  except ClientError:
372
360
  account.organizational_unit = f"OU-{parent['Id']}"
373
-
374
- self.metrics['api_calls_made'] += 1
375
-
361
+
362
+ self.metrics["api_calls_made"] += 1
363
+
376
364
  except ClientError:
377
365
  # Continue with other accounts
378
- self.metrics['errors_encountered'] += 1
379
-
366
+ self.metrics["errors_encountered"] += 1
367
+
380
368
  progress.advance(task)
381
369
 
382
370
  async def get_complete_organization_structure(self, include_tags: bool = False) -> Dict:
383
371
  """
384
372
  Get complete organization structure with caching.
385
-
373
+
386
374
  This method provides compatibility with existing inventory module patterns.
387
-
375
+
388
376
  Args:
389
377
  include_tags: Whether to include account tags
390
-
378
+
391
379
  Returns:
392
380
  Complete organization structure dictionary
393
381
  """
394
382
  # Check for complete cached data
395
- cached_data = self._get_cached_data('complete')
383
+ cached_data = self._get_cached_data("complete")
396
384
  if cached_data:
397
385
  print_info("🚀 Using cached complete organization structure")
398
386
  return cached_data
399
387
 
400
388
  print_info("🏢 Discovering complete organization structure...")
401
-
389
+
402
390
  # Get accounts and OUs
403
391
  accounts = await self.get_organization_accounts(include_tags=include_tags)
404
392
  ous = await self.get_organizational_units()
405
-
393
+
406
394
  # Get organization info
407
395
  org_info = await self._get_organization_info()
408
-
396
+
409
397
  # Build complete structure
410
398
  complete_data = {
411
- 'status': 'completed',
412
- 'discovery_type': 'unified_organizations_api',
413
- 'organization_info': org_info,
414
- 'accounts': {
415
- 'total_accounts': len(accounts),
416
- 'active_accounts': len([a for a in accounts if a.status == 'ACTIVE']),
417
- 'discovered_accounts': [a.to_dict() for a in accounts],
418
- 'discovery_method': 'organizations_api',
399
+ "status": "completed",
400
+ "discovery_type": "unified_organizations_api",
401
+ "organization_info": org_info,
402
+ "accounts": {
403
+ "total_accounts": len(accounts),
404
+ "active_accounts": len([a for a in accounts if a.status == "ACTIVE"]),
405
+ "discovered_accounts": [a.to_dict() for a in accounts],
406
+ "discovery_method": "organizations_api",
419
407
  },
420
- 'organizational_units': {
421
- 'total_ous': len(ous),
422
- 'organizational_units': [asdict(ou) for ou in ous],
423
- 'discovery_method': 'organizations_api',
408
+ "organizational_units": {
409
+ "total_ous": len(ous),
410
+ "organizational_units": [asdict(ou) for ou in ous],
411
+ "discovery_method": "organizations_api",
424
412
  },
425
- 'metrics': self.metrics.copy(),
426
- 'timestamp': datetime.now().isoformat(),
413
+ "metrics": self.metrics.copy(),
414
+ "timestamp": datetime.now().isoformat(),
427
415
  }
428
-
416
+
429
417
  # Cache the complete structure
430
418
  self._set_cached_data(accounts, ous, complete_data)
431
-
419
+
432
420
  return complete_data
433
421
 
434
422
  async def _get_organization_info(self) -> Dict:
435
423
  """Get high-level organization information"""
436
424
  if not self.client:
437
425
  return {
438
- 'organization_id': 'unavailable',
439
- 'master_account_id': 'unavailable',
440
- 'master_account_email': 'unavailable',
441
- 'feature_set': 'unavailable',
442
- 'available_policy_types': [],
443
- 'discovery_method': 'unavailable',
426
+ "organization_id": "unavailable",
427
+ "master_account_id": "unavailable",
428
+ "master_account_email": "unavailable",
429
+ "feature_set": "unavailable",
430
+ "available_policy_types": [],
431
+ "discovery_method": "unavailable",
444
432
  }
445
433
 
446
434
  try:
447
435
  org_response = self.client.describe_organization()
448
- org = org_response['Organization']
449
- self.metrics['api_calls_made'] += 1
450
-
436
+ org = org_response["Organization"]
437
+ self.metrics["api_calls_made"] += 1
438
+
451
439
  return {
452
- 'organization_id': org['Id'],
453
- 'master_account_id': org['MasterAccountId'],
454
- 'master_account_email': org['MasterAccountEmail'],
455
- 'feature_set': org['FeatureSet'],
456
- 'available_policy_types': [pt['Type'] for pt in org.get('AvailablePolicyTypes', [])],
457
- 'discovery_method': 'organizations_api',
440
+ "organization_id": org["Id"],
441
+ "master_account_id": org["MasterAccountId"],
442
+ "master_account_email": org["MasterAccountEmail"],
443
+ "feature_set": org["FeatureSet"],
444
+ "available_policy_types": [pt["Type"] for pt in org.get("AvailablePolicyTypes", [])],
445
+ "discovery_method": "organizations_api",
458
446
  }
459
-
447
+
460
448
  except ClientError as e:
461
449
  print_warning(f"Failed to get organization info: {e}")
462
450
  return {
463
- 'organization_id': 'error',
464
- 'master_account_id': 'error',
465
- 'master_account_email': 'error',
466
- 'feature_set': 'error',
467
- 'available_policy_types': [],
468
- 'discovery_method': 'failed',
469
- 'error': str(e),
451
+ "organization_id": "error",
452
+ "master_account_id": "error",
453
+ "master_account_email": "error",
454
+ "feature_set": "error",
455
+ "available_policy_types": [],
456
+ "discovery_method": "failed",
457
+ "error": str(e),
470
458
  }
471
459
 
472
460
  def get_account_name_mapping(self) -> Dict[str, str]:
473
461
  """
474
462
  Get account ID to name mapping for compatibility with FinOps module.
475
-
463
+
476
464
  Returns:
477
465
  Dictionary mapping account IDs to account names
478
466
  """
479
- cached_accounts = self._get_cached_data('accounts')
467
+ cached_accounts = self._get_cached_data("accounts")
480
468
  if not cached_accounts:
481
469
  # Try to refresh cache
482
470
  import asyncio
471
+
483
472
  try:
484
- cached_accounts = asyncio.get_event_loop().run_until_complete(
485
- self.get_organization_accounts()
486
- )
473
+ cached_accounts = asyncio.get_event_loop().run_until_complete(self.get_organization_accounts())
487
474
  except:
488
475
  return {}
489
-
476
+
490
477
  return {account.account_id: account.name for account in cached_accounts}
491
478
 
492
479
  def invalidate_cache(self):
493
480
  """Manually invalidate the global cache"""
494
481
  with _cache_lock:
495
- _GLOBAL_ORGS_CACHE['data'] = None
496
- _GLOBAL_ORGS_CACHE['accounts'] = None
497
- _GLOBAL_ORGS_CACHE['organizational_units'] = None
498
- _GLOBAL_ORGS_CACHE['timestamp'] = None
499
-
482
+ _GLOBAL_ORGS_CACHE["data"] = None
483
+ _GLOBAL_ORGS_CACHE["accounts"] = None
484
+ _GLOBAL_ORGS_CACHE["organizational_units"] = None
485
+ _GLOBAL_ORGS_CACHE["timestamp"] = None
486
+
500
487
  print_info("🗑️ Organizations cache invalidated")
501
488
 
502
489
  def get_cache_status(self) -> Dict:
503
490
  """Get cache status and metrics"""
504
491
  with _cache_lock:
505
492
  return {
506
- 'cache_valid': self._is_cache_valid(),
507
- 'cache_timestamp': _GLOBAL_ORGS_CACHE.get('timestamp'),
508
- 'ttl_minutes': self.cache_ttl_minutes,
509
- 'metrics': self.metrics.copy(),
510
- 'accounts_cached': len(_GLOBAL_ORGS_CACHE.get('accounts', [])),
511
- 'ous_cached': len(_GLOBAL_ORGS_CACHE.get('organizational_units', [])),
493
+ "cache_valid": self._is_cache_valid(),
494
+ "cache_timestamp": _GLOBAL_ORGS_CACHE.get("timestamp"),
495
+ "ttl_minutes": self.cache_ttl_minutes,
496
+ "metrics": self.metrics.copy(),
497
+ "accounts_cached": len(_GLOBAL_ORGS_CACHE.get("accounts", [])),
498
+ "ous_cached": len(_GLOBAL_ORGS_CACHE.get("organizational_units", [])),
512
499
  }
513
500
 
514
501
 
515
502
  # Factory functions for easy integration with existing modules
516
503
  def get_unified_organizations_client(
517
- management_profile: Optional[str] = None,
518
- cache_ttl_minutes: int = 30
504
+ management_profile: Optional[str] = None, cache_ttl_minutes: int = 30
519
505
  ) -> UnifiedOrganizationsClient:
520
506
  """
521
507
  Factory function to get unified Organizations client.
522
-
508
+
523
509
  Args:
524
510
  management_profile: AWS profile with Organizations access
525
511
  cache_ttl_minutes: Cache TTL in minutes
526
-
512
+
527
513
  Returns:
528
514
  UnifiedOrganizationsClient instance
529
515
  """
@@ -531,16 +517,15 @@ def get_unified_organizations_client(
531
517
 
532
518
 
533
519
  async def get_organization_accounts(
534
- management_profile: Optional[str] = None,
535
- include_tags: bool = False
520
+ management_profile: Optional[str] = None, include_tags: bool = False
536
521
  ) -> List[OrganizationAccount]:
537
522
  """
538
523
  Convenience function to get organization accounts.
539
-
524
+
540
525
  Args:
541
526
  management_profile: AWS profile with Organizations access
542
527
  include_tags: Whether to include account tags
543
-
528
+
544
529
  Returns:
545
530
  List of OrganizationAccount objects
546
531
  """
@@ -548,19 +533,16 @@ async def get_organization_accounts(
548
533
  return await client.get_organization_accounts(include_tags)
549
534
 
550
535
 
551
- async def get_organization_structure(
552
- management_profile: Optional[str] = None,
553
- include_tags: bool = False
554
- ) -> Dict:
536
+ async def get_organization_structure(management_profile: Optional[str] = None, include_tags: bool = False) -> Dict:
555
537
  """
556
538
  Convenience function to get complete organization structure.
557
-
539
+
558
540
  This function provides backward compatibility with existing inventory module.
559
-
541
+
560
542
  Args:
561
543
  management_profile: AWS profile with Organizations access
562
544
  include_tags: Whether to include account tags
563
-
545
+
564
546
  Returns:
565
547
  Complete organization structure dictionary
566
548
  """
@@ -570,10 +552,10 @@ async def get_organization_structure(
570
552
 
571
553
  # Export public interface
572
554
  __all__ = [
573
- 'UnifiedOrganizationsClient',
574
- 'OrganizationAccount',
575
- 'OrganizationalUnit',
576
- 'get_unified_organizations_client',
577
- 'get_organization_accounts',
578
- 'get_organization_structure',
579
- ]
555
+ "UnifiedOrganizationsClient",
556
+ "OrganizationAccount",
557
+ "OrganizationalUnit",
558
+ "get_unified_organizations_client",
559
+ "get_organization_accounts",
560
+ "get_organization_structure",
561
+ ]