Code/scripts/gestion/beautify.py
2025-05-23 13:35:27 +02:00

429 lines
18 KiB
Python

"""
Version 1.0
Date 27/03/2025
Auteur : Stéphan Peccini
Ce script permet de récupérer un graphe DOT syntaxiquement correct, pour
le formater selon la structure hiérarchique souhaitée.
Exemple d'utilisation :
-----------------------
Le script ihh.py met à jour les données ihh en fonction des nouvelles informations.
Pour faciliter le travail, tout est fait avec un graphe NetworkX et ensuite sauvé
au format DOT sans aucune structure hiérarchique.
Il suffit alors de passer le fichier résultat de ihh.py pour avoir un fichier DOT
conforme aux attentes.
"""
import sys
import networkx as nx
from networkx.drawing.nx_agraph import read_dot
import logging
logging.basicConfig(
filename="beautify_debug.log",
filemode="w",
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s"
)
def formater_noeuds_par_niveau(schema, niveau, indentation=4):
"""
Génère la syntaxe DOT avec une hiérarchie complète de sous-graphes imbriqués:
- Sous-graphes pour les nœuds du niveau spécifié (0, 1, 2)
- Sous-graphes pour les nœuds de niveau 10 liés à ces nœuds
- Sous-graphes pour les nœuds de niveau 11 liés aux nœuds de niveau 10
- Pour les nœuds de niveau 11, inclut leurs nœuds destination (sauf niveau 99)
"""
# Convertir le niveau en chaîne si nécessaire
niveau_str = str(niveau)
# Identifier les nœuds du niveau spécifié
noeuds_niveau = [noeud for noeud, attr in schema.nodes(data=True)
if attr.get('niveau') == niveau_str]
# Récupérer les attributs de ces nœuds
attributs_niveau = {noeud: schema.nodes[noeud] for noeud in noeuds_niveau}
# Définir les couleurs selon la légende
couleurs_operations = {
"Assemblage": "#a0d6ff",
"Niveau Composant": "#b3ffe0",
"Fabrication": "#ffe0cc",
"Extraction": "#ffd699",
"Reserves": "#ffd699",
"Pays": "#e6f2ff",
"Zone": "#e6f2ff",
"Entreprise": "#f0f0f0"
}
# Fonction pour déterminer l'opération et la couleur
def determiner_operation_et_couleur(nom_noeud):
operation = nom_noeud.split('_')[0] if '_' in nom_noeud else nom_noeud
couleur = "#ffffff" # Blanc par défaut
for op_cle, op_couleur in couleurs_operations.items():
if op_cle in operation:
couleur = op_couleur
break
return operation, couleur
# Fonction pour formater une arête avec ses attributs
def formater_arete(source, dest, edge_attrs, indent_level):
edge_str = f"{indent_level}{source} -> {dest} ["
edge_attrs_formatted = []
for edge_attr, edge_val in edge_attrs.items():
if edge_val:
edge_attrs_formatted.append(f'{edge_attr}="{edge_val}"')
edge_str += ", ".join(edge_attrs_formatted)
edge_str += "];\n"
return edge_str
# Fonction pour formater un nœud avec ses attributs
def formater_noeud(nom_noeud, attributs_noeud, indent_level):
node_str = f"{indent_level}{nom_noeud} ["
attrs_formattés = []
for attr_nom, attr_val in attributs_noeud.items():
if attr_val and attr_nom in ordre_attributs:
attrs_formattés.append(f'{attr_nom}="{attr_val}"')
node_str += ", ".join(attrs_formattés)
node_str += "];\n"
return node_str
# Fonction pour trouver toutes les relations sortantes d'un nœud
def trouver_relations_sortantes(noeud_source):
relations = []
for s, d, attrs in schema.edges(data=True):
if s == noeud_source:
relations.append((s, d, attrs))
# Suppression des doublons en convertissant le dictionnaire en tuple trié
relations_sans_doublons = set((a, b, tuple(sorted(c.items()))) for a, b, c in relations)
# Reconversion en dictionnaire
relations_finales = [(a, b, dict(c)) for a, b, c in relations_sans_doublons]
return relations_finales
# Définir les niveaux d'indentation
indent = " " * (indentation + 4)
indent_inner = " " * (indentation + 8)
indent_inner2 = " " * (indentation + 12)
indent_inner3 = " " * (indentation + 16)
# Préparer la chaîne de sortie
sortie = f"\n{indent}// Sous-graphes pour les nœuds de niveau {niveau} avec leurs relations\n"
# Définir l'ordre des attributs
ordre_attributs = ["ihh_pays", "ihh_acteurs", "ivc", "label", "niveau", "fillcolor", "orphelin", "shape", "style", "fontname"]
# Formater chaque nœud comme un subgraph avec ses relations sortantes
for nom_noeud, attributs in attributs_niveau.items():
# Débuter le sous-graphe principal
sortie += f"{indent}subgraph cluster_{nom_noeud} {{\n"
sortie += f"{indent_inner}label=\"{nom_noeud}\";\n"
# Ajouter les autres attributs du sous-graphe
for attr_nom, attr_valeur in attributs.items():
if attr_valeur and attr_nom in ["style", "fillcolor", "color", "fontname"]:
sortie += f"{indent_inner}{attr_nom}=\"{attr_valeur}\";\n"
# Ajouter le nœud lui-même dans le sous-graphe
sortie += formater_noeud(nom_noeud, attributs, indent_inner)
# Trouver toutes les relations dont ce nœud est la source
relations_sortantes = []
sous_graphes_niveau_10 = {} # Dictionnaire pour stocker les sous-graphes de niveau 10
for source, dest, edge_attrs in trouver_relations_sortantes(nom_noeud):
# Vérifier si la destination est un nœud de niveau 10
dest_attrs = schema.nodes[dest]
# est_niveau_10 = dest_attrs.get('niveau') == "10"
est_niveau_10 = str(dest_attrs.get('niveau')) in {"10", "1010"}
# Formater l'arête pour le sous-graphe parent
edge_str = formater_arete(source, dest, edge_attrs, indent_inner)
relations_sortantes.append(edge_str)
# Traiter les nœuds de niveau 10
if est_niveau_10 and dest not in sous_graphes_niveau_10:
# Début du sous-graphe de niveau 10
operation, couleur = determiner_operation_et_couleur(dest)
sg_str = f"\n{indent_inner}subgraph cluster_{dest} {{\n"
sg_str += f"{indent_inner2}label=\"{dest}\";\n"
# Couleur de remplissage
if 'fillcolor' in dest_attrs and dest_attrs['fillcolor']:
sg_str += f"{indent_inner2}fillcolor=\"{dest_attrs['fillcolor']}\";\n"
else:
sg_str += f"{indent_inner2}fillcolor=\"{couleur}\";\n"
sg_str += f"{indent_inner2}style=\"filled\";\n"
# Ajouter le nœud de niveau 10 lui-même
sg_str += formater_noeud(dest, dest_attrs, indent_inner2)
# Collecter les relations sortantes du nœud de niveau 10
relations_niveau_10 = trouver_relations_sortantes(dest)
sous_graphes_niveau_11 = {} # Pour stocker les sous-graphes de niveau 11
if relations_niveau_10:
sg_str += f"\n{indent_inner2}// Relations sortantes du nœud de niveau 10\n"
for src, dst, rel_attrs in relations_niveau_10:
# Ajouter la relation
sg_str += formater_arete(src, dst, rel_attrs, indent_inner2)
# Vérifier si la destination est un nœud de niveau 11
if dst in schema.nodes:
dst_attrs = schema.nodes[dst]
dst_niveau = dst_attrs.get('niveau', '')
# Créer un sous-graphe pour les nœuds de niveau 11
if (dst_niveau == "11" or dst_niveau == "1011") and dst not in sous_graphes_niveau_11:
# Début du sous-graphe de niveau 11
dst_operation, dst_couleur = determiner_operation_et_couleur(dst)
dst_sg = f"\n{indent_inner2}subgraph cluster_{dst} {{\n"
dst_sg += f"{indent_inner3}label=\"{dst}\";\n"
# Couleur de remplissage
if 'fillcolor' in dst_attrs and dst_attrs['fillcolor']:
dst_sg += f"{indent_inner3}fillcolor=\"{dst_attrs['fillcolor']}\";\n"
else:
dst_sg += f"{indent_inner3}fillcolor=\"{dst_couleur}\";\n"
dst_sg += f"{indent_inner3}style=\"filled\";\n"
# Ajouter le nœud de niveau 11 lui-même
dst_sg += formater_noeud(dst, dst_attrs, indent_inner3)
# Collecter les relations sortantes du nœud de niveau 11
relations_niveau_11 = trouver_relations_sortantes(dst)
noeuds_destination = {} # Pour stocker les nœuds destination
relations_additionnelles = [] # Pour stocker les relations des nœuds destination
if relations_niveau_11:
dst_sg += f"\n{indent_inner3}// Relations sortantes du nœud de niveau 11\n"
for n11_src, n11_dst, n11_attrs in relations_niveau_11:
# Ajouter la relation
dst_sg += formater_arete(n11_src, n11_dst, n11_attrs, indent_inner3)
# Ajouter le nœud destination sauf s'il est de niveau 99
if n11_dst in schema.nodes:
n11_dst_attrs = schema.nodes[n11_dst]
n11_dst_niveau = n11_dst_attrs.get('niveau', '')
if n11_dst_niveau != "99" and n11_dst not in noeuds_destination and n11_dst != dst:
noeuds_destination[n11_dst] = n11_dst_attrs
# Collecter les relations sortantes du nœud destination
for dest_src, dest_dst, dest_attrs in trouver_relations_sortantes(n11_dst):
relations_additionnelles.append((dest_src, dest_dst, dest_attrs))
# Ajouter les nœuds destination
for dest_nom, dest_attrs in noeuds_destination.items():
dst_sg += formater_noeud(dest_nom, dest_attrs, indent_inner3)
# Ajouter les relations additionnelles
if relations_additionnelles:
dst_sg += f"\n{indent_inner3}// Relations des nœuds destination\n"
for rel_src, rel_dst, rel_attrs in relations_additionnelles:
dst_sg += formater_arete(rel_src, rel_dst, rel_attrs, indent_inner3)
# Fermer le sous-graphe de niveau 11
dst_sg += f"{indent_inner2}}}\n"
# Ajouter à la collection
sous_graphes_niveau_11[dst] = dst_sg
# Ajouter les sous-graphes de niveau 11 au sous-graphe de niveau 10
if sous_graphes_niveau_11:
sg_str += "".join(sous_graphes_niveau_11.values())
# Fermer le sous-graphe de niveau 10
sg_str += f"{indent_inner}}}\n"
# Ajouter à la collection
sous_graphes_niveau_10[dest] = sg_str
# Ajouter les relations au sous-graphe principal
if relations_sortantes:
sortie += f"\n{indent_inner}// Relations sortantes\n"
sortie += "".join(relations_sortantes)
# Ajouter les sous-graphes de niveau 10
if sous_graphes_niveau_10:
sortie += "\n"
sortie += "".join(sous_graphes_niveau_10.values())
# Terminer le sous-graphe principal
sortie += f"{indent}}}\n\n"
return sortie
def generer_rank_same(schema, indentation=4):
"""
Génère des instructions rank=same pour les nœuds de même niveau dans un graphe DOT.
Args:
schema: Le graphe NetworkX chargé depuis un fichier DOT
indentation: Nombre d'espaces pour l'indentation (par défaut 4)
Returns:
Une chaîne contenant les instructions rank=same formatées pour le fichier DOT
"""
# Dictionnaire pour stocker les nœuds par niveau
noeuds_par_niveau = {}
# Parcourir tous les nœuds du graphe
for noeud, attrs in schema.nodes(data=True):
niveau = attrs.get('niveau')
if niveau: # Si le nœud a un attribut niveau
if niveau not in noeuds_par_niveau:
noeuds_par_niveau[niveau] = []
noeuds_par_niveau[niveau].append(noeud)
# Préparer l'indentation
indent = " " * indentation
# Générer les instructions rank=same
sortie = "\n" + indent + "// Alignement des nœuds par niveau\n"
# Trier les niveaux numériquement
for niveau, noeuds in sorted(noeuds_par_niveau.items(), key=lambda x: int(x[0]) if x[0].isdigit() else 99):
if noeuds: # S'il y a des nœuds pour ce niveau
sortie += indent + "{ rank=same; "
sortie += "; ".join(noeuds)
sortie += "; }\n"
return sortie
def formater_attributs(attributs):
# Filtrer les attributs non vides et les formater
formatted_attrs = []
for attr, value in attributs.items():
if value: # Ne pas inclure les attributs vides
# Ajouter des guillemets pour les valeurs textuelles (sauf pour certains attributs)
if attr in ('shape', 'style') or value.isdigit():
formatted_attrs.append(f"{attr}={value}")
else:
formatted_attrs.append(f'{attr}="{value}"')
# Gérer le cas spécial pour style=filled si nécessaire
if 'style' not in attributs or not attributs['style']:
formatted_attrs.append("style=filled")
return formatted_attrs
def main(fichier_entree, fichier_sortie):
# Charger le graphe DOT avec NetworkX
try:
schema = read_dot(fichier_entree)
except Exception as e:
logging.error(f"Erreur de lecture DOT : {e}", exc_info=True)
print(f"Erreur à l'ouverture du fichier DOT : {e}")
return
sortie = """digraph Hierarchie_Composants_Electroniques_Simplifiee {
// Configuration globale
graph [compound="True", rankdir="TB", ranksep="10.0"]
node [fontname="Arial", shape=box, style=filled];
edge [fontname="Arial", fontsize=10, style=filled];
"""
# Subgraph ASSEMBLAGE
sortie += """
// Niveau Assemblage
subgraph cluster_assemblage {
label="ASSEMBLAGE";
bgcolor="#f0f0f0";
node [fillcolor="#a0d6ff"];
"""
# Ajout des informations pour tous les nœudss de niveau 0
sortie += formater_noeuds_par_niveau(schema, "0")
sortie += formater_noeuds_par_niveau(schema, "1000")
sortie += """ }"""
# Subgraph COMPOSANTS
sortie += """
// Niveau Composants
subgraph cluster_composants {
label="Composants";
bgcolor="#f0f0f0";
node [fillcolor="#a0d6ff"];"""
# Ajout des informations pour tous les nœudss de niveau 1
sortie += formater_noeuds_par_niveau(schema, "1")
sortie += formater_noeuds_par_niveau(schema, "1001")
sortie += """ }"""
# Subgraph MATÉRIAUX
sortie += """
// Niveau Matériaux
subgraph cluster_materiaux {
label="Matériaux";
bgcolor="#f0f0f0";
node [fillcolor="#a0d6ff"];"""
# Ajout des informations pour tous les nœudss de niveau 2
sortie += formater_noeuds_par_niveau(schema, "2")
sortie += """ }"""
# Subgraph PAYS GÉOGRAPHIQUES
sortie += """
// Niveau Pays géographiques
subgraph cluster_pays_geographiques {
label="Pays géographiques";
bgcolor="#f0f0f0";
node [fillcolor="#a0d6ff"];"""
# Ajout des informations pour tous les nœudss de niveau 99
sortie += formater_noeuds_par_niveau(schema, "99")
sortie += """ }"""
# Ajouter les instructions rank=same pour l'alignement horizontal
sortie += generer_rank_same(schema)
sortie += """
// Légende
subgraph cluster_legende {
label="LÉGENDE";
bgcolor="white";
node [shape=box, style=filled, width=0.2, height=0.2];
L1 [label="Fabrication des wafers", fillcolor="#f0fff0"];
L2 [label="Assemblage", fillcolor="#a0d6ff"];
L3 [label="Niveau Composant", fillcolor="#b3ffe0"];
L4 [label="Fabrication N3 (Matériaux)", fillcolor="#ffe0cc"];
L5 [label="Fabrication N3 (Terres Rares)", fillcolor="#ffd699"];
L6 [label="Extraction/Réserves", fillcolor="#ffd699"];
L7 [label="Pays/Zone geographique", fillcolor="#e6f2ff"];
L8 [label="Entreprise", fillcolor="#f0f0f0"];
L9 [label="Liens réserves", color="red", fontcolor="white"];
L10 [label="Liens opération(traitement, fabrication, assemblage)", color="purple", fontcolor="white"];
L11 [label="Liens extraction", color="orange", fontcolor="white"];
L12 [label="Liens d'origine géographique", color="darkgreen", fontcolor="white"];
L13 [label="Liens origine des minerais", color="darkblue", fontcolor="white"];
}
}
"""
try:
with open(f"{fichier_sortie}", "w", encoding="utf-8") as f:
print(f"{sortie}", file=f)
except FileNotFoundError:
print(f"Erreur : Le chemin vers '{fichier_sortie}' n'existe pas")
except PermissionError:
print(f"Erreur : Permissions insuffisantes pour écrire dans '{fichier_sortie}'")
except IOError as e:
print(f"Erreur d'E/S lors de l'écriture dans le fichier : {e}")
except Exception as e:
print(f"Erreur inattendue : {e}")
else:
print(f"Écriture dans '{fichier_sortie}' réussie")
if len(sys.argv) != 3:
print("Usage: python script.py fichier_entree.dot fichier_sortie.dot")
else:
fichier_entree = sys.argv[1]
fichier_sortie = sys.argv[2]
main(fichier_entree, fichier_sortie)