Code/app/fiches/generer.py
Stéphan Peccini f812fac89e
feat: Amelioration structure - tests, documentation et qualite du code
Cette mise a jour complete ameliore significativement la qualite et la maintenabilite du projet.

1. Extension de la couverture de tests

Couverture globale passee de 8% a 16% (+100%)
- Ajout de 25 nouveaux tests (total: 67 tests, 100% passent)
- Nouveaux fichiers de tests:
  * tests/unit/test_gitea.py (17 tests)
  * tests/unit/test_fiches_tickets.py (8 tests)

Etat de la couverture par module:
- utils/gitea.py: 100%
- utils/widgets.py: 100%
- utils/logger.py: 94%
- app/fiches/utils/tickets/core.py: 77%
- utils/graph_utils.py: 59%

2. Documentation d'architecture complete

Creation de 3 nouveaux documents (30 Ko total):
- docs/ARCHITECTURE.md (15 Ko)
  * Architecture complete du projet
  * Flux de donnees detailles
  * Indices de vulnerabilite (IHH, ISG, ICS, IVC)
  * Structure du graphe NetworkX

- docs/MODULES.md (15 Ko)
  * Guide des 11 modules principaux
  * Exemples de code (15+ snippets)
  * Bonnes pratiques
  * Guide de depannage

- docs/README.md (4 Ko)
  * Index de toute la documentation

Contenu documente:
- 5 modules applicatifs
- 6 modules utilitaires
- 4 indices de vulnerabilite avec formules et seuils
- Conventions de code

3. Reorganisation de la documentation

Structure finale optimisee:
- Racine: README.md (mis a jour) + Instructions.md
- docs/: 11 documents organises par categorie

Fichiers deplaces vers docs/:
- README_connexion.md -> docs/CONNEXION.md
- GUIDE_LOGS.md -> docs/
- GUIDE_RUFF.md -> docs/
- RAPPORT_RUFF.md -> docs/
- RAPPORT_CORRECTIONS_AUTO.md -> docs/
- REFACTORING_REPORT.md -> docs/
- VERIFICATION_LOGS.md -> docs/
- TODO_IA_BATCH.md -> docs/

4. Ajout de docstrings

52 fonctions documentees en style Google (100%)
Documentation en francais avec Args, Returns, Raises

5. Corrections automatiques Ruff

Application de 347 corrections automatiques:
- Formatage du code (line-length: 120)
- Organisation des imports
- Simplifications syntaxiques
- Suppressions de code mort
- Ameliorations de performance

6. Configuration qualite du code

Nouveaux fichiers:
- pyproject.toml: configuration Ruff complete
- .vscode/settings.json: integration Ruff avec formatOnSave
- GUIDE_RUFF.md: documentation du linter
- GUIDE_LOGS.md: documentation du logging
- .gitignore: ajout htmlcov/ pour rapports de couverture

Etat final du projet:
- Linter: Ruff configure (15 regles actives)
- Tests: 67 tests (100% passent)
- Couverture de code: 16%
- Docstrings: 52/52 (100%)
- Documentation: 11 fichiers organises

Impact:
- Tests plus robustes et maintenables
- Documentation technique complete
- Meilleure organisation des fichiers
- Workflow optimise avec Ruff
- Code pret pour integration continue

References:
- Architecture: docs/ARCHITECTURE.md
- Guide modules: docs/MODULES.md
- Tests: tests/unit/
- Configuration: pyproject.toml

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-07 19:00:49 +01:00

209 lines
7.8 KiB
Python

