"""Rendu HTML « Modules audités » — Sprint 97 (B.6).
Suite directe ``picarones/core/module_policy.py``. Pattern
identique aux autres rendus : server-side, pas de JS, anti-
injection systématique.
Vue
---
Tableau récapitulatif des modules utilisés dans une pipeline
composée, chacun avec :
- Statut d'audit (✓ vert si tous les checks passent, ✗ rouge
sinon, avec compte des échecs) ;
- Métadonnées : version, auteur, licence ;
- Citation académique si fournie ;
- Lien vers la homepage si fourni.
Adaptive : ``""`` si la liste est vide.
Note d'intégration
------------------
Module pur — l'utilisateur compose la liste depuis sa
``PipelineSpec`` augmentée des ``ModuleManifest`` :
.. code-block:: python
from picarones.measurements.module_policy import audit_module
from picarones.report.module_audit_render import build_module_audit_html
audits = []
for step in pipeline.steps:
manifest = step.module.manifest # convention applicative
result = audit_module(step.module, manifest)
audits.append({
"manifest": manifest.as_dict(),
"audit": result.as_dict(),
})
html = build_module_audit_html(audits, labels)
"""
from __future__ import annotations
from html import escape as _e
from typing import Optional
def _passed_badge(passed: bool, n_failed: int, label_pass: str,
label_fail: str) -> str:
if passed:
return (
f''
f'✓ {_e(label_pass)}'
)
return (
f''
f'✗ {_e(label_fail)} ({n_failed})'
)
def build_module_audit_html(
audits: Optional[list],
labels: Optional[dict[str, str]] = None,
) -> str:
"""Construit la vue HTML « Modules audités ».
Parameters
----------
audits:
Liste de dicts ``{"manifest": ManifestDict, "audit":
AuditResultDict}``. Si vide ou ``None``, retourne ``""``.
labels:
Dict i18n. Clés sous le préfixe ``audit_*``.
"""
if not audits:
return ""
rows = [
a for a in audits
if isinstance(a, dict)
and isinstance(a.get("manifest"), dict)
and isinstance(a.get("audit"), dict)
]
if not rows:
return ""
labels = labels or {}
title = labels.get("audit_title", "Modules audités")
note = labels.get(
"audit_note",
"Récapitulatif des modules utilisés dans la pipeline "
"composée. Un module qui ne passe pas l'audit n'est "
"pas exécutable. Métadonnées issues du manifest fourni "
"par le contributeur (auteur, licence, citation).",
)
label_pass = labels.get("audit_pass", "audit OK")
label_fail = labels.get("audit_fail", "checks échoués")
h_module = labels.get("audit_module", "Module")
h_status = labels.get("audit_status", "Audit")
h_version = labels.get("audit_version", "Version")
h_author = labels.get("audit_author", "Auteur")
h_license = labels.get("audit_license", "Licence")
h_io = labels.get("audit_io", "Entrée → sortie")
h_citation = labels.get("audit_citation", "Citation")
h_homepage = labels.get("audit_homepage", "Page projet")
parts = [
'',
f'
{_e(title)}
',
f'
'
f'{_e(note)}
',
'
',
'
',
]
for col in (h_module, h_status, h_version, h_author,
h_license, h_io, h_citation, h_homepage):
parts.append(
f'
'
f'{_e(col)}
'
)
parts.append("
")
for entry in rows:
manifest = entry["manifest"]
audit = entry["audit"]
name = str(manifest.get("name") or "?")
version = str(manifest.get("version") or "—")
author = str(manifest.get("author") or "—")
license_ = str(manifest.get("license") or "—")
in_types = ", ".join(manifest.get("input_types") or []) or "—"
out_types = ", ".join(manifest.get("output_types") or []) or "—"
citation = manifest.get("citation") or ""
homepage = manifest.get("homepage") or ""
passed = bool(audit.get("passed"))
n_failed = int(audit.get("n_failed") or 0)
status_cell = _passed_badge(
passed, n_failed, label_pass, label_fail,
)
# Citation : tronqué si trop long
citation_str = str(citation)[:120]
if len(str(citation)) > 120:
citation_str += "…"
citation_cell = (
_e(citation_str) if citation_str.strip() else "—"
)
# Homepage : on n'auto-link **pas** (anti-injection +
# honnêteté : l'URL peut pointer ailleurs). On affiche
# le texte échappé tel quel.
homepage_cell = (
_e(str(homepage))[:80] + ("…" if len(str(homepage)) > 80 else "")
) if str(homepage).strip() else "—"
parts.append(
f'