# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ Generate a simplified draw.io diagram showing container-to-container imports. Aggregates all module imports into container-level relationships. """ import csv from pathlib import Path from typing import Dict, Tuple from collections import defaultdict import html import math # Paths SCRIPT_DIR = Path(__file__).parent INPUT_FILE = SCRIPT_DIR / "import_analysis.csv" OUTPUT_FILE = SCRIPT_DIR / "import_diagram_containers.drawio" # Container colors CONTAINER_COLORS = { "app": "#dae8fc", # Light blue "aichat": "#d5e8d4", # Light green "auth": "#ffe6cc", # Light orange "connectors": "#e1d5e7", # Light purple "datamodels": "#fff2cc", # Light yellow "interfaces": "#f8cecc", # Light red "routes": "#d0cee2", # Light violet "security": "#fad7ac", # Peach "services": "#b1ddf0", # Sky blue "shared": "#f0fff0", # Honeydew "workflows": "#f5f5f5", # Light gray "features": "#e2efda", # Sage green } def _getContainer(moduleName: str) -> str: """Extract container name from module path.""" if moduleName == "gateway.app": return "app" parts = moduleName.replace("gateway.", "").split(".") if len(parts) < 2: return "app" container = parts[1] # modules.XXX or tests.XXX or scripts # Skip tests and scripts if container in ("tests", "scripts") or container.startswith("script_"): return None if parts[0] in ("tests", "scripts"): return None # Handle features sub-containers if container == "features" and len(parts) > 2: return f"features.{parts[2]}" return container def _parseContainerImports() -> Tuple[Dict[str, int], Dict[Tuple[str, str], int]]: """ Parse import_analysis.csv and return: - containerModuleCounts: Dict mapping container name to module count - containerEdges: Dict mapping (source_container, target_container) to import count """ containerModules = defaultdict(set) containerEdges = defaultdict(int) with open(INPUT_FILE, "r", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: sourceFull = row["module_name"] targetFull = row["imported_module_name"] # Skip external imports and relative imports if not targetFull.startswith("modules."): continue if targetFull.startswith("(relative)"): continue # Add gateway prefix to target for consistency targetFull = f"gateway.{targetFull}" # Get containers sourceContainer = _getContainer(sourceFull) targetContainer = _getContainer(targetFull) # Skip if either is None (tests/scripts) if sourceContainer is None or targetContainer is None: continue # Track modules per container containerModules[sourceContainer].add(sourceFull) containerModules[targetContainer].add(targetFull) # Skip self-imports (within same container) if sourceContainer == targetContainer: continue # Count container-to-container imports containerEdges[(sourceContainer, targetContainer)] += 1 # Convert module sets to counts containerModuleCounts = {k: len(v) for k, v in containerModules.items()} return containerModuleCounts, dict(containerEdges) def _generateDrawio(containerModuleCounts: Dict[str, int], containerEdges: Dict[Tuple[str, str], int]) -> str: """Generate draw.io XML content with container nodes and aggregated edges.""" containers = sorted(containerModuleCounts.keys()) numContainers = len(containers) # Arrange containers in a circle for better visibility centerX = 600 centerY = 500 radius = 400 nodeWidth = 140 nodeHeight = 60 # Calculate positions containerPositions = {} for i, container in enumerate(containers): angle = (2 * math.pi * i / numContainers) - math.pi / 2 # Start from top x = centerX + radius * math.cos(angle) - nodeWidth / 2 y = centerY + radius * math.sin(angle) - nodeHeight / 2 containerPositions[container] = (int(x), int(y)) cells = [] # Create container nodes for container in containers: x, y = containerPositions[container] moduleCount = containerModuleCounts[container] # Get color baseContainer = container.split(".")[0] color = CONTAINER_COLORS.get(baseContainer, "#ffffff") # Create node label = f"{container}\\n({moduleCount} modules)" cells.append(f''' ''') # Create edges with import counts edgeId = 1000 for (source, target), count in sorted(containerEdges.items(), key=lambda x: -x[1]): sourceId = f"container_{source.replace('.', '_')}" targetId = f"container_{target.replace('.', '_')}" # Thicker line for more imports strokeWidth = min(1 + count // 10, 5) cells.append(f''' ''') edgeId += 1 # Assemble XML xml = f''' {chr(10).join(cells)} ''' return xml def main(): """Main function.""" print("Parsing container imports...") containerModuleCounts, containerEdges = _parseContainerImports() print(f"Found {len(containerModuleCounts)} containers") print(f"Found {len(containerEdges)} container-to-container relationships") # Print summary print("\nContainer Import Summary:") print("-" * 60) # Sort by total imports (outgoing) outgoingCounts = defaultdict(int) incomingCounts = defaultdict(int) for (source, target), count in containerEdges.items(): outgoingCounts[source] += count incomingCounts[target] += count for container in sorted(containerModuleCounts.keys()): modules = containerModuleCounts[container] outgoing = outgoingCounts.get(container, 0) incoming = incomingCounts.get(container, 0) print(f" {container:25} {modules:3} modules | imports: {outgoing:4} out, {incoming:4} in") print("\nTop 15 Container Dependencies:") print("-" * 60) sortedEdges = sorted(containerEdges.items(), key=lambda x: -x[1])[:15] for (source, target), count in sortedEdges: print(f" {source:25} -> {target:25} : {count:4} imports") print("\nGenerating draw.io diagram...") xml = _generateDrawio(containerModuleCounts, containerEdges) with open(OUTPUT_FILE, "w", encoding="utf-8") as f: f.write(xml) print(f"\nDiagram saved to: {OUTPUT_FILE}") if __name__ == "__main__": main()