Source code for codonadaptpy.reporting

"""
CodonAdaptPy Reproducible File Reports

This module exports analysis and optimization results to JSON, CSV, TSV, FASTA,
Excel, HTML, and optional PDF. Reports include software, input checksum,
genetic-code, reference-version, random-seed, and command provenance already
captured by the analyzer.

Classes:
    - ReportGenerator: Write deterministic machine-readable and human reports.

:Created: July 20, 2026
:Updated: July 20, 2026
:Author: Naveen Duhan
:Version: 1.0.2
"""

from __future__ import annotations

import csv
import html
import json
from dataclasses import asdict
from pathlib import Path
from typing import Any

from .exceptions import OptionalDependencyError
from .models import AnalysisResult
from .optimizer import OptimizationResult


[docs] class ReportGenerator: """Export package results to machine-readable and publication-friendly files."""
[docs] def write_json(self, results: list[AnalysisResult], path: str | Path) -> Path: """Write nested analysis results as indented JSON.""" destination = Path(path) destination.write_text( json.dumps([result.to_dict() for result in results], indent=2, sort_keys=True) + "\n", encoding="utf-8" ) return destination
[docs] def write_table(self, results: list[AnalysisResult], path: str | Path, *, delimiter: str | None = None) -> Path: """Write one flattened scalar-metric row per sequence as CSV or TSV.""" destination = Path(path) separator = delimiter or ("\t" if destination.suffix.lower() == ".tsv" else ",") rows = [self._summary_row(result) for result in results] fields = sorted({key for row in rows for key in row}) with destination.open("w", encoding="utf-8", newline="") as handle: writer = csv.DictWriter(handle, fieldnames=fields, delimiter=separator, lineterminator="\n") writer.writeheader() writer.writerows(rows) return destination
[docs] def write_html(self, results: list[AnalysisResult], path: str | Path) -> Path: """Write a self-contained HTML summary without a template dependency.""" destination = Path(path) rows = [self._summary_row(result) for result in results] fields = sorted({key for row in rows for key in row}) header = "".join(f"<th>{html.escape(field)}</th>" for field in fields) body = "".join( "<tr>" + "".join(f"<td>{html.escape(str(row.get(field, '')))}</td>" for field in fields) + "</tr>" for row in rows ) document = f"""<!doctype html> <html lang="en"> <head> <meta charset="utf-8"><title>CodonAdaptPy report</title> <style> body {{ font-family: system-ui; margin: 2rem; color: #18212b; }} table {{ border-collapse: collapse; width: 100%; }} th, td {{ border: 1px solid #ccd3da; padding: .45rem; text-align: right; }} th {{ background: #edf3f7; }} td:first-child, th:first-child {{ text-align: left; }} .note {{ padding: 1rem; background: #fff6dc; border-left: 4px solid #d99b00; }} </style> </head> <body> <h1>CodonAdaptPy analysis report</h1> <p class="note">Codon-usage similarity is descriptive and is not direct proof of expression, replication, virulence, host switching, transmission, or fitness.</p> <table><thead><tr>{header}</tr></thead><tbody>{body}</tbody></table> </body> </html>""" destination.write_text(document, encoding="utf-8") return destination
[docs] def write_excel(self, results: list[AnalysisResult], path: str | Path) -> Path: """Write summary, codon counts, RSCU, validation, and host sheets.""" try: import pandas as pd except ImportError as exc: raise OptionalDependencyError("Excel reports require CodonAdaptPy[analysis,io].") from exc destination = Path(path) with pd.ExcelWriter(destination) as writer: pd.DataFrame([self._summary_row(result) for result in results]).to_excel( writer, sheet_name="Summary", index=False ) pd.DataFrame([{"identifier": result.identifier, **result.codon_counts} for result in results]).to_excel( writer, sheet_name="Codon_counts", index=False ) pd.DataFrame([{"identifier": result.identifier, **result.rscu} for result in results]).to_excel( writer, sheet_name="RSCU", index=False ) pd.DataFrame( [{"identifier": result.identifier, **result.dinucleotide_odds} for result in results] ).to_excel(writer, sheet_name="Dinucleotide_OE", index=False) pd.DataFrame( [{"identifier": result.identifier, **result.dinucleotide_bias} for result in results] ).to_excel(writer, sheet_name="Dinucleotide_bias", index=False) validation_rows = [ {"identifier": result.identifier, **asdict(issue)} for result in results for issue in result.validation.issues ] pd.DataFrame(validation_rows).to_excel(writer, sheet_name="Validation", index=False) host_rows = [ {"identifier": result.identifier, "host": host, **metrics} for result in results for host, metrics in result.host_comparisons.items() ] pd.DataFrame(host_rows).to_excel(writer, sheet_name="Hosts", index=False) return destination
[docs] def write_pdf(self, results: list[AnalysisResult], path: str | Path) -> Path: """Render the HTML report to PDF using the optional report extra.""" try: from weasyprint import HTML except ImportError as exc: raise OptionalDependencyError("PDF reports require CodonAdaptPy[report].") from exc destination = Path(path) html_path = destination.with_suffix(".html") self.write_html(results, html_path) HTML(filename=str(html_path)).write_pdf(str(destination)) return destination
[docs] def write_optimized_fasta(self, result: OptimizationResult, path: str | Path) -> Path: """Write ranked optimization candidates in FASTA format.""" destination = Path(path) lines: list[str] = [] for candidate in result.candidates: lines.extend( [ f">candidate_{candidate.rank} score={candidate.score:.6f} " f"cai={candidate.cai:.6f} gc={candidate.gc:.6f}", candidate.sequence, ] ) destination.write_text("\n".join(lines) + "\n", encoding="utf-8") return destination
@staticmethod def _summary_row(result: AnalysisResult) -> dict[str, Any]: """Flatten scalar metrics and host metrics for tabular exports.""" row: dict[str, Any] = {"identifier": result.identifier, "valid": result.validation.is_valid} row.update( { key: value for key, value in result.metrics.items() if isinstance(value, (str, int, float, bool)) or value is None } ) row.update({f"dinucleotide_oe.{pair}": value for pair, value in result.dinucleotide_odds.items()}) row.update({f"dinucleotide_bias.{pair}": value for pair, value in result.dinucleotide_bias.items()}) for host, metrics in result.host_comparisons.items(): row.update({f"host.{host}.{key}": value for key, value in metrics.items()}) return row