"""Module de génération des fiches pour l'application.
Fonctions principales :
1. `remplacer_latex_par_mathml`
2. `markdown_to_html_rgaa`
3. `rendu_html`
4. `generer_fiche`
Toutes ces fonctions gèrent la conversion et le rendu de contenu Markdown
vers du HTML structuré avec des mathématiques, respectant les règles RGAA.
"""
import os
import re
import markdown
import pypandoc
import streamlit as st
import yaml
from bs4 import BeautifulSoup
from latex2mathml.converter import convert as latex_to_mathml
from app.fiches.utils import (
build_dynamic_sections,
build_ihh_sections,
build_isg_sections,
build_ivc_sections,
build_minerai_sections,
build_production_sections,
render_fiche_markdown,
)
# === Fonctions de transformation ===
def remplacer_latex_par_mathml(markdown_text: str) -> str:
"""Remplace les formules LaTeX par des blocs MathML.
Args:
markdown_text (str): Texte Markdown contenant du LaTeX.
Returns:
str: Le même texte avec les formules LaTeX converties en MathML.
"""
def remplacer_bloc_display(match):
formule_latex = match.group(1).strip()
try:
mathml = latex_to_mathml(formule_latex, display='block')
return f'<div class="math-block">{mathml}</div>'
except Exception as e:
return f"<pre>Erreur LaTeX block: {e}</pre>"
def remplacer_bloc_inline(match):
formule_latex = match.group(1).strip()
try:
mathml = latex_to_mathml(formule_latex, display='inline')
return f'<span class="math-inline">{mathml}</span>'
except Exception as e:
return f"<code>Erreur LaTeX inline: {e}</code>"
markdown_text = re.sub(r"\$\$(.*?)\$\$", remplacer_bloc_display, markdown_text, flags=re.DOTALL)
markdown_text = re.sub(r"(?<!\$)\$(.+?)\$(?!\$)", remplacer_bloc_inline, markdown_text, flags=re.DOTALL)
return markdown_text
def markdown_to_html_rgaa(markdown_text: str, caption_text: str|None) -> str:
"""Convertit un texte Markdown en HTML structuré accessible.
Args:
markdown_text (str): Texte Markdown à convertir.
caption_text (str, optional): Titre du tableau si applicable.
Returns:
str: Le HTML structuré avec des attributs de contraintes ARIA.
"""
html = markdown.markdown(markdown_text, extensions=['tables'])
soup = BeautifulSoup(html, "html.parser")
for i, table in enumerate(soup.find_all("table"), start=1):
table["role"] = "table"
table["summary"] = caption_text
if caption_text:
caption = soup.new_tag("caption")
caption.string = caption_text
table.insert(len(table.contents), caption)
for th in table.find_all("th"):
th["scope"] = "col"
return str(soup)
def rendu_html(contenu_md: str) -> list[str]:
"""Rend le contenu Markdown en HTML avec une structure spécifique.
Args:
contenu_md (str): Texte Markdown à formater.
Returns:
list[str]: Liste d'étapes de construction du HTML final.
"""
lignes = contenu_md.split('\n')
sections_n1 = []
section_n1_actuelle = {"titre": None, "intro": [], "sections_n2": {}}
section_n2_actuelle = None
for ligne in lignes:
if re.match(r'^#[^#]', ligne):
if section_n1_actuelle["titre"] or section_n1_actuelle["intro"] or section_n1_actuelle["sections_n2"]:
sections_n1.append(section_n1_actuelle)
section_n1_actuelle = {"titre": ligne.strip('# ').strip(), "intro": [], "sections_n2": {}}
section_n2_actuelle = None
elif re.match(r'^##[^#]', ligne):
section_n2_actuelle = ligne.strip('# ').strip()
section_n1_actuelle["sections_n2"][section_n2_actuelle] = [f"## {section_n2_actuelle}"]
elif section_n2_actuelle:
section_n1_actuelle["sections_n2"][section_n2_actuelle].append(ligne)
else:
section_n1_actuelle["intro"].append(ligne)
if section_n1_actuelle["titre"] or section_n1_actuelle["intro"] or section_n1_actuelle["sections_n2"]:
sections_n1.append(section_n1_actuelle)
bloc_titre = sections_n1[0]["titre"] if sections_n1 and sections_n1[0]["titre"] else "fiche"
titre_id = re.sub(r'\W+', '-', bloc_titre.lower()).strip('-')
html_output = [f'<section role="region" aria-labelledby="{titre_id}">', f'<h1 id="{titre_id}">{bloc_titre}</h1>']
for bloc in sections_n1:
if bloc["titre"] and bloc["titre"] != bloc_titre:
html_output.append(f"<h2>{bloc['titre']}</h2>")
if bloc["intro"]:
intro_md = remplacer_latex_par_mathml("\n".join(bloc["intro"]))
html_intro = markdown_to_html_rgaa(intro_md, None)
html_output.append(html_intro)
for sous_titre, contenu in bloc["sections_n2"].items():
contenu_md = remplacer_latex_par_mathml("\n".join(contenu))
contenu_html = markdown_to_html_rgaa(contenu_md, caption_text=sous_titre)
html_output.append(f"<details><summary>{sous_titre}</summary>{contenu_html}</details>")
html_output.append("</section>")
return html_output
def generer_fiche(md_source: str, dossier: str, nom_fichier: str, seuils: dict) -> str:
"""Génère un document PDF et son HTML correspondant pour une fiche.
Args:
md_source (str): Texte Markdown source contenant la fiche.
dossier (str): Dossier/rubrique de destination.
nom_fichier (str): Nom du fichier (sans extension).
seuils (dict): Valeurs de seuils pour l'analyse.
Returns:
str: Chemin absolu vers le fichier HTML généré.
Notes:
Cette fonction :
- Convertit et formate les données Markdown.
- Génère un document PDF sous format XeLaTeX.
- Crée un document HTML accessible avec des mathématiques.
"""
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, license_path="assets/licence.md")
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)
# Génération automatique du PDF
pdf_dir = os.path.join("static", "Fiches", dossier)
os.makedirs(pdf_dir, exist_ok=True)
# Construire le chemin PDF correspondant (même nom que .md, mais .pdf)
nom_pdf = os.path.splitext(nom_fichier)[0] + ".pdf"
pdf_path = os.path.join(pdf_dir, nom_pdf)
try:
pypandoc.convert_file(
md_path,
to="pdf",
outputfile=pdf_path,
extra_args=["--pdf-engine=xelatex", "-V", "geometry:margin=2cm"]
)
except Exception as e:
st.error(f"[ERREUR] Génération PDF échouée pour {md_path}: {e}")
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")
with open(html_path, "w", encoding="utf-8") as f:
f.write("\n".join(html_output))
return html_path