Picarones / picarones /reports /csv /render.py
Claude
feat(sprint-H.3)!: renommage reports_v2/ → reports/
9011070 unverified
Raw
History Blame
3.58 kB
"""``CsvReportRenderer`` — Sprint A14-S42.
Rendu CSV d'un ``RunResult`` : une ligne par paire
(document × pipeline × view × metric) avec sa valeur numérique ou
le marqueur ``OMITTED`` (pas de score factice).
Cohérent avec la convention du rewrite : pour les pipelines qui ne
produisent pas un type d'artefact accepté par une vue, on émet
``OMITTED`` dans la cellule ``value`` plutôt que ``0`` ou ``""``.
Le consommateur (Pandas, Excel, awk, ...) sait que l'omission est
l'information.
Usage
-----
::
from picarones.reports.csv import CsvReportRenderer
csv_text = CsvReportRenderer().render(run_result)
Path("rapport.csv").write_text(csv_text, encoding="utf-8")
Format
------
Colonnes (dans l'ordre) :
::
run_id, document_id, pipeline_name, view_name,
metric_name, value, status
- ``run_id`` : ``RunManifest.run_id``.
- ``status`` : ``"ok"``, ``"failed_metric"`` (la métrique a levé),
``"omitted"`` (le pipeline ne produit pas d'artefact pour la vue).
- ``value`` : valeur numérique formatée à 6 décimales, ou vide si
``status != "ok"``.
Anti-sur-ingénierie
-------------------
- Pas de pivot par moteur — chaque ligne est self-contained. Le
consommateur pivote en 2 lignes Pandas si besoin.
- Pas d'escape custom — on utilise ``csv.writer`` qui gère les
virgules et guillemets dans les values.
- Pas de séparateur configurable (``,`` fixe) — un test garde-fou
vérifie le déterminisme du contenu.
"""
from __future__ import annotations
import csv
import io
from typing import Any
from picarones.app.results import RunResult
class CsvReportRenderer:
"""Rendu CSV stateless d'un RunResult."""
HEADER: tuple[str, ...] = (
"run_id",
"document_id",
"pipeline_name",
"view_name",
"metric_name",
"value",
"status",
)
def render(self, result: RunResult) -> str:
"""Retourne le contenu CSV (stringly typed) prêt à écrire."""
buf = io.StringIO()
writer = csv.writer(buf)
writer.writerow(self.HEADER)
run_id = result.manifest.run_id
for doc_result in result.document_results:
for view_result in doc_result.view_results:
pipeline_name = view_result.pipeline_name
for metric_name, value in view_result.metric_values.items():
writer.writerow([
run_id,
doc_result.document_id,
pipeline_name,
view_result.view_name,
metric_name,
self._format_value(value),
"ok",
])
for metric_name, _err in view_result.failed_metrics.items():
writer.writerow([
run_id,
doc_result.document_id,
pipeline_name,
view_result.view_name,
metric_name,
"",
"failed_metric",
])
return buf.getvalue()
@staticmethod
def _format_value(value: Any) -> str:
"""Formate la valeur numérique à 6 décimales pour
déterminisme cross-OS (évite ``1.0000000000000002`` sur
certains floats)."""
if isinstance(value, bool):
return "1" if value else "0"
if isinstance(value, (int, float)):
return f"{float(value):.6f}"
return str(value)
__all__ = ["CsvReportRenderer"]