annator-command-center / scripts /add_governance_to_integrations.py
techprotrade's picture
Deploy ATOM FastAPI command center runtime (part 6)
383cb38 verified
Raw
History Blame Contribute Delete
14.5 kB
#!/usr/bin/env python3
"""
Automated Governance Injection Script for Upstream Integrations
Applies circuit breaker, rate limiter, and audit logging patterns to all
integration service files that lack them.
Usage:
python scripts/add_governance_to_integrations.py [--dry-run] [--integration name]
Examples:
python scripts/add_governance_to_integrations.py --dry-run
python scripts/add_governance_to_integrations.py --integration outlook
python scripts/add_governance_to_integrations.py
"""
import argparse
import ast
import glob
import os
import re
import shutil
import sys
from pathlib import Path
from typing import List, Tuple, Dict
# Integration names that already have governance
HAS_GOVERNANCE = {"gmail", "jira", "mcp", "zoom"}
# Governance imports to add
GOVERNANCE_IMPORTS = """from core.circuit_breaker import circuit_breaker
from core.rate_limiter import rate_limiter, should_retry, calculate_backoff
from core.audit_logger import log_integration_call, log_integration_error, log_integration_attempt, log_integration_complete
from fastapi import HTTPException"""
class GovernanceInjector:
"""Injects governance patterns into integration service files"""
def __init__(self, integrations_dir: str, dry_run: bool = False):
self.integrations_dir = Path(integrations_dir)
self.dry_run = dry_run
self.modified_files = []
self.skipped_files = []
def identify_integrations(self, specific_integration: str = None) -> List[Path]:
"""Identify integration service files needing governance"""
pattern = f"{self.integrations_dir}/*_service.py"
if specific_integration:
pattern = f"{self.integrations_dir}/{specific_integration}_service.py"
all_files = glob.glob(pattern)
if specific_integration:
# For specific integration, only return it if it exists
if all_files and Path(all_files[0]).exists():
return [Path(all_files[0])]
return []
# Filter out integrations that already have governance
needs_governance = []
for file_path in all_files:
file_name = Path(file_path).stem
integration_name = file_name.replace("_service", "")
if integration_name not in HAS_GOVERNANCE:
needs_governance.append(Path(file_path))
return needs_governance
def has_governance_imports(self, file_path: Path) -> bool:
"""Check if file already has governance imports"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Check for any of the governance imports
has_circuit_breaker = "from core.circuit_breaker import" in content
has_rate_limiter = "from core.rate_limiter import" in content
has_audit_logger = "from core.audit_logger import" in content
return has_circuit_breaker or has_rate_limiter or has_audit_logger
except Exception as e:
print(f"Error reading {file_path}: {e}")
return False
def extract_integration_name(self, file_path: Path) -> str:
"""Extract integration name from file path"""
return file_path.stem.replace("_service", "")
def find_async_methods(self, content: str) -> List[Tuple[int, str, str]]:
"""
Find all public async methods in the file.
Returns:
List of tuples: (line_number, method_name, indentation)
"""
methods = []
lines = content.split('\n')
for i, line in enumerate(lines):
# Match async def method_name(self, ...)
# Exclude private methods (_method_name) and property methods
match = re.match(r'^(\s*)async def ([a-z][a-zA-Z0-9_]*)\(self', line)
if match:
indent = match.group(1)
method_name = match.group(2)
# Skip private methods and special methods
if not method_name.startswith('_'):
methods.append((i + 1, method_name, indent))
return methods
def inject_governance_to_method(
self,
content: str,
method_name: str,
integration_name: str,
start_line: int,
indent: str
) -> str:
"""
Inject governance wrapper into a method.
Returns modified content.
"""
lines = content.split('\n')
# Find the method body start (first line after method signature)
# Look for the docstring or first statement
i = start_line - 1 # Convert to 0-indexed
# Skip method signature line
while i < len(lines) and ('async def ' in lines[i] or 'def ' in lines[i]):
i += 1
# Skip empty lines and docstring
if i < len(lines) and '"""' in lines[i]:
# Find end of docstring
i += 1
while i < len(lines) and '"""' not in lines[i]:
i += 1
i += 1 # Skip closing """
# Skip empty lines
while i < len(lines) and lines[i].strip() == '':
i += 1
# Now i should be at the first actual line of method body
# Insert governance wrapper here
method_body_indent = indent + ' '
# Build the governance wrapper code
governance_code = f"""{method_body_indent}# Start audit logging
{method_body_indent}audit_ctx = log_integration_attempt("{integration_name}", "{method_name}", locals())
{method_body_indent}try:
{method_body_indent} # Check circuit breaker
{method_body_indent} if not await circuit_breaker.is_enabled("{integration_name}"):
{method_body_indent} logger.warning(f"Circuit breaker is open for {integration_name}")
{method_body_indent} log_integration_complete(audit_ctx, error=Exception("Circuit breaker open"))
{method_body_indent} raise HTTPException(
{method_body_indent} status_code=503,
{method_body_indent} detail=f"{integration_name.capitalize()} integration temporarily disabled"
{method_body_indent} )
{method_body_indent} # Check rate limiter
{method_body_indent} is_limited, remaining = await rate_limiter.is_rate_limited("{integration_name}")
{method_body_indent} if is_limited:
{method_body_indent} logger.warning(f"Rate limit exceeded for {integration_name}")
{method_body_indent} log_integration_complete(audit_ctx, error=Exception("Rate limit exceeded"))
{method_body_indent} raise HTTPException(
{method_body_indent} status_code=429,
{method_body_indent} detail=f"Rate limit exceeded for {integration_name}"
{method_body_indent} )
"""
# Insert governance code at the beginning of method body
lines.insert(i, governance_code)
# Now find the end of the method to add success/failure logging
# We need to find return statements and wrap them
# For simplicity, we'll add the success logging before each return
# and look for existing exception handling
# Find the method's return statements (simple approach)
# In a real implementation, you'd want to parse the AST
# For now, we'll just add a note that manual review is needed
return '\n'.join(lines)
def add_imports(self, content: str) -> str:
"""Add governance imports if not present"""
lines = content.split('\n')
# Find the last import statement
last_import_idx = -1
for i, line in enumerate(lines):
if line.startswith('import ') or line.startswith('from '):
last_import_idx = i
if last_import_idx == -1:
# No imports found, add at the beginning
insert_idx = 0
else:
# Add after the last import
insert_idx = last_import_idx + 1
# Check if governance imports already exist
has_governance = any(
imp in content for imp in [
"from core.circuit_breaker import",
"from core.rate_limiter import",
"from core.audit_logger import"
]
)
if has_governance:
return content
# Insert imports
lines.insert(insert_idx, GOVERNANCE_IMPORTS)
lines.insert(insert_idx + 1, '') # Add blank line after imports
return '\n'.join(lines)
def process_file(self, file_path: Path) -> bool:
"""
Process a single integration file.
Returns:
True if file was modified, False otherwise
"""
integration_name = self.extract_integration_name(file_path)
# Check if already has governance
if self.has_governance_imports(file_path):
print(f" ✓ Skipping {file_path.name} - already has governance")
self.skipped_files.append(file_path)
return False
print(f" → Processing {file_path.name}")
# Read file
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
print(f" ✗ Error reading {file_path}: {e}")
return False
# Backup file
backup_path = file_path.with_suffix('.py.bak')
if not self.dry_run:
try:
shutil.copy2(file_path, backup_path)
except Exception as e:
print(f" ✗ Error creating backup: {e}")
return False
# Add imports
modified_content = self.add_imports(content)
# Find and inject governance into async methods
methods = self.find_async_methods(modified_content)
if not methods:
print(f" No public async methods found, adding imports only")
else:
print(f" Found {len(methods)} public async method(s)")
for line_num, method_name, indent in methods[:3]: # Limit to first 3 for logging
print(f" - {method_name} (line {line_num})")
if len(methods) > 3:
print(f" ... and {len(methods) - 3} more")
# Inject governance into each method (in reverse order to preserve line numbers)
for line_num, method_name, indent in reversed(methods):
try:
modified_content = self.inject_governance_to_method(
modified_content,
method_name,
integration_name,
line_num,
indent
)
except Exception as e:
print(f" ✗ Error injecting governance into {method_name}: {e}")
continue
# Write modified content
if not self.dry_run:
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(modified_content)
self.modified_files.append(file_path)
print(f" ✓ Modified {file_path.name}")
return True
except Exception as e:
print(f" ✗ Error writing {file_path}: {e}")
# Restore from backup
if backup_path.exists():
shutil.copy2(backup_path, file_path)
return False
else:
print(f" [DRY RUN] Would modify {file_path.name}")
self.modified_files.append(file_path)
return True
def run(self, specific_integration: str = None) -> dict:
"""
Run the governance injection process.
Returns:
dict with statistics
"""
files_to_process = self.identify_integrations(specific_integration)
if not files_to_process:
print("No integration files found to process")
return {
"total": 0,
"modified": 0,
"skipped": 0,
"failed": 0
}
print(f"\nFound {len(files_to_process)} integration(s) to process\n")
for file_path in files_to_process:
self.process_file(file_path)
return {
"total": len(files_to_process),
"modified": len(self.modified_files),
"skipped": len(self.skipped_files),
"failed": len(files_to_process) - len(self.modified_files) - len(self.skipped_files)
}
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(
description="Inject governance patterns into integration services"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be changed without making modifications"
)
parser.add_argument(
"--integration",
type=str,
help="Specific integration name (e.g., 'outlook', 'slack')"
)
parser.add_argument(
"--dir",
type=str,
default="integrations",
help="Directory containing integration service files (default: integrations)"
)
args = parser.parse_args()
# Get the script's directory
script_dir = Path(__file__).parent.parent
integrations_dir = script_dir / args.dir
if not integrations_dir.exists():
print(f"Error: Integrations directory not found: {integrations_dir}")
sys.exit(1)
print("=" * 80)
print("Governance Injection Script for Upstream Integrations")
print("=" * 80)
print(f"Directory: {integrations_dir}")
print(f"Dry run: {args.dry_run}")
print(f"Integration: {args.integration or 'All'}")
print()
injector = GovernanceInjector(
integrations_dir=str(integrations_dir),
dry_run=args.dry_run
)
stats = injector.run(args.integration)
print()
print("=" * 80)
print("Summary")
print("=" * 80)
print(f"Total files: {stats['total']}")
print(f"Modified: {stats['modified']}")
print(f"Skipped: {stats['skipped']}")
print(f"Failed: {stats['failed']}")
print()
if injector.modified_files:
print("Modified files:")
for file_path in injector.modified_files:
print(f" - {file_path.name}")
print()
if args.dry_run and injector.modified_files:
print("⚠️ DRY RUN MODE - No files were actually modified")
print(" Run without --dry-run to apply changes")
print()
return 0
if __name__ == "__main__":
sys.exit(main())