Restructuration pour simplification
This commit is contained in:
parent
e9d129f616
commit
1beb357e57
@ -365,9 +365,9 @@
|
||||
<p>Très forte dépendance au numérique embarqué. Usage final numérique plus faible mais stratégique.</p>
|
||||
<h3>Répartition des usages</h3>
|
||||
<ul>
|
||||
<li>Numérique final : 60%</li>
|
||||
<li>Numérique final : 20%</li>
|
||||
<li>Numérique embarqué : 30%</li>
|
||||
<li>Autres secteurs : 10%</li>
|
||||
<li>Autres secteurs : 50%</li>
|
||||
</ul>
|
||||
<h3>Tendance</h3>
|
||||
<ul>
|
||||
|
||||
131
README.md
131
README.md
@ -8,7 +8,7 @@ Or, dans le contexte actuel de « polycrise globale », marqué par des tensions
|
||||
|
||||
L'objectif de ce projet est de proposer à la fois des données les plus complètes possibles sur cette chaîne, et de fournir les outils nécessaires pour la visualisation des relations, les analyses, la lecture des fiches des opérations (assemblage, fabrication, extraction, …).
|
||||
|
||||
Le code proposé répond à la partie outillage.
|
||||
Le code proposé répond à la partie outillage, avec une architecture modulaire et simplifiée pour faciliter la maintenance et l'évolution.
|
||||
|
||||
## Principe général
|
||||
|
||||
@ -76,75 +76,98 @@ Pour automatiser le lancement, il est intégré dans systemd :
|
||||
|
||||
### fabnum.py
|
||||
|
||||
Le cœur de l’application. Ce script gère :
|
||||
Le cœur de l'application. Ce script sert de point d'entrée et d'orchestrateur pour tous les modules fonctionnels :
|
||||
|
||||
l’interface utilisateur avec Streamlit,
|
||||
- **Configuration de l'interface** : Initialisation de Streamlit et chargement des thèmes
|
||||
- **Gestion de la navigation** : Coordination entre les différents modules selon l'onglet sélectionné
|
||||
- **Chargement des données** : Acquisition des données depuis le backend Gitea
|
||||
- **Orchestration des modules** :
|
||||
- Module Fiches pour la consultation documentaire
|
||||
- Module Analyse pour l'étude des chaînes de dépendance
|
||||
- Module Visualisations pour les représentations graphiques
|
||||
- Module Personnalisation pour la modification du graphe
|
||||
|
||||
le chargement des données depuis le backend Gitea (schéma, instructions, fiches),
|
||||
Ce fichier est conçu de manière modulaire, déléguant les fonctionnalités spécifiques aux modules spécialisés, ce qui facilite la maintenance et les évolutions futures.
|
||||
|
||||
l’analyse des chaînes de dépendances critiques (par Sankey interactif),
|
||||
## Architecture et principes de conception
|
||||
|
||||
les visualisations statistiques (IHH, IVC, ISG),
|
||||
### Modularité et simplification
|
||||
|
||||
la navigation hiérarchique dans les fiches,
|
||||
L'application a été restructurée selon les principes suivants :
|
||||
|
||||
et la personnalisation de produits finaux.
|
||||
1. **Séparation des responsabilités** : Chaque module a une fonction bien définie
|
||||
2. **Modularité** : Les fonctionnalités sont décomposées en composants réutilisables
|
||||
3. **Simplification des fonctions** : Les fonctions complexes ont été divisées en sous-fonctions plus simples
|
||||
4. **Documentation intégrée** : Chaque module dispose de sa propre documentation
|
||||
|
||||
Il orchestre tous les composants de l’application, notamment :
|
||||
### Modules principaux
|
||||
|
||||
connexion.py pour l’authentification via Gitea,
|
||||
- **Module Analyse** : [Voir documentation](app/analyse/README.md)
|
||||
- Permet d'analyser les relations entre les différents éléments de la chaîne
|
||||
- Génère des visualisations Sankey pour identifier les vulnérabilités
|
||||
|
||||
utils/ pour les fonctions métiers (import graph, traitement, visualisation),
|
||||
- **Module Fiches** : [Voir documentation](app/fiches/README.md)
|
||||
- Gère la consultation et l'affichage des fiches techniques
|
||||
- Intègre un système de tickets pour la collaboration
|
||||
|
||||
components/ pour l’affichage modulaire (sidebar, header, footer, fiches),
|
||||
- **Module Personnalisation** : [Voir documentation](app/personnalisation/README.md)
|
||||
- Permet d'ajouter et modifier des produits personnalisés
|
||||
- Offre des fonctionnalités d'import/export de configurations
|
||||
|
||||
et tickets_fiche.py pour la consultation et la création de tickets Gitea liés aux fiches.
|
||||
- **Module Visualisations** : [Voir documentation](app/visualisations/README.md)
|
||||
- Propose diverses visualisations graphiques des données
|
||||
- Permet d'explorer les données sous différents angles
|
||||
|
||||
Le fichier récupère automatiquement les données du dépôt Gitea configuré, et permet aux utilisateurs d’interagir avec les graphes, les métadonnées et les visualisations en toute autonomie
|
||||
- **Composants d'interface** : [Voir documentation](components/README.md)
|
||||
- Fournit des éléments d'interface réutilisables
|
||||
- Assure la cohérence visuelle de l'application
|
||||
|
||||
### tickets_fiche.py
|
||||
|
||||
Ce module assure la liaison entre les fiches documentaires et le système de tickets Gitea. Il permet :
|
||||
|
||||
de rechercher automatiquement les tickets ouverts liés à une fiche (via les labels définis dans fiches_labels.csv),
|
||||
|
||||
de les afficher classés par statut (En cours, Terminés, etc.),
|
||||
|
||||
de consulter les commentaires associés à chaque ticket,
|
||||
|
||||
de proposer un formulaire complet pour créer un nouveau ticket structuré à partir d’un modèle Markdown,
|
||||
|
||||
de prévisualiser et publier ce ticket directement via l’API Gitea.
|
||||
|
||||
Il gère également :
|
||||
|
||||
la détection de conflits ou erreurs lors des appels réseau,
|
||||
|
||||
l’automatisation du remplissage des champs (fiche concernée, type de contribution, etc.),
|
||||
|
||||
et la prise en compte des environnements (ENV) et des permissions via token.
|
||||
|
||||
Ce fichier est essentiel pour assurer la participation collaborative autour des fiches de la chaîne numérique.
|
||||
- **Utilitaires partagés** : [Voir documentation](utils/README.md)
|
||||
- Centralise les fonctions communes utilisées par les différents modules
|
||||
- Gère l'interaction avec l'API Gitea et la manipulation des graphes
|
||||
|
||||
### Organisation du code
|
||||
|
||||
fabnum_app/
|
||||
├── app.py / fabnum.py # Point d'entrée principal
|
||||
├── config.py # Chargement des variables d’environnement
|
||||
├── utils/
|
||||
│ ├── gitea.py # Connexion API Gitea
|
||||
│ ├── graph_utils.py # Chemins, criticité, extraction de données
|
||||
│ └── visualisation.py # Graphiques Altair, Plotly
|
||||
├── components/
|
||||
│ ├── sidebar.py # Menu latéral
|
||||
│ ├── header.py # En-tête HTML
|
||||
L'application est organisée de façon modulaire, avec une structure simplifiée qui facilite la maintenance :
|
||||
|
||||
```
|
||||
fabnum-dev/
|
||||
├── fabnum.py # Point d'entrée principal
|
||||
├── config.py # Configuration et variables d'environnement
|
||||
├── app/ # Modules fonctionnels principaux
|
||||
│ ├── analyse/ # Module d'analyse des chaînes de dépendance
|
||||
│ │ ├── interface.py # Interface utilisateur pour l'analyse
|
||||
│ │ ├── sankey.py # Génération des diagrammes Sankey
|
||||
│ │ └── README.md # Documentation du module
|
||||
│ ├── fiches/ # Gestion et affichage des fiches
|
||||
│ │ ├── interface.py # Interface utilisateur pour les fiches
|
||||
│ │ ├── generer.py # Génération des fiches
|
||||
│ │ ├── utils/ # Utilitaires spécifiques aux fiches
|
||||
│ │ └── README.md # Documentation du module
|
||||
│ ├── personnalisation/ # Personnalisation de la chaîne
|
||||
│ │ ├── interface.py # Interface principale
|
||||
│ │ ├── ajout.py # Ajout de produits
|
||||
│ │ ├── modification.py # Modification de produits
|
||||
│ │ ├── import_export.py # Import/export de configurations
|
||||
│ │ └── README.md # Documentation du module
|
||||
│ └── visualisations/ # Visualisations graphiques
|
||||
│ ├── interface.py # Interface des visualisations
|
||||
│ └── README.md # Documentation du module
|
||||
├── components/ # Composants d'interface réutilisables
|
||||
│ ├── sidebar.py # Barre latérale de navigation
|
||||
│ ├── header.py # En-tête de l'application
|
||||
│ ├── footer.py # Pied de page
|
||||
│ └── fiches.py # Lecture et affichage des fiches
|
||||
├── tickets_fiche.py # Gestion des tickets associés aux fiches
|
||||
├── assets/
|
||||
│ ├── styles.css # Feuille de style personnalisée
|
||||
│ └── impact_co2.js # Script pour calcul d’impact environnemental
|
||||
├── .env # Configuration versionnée (sans secrets)
|
||||
│ └── README.md # Documentation des composants
|
||||
├── utils/ # Utilitaires partagés
|
||||
│ ├── gitea.py # Connexion API Gitea
|
||||
│ ├── graph_utils.py # Manipulation des graphes
|
||||
│ └── README.md # Documentation des utilitaires
|
||||
├── assets/ # Ressources statiques
|
||||
│ ├── styles/ # Feuilles de style CSS
|
||||
│ └── impact_co2.js # Calcul d'impact environnemental
|
||||
├── .env # Configuration versionnée
|
||||
├── .env.local # Configuration locale (non versionnée)
|
||||
├── .gitignore # Exclusion des fichiers sensibles
|
||||
└── requirements.txt # Dépendances Python
|
||||
```
|
||||
|
||||
Chaque module dispose de sa propre documentation détaillée dans un fichier README.md.
|
||||
|
||||
45
README_connexion.md
Normal file
45
README_connexion.md
Normal file
@ -0,0 +1,45 @@
|
||||
# Authentification à FabNum avec un token Gitea
|
||||
|
||||
Ce document explique comment générer et utiliser un token d'accès personnel Gitea pour vous connecter à l'application FabNum.
|
||||
|
||||
## Caractéristiques du token requis
|
||||
|
||||
Pour pouvoir vous connecter à FabNum, votre token Gitea doit répondre aux critères suivants :
|
||||
|
||||
- Être associé à un compte utilisateur qui est membre de l'équipe **"Administrateurs"** dans l'organisation **"FabNum"**
|
||||
- Disposer des permissions suffisantes pour :
|
||||
- Accéder aux informations de l'utilisateur
|
||||
- Lister les équipes d'une organisation
|
||||
- Vérifier l'appartenance d'un utilisateur à une équipe
|
||||
|
||||
## Procédure pour générer un nouveau token
|
||||
|
||||
1. **Connectez-vous à Gitea** :
|
||||
- Rendez-vous sur https://fabnum-git.peccini.fr et connectez-vous avec vos identifiants
|
||||
|
||||
2. **Accédez aux paramètres de votre profil** :
|
||||
- Cliquez sur votre avatar en haut à droite
|
||||
- Sélectionnez "Paramètres"
|
||||
|
||||
3. **Générez un nouveau token d'accès** :
|
||||
- Dans le menu latéral gauche, cliquez sur "Applications"
|
||||
- Faites défiler jusqu'à la section "Tokens d'accès personnel"
|
||||
- Cliquez sur "Générer un nouveau token"
|
||||
|
||||
4. **Configurez le token** :
|
||||
- Donnez un nom descriptif à votre token (par exemple "Token FabNum - Connexion")
|
||||
- **Important** : Sélectionnez les autorisations suivantes au minimum :
|
||||
- ✅ `read:user` - Accès en lecture pour les informations utilisateur
|
||||
- ✅ `read:org` - Accès en lecture pour les organisations et équipes
|
||||
|
||||
5. **Générez et copiez le token** :
|
||||
- Cliquez sur "Générer un token"
|
||||
- **IMPORTANT** : Copiez immédiatement le token généré et conservez-le en lieu sûr. Gitea ne l'affichera plus jamais après cette étape.
|
||||
|
||||
## Utilisation du token
|
||||
|
||||
Une fois le token généré, utilisez-le pour vous connecter à l'application FabNum :
|
||||
|
||||
1. Accédez à l'application FabNum
|
||||
2. Dans le formulaire d'authentification, collez votre token d'accès personnel Gitea
|
||||
3. Cliquez sur "Se connecter"
|
||||
42
app/analyse/README.md
Normal file
42
app/analyse/README.md
Normal file
@ -0,0 +1,42 @@
|
||||
# Module d'Analyse
|
||||
|
||||
Ce module permet d'analyser les relations entre les différentes parties de la chaîne de fabrication du numérique. Il offre des outils pour visualiser les flux et identifier les vulnérabilités potentielles dans la chaîne d'approvisionnement.
|
||||
|
||||
## Structure du module
|
||||
|
||||
Le module d'analyse comprend deux composants principaux :
|
||||
|
||||
- **interface.py** : Gère l'interface utilisateur pour paramétrer les analyses
|
||||
- **sankey.py** : Génère les diagrammes de flux (Sankey) pour visualiser les relations entre les éléments
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
### Interface d'analyse
|
||||
L'interface permet de :
|
||||
- Sélectionner les niveaux de départ et d'arrivée pour l'analyse (produits, composants, minerais, opérations, etc.)
|
||||
- Filtrer les données par minerais spécifiques
|
||||
- Effectuer une sélection fine des nœuds de départ et d'arrivée
|
||||
- Appliquer des filtres pour identifier les vulnérabilités :
|
||||
- Filtres ICS (criticité pour un composant)
|
||||
- Filtres IVC (criticité par rapport à la concurrence sectorielle)
|
||||
- Filtres IHH (concentration géographique ou industrielle)
|
||||
- Filtres ISG (instabilité des pays)
|
||||
- Choisir la logique de filtrage (OU, ET)
|
||||
|
||||
### Visualisation Sankey
|
||||
Le module génère des diagrammes Sankey qui :
|
||||
- Affichent les flux entre les différents éléments de la chaîne
|
||||
- Mettent en évidence les relations de dépendance
|
||||
- Permettent d'identifier visuellement les goulots d'étranglement potentiels
|
||||
- Sont interactifs et permettent d'explorer la chaîne de valeur
|
||||
|
||||
## Utilisation
|
||||
|
||||
1. Sélectionnez un niveau de départ (ex : Produit final, Composant)
|
||||
2. Choisissez un niveau d'arrivée (ex : Pays géographique, Acteur d'opération)
|
||||
3. Si nécessaire, filtrez par minerais spécifiques
|
||||
4. Affinez votre sélection avec des nœuds de départ et d'arrivée spécifiques
|
||||
5. Appliquez les filtres de vulnérabilité souhaités
|
||||
6. Lancez l'analyse pour générer le diagramme Sankey
|
||||
|
||||
Le diagramme résultant permet d'identifier visuellement les relations et points de vulnérabilité dans la chaîne d'approvisionnement du numérique.
|
||||
2
app/analyse/__init__.py
Normal file
2
app/analyse/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# __init__.py – app/fiches
|
||||
from .interface import interface_analyse
|
||||
163
app/analyse/interface.py
Normal file
163
app/analyse/interface.py
Normal file
@ -0,0 +1,163 @@
|
||||
import streamlit as st
|
||||
|
||||
from .sankey import afficher_sankey
|
||||
|
||||
niveau_labels = {
|
||||
0: "Produit final",
|
||||
1: "Composant",
|
||||
2: "Minerai",
|
||||
10: "Opération",
|
||||
11: "Pays d'opération",
|
||||
12: "Acteur d'opération",
|
||||
99: "Pays géographique"
|
||||
}
|
||||
|
||||
inverse_niveau_labels = {v: k for k, v in niveau_labels.items()}
|
||||
|
||||
|
||||
def preparer_graphe(G):
|
||||
"""Nettoie et prépare le graphe pour l'analyse."""
|
||||
niveaux_temp = {
|
||||
node: int(str(attrs.get("niveau")).strip('"'))
|
||||
for node, attrs in G.nodes(data=True)
|
||||
if attrs.get("niveau") and str(attrs.get("niveau")).strip('"').isdigit()
|
||||
}
|
||||
G.remove_nodes_from([n for n in G.nodes() if n not in niveaux_temp])
|
||||
G.remove_nodes_from(
|
||||
[n for n in G.nodes() if niveaux_temp.get(n) == 10 and 'Reserves' in n])
|
||||
return G, niveaux_temp
|
||||
|
||||
|
||||
def selectionner_niveaux():
|
||||
"""Interface pour sélectionner les niveaux de départ et d'arrivée."""
|
||||
st.markdown("## Sélection des nœuds de départ et d'arrivée")
|
||||
valeur_defaut = "-- Sélectionner un niveau --"
|
||||
niveau_choix = [valeur_defaut] + list(niveau_labels.values())
|
||||
|
||||
niveau_depart = st.selectbox("Niveau de départ", niveau_choix, key="analyse_niveau_depart")
|
||||
if niveau_depart == valeur_defaut:
|
||||
return None, None
|
||||
|
||||
niveau_depart_int = inverse_niveau_labels[niveau_depart]
|
||||
niveaux_arrivee_possibles = [v for k, v in niveau_labels.items() if k > niveau_depart_int]
|
||||
niveaux_arrivee_choix = [valeur_defaut] + niveaux_arrivee_possibles
|
||||
|
||||
analyse_niveau_arrivee = st.selectbox("Niveau d'arrivée", niveaux_arrivee_choix, key="analyse_niveau_arrivee")
|
||||
if analyse_niveau_arrivee == valeur_defaut:
|
||||
return niveau_depart_int, None
|
||||
|
||||
niveau_arrivee_int = inverse_niveau_labels[analyse_niveau_arrivee]
|
||||
return niveau_depart_int, niveau_arrivee_int
|
||||
|
||||
|
||||
def selectionner_minerais(G, niveau_depart, niveau_arrivee):
|
||||
"""Interface pour sélectionner les minerais si nécessaire."""
|
||||
minerais_selection = None
|
||||
if niveau_depart < 2 < niveau_arrivee:
|
||||
st.markdown("### Sélectionner un ou plusieurs minerais")
|
||||
# Tous les nœuds de niveau 2 (minerai)
|
||||
minerais_nodes = sorted([
|
||||
n for n, d in G.nodes(data=True)
|
||||
if d.get("niveau") and int(str(d.get("niveau")).strip('"')) == 2
|
||||
])
|
||||
|
||||
minerais_selection = st.multiselect(
|
||||
"Filtrer par minerais (optionnel)",
|
||||
minerais_nodes,
|
||||
key="analyse_minerais"
|
||||
)
|
||||
|
||||
return minerais_selection
|
||||
|
||||
|
||||
def selectionner_noeuds(G, niveaux_temp, niveau_depart, niveau_arrivee):
|
||||
"""Interface pour sélectionner les nœuds spécifiques de départ et d'arrivée."""
|
||||
st.markdown("---")
|
||||
st.markdown("## Sélection fine des items")
|
||||
|
||||
depart_nodes = [n for n in G.nodes() if niveaux_temp.get(n) == niveau_depart]
|
||||
arrivee_nodes = [n for n in G.nodes() if niveaux_temp.get(n) == niveau_arrivee]
|
||||
|
||||
noeuds_depart = st.multiselect("Filtrer par noeuds de départ (optionnel)",
|
||||
sorted(depart_nodes),
|
||||
key="analyse_noeuds_depart")
|
||||
noeuds_arrivee = st.multiselect("Filtrer par noeuds d'arrivée (optionnel)",
|
||||
sorted(arrivee_nodes),
|
||||
key="analyse_noeuds_arrivee")
|
||||
|
||||
noeuds_depart = noeuds_depart if noeuds_depart else None
|
||||
noeuds_arrivee = noeuds_arrivee if noeuds_arrivee else None
|
||||
|
||||
return noeuds_depart, noeuds_arrivee
|
||||
|
||||
|
||||
def configurer_filtres_vulnerabilite():
|
||||
"""Interface pour configurer les filtres de vulnérabilité."""
|
||||
st.markdown("---")
|
||||
st.markdown("## Sélection des filtres pour identifier les vulnérabilités")
|
||||
|
||||
filtrer_ics = st.checkbox("Filtrer les chemins contenant au moins minerai critique pour un composant (ICS > 66 %)",
|
||||
key="analyse_filtrer_ics")
|
||||
filtrer_ivc = st.checkbox("Filtrer les chemins contenant au moins un minerai critique par rapport à la concurrence sectorielle (IVC > 30)",
|
||||
key="analyse_filtrer_ivc")
|
||||
filtrer_ihh = st.checkbox("Filtrer les chemins contenant au moins une opération critique par rapport à la concentration géographique ou industrielle (IHH pays ou acteurs > 25)",
|
||||
key="analyse_filtrer_ihh")
|
||||
|
||||
ihh_type = "Pays"
|
||||
if filtrer_ihh:
|
||||
ihh_type = st.radio("Appliquer le filtre IHH sur :",
|
||||
["Pays", "Acteurs"],
|
||||
horizontal=True,
|
||||
key="analyse_ihh_type")
|
||||
|
||||
filtrer_isg = st.checkbox("Filtrer les chemins contenant un pays instable (ISG ≥ 60)",
|
||||
key="analyse_filtrer_isg")
|
||||
logique_filtrage = st.radio("Logique de filtrage",
|
||||
["OU", "ET"],
|
||||
horizontal=True,
|
||||
key="analyse_logique_filtrage")
|
||||
|
||||
return filtrer_ics, filtrer_ivc, filtrer_ihh, ihh_type, filtrer_isg, logique_filtrage
|
||||
|
||||
|
||||
def interface_analyse(G_temp):
|
||||
try:
|
||||
st.markdown("# Analyse")
|
||||
|
||||
# Préparation du graphe
|
||||
G_temp, niveaux_temp = preparer_graphe(G_temp)
|
||||
|
||||
# Sélection des niveaux
|
||||
niveau_depart, niveau_arrivee = selectionner_niveaux()
|
||||
if niveau_depart is None or niveau_arrivee is None:
|
||||
return
|
||||
|
||||
# Sélection des minerais si nécessaire
|
||||
minerais_selection = selectionner_minerais(G_temp, niveau_depart, niveau_arrivee)
|
||||
|
||||
# Sélection fine des noeuds
|
||||
noeuds_depart, noeuds_arrivee = selectionner_noeuds(G_temp, niveaux_temp, niveau_depart, niveau_arrivee)
|
||||
|
||||
# Configuration des filtres de vulnérabilité
|
||||
filtrer_ics, filtrer_ivc, filtrer_ihh, ihh_type, filtrer_isg, logique_filtrage = configurer_filtres_vulnerabilite()
|
||||
|
||||
# Lancement de l'analyse
|
||||
st.markdown("---")
|
||||
if st.button("Lancer l'analyse", type="primary", key="analyse_lancer"):
|
||||
afficher_sankey(
|
||||
G_temp,
|
||||
niveau_depart=niveau_depart,
|
||||
niveau_arrivee=niveau_arrivee,
|
||||
noeuds_depart=noeuds_depart,
|
||||
noeuds_arrivee=noeuds_arrivee,
|
||||
minerais=minerais_selection,
|
||||
filtrer_ics=filtrer_ics,
|
||||
filtrer_ivc=filtrer_ivc,
|
||||
filtrer_ihh=filtrer_ihh,
|
||||
ihh_type=ihh_type,
|
||||
filtrer_isg=filtrer_isg,
|
||||
logique_filtrage=logique_filtrage
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Erreur de prévisualisation du graphe : {e}")
|
||||
350
app/analyse/sankey.py
Normal file
350
app/analyse/sankey.py
Normal file
@ -0,0 +1,350 @@
|
||||
import streamlit as st
|
||||
from networkx.drawing.nx_agraph import write_dot
|
||||
import pandas as pd
|
||||
import plotly.graph_objects as go
|
||||
import networkx as nx
|
||||
import logging
|
||||
import tempfile
|
||||
|
||||
from utils.graph_utils import (
|
||||
extraire_chemins_depuis,
|
||||
extraire_chemins_vers,
|
||||
couleur_noeud
|
||||
)
|
||||
|
||||
niveau_labels = {
|
||||
0: "Produit final",
|
||||
1: "Composant",
|
||||
2: "Minerai",
|
||||
10: "Opération",
|
||||
11: "Pays d'opération",
|
||||
12: "Acteur d'opération",
|
||||
99: "Pays géographique"
|
||||
}
|
||||
|
||||
inverse_niveau_labels = {v: k for k, v in niveau_labels.items()}
|
||||
|
||||
def extraire_niveaux(G):
|
||||
"""Extrait les niveaux des nœuds du graphe"""
|
||||
niveaux = {}
|
||||
for node, attrs in G.nodes(data=True):
|
||||
niveau_str = attrs.get("niveau")
|
||||
try:
|
||||
if niveau_str:
|
||||
niveaux[node] = int(str(niveau_str).strip('"'))
|
||||
except ValueError:
|
||||
logging.warning(f"Niveau non entier pour le noeud {node}: {niveau_str}")
|
||||
return niveaux
|
||||
|
||||
def extraire_criticite(G, u, v):
|
||||
"""Extrait la criticité d'un lien entre deux nœuds"""
|
||||
data = G.get_edge_data(u, v)
|
||||
if not data:
|
||||
return 0
|
||||
if isinstance(data, dict) and all(isinstance(k, int) for k in data):
|
||||
return float(data[0].get("criticite", 0))
|
||||
return float(data.get("criticite", 0))
|
||||
|
||||
def extraire_chemins_selon_criteres(G, niveaux, niveau_depart, noeuds_depart, noeuds_arrivee, minerais):
|
||||
"""Extrait les chemins selon les critères spécifiés"""
|
||||
chemins = []
|
||||
if noeuds_depart and noeuds_arrivee:
|
||||
for nd in noeuds_depart:
|
||||
for na in noeuds_arrivee:
|
||||
tous_chemins = extraire_chemins_depuis(G, nd)
|
||||
chemins.extend([chemin for chemin in tous_chemins if na in chemin])
|
||||
elif noeuds_depart:
|
||||
for nd in noeuds_depart:
|
||||
chemins.extend(extraire_chemins_depuis(G, nd))
|
||||
elif noeuds_arrivee:
|
||||
for na in noeuds_arrivee:
|
||||
chemins.extend(extraire_chemins_vers(G, na, niveau_depart))
|
||||
else:
|
||||
sources_depart = [n for n in G.nodes() if niveaux.get(n) == niveau_depart]
|
||||
for nd in sources_depart:
|
||||
chemins.extend(extraire_chemins_depuis(G, nd))
|
||||
|
||||
if minerais:
|
||||
chemins = [chemin for chemin in chemins if any(n in minerais for n in chemin)]
|
||||
|
||||
return chemins
|
||||
|
||||
def verifier_critere_ihh(G, chemin, niveaux, ihh_type):
|
||||
"""Vérifie si un chemin respecte le critère IHH (concentration géographique ou industrielle)"""
|
||||
ihh_field = "ihh_pays" if ihh_type == "Pays" else "ihh_acteurs"
|
||||
for i in range(len(chemin) - 1):
|
||||
u, v = chemin[i], chemin[i + 1]
|
||||
niveau_u = niveaux.get(u)
|
||||
niveau_v = niveaux.get(v)
|
||||
|
||||
if niveau_u in (10, 1010) and int(G.nodes[u].get(ihh_field, 0)) > 25:
|
||||
return True
|
||||
if niveau_v in (10, 1010) and int(G.nodes[v].get(ihh_field, 0)) > 25:
|
||||
return True
|
||||
return False
|
||||
|
||||
def verifier_critere_ivc(G, chemin, niveaux):
|
||||
"""Vérifie si un chemin respecte le critère IVC (criticité par rapport à la concurrence sectorielle)"""
|
||||
for i in range(len(chemin) - 1):
|
||||
u = chemin[i]
|
||||
niveau_u = niveaux.get(u)
|
||||
if niveau_u in (2, 1002) and int(G.nodes[u].get("ivc", 0)) > 30:
|
||||
return True
|
||||
return False
|
||||
|
||||
def verifier_critere_ics(G, chemin, niveaux):
|
||||
"""Vérifie si un chemin respecte le critère ICS (criticité d'un minerai pour un composant)"""
|
||||
for i in range(len(chemin) - 1):
|
||||
u, v = chemin[i], chemin[i + 1]
|
||||
niveau_u = niveaux.get(u)
|
||||
niveau_v = niveaux.get(v)
|
||||
|
||||
if ((niveau_u == 1 and niveau_v == 2) or
|
||||
(niveau_u == 1001 and niveau_v == 1002) or
|
||||
(niveau_u == 10 and niveau_v in (1000, 1001))) and extraire_criticite(G, u, v) > 0.66:
|
||||
return True
|
||||
return False
|
||||
|
||||
def verifier_critere_isg(G, chemin, niveaux):
|
||||
"""Vérifie si un chemin contient un pays instable (ISG ≥ 60)"""
|
||||
for i in range(len(chemin) - 1):
|
||||
u, v = chemin[i], chemin[i + 1]
|
||||
|
||||
for n in (u, v):
|
||||
if niveaux.get(n) == 99 and int(G.nodes[n].get("isg", 0)) >= 60:
|
||||
return True
|
||||
elif niveaux.get(n) in (11, 12, 1011, 1012):
|
||||
for succ in G.successors(n):
|
||||
if niveaux.get(succ) == 99 and int(G.nodes[succ].get("isg", 0)) >= 60:
|
||||
return True
|
||||
return False
|
||||
|
||||
def extraire_liens_filtres(chemins, niveaux, niveau_depart, niveau_arrivee, niveaux_speciaux):
|
||||
"""Extrait les liens des chemins en respectant les niveaux"""
|
||||
liens = set()
|
||||
for chemin in chemins:
|
||||
for i in range(len(chemin) - 1):
|
||||
u, v = chemin[i], chemin[i + 1]
|
||||
niveau_u = niveaux.get(u, 999)
|
||||
niveau_v = niveaux.get(v, 999)
|
||||
if (
|
||||
(niveau_depart <= niveau_u <= niveau_arrivee or niveau_u in niveaux_speciaux)
|
||||
and (niveau_depart <= niveau_v <= niveau_arrivee or niveau_v in niveaux_speciaux)
|
||||
):
|
||||
liens.add((u, v))
|
||||
return liens
|
||||
|
||||
def filtrer_chemins_par_criteres(G, chemins, niveaux, niveau_depart, niveau_arrivee,
|
||||
filtrer_ics, filtrer_ivc, filtrer_ihh, ihh_type, filtrer_isg, logique_filtrage):
|
||||
"""Filtre les chemins selon les critères de vulnérabilité"""
|
||||
niveaux_speciaux = [1000, 1001, 1002, 1010, 1011, 1012]
|
||||
|
||||
# Extraction des liens sans filtrage
|
||||
liens_chemins = extraire_liens_filtres(chemins, niveaux, niveau_depart, niveau_arrivee, niveaux_speciaux)
|
||||
|
||||
# Si aucun filtre n'est appliqué, retourner tous les chemins
|
||||
if not any([filtrer_ics, filtrer_ivc, filtrer_ihh, filtrer_isg]):
|
||||
return liens_chemins, set()
|
||||
|
||||
# Application des filtres sur les chemins
|
||||
chemins_filtres = set()
|
||||
for chemin in chemins:
|
||||
# Vérification des critères pour ce chemin
|
||||
has_ihh = filtrer_ihh and verifier_critere_ihh(G, chemin, niveaux, ihh_type)
|
||||
has_ivc = filtrer_ivc and verifier_critere_ivc(G, chemin, niveaux)
|
||||
has_criticite = filtrer_ics and verifier_critere_ics(G, chemin, niveaux)
|
||||
has_isg_critique = filtrer_isg and verifier_critere_isg(G, chemin, niveaux)
|
||||
|
||||
# Appliquer la logique de filtrage
|
||||
if logique_filtrage == "ET":
|
||||
keep = True
|
||||
if filtrer_ihh: keep = keep and has_ihh
|
||||
if filtrer_ivc: keep = keep and has_ivc
|
||||
if filtrer_ics: keep = keep and has_criticite
|
||||
if filtrer_isg: keep = keep and has_isg_critique
|
||||
if keep:
|
||||
chemins_filtres.add(tuple(chemin))
|
||||
elif logique_filtrage == "OU":
|
||||
if has_ihh or has_ivc or has_criticite or has_isg_critique:
|
||||
chemins_filtres.add(tuple(chemin))
|
||||
|
||||
# Extraction des liens après filtrage
|
||||
liens_filtres = extraire_liens_filtres(
|
||||
chemins_filtres, niveaux, niveau_depart, niveau_arrivee, niveaux_speciaux
|
||||
)
|
||||
|
||||
return liens_filtres, chemins_filtres
|
||||
|
||||
def couleur_criticite(p):
|
||||
"""Retourne la couleur en fonction du niveau de criticité"""
|
||||
if p <= 0.33:
|
||||
return "darkgreen"
|
||||
elif p <= 0.66:
|
||||
return "orange"
|
||||
else:
|
||||
return "darkred"
|
||||
|
||||
def edge_info(G, u, v):
|
||||
"""Génère l'info-bulle pour un lien"""
|
||||
data = G.get_edge_data(u, v)
|
||||
if not data:
|
||||
return f"Relation : {u} → {v}"
|
||||
if isinstance(data, dict) and all(isinstance(k, int) for k in data):
|
||||
data = data[0]
|
||||
base = [f"{k}: {v}" for k, v in data.items()]
|
||||
return f"Relation : {u} → {v}<br>" + "<br>".join(base)
|
||||
|
||||
def preparer_donnees_sankey(G, liens_chemins, niveaux, chemins):
|
||||
"""Prépare les données pour le graphique Sankey"""
|
||||
df_liens = pd.DataFrame(list(liens_chemins), columns=["source", "target"])
|
||||
df_liens = df_liens.groupby(["source", "target"]).size().reset_index(name="value")
|
||||
|
||||
df_liens["criticite"] = df_liens.apply(
|
||||
lambda row: extraire_criticite(G, row["source"], row["target"]), axis=1)
|
||||
df_liens["value"] = 0.1
|
||||
|
||||
# Ne garder que les nœuds effectivement connectés
|
||||
niveaux_speciaux = [1000, 1001, 1002, 1010, 1011, 1012]
|
||||
|
||||
# Inclure les nœuds connectés + tous les nœuds 10xx traversés dans les chemins
|
||||
noeuds_utilises = set(df_liens["source"]) | set(df_liens["target"])
|
||||
for chemin in chemins:
|
||||
for n in chemin:
|
||||
if niveaux.get(n) in niveaux_speciaux:
|
||||
noeuds_utilises.add(n)
|
||||
|
||||
df_liens["color"] = df_liens.apply(
|
||||
lambda row: couleur_criticite(row["criticite"]) if row["criticite"] > 0 else "gray",
|
||||
axis=1
|
||||
)
|
||||
|
||||
all_nodes = pd.unique(df_liens[["source", "target"]].values.ravel())
|
||||
sorted_nodes = sorted(
|
||||
all_nodes, key=lambda x: niveaux.get(x, 99), reverse=True)
|
||||
node_indices = {name: i for i, name in enumerate(sorted_nodes)}
|
||||
|
||||
customdata = []
|
||||
for n in sorted_nodes:
|
||||
info = [f"{k}: {v}" for k, v in G.nodes[n].items()]
|
||||
niveau = niveaux.get(n, 99)
|
||||
|
||||
# Ajout d'un ISG hérité si applicable
|
||||
if niveau in (11, 12, 1011, 1012):
|
||||
for succ in G.successors(n):
|
||||
if niveaux.get(succ) == 99 and "isg" in G.nodes[succ]:
|
||||
isg_val = G.nodes[succ]["isg"]
|
||||
info.append(f"isg (géographique): {isg_val}")
|
||||
break
|
||||
|
||||
customdata.append("<br>".join(info))
|
||||
|
||||
link_customdata = [
|
||||
edge_info(G, row["source"], row["target"]) for _, row in df_liens.iterrows()
|
||||
]
|
||||
|
||||
return df_liens, sorted_nodes, customdata, link_customdata, node_indices
|
||||
|
||||
def creer_graphique_sankey(G, niveaux, df_liens, sorted_nodes, customdata, link_customdata, node_indices):
|
||||
"""Crée et retourne le graphique Sankey"""
|
||||
sources = df_liens["source"].map(node_indices).tolist()
|
||||
targets = df_liens["target"].map(node_indices).tolist()
|
||||
values = df_liens["value"].tolist()
|
||||
|
||||
fig = go.Figure(go.Sankey(
|
||||
arrangement="snap",
|
||||
node=dict(
|
||||
pad=10,
|
||||
thickness=8,
|
||||
label=sorted_nodes,
|
||||
x=[niveaux.get(n, 99) / 100 for n in sorted_nodes],
|
||||
color=[couleur_noeud(n, niveaux, G) for n in sorted_nodes],
|
||||
customdata=customdata,
|
||||
hovertemplate="%{customdata}<extra></extra>"
|
||||
),
|
||||
link=dict(
|
||||
source=sources,
|
||||
target=targets,
|
||||
value=values,
|
||||
color=df_liens["color"].tolist(),
|
||||
customdata=link_customdata,
|
||||
hovertemplate="%{customdata}<extra></extra>"
|
||||
)
|
||||
))
|
||||
|
||||
fig.update_layout(
|
||||
title_text="Hiérarchie filtrée par niveaux et noeuds",
|
||||
paper_bgcolor="white",
|
||||
plot_bgcolor="white"
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
def exporter_graphe_filtre(G, liens_chemins):
|
||||
"""Gère l'export du graphe filtré au format DOT"""
|
||||
if not st.session_state.get("logged_in", False) or not liens_chemins:
|
||||
return
|
||||
|
||||
G_export = nx.DiGraph()
|
||||
for u, v in liens_chemins:
|
||||
G_export.add_node(u, **G.nodes[u])
|
||||
G_export.add_node(v, **G.nodes[v])
|
||||
data = G.get_edge_data(u, v)
|
||||
if isinstance(data, dict) and all(isinstance(k, int) for k in data):
|
||||
G_export.add_edge(u, v, **data[0])
|
||||
elif isinstance(data, dict):
|
||||
G_export.add_edge(u, v, **data)
|
||||
else:
|
||||
G_export.add_edge(u, v)
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".dot", mode="w", encoding="utf-8") as f:
|
||||
write_dot(G_export, f.name)
|
||||
dot_path = f.name
|
||||
|
||||
with open(dot_path, encoding="utf-8") as f:
|
||||
st.download_button(
|
||||
label="Télécharger le fichier DOT filtré",
|
||||
data=f.read(),
|
||||
file_name="graphe_filtré.dot",
|
||||
mime="text/plain"
|
||||
)
|
||||
|
||||
def afficher_sankey(
|
||||
G,
|
||||
niveau_depart, niveau_arrivee,
|
||||
noeuds_depart=None, noeuds_arrivee=None,
|
||||
minerais=None,
|
||||
filtrer_ics=False, filtrer_ivc=False,
|
||||
filtrer_ihh=False, ihh_type="Pays", filtrer_isg=False,
|
||||
logique_filtrage="OU"):
|
||||
|
||||
# Étape 1 : Extraction des niveaux des nœuds
|
||||
niveaux = extraire_niveaux(G)
|
||||
|
||||
# Étape 2 : Extraction des chemins selon les critères
|
||||
chemins = extraire_chemins_selon_criteres(G, niveaux, niveau_depart, noeuds_depart, noeuds_arrivee, minerais)
|
||||
|
||||
if not chemins:
|
||||
st.warning("Aucun chemin trouvé pour les critères spécifiés.")
|
||||
return
|
||||
|
||||
# Étape 3 : Filtrage des chemins selon les critères de vulnérabilité
|
||||
liens_chemins, chemins_filtres = filtrer_chemins_par_criteres(
|
||||
G, chemins, niveaux, niveau_depart, niveau_arrivee,
|
||||
filtrer_ics, filtrer_ivc, filtrer_ihh, ihh_type, filtrer_isg, logique_filtrage
|
||||
)
|
||||
|
||||
if not liens_chemins:
|
||||
st.warning("Aucun chemin ne correspond aux critères.")
|
||||
return
|
||||
|
||||
# Étape 4 : Préparation des données pour le graphique Sankey
|
||||
df_liens, sorted_nodes, customdata, link_customdata, node_indices = preparer_donnees_sankey(
|
||||
G, liens_chemins, niveaux, chemins_filtres if any([filtrer_ics, filtrer_ivc, filtrer_ihh, filtrer_isg]) else chemins
|
||||
)
|
||||
|
||||
# Étape 5 : Création et affichage du graphique Sankey
|
||||
fig = creer_graphique_sankey(G, niveaux, df_liens, sorted_nodes, customdata, link_customdata, node_indices)
|
||||
st.plotly_chart(fig)
|
||||
|
||||
# Étape 6 : Export optionnel du graphe filtré
|
||||
exporter_graphe_filtre(G, liens_chemins)
|
||||
44
app/fiches/README.md
Normal file
44
app/fiches/README.md
Normal file
@ -0,0 +1,44 @@
|
||||
# Module Fiches
|
||||
|
||||
Ce module gère la consultation, l'affichage et la manipulation des fiches techniques dans l'application FabNum. Il permet aux utilisateurs de naviguer dans la documentation structurée des différents éléments de la chaîne de fabrication du numérique.
|
||||
|
||||
## Structure du module
|
||||
|
||||
Le module fiches est organisé comme suit :
|
||||
|
||||
- **interface.py** : Point d'entrée principal qui gère l'interface utilisateur des fiches
|
||||
- **generer.py** : Responsable de la génération et du rendu des fiches
|
||||
- **utils/** : Contient des utilitaires spécifiques aux fiches
|
||||
- **dynamic/** : Composants dynamiques pour le rendu des fiches
|
||||
- **assemblage_fabrication/** : Traitement des opérations d'assemblage et de fabrication
|
||||
- **indice/** : Calcul et affichage des différents indices (IHH, IVC, etc.)
|
||||
- **minerai/** : Traitement spécifique aux fiches de minerais
|
||||
- **utils/** : Utilitaires partagés pour le rendu dynamique
|
||||
- **tickets/** : Gestion des tickets liés aux fiches
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
### Navigation et consultation
|
||||
- Affichage hiérarchique des catégories de fiches
|
||||
- Recherche de fiches par mots-clés
|
||||
- Visualisation des fiches au format Markdown avec rendu dynamique
|
||||
- Navigation entre fiches connexes
|
||||
|
||||
### Traitement dynamique
|
||||
- Génération de tableaux de synthèse pour les indices de criticité
|
||||
- Rendu des graphiques et visualisations intégrées aux fiches
|
||||
- Traitement des blocs YAML pour les opérations spécifiques
|
||||
|
||||
### Gestion des tickets
|
||||
- Création de tickets pour signaler des corrections ou des améliorations
|
||||
- Visualisation des tickets existants liés à une fiche
|
||||
- Interface structurée pour soumettre des contributions
|
||||
|
||||
## Utilisation
|
||||
|
||||
1. Accédez à l'onglet "Fiches" dans l'interface principale
|
||||
2. Explorez les catégories ou utilisez la recherche pour trouver une fiche
|
||||
3. Consultez le contenu de la fiche avec ses visualisations dynamiques
|
||||
4. Si nécessaire, utilisez l'option "Créer un ticket" pour proposer des modifications
|
||||
|
||||
Ce module est au cœur de la consultation documentaire de l'application, permettant de naviguer efficacement dans la base de connaissances sur la chaîne de fabrication du numérique.
|
||||
2
app/fiches/__init__.py
Normal file
2
app/fiches/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# __init__.py – app/fiches
|
||||
from .interface import interface_fiches
|
||||
@ -1,23 +1,12 @@
|
||||
# === Constantes et imports ===
|
||||
import streamlit as st
|
||||
import requests
|
||||
import re
|
||||
import os
|
||||
import yaml
|
||||
import markdown
|
||||
from bs4 import BeautifulSoup
|
||||
from datetime import datetime, timezone
|
||||
from latex2mathml.converter import convert as latex_to_mathml
|
||||
from utils.tickets.display import afficher_tickets_par_fiche
|
||||
from utils.tickets.creation import formulaire_creation_ticket_dynamique
|
||||
from utils.tickets.core import rechercher_tickets_gitea
|
||||
from .utils.fiche_utils import render_fiche_markdown
|
||||
|
||||
from config import GITEA_TOKEN, GITEA_URL, ORGANISATION, DEPOT_FICHES, ENV, FICHES_CRITICITE
|
||||
|
||||
from utils.gitea import charger_arborescence_fiches, recuperer_date_dernier_commit
|
||||
|
||||
from utils.fiche_utils import load_seuils, render_fiche_markdown
|
||||
from utils.dynamic import (
|
||||
from .utils.dynamic import (
|
||||
build_dynamic_sections,
|
||||
build_ivc_sections,
|
||||
build_ihh_sections,
|
||||
@ -26,35 +15,6 @@ from utils.dynamic import (
|
||||
build_minerai_sections
|
||||
)
|
||||
|
||||
# === Logique métier ===
|
||||
def fichier_plus_recent(chemin_fichier, reference):
|
||||
try:
|
||||
modif = datetime.fromtimestamp(os.path.getmtime(chemin_fichier), tz=timezone.utc)
|
||||
return modif > reference
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def doit_regenerer_fiche(html_path, fiche_type, fiche_choisie, commit_url, fichiers_criticite):
|
||||
if not os.path.exists(html_path):
|
||||
return True
|
||||
|
||||
local_mtime = datetime.fromtimestamp(os.path.getmtime(html_path), tz=timezone.utc)
|
||||
remote_mtime = recuperer_date_dernier_commit(commit_url)
|
||||
|
||||
if remote_mtime is None or remote_mtime > local_mtime:
|
||||
return True
|
||||
|
||||
if fichier_plus_recent(fichiers_criticite.get("IHH"), local_mtime):
|
||||
return True
|
||||
|
||||
if fiche_type == "minerai" or "minerai" in fiche_choisie.lower():
|
||||
if fichier_plus_recent(fichiers_criticite.get("IVC"), local_mtime):
|
||||
return True
|
||||
if fichier_plus_recent(fichiers_criticite.get("ICS"), local_mtime):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
# === Fonctions de transformation ===
|
||||
def remplacer_latex_par_mathml(markdown_text):
|
||||
def remplacer_bloc_display(match):
|
||||
@ -91,33 +51,7 @@ def markdown_to_html_rgaa(markdown_text, caption_text=None):
|
||||
th["scope"] = "col"
|
||||
return str(soup)
|
||||
|
||||
# === Fonctions principales ===
|
||||
def creer_fiche(md_source, dossier, nom_fichier, seuils):
|
||||
front_match = re.match(r"(?s)^---\n(.*?)\n---\n", md_source)
|
||||
context = yaml.safe_load(front_match.group(1)) if front_match else {}
|
||||
|
||||
type_fiche = context.get("type_fiche")
|
||||
if type_fiche == "indice":
|
||||
indice = context.get("indice_court")
|
||||
if indice == "ICS":
|
||||
md_source = build_dynamic_sections(md_source)
|
||||
elif indice == "IVC":
|
||||
md_source = build_ivc_sections(md_source)
|
||||
elif indice == "IHH":
|
||||
md_source = build_ihh_sections(md_source)
|
||||
elif indice == "ISG":
|
||||
md_source = build_isg_sections(md_source)
|
||||
elif type_fiche in ["assemblage", "fabrication"]:
|
||||
md_source = build_production_sections(md_source)
|
||||
elif type_fiche == "minerai":
|
||||
md_source = build_minerai_sections(md_source)
|
||||
|
||||
contenu_md = render_fiche_markdown(md_source, seuils)
|
||||
md_path = os.path.join("Fiches", dossier, nom_fichier)
|
||||
os.makedirs(os.path.dirname(md_path), exist_ok=True)
|
||||
with open(md_path, "w", encoding="utf-8") as f:
|
||||
f.write(contenu_md)
|
||||
|
||||
def rendu_html(contenu_md):
|
||||
lignes = contenu_md.split('\n')
|
||||
sections_n1 = []
|
||||
section_n1_actuelle = {"titre": None, "intro": [], "sections_n2": {}}
|
||||
@ -156,6 +90,38 @@ def creer_fiche(md_source, dossier, nom_fichier, seuils):
|
||||
html_output.append(f"<details><summary>{sous_titre}</summary>{contenu_html}</details>")
|
||||
|
||||
html_output.append("</section>")
|
||||
|
||||
return html_output
|
||||
|
||||
def generer_fiche(md_source, dossier, nom_fichier, seuils):
|
||||
front_match = re.match(r"(?s)^---\n(.*?)\n---\n", md_source)
|
||||
context = yaml.safe_load(front_match.group(1)) if front_match else {}
|
||||
|
||||
type_fiche = context.get("type_fiche")
|
||||
if type_fiche == "indice":
|
||||
indice = context.get("indice_court")
|
||||
if indice == "ICS":
|
||||
md_source = build_dynamic_sections(md_source)
|
||||
elif indice == "IVC":
|
||||
md_source = build_ivc_sections(md_source)
|
||||
elif indice == "IHH":
|
||||
md_source = build_ihh_sections(md_source)
|
||||
elif indice == "ISG":
|
||||
md_source = build_isg_sections(md_source)
|
||||
elif type_fiche in ["assemblage", "fabrication"]:
|
||||
md_source = build_production_sections(md_source)
|
||||
elif type_fiche == "minerai":
|
||||
md_source = build_minerai_sections(md_source)
|
||||
|
||||
contenu_md = render_fiche_markdown(md_source, seuils)
|
||||
|
||||
md_path = os.path.join("Fiches", dossier, nom_fichier)
|
||||
os.makedirs(os.path.dirname(md_path), exist_ok=True)
|
||||
with open(md_path, "w", encoding="utf-8") as f:
|
||||
f.write(contenu_md)
|
||||
|
||||
html_output = rendu_html(contenu_md)
|
||||
|
||||
html_dir = os.path.join("HTML", dossier)
|
||||
os.makedirs(html_dir, exist_ok=True)
|
||||
html_path = os.path.join(html_dir, os.path.splitext(nom_fichier)[0] + ".html")
|
||||
@ -163,64 +129,3 @@ def creer_fiche(md_source, dossier, nom_fichier, seuils):
|
||||
f.write("\n".join(html_output))
|
||||
|
||||
return html_path
|
||||
|
||||
def afficher_fiches():
|
||||
if "fiches_arbo" not in st.session_state:
|
||||
st.session_state["fiches_arbo"] = charger_arborescence_fiches()
|
||||
|
||||
arbo = st.session_state.get("fiches_arbo", {})
|
||||
if not arbo:
|
||||
st.warning("Aucune fiche disponible pour le moment.")
|
||||
return
|
||||
|
||||
dossiers = sorted(arbo.keys(), key=lambda x: x.lower())
|
||||
dossier_choisi = st.selectbox("Choisissez un dossier", ["-- Sélectionner un dossier --"] + dossiers)
|
||||
|
||||
if dossier_choisi and dossier_choisi != "-- Sélectionner un dossier --":
|
||||
fiches = arbo.get(dossier_choisi, [])
|
||||
noms_fiches = [f['nom'] for f in fiches]
|
||||
fiche_choisie = st.selectbox("Choisissez une fiche", ["-- Sélectionner une fiche --"] + noms_fiches)
|
||||
|
||||
if fiche_choisie and fiche_choisie != "-- Sélectionner une fiche --":
|
||||
fiche_info = next((f for f in fiches if f["nom"] == fiche_choisie), None)
|
||||
if fiche_info:
|
||||
try:
|
||||
headers = {"Authorization": f"token {GITEA_TOKEN}"}
|
||||
reponse_fiche = requests.get(fiche_info["download_url"], headers=headers)
|
||||
reponse_fiche.raise_for_status()
|
||||
md_source = reponse_fiche.text
|
||||
|
||||
if "seuils" not in st.session_state:
|
||||
SEUILS = load_seuils("assets/config.yaml")
|
||||
st.session_state["seuils"] = SEUILS
|
||||
else:
|
||||
SEUILS = st.session_state["seuils"]
|
||||
|
||||
html_path = os.path.join("HTML", dossier_choisi, os.path.splitext(fiche_choisie)[0] + ".html")
|
||||
path_relative = f"Documents/{dossier_choisi}/{fiche_choisie}"
|
||||
commits_url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/commits?path={path_relative}&sha={ENV}"
|
||||
|
||||
regenerate = doit_regenerer_fiche(
|
||||
html_path=html_path,
|
||||
fiche_type=fiche_info.get("type", ""),
|
||||
fiche_choisie=fiche_choisie,
|
||||
commit_url=commits_url,
|
||||
fichiers_criticite=FICHES_CRITICITE
|
||||
)
|
||||
|
||||
if regenerate:
|
||||
st.info("DEBUG : Régénération de la fiche")
|
||||
html_path = creer_fiche(md_source, dossier_choisi, fiche_choisie, SEUILS)
|
||||
else:
|
||||
st.info("DEBUG : Pas de régénération")
|
||||
|
||||
with open(html_path, "r", encoding="utf-8") as f:
|
||||
st.markdown(f.read(), unsafe_allow_html=True)
|
||||
|
||||
st.markdown("<hr style='border: 1px solid #ccc; margin: 2rem 0;' />", unsafe_allow_html=True)
|
||||
st.markdown("## Gestion des tickets pour cette fiche")
|
||||
afficher_tickets_par_fiche(rechercher_tickets_gitea(fiche_choisie))
|
||||
formulaire_creation_ticket_dynamique(fiche_choisie)
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Erreur lors du chargement de la fiche : {e}")
|
||||
78
app/fiches/interface.py
Normal file
78
app/fiches/interface.py
Normal file
@ -0,0 +1,78 @@
|
||||
# === Constantes et imports ===
|
||||
import streamlit as st
|
||||
import requests
|
||||
import os
|
||||
|
||||
from .utils.tickets.display import afficher_tickets_par_fiche
|
||||
from .utils.tickets.creation import formulaire_creation_ticket_dynamique
|
||||
from .utils.tickets.core import rechercher_tickets_gitea
|
||||
|
||||
from config import GITEA_TOKEN, GITEA_URL, ORGANISATION, DEPOT_FICHES, ENV, FICHES_CRITICITE
|
||||
|
||||
from utils.gitea import charger_arborescence_fiches
|
||||
|
||||
from .utils.fiche_utils import load_seuils, doit_regenerer_fiche
|
||||
|
||||
from .generer import generer_fiche
|
||||
|
||||
def interface_fiches():
|
||||
st.markdown("# Affichage des fiches")
|
||||
st.markdown("Sélectionner d'abord l'opération que vous souhaitez examiner et ensuite choisisez la fiche à lire.")
|
||||
st.markdown("---")
|
||||
|
||||
if "fiches_arbo" not in st.session_state:
|
||||
st.session_state["fiches_arbo"] = charger_arborescence_fiches()
|
||||
|
||||
arbo = st.session_state.get("fiches_arbo", {})
|
||||
if not arbo:
|
||||
st.warning("Aucune fiche disponible pour le moment.")
|
||||
return
|
||||
|
||||
dossiers = sorted(arbo.keys(), key=lambda x: x.lower())
|
||||
dossier_choisi = st.selectbox("Choisissez un dossier", ["-- Sélectionner un dossier --"] + dossiers)
|
||||
|
||||
if dossier_choisi and dossier_choisi != "-- Sélectionner un dossier --":
|
||||
fiches = arbo.get(dossier_choisi, [])
|
||||
noms_fiches = [f['nom'] for f in fiches]
|
||||
fiche_choisie = st.selectbox("Choisissez une fiche", ["-- Sélectionner une fiche --"] + noms_fiches)
|
||||
|
||||
if fiche_choisie and fiche_choisie != "-- Sélectionner une fiche --":
|
||||
fiche_info = next((f for f in fiches if f["nom"] == fiche_choisie), None)
|
||||
if fiche_info:
|
||||
try:
|
||||
headers = {"Authorization": f"token {GITEA_TOKEN}"}
|
||||
reponse_fiche = requests.get(fiche_info["download_url"], headers=headers)
|
||||
reponse_fiche.raise_for_status()
|
||||
md_source = reponse_fiche.text
|
||||
|
||||
if "seuils" not in st.session_state:
|
||||
SEUILS = load_seuils("assets/config.yaml")
|
||||
st.session_state["seuils"] = SEUILS
|
||||
else:
|
||||
SEUILS = st.session_state["seuils"]
|
||||
|
||||
html_path = os.path.join("HTML", dossier_choisi, os.path.splitext(fiche_choisie)[0] + ".html")
|
||||
path_relative = f"Documents/{dossier_choisi}/{fiche_choisie}"
|
||||
commits_url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/commits?path={path_relative}&sha={ENV}"
|
||||
|
||||
regenerate = doit_regenerer_fiche(
|
||||
html_path=html_path,
|
||||
fiche_type=fiche_info.get("type", ""),
|
||||
fiche_choisie=fiche_choisie,
|
||||
commit_url=commits_url,
|
||||
fichiers_criticite=FICHES_CRITICITE
|
||||
)
|
||||
|
||||
if regenerate:
|
||||
html_path = generer_fiche(md_source, dossier_choisi, fiche_choisie, SEUILS)
|
||||
|
||||
with open(html_path, "r", encoding="utf-8") as f:
|
||||
st.markdown(f.read(), unsafe_allow_html=True)
|
||||
|
||||
st.markdown("<hr style='border: 1px solid #ccc; margin: 2rem 0;' />", unsafe_allow_html=True)
|
||||
st.markdown("## Gestion des tickets pour cette fiche")
|
||||
afficher_tickets_par_fiche(rechercher_tickets_gitea(fiche_choisie))
|
||||
formulaire_creation_ticket_dynamique(fiche_choisie)
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Erreur lors du chargement de la fiche : {e}")
|
||||
@ -7,11 +7,11 @@ from ..utils.pastille import pastille
|
||||
|
||||
IHH_RE = re.compile(r"```yaml\s+opération:(.*?)```", re.S | re.I)
|
||||
|
||||
def _synth_ihh(operations: list[dict]) -> str:
|
||||
def _extraire_donnees_operations(operations: list[dict]) -> dict:
|
||||
"""Extrait et organise les données des opérations par item."""
|
||||
data_by_item = {}
|
||||
|
||||
for op in operations:
|
||||
# nom = op.get('nom', '')
|
||||
item_id = op.get('minerai', op.get('produit', op.get('composant', '')))
|
||||
if not item_id:
|
||||
continue
|
||||
@ -44,58 +44,98 @@ def _synth_ihh(operations: list[dict]) -> str:
|
||||
data_by_item[item_id]['fabrication_ihh_pays'] = op['fabrication'].get('ihh_pays', '-')
|
||||
data_by_item[item_id]['fabrication_ihh_acteurs'] = op['fabrication'].get('ihh_acteurs', '-')
|
||||
|
||||
result = []
|
||||
return data_by_item
|
||||
|
||||
|
||||
def _generer_tableau_produits(produits: dict) -> str:
|
||||
"""Génère un tableau markdown pour les produits."""
|
||||
if not produits:
|
||||
return ""
|
||||
|
||||
resultat = ["\n\n## Assemblage des produits\n"]
|
||||
lignes = [
|
||||
"| Produit | Assemblage IHH Pays | Assemblage IHH Acteurs |",
|
||||
"| :-- | :--: | :--: |"
|
||||
]
|
||||
|
||||
for produit, data in sorted(produits.items()):
|
||||
pastille_1 = pastille("IHH", data['assemblage_ihh_pays'])
|
||||
pastille_2 = pastille("IHH", data['assemblage_ihh_acteurs'])
|
||||
lignes.append(
|
||||
f"| {produit} | {pastille_1} {data['assemblage_ihh_pays']} | {pastille_2} {data['assemblage_ihh_acteurs']} |"
|
||||
)
|
||||
|
||||
resultat.append("\n".join(lignes))
|
||||
return "\n".join(resultat)
|
||||
|
||||
|
||||
def _generer_tableau_composants(composants: dict) -> str:
|
||||
"""Génère un tableau markdown pour les composants."""
|
||||
if not composants:
|
||||
return ""
|
||||
|
||||
resultat = ["\n\n## Fabrication des composants\n"]
|
||||
lignes = [
|
||||
"| Composant | Fabrication IHH Pays | Fabrication IHH Acteurs |",
|
||||
"| :-- | :--: | :--: |"
|
||||
]
|
||||
|
||||
for composant, data in sorted(composants.items()):
|
||||
pastille_1 = pastille("IHH", data['fabrication_ihh_pays'])
|
||||
pastille_2 = pastille("IHH", data['fabrication_ihh_acteurs'])
|
||||
lignes.append(
|
||||
f"| {composant} | {pastille_1} {data['fabrication_ihh_pays']} | {pastille_2} {data['fabrication_ihh_acteurs']} |"
|
||||
)
|
||||
|
||||
resultat.append("\n".join(lignes))
|
||||
return "\n".join(resultat)
|
||||
|
||||
|
||||
def _generer_tableau_minerais(minerais: dict) -> str:
|
||||
"""Génère un tableau markdown pour les minerais."""
|
||||
if not minerais:
|
||||
return ""
|
||||
|
||||
resultat = ["\n\n## Opérations sur les minerais\n"]
|
||||
lignes = [
|
||||
"| Minerai | Extraction IHH Pays | Extraction IHH Acteurs | Réserves IHH Pays | Traitement IHH Pays | Traitement IHH Acteurs |",
|
||||
"| :-- | :--: | :--: | :--: | :--: | :--: |"
|
||||
]
|
||||
|
||||
for minerai, data in sorted(minerais.items()):
|
||||
pastille_1 = pastille("IHH", data['extraction_ihh_pays'])
|
||||
pastille_2 = pastille("IHH", data['extraction_ihh_acteurs'])
|
||||
pastille_3 = pastille("IHH", data['reserves_ihh_pays'])
|
||||
pastille_4 = pastille("IHH", data['traitement_ihh_pays'])
|
||||
pastille_5 = pastille("IHH", data['traitement_ihh_acteurs'])
|
||||
lignes.append(
|
||||
f"| {minerai} | {pastille_1} {data['extraction_ihh_pays']} | {pastille_2} {data['extraction_ihh_acteurs']} | "
|
||||
f"{pastille_3} {data['reserves_ihh_pays']} | {pastille_4} {data['traitement_ihh_pays']} | {pastille_5} {data['traitement_ihh_acteurs']} |"
|
||||
)
|
||||
|
||||
resultat.append("\n".join(lignes))
|
||||
return "\n".join(resultat)
|
||||
|
||||
|
||||
def _synth_ihh(operations: list[dict]) -> str:
|
||||
"""Génère des tableaux de synthèse pour les indices HHI à partir des opérations."""
|
||||
# Extraction et organisation des données
|
||||
data_by_item = _extraire_donnees_operations(operations)
|
||||
|
||||
# Catégorisation des items
|
||||
produits = {k: v for k, v in data_by_item.items() if v['type'] == 'produit'}
|
||||
if produits:
|
||||
result.append("\n\n## Assemblage des produits\n")
|
||||
produit_lignes = [
|
||||
"| Produit | Assemblage IHH Pays | Assemblage IHH Acteurs |",
|
||||
"| :-- | :--: | :--: |"
|
||||
]
|
||||
for produit, data in sorted(produits.items()):
|
||||
pastille_1 = pastille("IHH", data['assemblage_ihh_pays'])
|
||||
pastille_2 = pastille("IHH", data['assemblage_ihh_acteurs'])
|
||||
produit_lignes.append(
|
||||
f"| {produit} | {pastille_1} {data['assemblage_ihh_pays']} | {pastille_2} {data['assemblage_ihh_acteurs']} |"
|
||||
)
|
||||
result.append("\n".join(produit_lignes))
|
||||
|
||||
composants = {k: v for k, v in data_by_item.items() if v['type'] == 'composant'}
|
||||
if composants:
|
||||
result.append("\n\n## Fabrication des composants\n")
|
||||
composant_lignes = [
|
||||
"| Composant | Fabrication IHH Pays | Fabrication IHH Acteurs |",
|
||||
"| :-- | :--: | :--: |"
|
||||
]
|
||||
for composant, data in sorted(composants.items()):
|
||||
pastille_1 = pastille("IHH", data['fabrication_ihh_pays'])
|
||||
pastille_2 = pastille("IHH", data['fabrication_ihh_acteurs'])
|
||||
composant_lignes.append(
|
||||
f"| {composant} | {pastille_1} {data['fabrication_ihh_pays']} | {pastille_2} {data['fabrication_ihh_acteurs']} |"
|
||||
)
|
||||
result.append("\n".join(composant_lignes))
|
||||
|
||||
minerais = {k: v for k, v in data_by_item.items() if v['type'] == 'minerai'}
|
||||
if minerais:
|
||||
result.append("\n\n## Opérations sur les minerais\n")
|
||||
minerai_lignes = [
|
||||
"| Minerai | Extraction IHH Pays | Extraction IHH Acteurs | Réserves IHH Pays | Traitement IHH Pays | Traitement IHH Acteurs |",
|
||||
"| :-- | :--: | :--: | :--: | :--: | :--: |"
|
||||
]
|
||||
for minerai, data in sorted(minerais.items()):
|
||||
pastille_1 = pastille("IHH", data['extraction_ihh_pays'])
|
||||
pastille_2 = pastille("IHH", data['extraction_ihh_acteurs'])
|
||||
pastille_3 = pastille("IHH", data['reserves_ihh_pays'])
|
||||
pastille_4 = pastille("IHH", data['traitement_ihh_pays'])
|
||||
pastille_5 = pastille("IHH", data['traitement_ihh_acteurs'])
|
||||
minerai_lignes.append(
|
||||
f"| {minerai} | {pastille_1} {data['extraction_ihh_pays']} | {pastille_2} {data['extraction_ihh_acteurs']} | "
|
||||
f"{pastille_3} {data['reserves_ihh_pays']} | {pastille_4} {data['traitement_ihh_pays']} | {pastille_5} {data['traitement_ihh_acteurs']} |"
|
||||
)
|
||||
result.append("\n".join(minerai_lignes))
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
# Génération des tableaux pour chaque catégorie
|
||||
tableaux = [
|
||||
_generer_tableau_produits(produits),
|
||||
_generer_tableau_composants(composants),
|
||||
_generer_tableau_minerais(minerais)
|
||||
]
|
||||
|
||||
# Assemblage du résultat final
|
||||
return "\n".join([t for t in tableaux if t])
|
||||
|
||||
def build_ihh_sections(md: str) -> str:
|
||||
segments = []
|
||||
@ -282,7 +282,7 @@ def build_minerai_ivc_section(md: str) -> str:
|
||||
try:
|
||||
front_matter = yaml.safe_load(front_match.group(1))
|
||||
type_fiche = front_matter.get("type_fiche")
|
||||
produit = front_matter.get("produit")
|
||||
produit = front_matter.get("schema")
|
||||
|
||||
# Vérifier si c'est bien une fiche de minerai
|
||||
if type_fiche != "minerai" or not produit:
|
||||
@ -342,7 +342,7 @@ def build_minerai_ics_section(md: str) -> str:
|
||||
try:
|
||||
front_matter = yaml.safe_load(front_match.group(1))
|
||||
type_fiche = front_matter.get("type_fiche")
|
||||
produit = front_matter.get("produit")
|
||||
produit = front_matter.get("schema")
|
||||
|
||||
# Vérifier si c'est bien une fiche de minerai
|
||||
if type_fiche != "minerai" or not produit:
|
||||
@ -401,7 +401,7 @@ def build_minerai_ics_composant_section(md: str) -> str:
|
||||
try:
|
||||
front_matter = yaml.safe_load(front_match.group(1))
|
||||
type_fiche = front_matter.get("type_fiche")
|
||||
produit = front_matter.get("produit")
|
||||
produit = front_matter.get("schema")
|
||||
|
||||
# Vérifier si c'est bien une fiche de minerai
|
||||
if type_fiche != "minerai" or not produit:
|
||||
@ -419,7 +419,11 @@ def build_minerai_ics_composant_section(md: str) -> str:
|
||||
|
||||
# Rechercher toutes les sections de composants liés au minerai
|
||||
# Le pattern cherche les titres de niveau 2 de la forme "## * -> Minerai"
|
||||
composant_sections_pattern = rf"## ([^>]+) -> {produit} - .*?\n([\s\S]*?)(?=\n## |$)"
|
||||
composant_sections_pattern = re.compile(
|
||||
rf"^## ([^>]+) -> {re.escape(produit)} - .*?\n([\s\S]*?)(?=^## |\Z)",
|
||||
re.MULTILINE | re.DOTALL
|
||||
)
|
||||
|
||||
composant_sections = re.finditer(composant_sections_pattern, ics_content)
|
||||
|
||||
all_composant_content = []
|
||||
@ -465,7 +469,7 @@ def build_minerai_sections(md: str) -> str:
|
||||
try:
|
||||
front_matter = yaml.safe_load(front_match.group(1))
|
||||
type_fiche = front_matter.get("type_fiche")
|
||||
produit = front_matter.get("produit")
|
||||
produit = front_matter.get("schema") # le produit à rechercher est schema pour faire le lien avec le graphe
|
||||
|
||||
# Vérifier si c'est bien une fiche de minerai
|
||||
if type_fiche != "minerai" or not produit:
|
||||
@ -12,9 +12,13 @@ Usage :
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import frontmatter, yaml, jinja2, re, textwrap, pathlib
|
||||
import frontmatter, yaml, jinja2, re, pathlib
|
||||
from typing import Dict
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import os
|
||||
from utils.gitea import recuperer_date_dernier_commit
|
||||
|
||||
|
||||
def load_seuils(path: str | pathlib.Path = "config/indices_seuils.yaml") -> Dict:
|
||||
"""Charge le fichier YAML des seuils et renvoie le dict 'seuils'."""
|
||||
@ -61,3 +65,32 @@ def render_fiche_markdown(md_text: str, seuils: Dict) -> str:
|
||||
{rendered_body}"""
|
||||
|
||||
return rendered_body
|
||||
|
||||
|
||||
def fichier_plus_recent(chemin_fichier, reference):
|
||||
try:
|
||||
modif = datetime.fromtimestamp(os.path.getmtime(chemin_fichier), tz=timezone.utc)
|
||||
return modif > reference
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def doit_regenerer_fiche(html_path, fiche_type, fiche_choisie, commit_url, fichiers_criticite):
|
||||
if not os.path.exists(html_path):
|
||||
return True
|
||||
|
||||
local_mtime = datetime.fromtimestamp(os.path.getmtime(html_path), tz=timezone.utc)
|
||||
remote_mtime = recuperer_date_dernier_commit(commit_url)
|
||||
|
||||
if remote_mtime is None or remote_mtime > local_mtime:
|
||||
return True
|
||||
|
||||
if fichier_plus_recent(fichiers_criticite.get("IHH"), local_mtime):
|
||||
return True
|
||||
|
||||
if fiche_type == "minerai" or "minerai" in fiche_choisie.lower():
|
||||
if fichier_plus_recent(fichiers_criticite.get("IVC"), local_mtime):
|
||||
return True
|
||||
if fichier_plus_recent(fichiers_criticite.get("ICS"), local_mtime):
|
||||
return True
|
||||
|
||||
return False
|
||||
139
app/fiches/utils/tickets/creation.py
Normal file
139
app/fiches/utils/tickets/creation.py
Normal file
@ -0,0 +1,139 @@
|
||||
# creation.py
|
||||
|
||||
import re
|
||||
import base64
|
||||
import streamlit as st
|
||||
from .core import charger_fiches_et_labels, construire_corps_ticket_markdown, creer_ticket_gitea, get_labels_existants, nettoyer_labels
|
||||
from config import ENV
|
||||
import requests
|
||||
|
||||
|
||||
def parser_modele_ticket(contenu_modele):
|
||||
"""Parse le modèle de ticket en sections."""
|
||||
sections = {}
|
||||
lignes = contenu_modele.splitlines()
|
||||
titre_courant, contenu = None, []
|
||||
|
||||
for ligne in lignes:
|
||||
if ligne.startswith("## "):
|
||||
if titre_courant:
|
||||
sections[titre_courant] = "\n".join(contenu).strip()
|
||||
titre_courant, contenu = ligne[3:].strip(), []
|
||||
elif titre_courant:
|
||||
contenu.append(ligne)
|
||||
|
||||
if titre_courant:
|
||||
sections[titre_courant] = "\n".join(contenu).strip()
|
||||
|
||||
return sections
|
||||
|
||||
|
||||
def generer_labels(fiche_selectionnee):
|
||||
"""Génère les labels basés sur la fiche sélectionnée."""
|
||||
labels, selected_ops = [], []
|
||||
correspondances = charger_fiches_et_labels()
|
||||
cible = correspondances.get(fiche_selectionnee)
|
||||
|
||||
if cible:
|
||||
if len(cible["operations"]) == 1:
|
||||
labels.append(cible["operations"][0])
|
||||
elif len(cible["operations"]) > 1:
|
||||
selected_ops = st.multiselect("Labels opération à associer",
|
||||
cible["operations"],
|
||||
default=cible["operations"])
|
||||
|
||||
return labels, selected_ops, cible
|
||||
|
||||
|
||||
def creer_champs_formulaire(sections, fiche_selectionnee):
|
||||
"""Crée les champs du formulaire basés sur les sections."""
|
||||
reponses = {}
|
||||
|
||||
for section, aide in sections.items():
|
||||
if "Type de contribution" in section:
|
||||
options = sorted(set(re.findall(r"- \[.\] (.+)", aide)))
|
||||
if "Autre" not in options:
|
||||
options.append("Autre")
|
||||
choix = st.radio("Type de contribution", options)
|
||||
reponses[section] = st.text_input("Précisez", "") if choix == "Autre" else choix
|
||||
elif "Fiche concernée" in section:
|
||||
url_fiche = f"https://fabnum-git.peccini.fr/FabNum/Fiches/src/branch/{ENV}/Documents/{fiche_selectionnee.replace(' ', '%20')}"
|
||||
reponses[section] = url_fiche
|
||||
st.text_input("Fiche concernée", value=url_fiche, disabled=True)
|
||||
elif "Sujet de la proposition" in section:
|
||||
reponses[section] = st.text_input(section, help=aide)
|
||||
else:
|
||||
reponses[section] = st.text_area(section, help=aide)
|
||||
|
||||
return reponses
|
||||
|
||||
|
||||
def afficher_controles_formulaire():
|
||||
"""Affiche les boutons de contrôle du formulaire."""
|
||||
col1, col2 = st.columns(2)
|
||||
if col1.button("Prévisualiser le ticket"):
|
||||
st.session_state.previsualiser = True
|
||||
if col2.button("Annuler"):
|
||||
st.session_state.previsualiser = False
|
||||
st.rerun()
|
||||
|
||||
|
||||
def gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible):
|
||||
"""Gère la prévisualisation et la soumission du ticket."""
|
||||
if not st.session_state.get("previsualiser", False):
|
||||
return
|
||||
|
||||
st.subheader("Prévisualisation du ticket")
|
||||
for section, texte in reponses.items():
|
||||
st.markdown(f"#### {section}")
|
||||
st.code(texte, language="markdown")
|
||||
|
||||
titre_ticket = reponses.get("Sujet de la proposition", "").strip() or "Ticket FabNum"
|
||||
final_labels = nettoyer_labels(labels + selected_ops + ([cible["item"]] if cible else []))
|
||||
|
||||
st.markdown(f"**Résumé :**\n- **Titre** : `{titre_ticket}`\n- **Labels** : `{', '.join(final_labels)}`")
|
||||
|
||||
if st.button("Confirmer la création du ticket"):
|
||||
labels_existants = get_labels_existants()
|
||||
labels_ids = [labels_existants[l] for l in final_labels if l in labels_existants]
|
||||
if "Backlog" in labels_existants:
|
||||
labels_ids.append(labels_existants["Backlog"])
|
||||
|
||||
corps = construire_corps_ticket_markdown(reponses)
|
||||
creer_ticket_gitea(titre_ticket, corps, labels_ids)
|
||||
|
||||
st.session_state.previsualiser = False
|
||||
st.success("Ticket créé et formulaire vidé.")
|
||||
|
||||
|
||||
def formulaire_creation_ticket_dynamique(fiche_selectionnee):
|
||||
"""Fonction principale pour le formulaire de création de ticket."""
|
||||
with st.expander("Créer un nouveau ticket lié à cette fiche", expanded=False):
|
||||
# Chargement et vérification du modèle
|
||||
contenu_modele = charger_modele_ticket()
|
||||
if not contenu_modele:
|
||||
st.error("Impossible de charger le modèle de ticket.")
|
||||
return
|
||||
|
||||
# Traitement du modèle et génération du formulaire
|
||||
sections = parser_modele_ticket(contenu_modele)
|
||||
labels, selected_ops, cible = generer_labels(fiche_selectionnee)
|
||||
reponses = creer_champs_formulaire(sections, fiche_selectionnee)
|
||||
|
||||
# Gestion des contrôles et de la prévisualisation
|
||||
afficher_controles_formulaire()
|
||||
gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible)
|
||||
|
||||
|
||||
def charger_modele_ticket():
|
||||
from config import GITEA_URL, GITEA_TOKEN, ORGANISATION, DEPOT_FICHES
|
||||
|
||||
headers = {"Authorization": f"token {GITEA_TOKEN}"}
|
||||
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/contents/.gitea/ISSUE_TEMPLATE/Contenu.md"
|
||||
try:
|
||||
r = requests.get(url, headers=headers, timeout=10)
|
||||
r.raise_for_status()
|
||||
return base64.b64decode(r.json().get("content", "")).decode("utf-8")
|
||||
except Exception as e:
|
||||
st.error(f"Erreur chargement modèle : {e}")
|
||||
return ""
|
||||
40
app/personnalisation/README.md
Normal file
40
app/personnalisation/README.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Module de Personnalisation
|
||||
|
||||
Ce module permet aux utilisateurs de personnaliser la chaîne de fabrication en ajoutant, modifiant ou important des produits finaux et leurs composants. Il offre une interface interactive pour manipuler le graphe de la chaîne de fabrication.
|
||||
|
||||
## Structure du module
|
||||
|
||||
Le module de personnalisation est organisé comme suit :
|
||||
|
||||
- **interface.py** : Point d'entrée principal qui coordonne les différentes fonctionnalités de personnalisation
|
||||
- **ajout.py** : Gère l'ajout de nouveaux produits finaux et leurs relations avec les composants
|
||||
- **modification.py** : Permet de modifier les produits existants et leurs connexions
|
||||
- **import_export.py** : Facilite l'import et l'export de configurations de graphes
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
### Ajout de produits
|
||||
- Interface pour ajouter de nouveaux produits finaux personnalisés
|
||||
- Sélection des composants à associer au produit
|
||||
- Définition des opérations d'assemblage
|
||||
- Mise à jour automatique du graphe
|
||||
|
||||
### Modification de produits
|
||||
- Sélection des produits existants à modifier
|
||||
- Modification des relations avec les composants
|
||||
- Option de suppression des produits
|
||||
- Mise à jour des opérations d'assemblage
|
||||
|
||||
### Import/Export
|
||||
- Exportation du graphe au format JSON pour sauvegarde ou partage
|
||||
- Importation de configurations préexistantes
|
||||
- Restauration de versions précédentes du graphe
|
||||
|
||||
## Utilisation
|
||||
|
||||
1. Accédez à l'onglet "Personnalisation" dans l'interface principale
|
||||
2. Pour ajouter un produit, utilisez la section "Ajouter un produit final"
|
||||
3. Pour modifier un produit existant, utilisez la section "Modifier un produit final ajouté"
|
||||
4. Pour sauvegarder ou partager votre configuration, utilisez les options d'import/export
|
||||
|
||||
Ce module permet de créer des scénarios personnalisés pour analyser différentes configurations de la chaîne de fabrication du numérique et évaluer leurs vulnérabilités potentielles.
|
||||
6
app/personnalisation/__init__.py
Normal file
6
app/personnalisation/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
# __init__.py – app/personnalisation
|
||||
|
||||
from .interface import interface_personnalisation
|
||||
from .ajout import ajouter_produit
|
||||
from .modification import modifier_produit
|
||||
from .import_export import importer_exporter_graph
|
||||
22
app/personnalisation/ajout.py
Normal file
22
app/personnalisation/ajout.py
Normal file
@ -0,0 +1,22 @@
|
||||
import streamlit as st
|
||||
|
||||
def ajouter_produit(G):
|
||||
st.markdown("## Ajouter un nouveau produit final")
|
||||
new_prod = st.text_input("Nom du nouveau produit (unique)", key="new_prod")
|
||||
if new_prod:
|
||||
ops_dispo = sorted([
|
||||
n for n, d in G.nodes(data=True)
|
||||
if d.get("niveau") == "10"
|
||||
and any(G.has_edge(p, n) and G.nodes[p].get("niveau") == "0" for p in G.predecessors(n))
|
||||
])
|
||||
sel_new_op = st.selectbox("Opération d'assemblage (optionnelle)", ["-- Aucune --"] + ops_dispo, index=0)
|
||||
niveau1 = sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "1"])
|
||||
sel_comps = st.multiselect("Composants à lier", options=niveau1)
|
||||
if st.button("Créer le produit"):
|
||||
G.add_node(new_prod, niveau="0", personnalisation="oui", label=new_prod)
|
||||
if sel_new_op != "-- Aucune --":
|
||||
G.add_edge(new_prod, sel_new_op)
|
||||
for comp in sel_comps:
|
||||
G.add_edge(new_prod, comp)
|
||||
st.success(f"{new_prod} ajouté.")
|
||||
return G
|
||||
52
app/personnalisation/import_export.py
Normal file
52
app/personnalisation/import_export.py
Normal file
@ -0,0 +1,52 @@
|
||||
import streamlit as st
|
||||
import json
|
||||
|
||||
def importer_exporter_graph(G):
|
||||
st.markdown("## Sauvegarder ou restaurer la configuration")
|
||||
if st.button("Exporter configuration"):
|
||||
nodes = [n for n, d in G.nodes(data=True) if d.get("personnalisation") == "oui"]
|
||||
edges = [(u, v) for u, v in G.edges() if u in nodes]
|
||||
conf = {"nodes": nodes, "edges": edges}
|
||||
json_str = json.dumps(conf, ensure_ascii=False)
|
||||
st.download_button(
|
||||
label="Télécharger (JSON)",
|
||||
data=json_str,
|
||||
file_name="config_personnalisation.json",
|
||||
mime="application/json"
|
||||
)
|
||||
|
||||
uploaded = st.file_uploader("Importer une configuration JSON (max 100 Ko)", type=["json"])
|
||||
if uploaded:
|
||||
if uploaded.size > 100 * 1024:
|
||||
st.error("Fichier trop volumineux (max 100 Ko).")
|
||||
else:
|
||||
try:
|
||||
conf = json.loads(uploaded.read().decode("utf-8"))
|
||||
all_nodes = conf.get("nodes", [])
|
||||
all_edges = conf.get("edges", [])
|
||||
|
||||
if not all_nodes:
|
||||
st.warning("Aucun produit trouvé dans le fichier.")
|
||||
else:
|
||||
st.markdown("### Sélection des produits à restaurer")
|
||||
sel_nodes = st.multiselect(
|
||||
"Produits à restaurer",
|
||||
options=all_nodes,
|
||||
default=all_nodes,
|
||||
key="restaurer_selection"
|
||||
)
|
||||
|
||||
if st.button("Restaurer les éléments sélectionnés", type="primary"):
|
||||
for node in sel_nodes:
|
||||
if not G.has_node(node):
|
||||
G.add_node(node, niveau="0", personnalisation="oui", label=node)
|
||||
|
||||
for u, v in all_edges:
|
||||
if u in sel_nodes and v in sel_nodes + list(G.nodes()) and not G.has_edge(u, v):
|
||||
G.add_edge(u, v)
|
||||
|
||||
st.success("Configuration partielle restaurée avec succès.")
|
||||
except Exception as e:
|
||||
st.error(f"Erreur d'import : {e}")
|
||||
|
||||
return G
|
||||
24
app/personnalisation/interface.py
Normal file
24
app/personnalisation/interface.py
Normal file
@ -0,0 +1,24 @@
|
||||
# interface.py – app/personnalisation
|
||||
|
||||
import streamlit as st
|
||||
from .ajout import ajouter_produit
|
||||
from .modification import modifier_produit
|
||||
from .import_export import importer_exporter_graph
|
||||
|
||||
def interface_personnalisation(G):
|
||||
st.markdown("""
|
||||
# Personnalisation des produits finaux
|
||||
|
||||
Dans cette section, vous pouvez ajouter des produits finaux qui ne sont pas présents dans la liste,
|
||||
par exemple des produits que vous concevez vous-même.
|
||||
|
||||
Vous pouvez aussi enregistrer ou recharger vos modifications.
|
||||
|
||||
---
|
||||
""")
|
||||
|
||||
G = ajouter_produit(G)
|
||||
G = modifier_produit(G)
|
||||
G = importer_exporter_graph(G)
|
||||
|
||||
return G
|
||||
85
app/personnalisation/modification.py
Normal file
85
app/personnalisation/modification.py
Normal file
@ -0,0 +1,85 @@
|
||||
import streamlit as st
|
||||
|
||||
def get_produits_personnalises(G):
|
||||
"""Récupère la liste des produits personnalisés du niveau 0."""
|
||||
return sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "0" and d.get("personnalisation") == "oui"])
|
||||
|
||||
def supprimer_produit(G, prod):
|
||||
"""Supprime un produit du graphe."""
|
||||
G.remove_node(prod)
|
||||
st.success(f"{prod} supprimé.")
|
||||
st.session_state.pop("prod_sel", None)
|
||||
return G
|
||||
|
||||
def get_operations_disponibles(G):
|
||||
"""Récupère la liste des opérations d'assemblage disponibles."""
|
||||
return sorted([
|
||||
n for n, d in G.nodes(data=True)
|
||||
if d.get("niveau") == "10"
|
||||
and any(G.has_edge(p, n) and G.nodes[p].get("niveau") == "0" for p in G.predecessors(n))
|
||||
])
|
||||
|
||||
def get_operations_actuelles(G, prod):
|
||||
"""Récupère les opérations actuellement liées au produit."""
|
||||
return [succ for succ in G.successors(prod) if G.nodes[succ].get("niveau") == "10"]
|
||||
|
||||
def get_composants_niveau1(G):
|
||||
"""Récupère la liste des composants de niveau 1."""
|
||||
return sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "1"])
|
||||
|
||||
def get_composants_lies(G, prod):
|
||||
"""Récupère les composants actuellement liés au produit."""
|
||||
return [succ for succ in G.successors(prod) if G.nodes[succ].get("niveau") == "1"]
|
||||
|
||||
def mettre_a_jour_operations(G, prod, curr_ops, sel_op):
|
||||
"""Met à jour les opérations liées au produit."""
|
||||
for op in curr_ops:
|
||||
if sel_op == "-- Aucune --" or op != sel_op:
|
||||
G.remove_edge(prod, op)
|
||||
if sel_op != "-- Aucune --" and (not curr_ops or sel_op not in curr_ops):
|
||||
G.add_edge(prod, sel_op)
|
||||
return G
|
||||
|
||||
def mettre_a_jour_composants(G, prod, linked, nouveaux):
|
||||
"""Met à jour les composants liés au produit."""
|
||||
for comp in set(linked) - set(nouveaux):
|
||||
G.remove_edge(prod, comp)
|
||||
for comp in set(nouveaux) - set(linked):
|
||||
G.add_edge(prod, comp)
|
||||
return G
|
||||
|
||||
def modifier_produit(G):
|
||||
st.markdown("## Modifier un produit final ajouté")
|
||||
|
||||
# Sélection du produit à modifier
|
||||
produits0 = get_produits_personnalises(G)
|
||||
sel_display = st.multiselect("Produits à modifier", options=produits0)
|
||||
|
||||
if not sel_display:
|
||||
return G
|
||||
|
||||
# Obtention du produit sélectionné
|
||||
prod = sel_display[0]
|
||||
|
||||
# Suppression du produit si demandé
|
||||
if st.button(f"Supprimer {prod}"):
|
||||
return supprimer_produit(G, prod)
|
||||
|
||||
# Gestion des opérations d'assemblage
|
||||
ops_dispo = get_operations_disponibles(G)
|
||||
curr_ops = get_operations_actuelles(G, prod)
|
||||
default_idx = ops_dispo.index(curr_ops[0]) + 1 if curr_ops and curr_ops[0] in ops_dispo else 0
|
||||
sel_op = st.selectbox("Opération d'assemblage liée", ["-- Aucune --"] + ops_dispo, index=default_idx)
|
||||
|
||||
# Gestion des composants
|
||||
niveau1 = get_composants_niveau1(G)
|
||||
linked = get_composants_lies(G, prod)
|
||||
nouveaux = st.multiselect(f"Composants liés à {prod}", options=niveau1, default=linked)
|
||||
|
||||
# Mise à jour des liens si demandé
|
||||
if st.button(f"Mettre à jour {prod}"):
|
||||
G = mettre_a_jour_operations(G, prod, curr_ops, sel_op)
|
||||
G = mettre_a_jour_composants(G, prod, linked, nouveaux)
|
||||
st.success(f"{prod} mis à jour.")
|
||||
|
||||
return G
|
||||
39
app/visualisations/README.md
Normal file
39
app/visualisations/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Module de Visualisations
|
||||
|
||||
Ce module offre diverses visualisations graphiques pour analyser et comprendre la chaîne de fabrication du numérique. Il permet d'explorer les données sous différents angles et de produire des représentations visuelles informatives.
|
||||
|
||||
## Structure du module
|
||||
|
||||
Le module de visualisations est principalement constitué de :
|
||||
|
||||
- **interface.py** : Gère l'interface utilisateur pour les différentes visualisations disponibles
|
||||
- Utilise les bibliothèques de visualisation comme Plotly, Altair ou matplotlib pour générer les graphiques
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
### Types de visualisations
|
||||
|
||||
- **Cartographie** : Affichage des données sur des cartes pour visualiser la distribution géographique
|
||||
- **Graphiques de répartition** : Visualisation de la distribution des acteurs, pays et ressources
|
||||
- **Indices de criticité** : Représentations graphiques des différents indices :
|
||||
- IHH (Indice Herfindahl-Hirschman) pour la concentration des marchés
|
||||
- IVC (Indice de Vulnérabilité Concurrentielle)
|
||||
- ISG (Indice de Stabilité Géopolitique)
|
||||
- **Statistiques comparatives** : Comparaison entre différentes ressources ou composants
|
||||
|
||||
### Personnalisation des visualisations
|
||||
|
||||
- Filtres pour ajuster les données affichées
|
||||
- Options de tri et de regroupement
|
||||
- Configuration des paramètres visuels (couleurs, échelles, etc.)
|
||||
- Possibilité d'exporter les visualisations
|
||||
|
||||
## Utilisation
|
||||
|
||||
1. Accédez à l'onglet "Visualisations" dans l'interface principale
|
||||
2. Sélectionnez le type de visualisation souhaité
|
||||
3. Configurez les paramètres selon vos besoins d'analyse
|
||||
4. Interagissez avec les graphiques pour explorer les données
|
||||
5. Utilisez les options d'exportation si nécessaire pour sauvegarder les résultats
|
||||
|
||||
Ces visualisations constituent un outil puissant pour l'analyse des vulnérabilités et des dépendances dans la chaîne de fabrication du numérique, permettant d'identifier rapidement les points critiques.
|
||||
2
app/visualisations/__init__.py
Normal file
2
app/visualisations/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# __init__.py – app/fiches
|
||||
from .interface import interface_visualisations
|
||||
201
app/visualisations/interface.py
Normal file
201
app/visualisations/interface.py
Normal file
@ -0,0 +1,201 @@
|
||||
import streamlit as st
|
||||
import altair as alt
|
||||
import numpy as np
|
||||
from collections import Counter
|
||||
import pandas as pd
|
||||
|
||||
|
||||
def afficher_graphique_altair(df):
|
||||
ordre_personnalise = ['Assemblage', 'Fabrication', 'Traitement', 'Extraction']
|
||||
categories = [cat for cat in ordre_personnalise if cat in df['categorie'].unique()]
|
||||
for cat in categories:
|
||||
st.markdown(f"### {cat}")
|
||||
df_cat = df[df['categorie'] == cat].copy()
|
||||
|
||||
coord_pairs = list(zip(df_cat['ihh_pays'].round(1), df_cat['ihh_acteurs'].round(1)))
|
||||
counts = Counter(coord_pairs)
|
||||
|
||||
offset_x = []
|
||||
offset_y = {}
|
||||
seen = Counter()
|
||||
for pair in coord_pairs:
|
||||
rank = seen[pair]
|
||||
seen[pair] += 1
|
||||
if counts[pair] > 1:
|
||||
angle = rank * 1.5
|
||||
radius = 0.8 + 0.4 * rank
|
||||
offset_x.append(radius * np.cos(angle))
|
||||
offset_y[pair] = radius * np.sin(angle)
|
||||
else:
|
||||
offset_x.append(0)
|
||||
offset_y[pair] = 0
|
||||
|
||||
df_cat['ihh_pays'] += offset_x
|
||||
df_cat['ihh_acteurs'] += [offset_y[p] for p in coord_pairs]
|
||||
df_cat['ihh_pays_text'] = df_cat['ihh_pays'] + 0.5
|
||||
df_cat['ihh_acteurs_text'] = df_cat['ihh_acteurs'] + 0.5
|
||||
|
||||
base = alt.Chart(df_cat).encode(
|
||||
x=alt.X('ihh_pays:Q', title='IHH Pays (%)'),
|
||||
y=alt.Y('ihh_acteurs:Q', title='IHH Acteurs (%)'),
|
||||
size=alt.Size('criticite_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None),
|
||||
color=alt.Color('criticite_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred']))
|
||||
)
|
||||
|
||||
points = base.mark_circle(opacity=0.6)
|
||||
lines = alt.Chart(df_cat).mark_rule(strokeWidth=0.5, color='gray').encode(
|
||||
x='ihh_pays:Q', x2='ihh_pays_text:Q',
|
||||
y='ihh_acteurs:Q', y2='ihh_acteurs_text:Q'
|
||||
)
|
||||
|
||||
labels = alt.Chart(df_cat).mark_text(
|
||||
align='left', dx=3, dy=-3, fontSize=8, font='Arial', angle=335
|
||||
).encode(
|
||||
x='ihh_pays_text:Q',
|
||||
y='ihh_acteurs_text:Q',
|
||||
text='nom:N'
|
||||
)
|
||||
|
||||
hline_15 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='green').encode(y=alt.datum(15))
|
||||
hline_25 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='red').encode(y=alt.datum(25))
|
||||
vline_15 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='green').encode(x=alt.datum(15))
|
||||
vline_25 = alt.Chart(df_cat).mark_rule(strokeDash=[2,2], color='red').encode(x=alt.datum(25))
|
||||
|
||||
chart = (points + lines + labels + hline_15 + hline_25 + vline_15 + vline_25).properties(
|
||||
width=500,
|
||||
height=400,
|
||||
title=f"Concentration et criticité – {cat}"
|
||||
).interactive()
|
||||
|
||||
st.altair_chart(chart, use_container_width=True)
|
||||
|
||||
|
||||
def creer_graphes(donnees):
|
||||
if not donnees:
|
||||
st.warning("Aucune donnée à afficher.")
|
||||
return
|
||||
|
||||
try:
|
||||
df = pd.DataFrame(donnees)
|
||||
df['ivc_cat'] = df['ivc'].apply(lambda x: 1 if x <= 15 else (2 if x <= 30 else 3))
|
||||
|
||||
from collections import Counter
|
||||
coord_pairs = list(zip(df['ihh_extraction'].round(1), df['ihh_reserves'].round(1)))
|
||||
counts = Counter(coord_pairs)
|
||||
|
||||
offset_x, offset_y = [], {}
|
||||
seen = Counter()
|
||||
for pair in coord_pairs:
|
||||
rank = seen[pair]
|
||||
seen[pair] += 1
|
||||
if counts[pair] > 1:
|
||||
angle = rank * 1.5
|
||||
radius = 0.8 + 0.4 * rank
|
||||
offset_x.append(radius * np.cos(angle))
|
||||
offset_y[pair] = radius * np.sin(angle)
|
||||
else:
|
||||
offset_x.append(0)
|
||||
offset_y[pair] = 0
|
||||
|
||||
df['ihh_extraction'] += offset_x
|
||||
df['ihh_reserves'] += [offset_y[p] for p in coord_pairs]
|
||||
df['ihh_extraction_text'] = df['ihh_extraction'] + 0.5
|
||||
df['ihh_reserves_text'] = df['ihh_reserves'] + 0.5
|
||||
|
||||
base = alt.Chart(df).encode(
|
||||
x=alt.X('ihh_extraction:Q', title='IHH Extraction (%)'),
|
||||
y=alt.Y('ihh_reserves:Q', title='IHH Réserves (%)'),
|
||||
size=alt.Size('ivc_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None),
|
||||
color=alt.Color('ivc_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred'])),
|
||||
tooltip=['nom:N', 'ivc:Q', 'ihh_extraction:Q', 'ihh_reserves:Q']
|
||||
)
|
||||
|
||||
points = base.mark_circle(opacity=0.6)
|
||||
lines = alt.Chart(df).mark_rule(strokeWidth=0.5, color='gray').encode(
|
||||
x='ihh_extraction:Q', x2='ihh_extraction_text:Q',
|
||||
y='ihh_reserves:Q', y2='ihh_reserves_text:Q'
|
||||
)
|
||||
|
||||
labels = alt.Chart(df).mark_text(
|
||||
align='left', dx=10, dy=-10, fontSize=10, font='Arial', angle=335
|
||||
).encode(
|
||||
x='ihh_extraction_text:Q',
|
||||
y='ihh_reserves_text:Q',
|
||||
text='nom:N'
|
||||
)
|
||||
|
||||
hline_15 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='green').encode(y=alt.datum(15))
|
||||
hline_25 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='red').encode(y=alt.datum(25))
|
||||
vline_15 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='green').encode(x=alt.datum(15))
|
||||
vline_25 = alt.Chart(df).mark_rule(strokeDash=[2,2], color='red').encode(x=alt.datum(25))
|
||||
|
||||
chart = (points + lines + labels + hline_15 + hline_25 + vline_15 + vline_25).properties(
|
||||
width=600,
|
||||
height=500,
|
||||
title="Concentration des ressources critiques vs vulnérabilité IVC"
|
||||
).interactive()
|
||||
|
||||
st.altair_chart(chart, use_container_width=True)
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Erreur lors de la création du graphique : {e}")
|
||||
|
||||
|
||||
def lancer_visualisation_ihh_criticite(graph):
|
||||
try:
|
||||
import networkx as nx
|
||||
from utils.graph_utils import recuperer_donnees
|
||||
|
||||
niveaux = nx.get_node_attributes(graph, "niveau")
|
||||
noeuds = [n for n, v in niveaux.items() if v == "10" and "Reserves" not in n]
|
||||
noeuds.sort()
|
||||
|
||||
df = recuperer_donnees(graph, noeuds)
|
||||
if df.empty:
|
||||
st.warning("Aucune donnée à visualiser.")
|
||||
else:
|
||||
afficher_graphique_altair(df)
|
||||
except Exception as e:
|
||||
st.error(f"Erreur dans la visualisation IHH vs Criticité : {e}")
|
||||
|
||||
|
||||
def lancer_visualisation_ihh_ivc(graph):
|
||||
try:
|
||||
from utils.graph_utils import recuperer_donnees_2
|
||||
noeuds_niveau_2 = [
|
||||
n for n, data in graph.nodes(data=True)
|
||||
if data.get("niveau") == "2" and "ivc" in data
|
||||
]
|
||||
if not noeuds_niveau_2:
|
||||
return
|
||||
data = recuperer_donnees_2(graph, noeuds_niveau_2)
|
||||
creer_graphes(data)
|
||||
except Exception as e:
|
||||
st.error(f"Erreur dans la visualisation IHH vs IVC : {e}")
|
||||
|
||||
def interface_visualisations(G_temp, G_temp_ivc):
|
||||
st.markdown("# Visualisations")
|
||||
st.markdown("""## Indice de Herfindahl-Hirschmann - IHH vs Criticité
|
||||
|
||||
Entre 0 et 15%, concentration faible, entre 15 et 25%, modérée, au-delà, forte.
|
||||
|
||||
Taille des points = criticité substituabilité du minerai
|
||||
""")
|
||||
if st.button("Lancer", key="btn_ihh_criticite"):
|
||||
try:
|
||||
lancer_visualisation_ihh_criticite(G_temp)
|
||||
except Exception as e:
|
||||
st.error(f"Erreur dans la visualisation IHH vs Criticité : {e}")
|
||||
|
||||
st.markdown("""## Indice de Herfindahl-Hirschmann - IHH vs IVC
|
||||
|
||||
Entre 0 et 15%, concentration faible, entre 15 et 25%, modérée, au-delà, forte.
|
||||
|
||||
Taille des points = criticité concurrentielle du minerai
|
||||
""")
|
||||
|
||||
if st.button("Lancer", key="btn_ihh_ivc"):
|
||||
try:
|
||||
lancer_visualisation_ihh_ivc(G_temp_ivc)
|
||||
except Exception as e:
|
||||
st.error(f"Erreur dans la visualisation IHH vs IVC : {e}")
|
||||
@ -393,3 +393,17 @@ details {
|
||||
.math-block math {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Cache complètement la section d’actions Vega */
|
||||
.vega-actions {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Et aussi le <details> parent, s’il faut tout masquer */
|
||||
details[title="Click to view actions"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
div.stElementContainer.element-container.st-key-nom_utilisateur {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
48
components/README.md
Normal file
48
components/README.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Module Components
|
||||
|
||||
Ce module contient les composants d'interface utilisateur réutilisables pour l'application FabNum. Ces composants permettent de maintenir une apparence cohérente et de simplifier le développement en encapsulant des éléments d'interface communs.
|
||||
|
||||
## Structure du module
|
||||
|
||||
Le module components comprend plusieurs fichiers clés :
|
||||
|
||||
- **sidebar.py** : Gestion de la barre latérale de navigation et des fonctionnalités associées
|
||||
- **header.py** : Composant d'en-tête unifié pour toutes les pages
|
||||
- **footer.py** : Pied de page standardisé incluant les mentions légales et informations de contact
|
||||
- **fiches.py** : Composants spécifiques à l'affichage et à la manipulation des fiches
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
### Barre latérale (sidebar.py)
|
||||
- Menu de navigation principal entre les différentes sections
|
||||
- Options de configuration et de personnalisation
|
||||
- Affichage des informations sur l'impact environnemental
|
||||
- Gestion du thème (clair/sombre)
|
||||
|
||||
### En-tête (header.py)
|
||||
- Logo et identification visuelle de l'application
|
||||
- Titre et sous-titre de l'application
|
||||
- Messages système et notifications
|
||||
|
||||
### Pied de page (footer.py)
|
||||
- Informations légales et mentions de copyright
|
||||
- Liens vers les ressources externes
|
||||
- Informations de contact et de support
|
||||
|
||||
## Utilisation
|
||||
|
||||
Ces composants sont conçus pour être facilement intégrés dans les différentes pages de l'application. Exemple d'utilisation :
|
||||
|
||||
```python
|
||||
from components.sidebar import afficher_menu
|
||||
from components.header import afficher_entete
|
||||
from components.footer import afficher_pied_de_page
|
||||
|
||||
# Dans votre application principale
|
||||
afficher_entete()
|
||||
afficher_menu()
|
||||
# Contenu principal de la page
|
||||
afficher_pied_de_page()
|
||||
```
|
||||
|
||||
Cette approche modulaire permet de maintenir une interface cohérente tout en facilitant les mises à jour de l'interface utilisateur.
|
||||
@ -38,8 +38,20 @@ def connexion():
|
||||
|
||||
if not st.session_state.logged_in:
|
||||
with st.form("auth_form"):
|
||||
# 🧠 Ajout d'un champ identifiant fictif pour activer l'autocomplétion navigateur
|
||||
identifiant = st.text_input("Identifiant_token", value="fabnum-connexion", key="nom_utilisateur")
|
||||
token = st.text_input("Token d'accès personnel Gitea", type="password")
|
||||
submitted = st.form_submit_button("Se connecter")
|
||||
st.markdown("""
|
||||
<script>
|
||||
const containers = [...document.querySelectorAll('.stElementContainer')];
|
||||
containers.forEach(el => {
|
||||
if (el.innerText.includes("Identifiant_token")) {
|
||||
el.style.display = "none";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
if submitted and token:
|
||||
erreur = True
|
||||
@ -78,10 +90,12 @@ def connexion():
|
||||
if erreur:
|
||||
logger.warning(f"Accès refusé pour tentative avec token depuis IP {ip}")
|
||||
st.error("❌ Accès refusé.")
|
||||
|
||||
st.html("""
|
||||
</div>
|
||||
</section>
|
||||
""")
|
||||
""")
|
||||
|
||||
|
||||
def bouton_deconnexion():
|
||||
if st.session_state.get("logged_in", False):
|
||||
@ -1,5 +1,5 @@
|
||||
import streamlit as st
|
||||
from utils.connexion import connexion, bouton_deconnexion
|
||||
from components.connexion import connexion, bouton_deconnexion
|
||||
import streamlit.components.v1 as components
|
||||
|
||||
|
||||
|
||||
559
fabnum.py
559
fabnum.py
@ -1,29 +1,15 @@
|
||||
import streamlit as st
|
||||
from networkx.drawing.nx_agraph import read_dot, write_dot
|
||||
import pandas as pd
|
||||
import plotly.graph_objects as go
|
||||
import networkx as nx
|
||||
import logging
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
# Configuration Gitea
|
||||
from config import DOT_FILE, INSTRUCTIONS
|
||||
from config import INSTRUCTIONS
|
||||
|
||||
from utils.gitea import (
|
||||
charger_instructions_depuis_gitea,
|
||||
charger_schema_depuis_gitea
|
||||
charger_instructions_depuis_gitea
|
||||
)
|
||||
|
||||
from utils.graph_utils import (
|
||||
extraire_chemins_depuis,
|
||||
extraire_chemins_vers,
|
||||
lancer_personnalisation
|
||||
)
|
||||
|
||||
from utils.visualisation import (
|
||||
lancer_visualisation_ihh_criticite,
|
||||
lancer_visualisation_ihh_ivc
|
||||
charger_graphe
|
||||
)
|
||||
|
||||
from components.sidebar import (
|
||||
@ -32,10 +18,12 @@ from components.sidebar import (
|
||||
)
|
||||
|
||||
from components.header import afficher_entete
|
||||
|
||||
from components.footer import afficher_pied_de_page
|
||||
|
||||
from components.fiches import afficher_fiches
|
||||
from app.fiches import interface_fiches
|
||||
from app.visualisations import interface_visualisations
|
||||
from app.personnalisation import interface_personnalisation
|
||||
from app.analyse import interface_analyse
|
||||
|
||||
st.set_page_config(
|
||||
page_title="Fabnum – Analyse de chaîne",
|
||||
@ -46,49 +34,6 @@ st.set_page_config(
|
||||
|
||||
session_id = st.context.headers.get("x-session-id")
|
||||
|
||||
niveau_labels = {
|
||||
0: "Produit final",
|
||||
1: "Composant",
|
||||
2: "Minerai",
|
||||
10: "Opération",
|
||||
11: "Pays d'opération",
|
||||
12: "Acteur d'opération",
|
||||
99: "Pays géographique"
|
||||
}
|
||||
inverse_niveau_labels = {v: k for k, v in niveau_labels.items()}
|
||||
|
||||
# Une seule lecture du fichier, mais injection à chaque run
|
||||
if "base_css_content" not in st.session_state:
|
||||
with open("assets/styles/base.css") as f:
|
||||
st.session_state["base_css_content"] = f.read()
|
||||
|
||||
st.markdown(f"<style>{st.session_state['base_css_content']}</style>", unsafe_allow_html=True)
|
||||
|
||||
# Chargement initial des thèmes (variables CSS uniquement)
|
||||
if "theme_css_content_clair" not in st.session_state:
|
||||
with open("assets/styles/theme-light.css") as f:
|
||||
st.session_state["theme_css_content_clair"] = f.read()
|
||||
|
||||
if "theme_css_content_sombre" not in st.session_state:
|
||||
with open("assets/styles/theme-dark.css") as f:
|
||||
st.session_state["theme_css_content_sombre"] = f.read()
|
||||
|
||||
# Thème en cours
|
||||
current_theme = st.session_state.get("theme_mode", "Clair").lower()
|
||||
theme_css = st.session_state[f"theme_css_content_{current_theme}"]
|
||||
|
||||
# Injection des variables du thème
|
||||
st.markdown(f"<style>{theme_css}</style>", unsafe_allow_html=True)
|
||||
|
||||
# Chargement unique du CSS principal (base.css)
|
||||
if "base_css_content" not in st.session_state:
|
||||
with open("assets/styles/base.css") as f:
|
||||
st.session_state["base_css_content"] = f.read()
|
||||
|
||||
# Injection du style principal basé sur les variables
|
||||
st.markdown(f"<style>{st.session_state['base_css_content']}</style>", unsafe_allow_html=True)
|
||||
|
||||
|
||||
def get_total_bytes_for_session(session_id):
|
||||
total_bytes = 0
|
||||
try:
|
||||
@ -103,311 +48,57 @@ def get_total_bytes_for_session(session_id):
|
||||
st.error(f"Erreur lecture log: {e}")
|
||||
return total_bytes
|
||||
|
||||
# Intégration du fichier CSS externe
|
||||
# with open("assets/styles.css") as f:
|
||||
# st.markdown(f"<style>{f.read()}</style>", unsafe_allow_html=True)
|
||||
def charger_theme():
|
||||
# Une seule lecture du fichier, mais injection à chaque run
|
||||
if "base_css_content" not in st.session_state:
|
||||
with open("assets/styles/base.css") as f:
|
||||
st.session_state["base_css_content"] = f.read()
|
||||
|
||||
st.markdown(f"<style>{st.session_state['base_css_content']}</style>", unsafe_allow_html=True)
|
||||
|
||||
afficher_entete()
|
||||
# Chargement initial des thèmes (variables CSS uniquement)
|
||||
if "theme_css_content_clair" not in st.session_state:
|
||||
with open("assets/styles/theme-light.css") as f:
|
||||
st.session_state["theme_css_content_clair"] = f.read()
|
||||
|
||||
afficher_menu()
|
||||
if "theme_css_content_sombre" not in st.session_state:
|
||||
with open("assets/styles/theme-dark.css") as f:
|
||||
st.session_state["theme_css_content_sombre"] = f.read()
|
||||
|
||||
st.markdown("""
|
||||
<main role="main">
|
||||
""", unsafe_allow_html=True)
|
||||
# Thème en cours
|
||||
current_theme = st.session_state.get("theme_mode", "Clair").lower()
|
||||
theme_css = st.session_state[f"theme_css_content_{current_theme}"]
|
||||
|
||||
# Injection des variables du thème
|
||||
st.markdown(f"<style>{theme_css}</style>", unsafe_allow_html=True)
|
||||
|
||||
def couleur_noeud(n, niveaux, G):
|
||||
niveau = niveaux.get(n, 99)
|
||||
attrs = G.nodes[n]
|
||||
# Chargement unique du CSS principal (base.css)
|
||||
if "base_css_content" not in st.session_state:
|
||||
with open("assets/styles/base.css") as f:
|
||||
st.session_state["base_css_content"] = f.read()
|
||||
|
||||
# Niveau 99 : pays géographique avec isg
|
||||
if niveau == 99:
|
||||
isg = int(attrs.get("isg", -1))
|
||||
return (
|
||||
"darkred" if isg >= 60 else
|
||||
"orange" if isg >= 31 else
|
||||
"darkgreen" if isg >= 0 else
|
||||
"gray"
|
||||
)
|
||||
# Injection du style principal basé sur les variables
|
||||
st.markdown(f"<style>{st.session_state['base_css_content']}</style>", unsafe_allow_html=True)
|
||||
|
||||
# Niveau 11 ou 12 connecté à un pays géographique
|
||||
if niveau in (11, 12, 1011, 1012):
|
||||
for succ in G.successors(n):
|
||||
if niveaux.get(succ) == 99:
|
||||
isg = int(G.nodes[succ].get("isg", -1))
|
||||
return (
|
||||
"darkred" if isg >= 60 else
|
||||
"orange" if isg >= 31 else
|
||||
"darkgreen" if isg >= 0 else
|
||||
"gray"
|
||||
)
|
||||
def ouvrir_page():
|
||||
charger_theme()
|
||||
afficher_entete()
|
||||
afficher_menu()
|
||||
st.markdown("""
|
||||
<main role="main">
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# Logique existante pour IHH / IVC
|
||||
if niveau in (10, 1010) and attrs.get("ihh_pays"):
|
||||
ihh = int(attrs["ihh_pays"])
|
||||
return (
|
||||
"darkgreen" if ihh <= 15 else
|
||||
"orange" if ihh <= 25 else
|
||||
"darkred"
|
||||
)
|
||||
elif niveau == 2 and attrs.get("ivc"):
|
||||
ivc = int(attrs["ivc"])
|
||||
return (
|
||||
"darkgreen" if ivc <= 15 else
|
||||
"orange" if ivc <= 30 else
|
||||
"darkred"
|
||||
)
|
||||
def fermer_page():
|
||||
st.markdown("</div>", unsafe_allow_html=True)
|
||||
st.markdown("""</section>""", unsafe_allow_html=True)
|
||||
st.markdown("</main>", unsafe_allow_html=True)
|
||||
|
||||
return "lightblue"
|
||||
total_bytes = get_total_bytes_for_session(session_id)
|
||||
|
||||
afficher_pied_de_page()
|
||||
afficher_impact(total_bytes)
|
||||
|
||||
def afficher_sankey(
|
||||
G,
|
||||
niveau_depart, niveau_arrivee,
|
||||
noeuds_depart=None, noeuds_arrivee=None,
|
||||
minerais=None,
|
||||
filtrer_ics=False, filtrer_ivc=False,
|
||||
filtrer_ihh=False, filtrer_isg=False,
|
||||
logique_filtrage="OU"):
|
||||
|
||||
niveaux = {}
|
||||
|
||||
for node, attrs in G.nodes(data=True):
|
||||
niveau_str = attrs.get("niveau")
|
||||
try:
|
||||
if niveau_str:
|
||||
niveaux[node] = int(str(niveau_str).strip('"'))
|
||||
except ValueError:
|
||||
logging.warning(f"Niveau non entier pour le noeud {node}: {niveau_str}")
|
||||
|
||||
chemins = []
|
||||
if noeuds_depart and noeuds_arrivee:
|
||||
for nd in noeuds_depart:
|
||||
for na in noeuds_arrivee:
|
||||
tous_chemins = extraire_chemins_depuis(G, nd)
|
||||
chemins.extend([chemin for chemin in tous_chemins if na in chemin])
|
||||
elif noeuds_depart:
|
||||
for nd in noeuds_depart:
|
||||
chemins.extend(extraire_chemins_depuis(G, nd))
|
||||
elif noeuds_arrivee:
|
||||
for na in noeuds_arrivee:
|
||||
chemins.extend(extraire_chemins_vers(G, na, niveau_depart))
|
||||
else:
|
||||
sources_depart = [n for n in G.nodes() if niveaux.get(n) == niveau_depart]
|
||||
for nd in sources_depart:
|
||||
chemins.extend(extraire_chemins_depuis(G, nd))
|
||||
|
||||
if minerais:
|
||||
chemins = [chemin for chemin in chemins if any(n in minerais for n in chemin)]
|
||||
|
||||
def extraire_criticite(u, v):
|
||||
data = G.get_edge_data(u, v)
|
||||
if not data:
|
||||
return 0
|
||||
if isinstance(data, dict) and all(isinstance(k, int) for k in data):
|
||||
return float(data[0].get("criticite", 0))
|
||||
return float(data.get("criticite", 0))
|
||||
|
||||
liens_chemins = set()
|
||||
chemins_filtres = set()
|
||||
niveaux_speciaux = [1000, 1001, 1002, 1010, 1011, 1012]
|
||||
|
||||
for chemin in chemins:
|
||||
has_ihh = has_ivc = has_criticite = has_isg_critique = False
|
||||
|
||||
for i in range(len(chemin) - 1):
|
||||
u, v = chemin[i], chemin[i + 1]
|
||||
niveau_u = niveaux.get(u)
|
||||
niveau_v = niveaux.get(v)
|
||||
|
||||
if (
|
||||
(niveau_depart <= niveau_u <= niveau_arrivee or niveau_u in niveaux_speciaux)
|
||||
and (niveau_depart <= niveau_v <= niveau_arrivee or niveau_v in niveaux_speciaux)
|
||||
):
|
||||
liens_chemins.add((u, v))
|
||||
|
||||
if filtrer_ihh and ihh_type:
|
||||
ihh_field = "ihh_pays" if ihh_type == "Pays" else "ihh_acteurs"
|
||||
if niveau_u in (10, 1010) and int(G.nodes[u].get(ihh_field, 0)) > 25:
|
||||
has_ihh = True
|
||||
if niveau_v in (10, 1010) and int(G.nodes[v].get(ihh_field, 0)) > 25:
|
||||
has_ihh = True
|
||||
|
||||
if filtrer_ivc and niveau_u in (2, 1002) and int(G.nodes[u].get("ivc", 0)) > 30:
|
||||
has_ivc = True
|
||||
|
||||
if filtrer_ics and ((niveau_u == 1 and niveau_v == 2) or (niveau_u == 1001 and niveau_v == 1002) or (niveau_u == 10 and niveau_v in (1000, 1001))) and extraire_criticite(u, v) > 0.66:
|
||||
has_criticite = True
|
||||
|
||||
for n in (u, v):
|
||||
if niveaux.get(n) == 99 and int(G.nodes[n].get("isg", 0)) >= 60:
|
||||
has_isg_critique = True
|
||||
elif niveaux.get(n) in (11, 12, 1011, 1012):
|
||||
for succ in G.successors(n):
|
||||
if niveaux.get(succ) == 99 and int(G.nodes[succ].get("isg", 0)) >= 60:
|
||||
has_isg_critique = True
|
||||
|
||||
if logique_filtrage == "ET":
|
||||
keep = True
|
||||
if filtrer_ihh:
|
||||
keep = keep and has_ihh
|
||||
if filtrer_ivc:
|
||||
keep = keep and has_ivc
|
||||
if filtrer_ics:
|
||||
keep = keep and has_criticite
|
||||
if filtrer_isg:
|
||||
keep = keep and has_isg_critique
|
||||
if keep:
|
||||
chemins_filtres.add(tuple(chemin))
|
||||
elif logique_filtrage == "OU":
|
||||
if (filtrer_ihh and has_ihh) or (filtrer_ivc and has_ivc) or (filtrer_ics and has_criticite) or (filtrer_isg and has_isg_critique):
|
||||
chemins_filtres.add(tuple(chemin))
|
||||
|
||||
if any([filtrer_ics, filtrer_ivc, filtrer_ihh, filtrer_isg]):
|
||||
chemins = list(chemins_filtres)
|
||||
liens_chemins = set()
|
||||
for chemin in chemins:
|
||||
for i in range(len(chemin) - 1):
|
||||
u, v = chemin[i], chemin[i + 1]
|
||||
niveau_u = niveaux.get(u, 999)
|
||||
niveau_v = niveaux.get(v, 999)
|
||||
if (
|
||||
(niveau_depart <= niveau_u <= niveau_arrivee or niveau_u in niveaux_speciaux)
|
||||
and (niveau_depart <= niveau_v <= niveau_arrivee or niveau_v in niveaux_speciaux)
|
||||
):
|
||||
liens_chemins.add((u, v))
|
||||
|
||||
if not liens_chemins:
|
||||
st.warning("Aucun chemin ne correspond aux critères.")
|
||||
return
|
||||
|
||||
df_liens = pd.DataFrame(list(liens_chemins), columns=["source", "target"])
|
||||
df_liens = df_liens.groupby(
|
||||
["source", "target"]).size().reset_index(name="value")
|
||||
|
||||
df_liens["criticite"] = df_liens.apply(
|
||||
lambda row: extraire_criticite(row["source"], row["target"]), axis=1)
|
||||
df_liens["value"] = 0.1
|
||||
|
||||
# Ne garder que les nœuds effectivement connectés
|
||||
niveaux_speciaux = [1000, 1001, 1002, 1010, 1011, 1012]
|
||||
|
||||
# Inclure les nœuds connectés + tous les nœuds 10xx traversés dans les chemins
|
||||
noeuds_utilises = set(df_liens["source"]) | set(df_liens["target"])
|
||||
for chemin in chemins:
|
||||
for n in chemin:
|
||||
if niveaux.get(n) in niveaux_speciaux:
|
||||
noeuds_utilises.add(n)
|
||||
|
||||
sorted_nodes = [
|
||||
n for n in sorted(G.nodes(), key=lambda x: niveaux.get(x, 99), reverse=True)
|
||||
if n in noeuds_utilises
|
||||
]
|
||||
|
||||
def couleur_criticite(p):
|
||||
if p <= 0.33:
|
||||
return "darkgreen"
|
||||
elif p <= 0.66:
|
||||
return "orange"
|
||||
else:
|
||||
return "darkred"
|
||||
|
||||
df_liens["color"] = df_liens.apply(
|
||||
lambda row: couleur_criticite(row["criticite"]) if row["criticite"] > 0 else "gray",
|
||||
axis=1
|
||||
)
|
||||
|
||||
all_nodes = pd.unique(df_liens[["source", "target"]].values.ravel())
|
||||
sorted_nodes = sorted(
|
||||
all_nodes, key=lambda x: niveaux.get(x, 99), reverse=True)
|
||||
node_indices = {name: i for i, name in enumerate(sorted_nodes)}
|
||||
|
||||
sources = df_liens["source"].map(node_indices).tolist()
|
||||
targets = df_liens["target"].map(node_indices).tolist()
|
||||
values = df_liens["value"].tolist()
|
||||
|
||||
customdata = []
|
||||
for n in sorted_nodes:
|
||||
info = [f"{k}: {v}" for k, v in G.nodes[n].items()]
|
||||
niveau = niveaux.get(n, 99)
|
||||
|
||||
# Ajout d’un ISG hérité si applicable
|
||||
if niveau in (11, 12, 1011, 1012):
|
||||
for succ in G.successors(n):
|
||||
if niveaux.get(succ) == 99 and "isg" in G.nodes[succ]:
|
||||
isg_val = G.nodes[succ]["isg"]
|
||||
info.append(f"isg (géographique): {isg_val}")
|
||||
break
|
||||
|
||||
customdata.append("<br>".join(info))
|
||||
|
||||
def edge_info(u, v):
|
||||
data = G.get_edge_data(u, v)
|
||||
if not data:
|
||||
return f"Relation : {u} → {v}"
|
||||
if isinstance(data, dict) and all(isinstance(k, int) for k in data):
|
||||
data = data[0]
|
||||
base = [f"{k}: {v}" for k, v in data.items()]
|
||||
return f"Relation : {u} → {v}<br>" + "<br>".join(base)
|
||||
|
||||
link_customdata = [
|
||||
edge_info(row["source"], row["target"]) for _, row in df_liens.iterrows()
|
||||
]
|
||||
|
||||
fig = go.Figure(go.Sankey(
|
||||
arrangement="snap",
|
||||
node=dict(
|
||||
pad=10,
|
||||
thickness=8,
|
||||
label=sorted_nodes,
|
||||
x=[niveaux.get(n, 99) / 100 for n in sorted_nodes],
|
||||
color=[couleur_noeud(n, niveaux, G) for n in sorted_nodes],
|
||||
customdata=customdata,
|
||||
hovertemplate="%{customdata}<extra></extra>"
|
||||
),
|
||||
link=dict(
|
||||
source=sources,
|
||||
target=targets,
|
||||
value=values,
|
||||
color=df_liens["color"].tolist(),
|
||||
customdata=link_customdata,
|
||||
hovertemplate="%{customdata}<extra></extra>"
|
||||
)
|
||||
))
|
||||
|
||||
fig.update_layout(
|
||||
title_text="Hiérarchie filtrée par niveaux et noeuds",
|
||||
paper_bgcolor="white",
|
||||
plot_bgcolor="white"
|
||||
)
|
||||
st.plotly_chart(fig)
|
||||
|
||||
if st.session_state.get("logged_in", False):
|
||||
if liens_chemins:
|
||||
G_export = nx.DiGraph()
|
||||
for u, v in liens_chemins:
|
||||
G_export.add_node(u, **G.nodes[u])
|
||||
G_export.add_node(v, **G.nodes[v])
|
||||
data = G.get_edge_data(u, v)
|
||||
if isinstance(data, dict) and all(isinstance(k, int) for k in data):
|
||||
G_export.add_edge(u, v, **data[0])
|
||||
elif isinstance(data, dict):
|
||||
G_export.add_edge(u, v, **data)
|
||||
else:
|
||||
G_export.add_edge(u, v)
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=".dot", mode="w", encoding="utf-8") as f:
|
||||
write_dot(G_export, f.name)
|
||||
dot_path = f.name
|
||||
|
||||
with open(dot_path, encoding="utf-8") as f:
|
||||
st.download_button(
|
||||
label="Télécharger le fichier DOT filtré",
|
||||
data=f.read(),
|
||||
file_name="graphe_filtré.dot",
|
||||
mime="text/plain"
|
||||
)
|
||||
ouvrir_page()
|
||||
|
||||
dot_file_path = None
|
||||
|
||||
@ -417,164 +108,20 @@ if st.session_state.onglet == "Instructions":
|
||||
st.markdown(markdown_content)
|
||||
|
||||
elif st.session_state.onglet == "Fiches":
|
||||
st.markdown("# Affichage des fiches")
|
||||
st.markdown("Sélectionner d'abord l'opération que vous souhaitez examiner et ensuite choisisez la fiche à lire.")
|
||||
st.markdown("---")
|
||||
afficher_fiches()
|
||||
interface_fiches()
|
||||
|
||||
else:
|
||||
# Charger le graphe une seule fois
|
||||
if "G_temp" not in st.session_state:
|
||||
try:
|
||||
if charger_schema_depuis_gitea(DOT_FILE):
|
||||
st.session_state["G_temp"] = read_dot(DOT_FILE)
|
||||
st.session_state["G_temp_ivc"] = st.session_state["G_temp"].copy()
|
||||
dot_file_path = True
|
||||
else:
|
||||
dot_file_path = False
|
||||
except Exception as e:
|
||||
st.error(f"Erreur de lecture du fichier DOT : {e}")
|
||||
dot_file_path = False
|
||||
else:
|
||||
dot_file_path = True
|
||||
|
||||
if dot_file_path:
|
||||
G_temp = st.session_state["G_temp"]
|
||||
G_temp_ivc = st.session_state["G_temp_ivc"]
|
||||
else:
|
||||
st.error("Impossible de charger le graphe pour cet onglet.")
|
||||
# Le graphe n'est pas nécessaire pour Instructions ou Fiches
|
||||
G_temp, G_temp_ivc, dot_file_path = charger_graphe()
|
||||
|
||||
if dot_file_path and st.session_state.onglet == "Analyse":
|
||||
try:
|
||||
niveaux_temp = {
|
||||
node: int(str(attrs.get("niveau")).strip('"'))
|
||||
for node, attrs in G_temp.nodes(data=True)
|
||||
if attrs.get("niveau") and str(attrs.get("niveau")).strip('"').isdigit()
|
||||
}
|
||||
G_temp.remove_nodes_from([n for n in G_temp.nodes() if n not in niveaux_temp])
|
||||
G_temp.remove_nodes_from(
|
||||
[n for n in G_temp.nodes() if niveaux_temp.get(n) == 10 and 'Reserves' in n])
|
||||
|
||||
st.markdown("# Analyse")
|
||||
st.markdown("## Sélection des nœuds de départ et d'arrivée")
|
||||
valeur_defaut = "-- Sélectionner un niveau --"
|
||||
niveau_choix = [valeur_defaut] + list(niveau_labels.values())
|
||||
|
||||
niveau_depart = st.selectbox("Niveau de départ", niveau_choix, key="analyse_niveau_depart")
|
||||
|
||||
if niveau_depart != "-- Sélectionner un niveau --":
|
||||
niveau_depart = inverse_niveau_labels[niveau_depart]
|
||||
niveaux_arrivee_possibles = [v for k, v in niveau_labels.items() if k > niveau_depart]
|
||||
|
||||
niveaux_arrivee_choix = [valeur_defaut] + niveaux_arrivee_possibles
|
||||
|
||||
analyse_niveau_arrivee = st.selectbox("Niveau d'arrivée", niveau_choix, key="analyse_niveau_arrivee")
|
||||
|
||||
if analyse_niveau_arrivee != "-- Sélectionner un niveau --":
|
||||
niveau_arrivee = inverse_niveau_labels[analyse_niveau_arrivee]
|
||||
|
||||
minerais_selection = None
|
||||
if niveau_depart < 2 < niveau_arrivee:
|
||||
st.markdown("### Sélectionner un ou plusieurs minerais")
|
||||
# Tous les nœuds de niveau 2 (minerai)
|
||||
minerais_nodes = sorted([
|
||||
n for n, d in G_temp.nodes(data=True)
|
||||
if d.get("niveau") and int(str(d.get("niveau")).strip('"')) == 2
|
||||
])
|
||||
|
||||
minerais_selection = st.multiselect(
|
||||
"Filtrer par minerais (optionnel)",
|
||||
minerais_nodes,
|
||||
key="analyse_minerais"
|
||||
)
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
depart_nodes = [n for n in G_temp.nodes() if niveaux_temp.get(n) == niveau_depart]
|
||||
arrivee_nodes = [n for n in G_temp.nodes() if niveaux_temp.get(n) == niveau_arrivee]
|
||||
|
||||
st.markdown("## Sélection fine des items")
|
||||
|
||||
noeuds_depart = st.multiselect("Filtrer par noeuds de départ (optionnel)", sorted(depart_nodes), key="analyse_noeuds_depart")
|
||||
|
||||
noeuds_arrivee = st.multiselect("Filtrer par noeuds d'arrivée (optionnel)", sorted(arrivee_nodes), key="analyse_noeuds_arrivee")
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
noeuds_depart = noeuds_depart if noeuds_depart else None
|
||||
noeuds_arrivee = noeuds_arrivee if noeuds_arrivee else None
|
||||
|
||||
st.markdown("## Sélection des filtres pour identifier les vulnérabilités")
|
||||
|
||||
filtrer_ics = st.checkbox("Filtrer les chemins contenant au moins minerai critique pour un composant (ICS > 66 %)", key="analyse_filtrer_ics")
|
||||
filtrer_ivc = st.checkbox("Filtrer les chemins contenant au moins un minerai critique par rapport à la concurrence sectorielle (IVC > 30)", key="analyse_filtrer_ivc")
|
||||
filtrer_ihh = st.checkbox("Filtrer les chemins contenant au moins une opération critique par rapport à la concentration géographique ou industrielle (IHH pays ou acteurs > 25)", key="analyse_filtrer_ihh")
|
||||
|
||||
ihh_type = None
|
||||
if filtrer_ihh:
|
||||
ihh_type = st.radio("Appliquer le filtre IHH sur :", ["Pays", "Acteurs"], horizontal=True, key="analyse_ihh_type")
|
||||
|
||||
filtrer_isg = st.checkbox("Filtrer les chemins contenant un pays instable (ISG ≥ 60)", key="analyse_filtrer_isg")
|
||||
logique_filtrage = st.radio("Logique de filtrage", ["OU", "ET"], horizontal=True, key="analyse_logique_filtrage")
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
if st.button("Lancer l’analyse", type="primary", key="analyse_lancer"):
|
||||
afficher_sankey(
|
||||
G_temp,
|
||||
niveau_depart=niveau_depart,
|
||||
niveau_arrivee=niveau_arrivee,
|
||||
noeuds_depart=noeuds_depart,
|
||||
noeuds_arrivee=noeuds_arrivee,
|
||||
minerais=minerais_selection,
|
||||
filtrer_ics=filtrer_ics,
|
||||
filtrer_ivc=filtrer_ivc,
|
||||
filtrer_ihh=filtrer_ihh,
|
||||
filtrer_isg=filtrer_isg,
|
||||
logique_filtrage=logique_filtrage
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Erreur de prévisualisation du graphe : {e}")
|
||||
interface_analyse(G_temp)
|
||||
|
||||
elif dot_file_path and st.session_state.onglet == "Visualisations":
|
||||
st.markdown("# Visualisations")
|
||||
st.markdown("""## Indice de Herfindahl-Hirschmann - IHH vs Criticité
|
||||
|
||||
Entre 0 et 15%, concentration faible, entre 15 et 25%, modérée, au-delà, forte.
|
||||
|
||||
Taille des points = criticité substituabilité du minerai
|
||||
""")
|
||||
if st.button("Lancer", key="btn_ihh_criticite"):
|
||||
try:
|
||||
lancer_visualisation_ihh_criticite(G_temp)
|
||||
except Exception as e:
|
||||
st.error(f"Erreur dans la visualisation IHH vs Criticité : {e}")
|
||||
|
||||
st.markdown("""## Indice de Herfindahl-Hirschmann - IHH vs IVC
|
||||
|
||||
Entre 0 et 15%, concentration faible, entre 15 et 25%, modérée, au-delà, forte.
|
||||
|
||||
Taille des points = criticité concurrentielle du minerai
|
||||
""")
|
||||
|
||||
if st.button("Lancer", key="btn_ihh_ivc"):
|
||||
try:
|
||||
lancer_visualisation_ihh_ivc(G_temp_ivc)
|
||||
except Exception as e:
|
||||
st.error(f"Erreur dans la visualisation IHH vs IVC : {e}")
|
||||
interface_visualisations(G_temp, G_temp_ivc)
|
||||
|
||||
elif dot_file_path and st.session_state.onglet == "Personnalisation":
|
||||
G_temp = lancer_personnalisation(G_temp)
|
||||
G_temp = interface_personnalisation(G_temp)
|
||||
|
||||
st.markdown("</div>", unsafe_allow_html=True)
|
||||
|
||||
st.markdown("""</section>""", unsafe_allow_html=True)
|
||||
|
||||
st.markdown("</main>", unsafe_allow_html=True)
|
||||
|
||||
total_bytes = get_total_bytes_for_session(session_id)
|
||||
|
||||
afficher_pied_de_page()
|
||||
|
||||
afficher_impact(total_bytes)
|
||||
fermer_page()
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
streamlit
|
||||
networkx
|
||||
pygraphviz
|
||||
pandas
|
||||
plotly
|
||||
requests
|
||||
kaleido>=0.2.1
|
||||
streamlit_browser_cookie
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile requirements.in
|
||||
#
|
||||
|
||||
47
utils/README.md
Normal file
47
utils/README.md
Normal file
@ -0,0 +1,47 @@
|
||||
# Module Utils
|
||||
|
||||
Ce module contient des utilitaires essentiels qui supportent le fonctionnement de l'application FabNum. Il fournit des fonctions génériques et des services partagés utilisés par les différents modules de l'application.
|
||||
|
||||
## Structure du module
|
||||
|
||||
Le module utils comprend plusieurs fichiers utilitaires clés :
|
||||
|
||||
- **gitea.py** : Fonctions pour interagir avec l'API Gitea (récupération des fiches, gestion des tickets, etc.)
|
||||
- **graph_utils.py** : Utilitaires pour manipuler, analyser et transformer les graphes de la chaîne de fabrication
|
||||
- **visualisation.py** : Fonctions communes pour la génération de visualisations et de graphiques
|
||||
|
||||
## Fonctionnalités
|
||||
|
||||
### Intégration Gitea (gitea.py)
|
||||
- Récupération des fiches et documents depuis le dépôt Gitea
|
||||
- Gestion de l'authentification et des tokens d'accès
|
||||
- Lecture et écriture des tickets et commentaires
|
||||
- Récupération des données de structure pour les fiches
|
||||
|
||||
### Manipulation de graphes (graph_utils.py)
|
||||
- Chargement et sauvegarde des graphes de dépendance
|
||||
- Calcul des chemins critiques et des niveaux de criticité
|
||||
- Fonctions d'analyse pour identifier les vulnérabilités
|
||||
- Transformation et filtrage des graphes pour différentes analyses
|
||||
|
||||
### Visualisation (visualisation.py)
|
||||
- Fonctions génériques pour générer des visualisations cohérentes
|
||||
- Helpers pour le formatage des données avant visualisation
|
||||
- Fonctions de conversion entre différents formats de données
|
||||
|
||||
## Utilisation
|
||||
|
||||
Ce module est conçu pour être importé par les autres composants de l'application. Exemple d'utilisation :
|
||||
|
||||
```python
|
||||
from utils.gitea import charger_instructions_depuis_gitea
|
||||
from utils.graph_utils import charger_graphe
|
||||
|
||||
# Chargement des instructions
|
||||
instructions = charger_instructions_depuis_gitea(INSTRUCTIONS_PATH)
|
||||
|
||||
# Chargement du graphe principal
|
||||
G_temp, G_temp_ivc, dot_file_path = charger_graphe()
|
||||
```
|
||||
|
||||
Ce module est fondamental pour l'application car il centralise les fonctions réutilisables et réduit la duplication de code entre les différents composants de l'application.
|
||||
@ -3,6 +3,13 @@ import pandas as pd
|
||||
import logging
|
||||
import streamlit as st
|
||||
import json
|
||||
from networkx.drawing.nx_agraph import read_dot
|
||||
|
||||
# Configuration Gitea
|
||||
from config import DOT_FILE
|
||||
from utils.gitea import (
|
||||
charger_schema_depuis_gitea
|
||||
)
|
||||
|
||||
|
||||
def extraire_chemins_depuis(G, source):
|
||||
@ -122,120 +129,69 @@ def recuperer_donnees_2(graph, noeuds_2):
|
||||
return donnees
|
||||
|
||||
|
||||
def lancer_personnalisation(G):
|
||||
st.markdown("""
|
||||
# Personnalisation des produits finaux
|
||||
def couleur_noeud(n, niveaux, G):
|
||||
niveau = niveaux.get(n, 99)
|
||||
attrs = G.nodes[n]
|
||||
|
||||
Dans cette section, vous pouvez ajouter des produits finaux qui ne sont pas présents dans la liste,
|
||||
par exemple des produits que vous concevez vous-même.
|
||||
|
||||
Vous pouvez aussi enregistrer ou recharger vos modifications.
|
||||
|
||||
---
|
||||
""")
|
||||
|
||||
st.markdown("## Ajouter un nouveau produit final")
|
||||
new_prod = st.text_input("Nom du nouveau produit (unique)", key="new_prod")
|
||||
if new_prod:
|
||||
ops_dispo = sorted([
|
||||
n for n, d in G.nodes(data=True)
|
||||
if d.get("niveau") == "10"
|
||||
and any(G.has_edge(p, n) and G.nodes[p].get("niveau") == "0" for p in G.predecessors(n))
|
||||
])
|
||||
sel_new_op = st.selectbox("Opération d'assemblage (optionnelle)", ["-- Aucune --"] + ops_dispo, index=0)
|
||||
niveau1 = sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "1"])
|
||||
sel_comps = st.multiselect("Composants à lier", options=niveau1)
|
||||
if st.button("Créer le produit"):
|
||||
G.add_node(new_prod, niveau="0", personnalisation="oui", label=new_prod)
|
||||
if sel_new_op != "-- Aucune --":
|
||||
G.add_edge(new_prod, sel_new_op)
|
||||
for comp in sel_comps:
|
||||
G.add_edge(new_prod, comp)
|
||||
st.success(f"{new_prod} ajouté.")
|
||||
|
||||
st.markdown("## Modifier un produit final ajouté")
|
||||
produits0 = sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "0" and d.get("personnalisation") == "oui"])
|
||||
sel_display = st.multiselect("Produits à modifier", options=produits0)
|
||||
if sel_display:
|
||||
prod = sel_display[0]
|
||||
if st.button(f"Supprimer {prod}"):
|
||||
G.remove_node(prod)
|
||||
st.success(f"{prod} supprimé.")
|
||||
st.session_state.pop("prod_sel", None)
|
||||
return G
|
||||
|
||||
ops_dispo = sorted([
|
||||
n for n, d in G.nodes(data=True)
|
||||
if d.get("niveau") == "10"
|
||||
and any(G.has_edge(p, n) and G.nodes[p].get("niveau") == "0" for p in G.predecessors(n))
|
||||
])
|
||||
curr_ops = [succ for succ in G.successors(prod) if G.nodes[succ].get("niveau") == "10"]
|
||||
default_idx = ops_dispo.index(curr_ops[0]) + 1 if curr_ops and curr_ops[0] in ops_dispo else 0
|
||||
sel_op = st.selectbox("Opération d'assemblage liée", ["-- Aucune --"] + ops_dispo, index=default_idx)
|
||||
|
||||
niveau1 = sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "1"])
|
||||
linked = [succ for succ in G.successors(prod) if G.nodes[succ].get("niveau") == "1"]
|
||||
nouveaux = st.multiselect(f"Composants liés à {prod}", options=niveau1, default=linked)
|
||||
|
||||
if st.button(f"Mettre à jour {prod}"):
|
||||
for op in curr_ops:
|
||||
if sel_op == "-- Aucune --" or op != sel_op:
|
||||
G.remove_edge(prod, op)
|
||||
if sel_op != "-- Aucune --" and (not curr_ops or sel_op not in curr_ops):
|
||||
G.add_edge(prod, sel_op)
|
||||
|
||||
for comp in set(linked) - set(nouveaux):
|
||||
G.remove_edge(prod, comp)
|
||||
for comp in set(nouveaux) - set(linked):
|
||||
G.add_edge(prod, comp)
|
||||
|
||||
st.success(f"{prod} mis à jour.")
|
||||
|
||||
st.markdown("## Sauvegarder ou restaurer la configuration")
|
||||
if st.button("Exporter configuration"):
|
||||
nodes = [n for n, d in G.nodes(data=True) if d.get("personnalisation") == "oui"]
|
||||
edges = [(u, v) for u, v in G.edges() if u in nodes]
|
||||
conf = {"nodes": nodes, "edges": edges}
|
||||
json_str = json.dumps(conf, ensure_ascii=False)
|
||||
st.download_button(
|
||||
label="Télécharger (JSON)",
|
||||
data=json_str,
|
||||
file_name="config_personnalisation.json",
|
||||
mime="application/json"
|
||||
# Niveau 99 : pays géographique avec isg
|
||||
if niveau == 99:
|
||||
isg = int(attrs.get("isg", -1))
|
||||
return (
|
||||
"darkred" if isg >= 60 else
|
||||
"orange" if isg >= 31 else
|
||||
"darkgreen" if isg >= 0 else
|
||||
"gray"
|
||||
)
|
||||
|
||||
uploaded = st.file_uploader("Importer une configuration JSON (max 100 Ko)", type=["json"])
|
||||
if uploaded:
|
||||
if uploaded.size > 100 * 1024:
|
||||
st.error("Fichier trop volumineux (max 100 Ko).")
|
||||
else:
|
||||
# Niveau 11 ou 12 connecté à un pays géographique
|
||||
if niveau in (11, 12, 1011, 1012):
|
||||
for succ in G.successors(n):
|
||||
if niveaux.get(succ) == 99:
|
||||
isg = int(G.nodes[succ].get("isg", -1))
|
||||
return (
|
||||
"darkred" if isg >= 60 else
|
||||
"orange" if isg >= 31 else
|
||||
"darkgreen" if isg >= 0 else
|
||||
"gray"
|
||||
)
|
||||
|
||||
# Logique existante pour IHH / IVC
|
||||
if niveau in (10, 1010) and attrs.get("ihh_pays"):
|
||||
ihh = int(attrs["ihh_pays"])
|
||||
return (
|
||||
"darkgreen" if ihh <= 15 else
|
||||
"orange" if ihh <= 25 else
|
||||
"darkred"
|
||||
)
|
||||
elif niveau == 2 and attrs.get("ivc"):
|
||||
ivc = int(attrs["ivc"])
|
||||
return (
|
||||
"darkgreen" if ivc <= 15 else
|
||||
"orange" if ivc <= 30 else
|
||||
"darkred"
|
||||
)
|
||||
|
||||
return "lightblue"
|
||||
|
||||
def charger_graphe():
|
||||
if "G_temp" not in st.session_state:
|
||||
try:
|
||||
conf = json.loads(uploaded.read().decode("utf-8"))
|
||||
all_nodes = conf.get("nodes", [])
|
||||
all_edges = conf.get("edges", [])
|
||||
|
||||
if not all_nodes:
|
||||
st.warning("Aucun produit trouvé dans le fichier.")
|
||||
if charger_schema_depuis_gitea(DOT_FILE):
|
||||
st.session_state["G_temp"] = read_dot(DOT_FILE)
|
||||
st.session_state["G_temp_ivc"] = st.session_state["G_temp"].copy()
|
||||
dot_file_path = True
|
||||
else:
|
||||
st.markdown("### Sélection des produits à restaurer")
|
||||
sel_nodes = st.multiselect(
|
||||
"Produits à restaurer",
|
||||
options=all_nodes,
|
||||
default=all_nodes,
|
||||
key="restaurer_selection"
|
||||
)
|
||||
|
||||
if st.button("Restaurer les éléments sélectionnés", type="primary"):
|
||||
for node in sel_nodes:
|
||||
if not G.has_node(node):
|
||||
G.add_node(node, niveau="0", personnalisation="oui", label=node)
|
||||
|
||||
for u, v in all_edges:
|
||||
if u in sel_nodes and v in sel_nodes + list(G.nodes()) and not G.has_edge(u, v):
|
||||
G.add_edge(u, v)
|
||||
|
||||
st.success("Configuration partielle restaurée avec succès.")
|
||||
dot_file_path = False
|
||||
except Exception as e:
|
||||
st.error(f"Erreur d'import : {e}")
|
||||
st.error(f"Erreur de lecture du fichier DOT : {e}")
|
||||
dot_file_path = False
|
||||
else:
|
||||
dot_file_path = True
|
||||
|
||||
return G
|
||||
if dot_file_path:
|
||||
G_temp = st.session_state["G_temp"]
|
||||
G_temp_ivc = st.session_state["G_temp_ivc"]
|
||||
else:
|
||||
st.error("Impossible de charger le graphe pour cet onglet.")
|
||||
|
||||
return G_temp, G_temp_ivc, dot_file_path
|
||||
|
||||
@ -1,100 +0,0 @@
|
||||
# creation.py
|
||||
|
||||
import re
|
||||
import base64
|
||||
import streamlit as st
|
||||
from .core import charger_fiches_et_labels, construire_corps_ticket_markdown, creer_ticket_gitea, get_labels_existants, nettoyer_labels
|
||||
from config import ENV
|
||||
import requests
|
||||
|
||||
|
||||
def formulaire_creation_ticket_dynamique(fiche_selectionnee):
|
||||
with st.expander("Créer un nouveau ticket lié à cette fiche", expanded=False):
|
||||
contenu_modele = charger_modele_ticket()
|
||||
if not contenu_modele:
|
||||
st.error("Impossible de charger le modèle de ticket.")
|
||||
return
|
||||
|
||||
# Découpe le modèle en sections
|
||||
sections, reponses = {}, {}
|
||||
lignes, titre_courant, contenu = contenu_modele.splitlines(), None, []
|
||||
for ligne in lignes:
|
||||
if ligne.startswith("## "):
|
||||
if titre_courant:
|
||||
sections[titre_courant] = "\n".join(contenu).strip()
|
||||
titre_courant, contenu = ligne[3:].strip(), []
|
||||
elif titre_courant:
|
||||
contenu.append(ligne)
|
||||
if titre_courant:
|
||||
sections[titre_courant] = "\n".join(contenu).strip()
|
||||
|
||||
# Labels prédéfinis selon la fiche
|
||||
labels, selected_ops = [], []
|
||||
correspondances = charger_fiches_et_labels()
|
||||
cible = correspondances.get(fiche_selectionnee)
|
||||
if cible:
|
||||
if len(cible["operations"]) == 1:
|
||||
labels.append(cible["operations"][0])
|
||||
elif len(cible["operations"]) > 1:
|
||||
selected_ops = st.multiselect("Labels opération à associer", cible["operations"], default=cible["operations"])
|
||||
|
||||
# Génération des champs
|
||||
for section, aide in sections.items():
|
||||
if "Type de contribution" in section:
|
||||
options = sorted(set(re.findall(r"- \[.\] (.+)", aide)))
|
||||
if "Autre" not in options:
|
||||
options.append("Autre")
|
||||
choix = st.radio("Type de contribution", options)
|
||||
reponses[section] = st.text_input("Précisez", "") if choix == "Autre" else choix
|
||||
elif "Fiche concernée" in section:
|
||||
url_fiche = f"https://fabnum-git.peccini.fr/FabNum/Fiches/src/branch/{ENV}/Documents/{fiche_selectionnee.replace(' ', '%20')}"
|
||||
reponses[section] = url_fiche
|
||||
st.text_input("Fiche concernée", value=url_fiche, disabled=True)
|
||||
elif "Sujet de la proposition" in section:
|
||||
reponses[section] = st.text_input(section, help=aide)
|
||||
else:
|
||||
reponses[section] = st.text_area(section, help=aide)
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
if col1.button("Prévisualiser le ticket"):
|
||||
st.session_state.previsualiser = True
|
||||
if col2.button("Annuler"):
|
||||
st.session_state.previsualiser = False
|
||||
st.rerun()
|
||||
|
||||
if st.session_state.get("previsualiser", False):
|
||||
st.subheader("Prévisualisation du ticket")
|
||||
for section, texte in reponses.items():
|
||||
st.markdown(f"#### {section}")
|
||||
st.code(texte, language="markdown")
|
||||
|
||||
titre_ticket = reponses.get("Sujet de la proposition", "").strip() or "Ticket FabNum"
|
||||
final_labels = nettoyer_labels(labels + selected_ops + ([cible["item"]] if cible else []))
|
||||
|
||||
st.markdown(f"**Résumé :**\n- **Titre** : `{titre_ticket}`\n- **Labels** : `{', '.join(final_labels)}`")
|
||||
|
||||
if st.button("Confirmer la création du ticket"):
|
||||
labels_existants = get_labels_existants()
|
||||
labels_ids = [labels_existants[l] for l in final_labels if l in labels_existants]
|
||||
if "Backlog" in labels_existants:
|
||||
labels_ids.append(labels_existants["Backlog"])
|
||||
|
||||
corps = construire_corps_ticket_markdown(reponses)
|
||||
creer_ticket_gitea(titre_ticket, corps, labels_ids)
|
||||
|
||||
st.session_state.previsualiser = False
|
||||
st.success("Ticket créé et formulaire vidé.")
|
||||
|
||||
|
||||
def charger_modele_ticket():
|
||||
from config import GITEA_URL, GITEA_TOKEN, ORGANISATION, DEPOT_FICHES
|
||||
|
||||
headers = {"Authorization": f"token {GITEA_TOKEN}"}
|
||||
url = f"{GITEA_URL}/repos/{ORGANISATION}/{DEPOT_FICHES}/contents/.gitea/ISSUE_TEMPLATE/Contenu.md"
|
||||
try:
|
||||
r = requests.get(url, headers=headers, timeout=10)
|
||||
r.raise_for_status()
|
||||
return base64.b64decode(r.json().get("content", "")).decode("utf-8")
|
||||
except Exception as e:
|
||||
st.error(f"Erreur chargement modèle : {e}")
|
||||
return ""
|
||||
Loading…
x
Reference in New Issue
Block a user