| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701 |
- # -*- coding: utf-8 -*-
- """
- Abhängigkeitsgraph für Service-Visualisierung.
- Ermöglicht die Erstellung von DOT- und Mermaid-Diagrammen
- zur Visualisierung von Service-Abhängigkeiten.
- """
- from __future__ import annotations
- from dataclasses import dataclass, field
- from enum import Enum, auto
- from typing import TYPE_CHECKING, Any, Iterator
- from trixy_core.service.enums import ServicePriority, ServiceGroup, ServiceState
- if TYPE_CHECKING:
- from trixy_core.service.iservice import IService
- class GraphFormat(Enum):
- """
- Ausgabeformate für den Graphen.
- """
- DOT = auto()
- """GraphViz DOT-Format."""
- MERMAID = auto()
- """Mermaid-Diagramm-Format."""
- JSON = auto()
- """JSON-Struktur."""
- ASCII = auto()
- """ASCII-Baum-Darstellung."""
- @dataclass
- class GraphNode:
- """
- Knoten im Abhängigkeitsgraphen.
- """
- name: str
- """Name des Services."""
- priority: ServicePriority
- """Priorität des Services."""
- group: ServiceGroup
- """Gruppe des Services."""
- state: ServiceState
- """Aktueller Zustand."""
- dependencies: list[str] = field(default_factory=list)
- """Liste der Abhängigkeiten."""
- dependents: list[str] = field(default_factory=list)
- """Liste der abhängigen Services."""
- metadata: dict[str, Any] = field(default_factory=dict)
- """Zusätzliche Metadaten."""
- def __hash__(self) -> int:
- """Hash für Set-Verwendung."""
- return hash(self.name)
- def __eq__(self, other: object) -> bool:
- """Gleichheitsvergleich."""
- if isinstance(other, GraphNode):
- return self.name == other.name
- return False
- @dataclass
- class GraphEdge:
- """
- Kante im Abhängigkeitsgraphen.
- """
- source: str
- """Quellknoten (abhängiger Service)."""
- target: str
- """Zielknoten (Abhängigkeit)."""
- edge_type: str = "depends_on"
- """Typ der Beziehung."""
- def __hash__(self) -> int:
- """Hash für Set-Verwendung."""
- return hash((self.source, self.target))
- def __eq__(self, other: object) -> bool:
- """Gleichheitsvergleich."""
- if isinstance(other, GraphEdge):
- return self.source == other.source and self.target == other.target
- return False
- @dataclass
- class GraphStyle:
- """
- Styling-Optionen für die Graphen-Ausgabe.
- """
- # Farben nach Priorität
- priority_colors: dict[ServicePriority, str] = field(default_factory=lambda: {
- ServicePriority.CRITICAL: "#ff6b6b",
- ServicePriority.CORE: "#feca57",
- ServicePriority.NETWORK: "#48dbfb",
- ServicePriority.MANAGER: "#1dd1a1",
- ServicePriority.PLUGIN: "#a55eea",
- ServicePriority.OPTIONAL: "#c8d6e5",
- })
- # Farben nach Zustand
- state_colors: dict[ServiceState, str] = field(default_factory=lambda: {
- ServiceState.STOPPED: "#c8d6e5",
- ServiceState.STARTING: "#feca57",
- ServiceState.RUNNING: "#1dd1a1",
- ServiceState.STOPPING: "#ff9f43",
- ServiceState.FAILED: "#ff6b6b",
- })
- # Formen nach Gruppe
- group_shapes: dict[ServiceGroup, str] = field(default_factory=lambda: {
- ServiceGroup.CRITICAL: "doubleoctagon",
- ServiceGroup.NETWORK: "hexagon",
- ServiceGroup.MANAGER: "box",
- ServiceGroup.AUDIO: "ellipse",
- ServiceGroup.ML: "diamond",
- ServiceGroup.UTILITY: "oval",
- ServiceGroup.PLUGIN: "component",
- })
- edge_color: str = "#576574"
- """Farbe für Kanten."""
- font_name: str = "Arial"
- """Schriftart."""
- font_size: int = 12
- """Schriftgröße."""
- class DependencyGraph:
- """
- Abhängigkeitsgraph für Services.
- Analysiert und visualisiert Service-Abhängigkeiten.
- Example:
- graph = DependencyGraph()
- graph.add_services(service_container.services)
- # DOT-Format exportieren
- dot = graph.to_dot()
- with open("services.dot", "w") as f:
- f.write(dot)
- # Mermaid-Diagramm
- mermaid = graph.to_mermaid()
- print(mermaid)
- # Zyklen erkennen
- cycles = graph.find_cycles()
- """
- def __init__(self, style: GraphStyle | None = None) -> None:
- """
- Initialisiert den Graphen.
- Args:
- style: Optionale Styling-Konfiguration.
- """
- self._nodes: dict[str, GraphNode] = {}
- self._edges: set[GraphEdge] = set()
- self._style = style or GraphStyle()
- @property
- def nodes(self) -> dict[str, GraphNode]:
- """Alle Knoten."""
- return dict(self._nodes)
- @property
- def edges(self) -> set[GraphEdge]:
- """Alle Kanten."""
- return set(self._edges)
- @property
- def node_count(self) -> int:
- """Anzahl der Knoten."""
- return len(self._nodes)
- @property
- def edge_count(self) -> int:
- """Anzahl der Kanten."""
- return len(self._edges)
- def add_service(self, service: "IService") -> GraphNode:
- """
- Fügt einen Service zum Graphen hinzu.
- Args:
- service: Der Service.
- Returns:
- Der erstellte GraphNode.
- """
- node = GraphNode(
- name=service.name,
- priority=service.PRIORITY,
- group=service.GROUP,
- state=service.state,
- dependencies=list(service.DEPENDENCIES),
- )
- self._nodes[service.name] = node
- # Kanten für Abhängigkeiten
- for dep in service.DEPENDENCIES:
- edge = GraphEdge(source=service.name, target=dep)
- self._edges.add(edge)
- return node
- def add_services(self, services: dict[str, "IService"]) -> None:
- """
- Fügt mehrere Services hinzu.
- Args:
- services: Dictionary der Services.
- """
- # Erst alle Nodes erstellen
- for service in services.values():
- self.add_service(service)
- # Dann Dependents berechnen
- self._compute_dependents()
- def _compute_dependents(self) -> None:
- """Berechnet die abhängigen Services für jeden Knoten."""
- for node in self._nodes.values():
- node.dependents = []
- for edge in self._edges:
- if edge.target in self._nodes:
- self._nodes[edge.target].dependents.append(edge.source)
- def get_node(self, name: str) -> GraphNode | None:
- """
- Gibt einen Knoten zurück.
- Args:
- name: Name des Knotens.
- Returns:
- GraphNode oder None.
- """
- return self._nodes.get(name)
- def get_dependencies(self, name: str) -> list[str]:
- """
- Gibt die Abhängigkeiten eines Knotens zurück.
- Args:
- name: Name des Knotens.
- Returns:
- Liste der Abhängigkeiten.
- """
- node = self._nodes.get(name)
- if node:
- return list(node.dependencies)
- return []
- def get_dependents(self, name: str) -> list[str]:
- """
- Gibt die abhängigen Services zurück.
- Args:
- name: Name des Knotens.
- Returns:
- Liste der abhängigen Services.
- """
- node = self._nodes.get(name)
- if node:
- return list(node.dependents)
- return []
- def get_all_dependencies(self, name: str) -> set[str]:
- """
- Gibt alle transitiven Abhängigkeiten zurück.
- Args:
- name: Name des Knotens.
- Returns:
- Set aller Abhängigkeiten (direkt und transitiv).
- """
- result: set[str] = set()
- visited: set[str] = set()
- def collect(node_name: str) -> None:
- if node_name in visited:
- return
- visited.add(node_name)
- node = self._nodes.get(node_name)
- if node:
- for dep in node.dependencies:
- result.add(dep)
- collect(dep)
- collect(name)
- return result
- def get_start_order(self) -> list[str]:
- """
- Berechnet die optimale Startreihenfolge.
- Returns:
- Liste der Service-Namen in Startreihenfolge.
- """
- # Topologische Sortierung mit Kahn's Algorithmus
- in_degree: dict[str, int] = {name: 0 for name in self._nodes}
- for edge in self._edges:
- if edge.source in in_degree and edge.target in self._nodes:
- in_degree[edge.source] += 1
- # Queue mit Knoten ohne Abhängigkeiten
- queue = [
- name for name, degree in in_degree.items()
- if degree == 0
- ]
- # Nach Priorität sortieren
- queue.sort(key=lambda n: (self._nodes[n].priority, n))
- result: list[str] = []
- while queue:
- # Nächsten Knoten verarbeiten
- current = queue.pop(0)
- result.append(current)
- # Abhängige aktualisieren
- for node in self._nodes.values():
- if current in node.dependencies:
- in_degree[node.name] -= 1
- if in_degree[node.name] == 0:
- queue.append(node.name)
- queue.sort(key=lambda n: (self._nodes[n].priority, n))
- return result
- def get_stop_order(self) -> list[str]:
- """
- Berechnet die optimale Stopp-Reihenfolge.
- Returns:
- Liste der Service-Namen in Stopp-Reihenfolge.
- """
- return list(reversed(self.get_start_order()))
- def find_cycles(self) -> list[list[str]]:
- """
- Findet Zyklen im Graphen.
- Returns:
- Liste von Zyklen (jeder Zyklus als Liste von Knoten).
- """
- cycles: list[list[str]] = []
- visited: set[str] = set()
- rec_stack: set[str] = set()
- path: list[str] = []
- def dfs(node_name: str) -> bool:
- visited.add(node_name)
- rec_stack.add(node_name)
- path.append(node_name)
- node = self._nodes.get(node_name)
- if node:
- for dep in node.dependencies:
- if dep not in visited:
- if dfs(dep):
- return True
- elif dep in rec_stack:
- # Zyklus gefunden
- cycle_start = path.index(dep)
- cycle = path[cycle_start:] + [dep]
- cycles.append(cycle)
- return True
- path.pop()
- rec_stack.remove(node_name)
- return False
- for name in self._nodes:
- if name not in visited:
- dfs(name)
- return cycles
- def get_roots(self) -> list[str]:
- """
- Gibt die Wurzelknoten zurück (ohne Abhängigkeiten).
- Returns:
- Liste der Wurzelknoten.
- """
- return [
- name for name, node in self._nodes.items()
- if not node.dependencies
- ]
- def get_leaves(self) -> list[str]:
- """
- Gibt die Blattknoten zurück (ohne Abhängige).
- Returns:
- Liste der Blattknoten.
- """
- return [
- name for name, node in self._nodes.items()
- if not node.dependents
- ]
- def to_dot(self, title: str = "Service Dependencies") -> str:
- """
- Exportiert als GraphViz DOT-Format.
- Args:
- title: Titel des Graphen.
- Returns:
- DOT-String.
- """
- lines: list[str] = [
- f'digraph "{title}" {{',
- f' rankdir=TB;',
- f' node [fontname="{self._style.font_name}", fontsize={self._style.font_size}];',
- f' edge [color="{self._style.edge_color}"];',
- "",
- ]
- # Cluster nach Gruppen
- groups: dict[ServiceGroup, list[GraphNode]] = {}
- for node in self._nodes.values():
- groups.setdefault(node.group, []).append(node)
- for group, nodes in groups.items():
- lines.append(f' subgraph cluster_{group.name.lower()} {{')
- lines.append(f' label="{group.name}";')
- lines.append(f' style=rounded;')
- lines.append(f' bgcolor="#f8f9fa";')
- for node in nodes:
- color = self._style.state_colors.get(node.state, "#c8d6e5")
- shape = self._style.group_shapes.get(node.group, "box")
- lines.append(
- f' "{node.name}" ['
- f'shape={shape}, '
- f'style=filled, '
- f'fillcolor="{color}", '
- f'label="{node.name}\\n({node.priority.name})"'
- f'];'
- )
- lines.append(' }')
- lines.append('')
- # Kanten
- for edge in self._edges:
- lines.append(f' "{edge.source}" -> "{edge.target}";')
- lines.append('}')
- return '\n'.join(lines)
- def to_mermaid(self, title: str = "Service Dependencies") -> str:
- """
- Exportiert als Mermaid-Diagramm.
- Args:
- title: Titel des Graphen.
- Returns:
- Mermaid-String.
- """
- lines: list[str] = [
- "```mermaid",
- "graph TD",
- f" %% {title}",
- "",
- ]
- # Knoten mit Styling
- for name, node in self._nodes.items():
- # Mermaid-Shapes basierend auf Gruppe
- shape_start, shape_end = self._get_mermaid_shape(node.group)
- state_class = node.state.name.lower()
- lines.append(
- f' {name}{shape_start}"{name}<br/>({node.priority.name})"{shape_end}'
- )
- lines.append("")
- # Kanten
- for edge in self._edges:
- lines.append(f' {edge.source} --> {edge.target}')
- lines.append("")
- # Styling
- lines.append(" %% Styles")
- for state, color in self._style.state_colors.items():
- lines.append(f" classDef {state.name.lower()} fill:{color}")
- # Klassen zuweisen
- for state in ServiceState:
- nodes_with_state = [
- name for name, node in self._nodes.items()
- if node.state == state
- ]
- if nodes_with_state:
- lines.append(
- f" class {','.join(nodes_with_state)} {state.name.lower()}"
- )
- lines.append("```")
- return '\n'.join(lines)
- def _get_mermaid_shape(self, group: ServiceGroup) -> tuple[str, str]:
- """
- Gibt die Mermaid-Shape-Zeichen für eine Gruppe zurück.
- Args:
- group: Die Service-Gruppe.
- Returns:
- Tupel (Start, Ende) für die Shape.
- """
- shapes = {
- ServiceGroup.CRITICAL: ("[[", "]]"), # Subroutine
- ServiceGroup.NETWORK: ("{{", "}}"), # Hexagon
- ServiceGroup.MANAGER: ("[", "]"), # Rectangle
- ServiceGroup.AUDIO: ("([", "])"), # Stadium
- ServiceGroup.ML: ("{", "}"), # Rhombus
- ServiceGroup.UTILITY: ("(", ")"), # Rounded
- ServiceGroup.PLUGIN: ("((", "))"), # Circle
- }
- return shapes.get(group, ("[", "]"))
- def to_json(self) -> dict[str, Any]:
- """
- Exportiert als JSON-Struktur.
- Returns:
- Dictionary-Repräsentation.
- """
- return {
- "nodes": [
- {
- "name": node.name,
- "priority": node.priority.name,
- "group": node.group.name,
- "state": node.state.name,
- "dependencies": node.dependencies,
- "dependents": node.dependents,
- "metadata": node.metadata,
- }
- for node in self._nodes.values()
- ],
- "edges": [
- {
- "source": edge.source,
- "target": edge.target,
- "type": edge.edge_type,
- }
- for edge in self._edges
- ],
- "statistics": {
- "node_count": self.node_count,
- "edge_count": self.edge_count,
- "roots": self.get_roots(),
- "leaves": self.get_leaves(),
- "has_cycles": len(self.find_cycles()) > 0,
- },
- }
- def to_ascii(self, root: str | None = None) -> str:
- """
- Exportiert als ASCII-Baum.
- Args:
- root: Optionaler Wurzelknoten.
- Returns:
- ASCII-Baum-String.
- """
- lines: list[str] = []
- visited: set[str] = set()
- def render_tree(name: str, prefix: str = "", is_last: bool = True) -> None:
- if name in visited:
- connector = "└── " if is_last else "├── "
- lines.append(f"{prefix}{connector}{name} (circular)")
- return
- visited.add(name)
- node = self._nodes.get(name)
- if not node:
- return
- connector = "└── " if is_last else "├── "
- state_marker = "●" if node.state == ServiceState.RUNNING else "○"
- lines.append(f"{prefix}{connector}{state_marker} {name} [{node.priority.name}]")
- # Abhängige rendern
- dependents = node.dependents
- for i, dep in enumerate(dependents):
- is_last_dep = i == len(dependents) - 1
- new_prefix = prefix + (" " if is_last else "│ ")
- render_tree(dep, new_prefix, is_last_dep)
- # Startpunkte bestimmen
- if root and root in self._nodes:
- roots = [root]
- else:
- roots = self.get_roots()
- for i, root_name in enumerate(sorted(roots)):
- is_last_root = i == len(roots) - 1
- render_tree(root_name, "", is_last_root)
- return '\n'.join(lines)
- def export(
- self,
- format: GraphFormat,
- title: str = "Service Dependencies",
- ) -> str:
- """
- Exportiert den Graphen im angegebenen Format.
- Args:
- format: Ausgabeformat.
- title: Titel des Graphen.
- Returns:
- Export-String.
- """
- if format == GraphFormat.DOT:
- return self.to_dot(title)
- elif format == GraphFormat.MERMAID:
- return self.to_mermaid(title)
- elif format == GraphFormat.JSON:
- import json
- return json.dumps(self.to_json(), indent=2)
- elif format == GraphFormat.ASCII:
- return self.to_ascii()
- else:
- raise ValueError(f"Unbekanntes Format: {format}")
- def validate(self) -> list[str]:
- """
- Validiert den Graphen.
- Returns:
- Liste von Warnungen/Fehlern.
- """
- issues: list[str] = []
- # Fehlende Abhängigkeiten prüfen
- for edge in self._edges:
- if edge.target not in self._nodes:
- issues.append(
- f"Fehlende Abhängigkeit: {edge.source} -> {edge.target}"
- )
- # Zyklen prüfen
- cycles = self.find_cycles()
- for cycle in cycles:
- issues.append(f"Zyklische Abhängigkeit: {' -> '.join(cycle)}")
- # Verwaiste Knoten
- orphans = [
- name for name, node in self._nodes.items()
- if not node.dependencies and not node.dependents
- ]
- for orphan in orphans:
- issues.append(f"Verwaister Service: {orphan}")
- return issues
|