from typing import List
from pylatex import (
Center,
Command,
Document,
Figure,
Itemize,
NoEscape,
Section,
Subsection,
Table,
Tabular,
)
from pylatex.package import Package
from ..utils.config import config
from ..utils.logging_config import setup_logger
logger = setup_logger(__name__)
[docs]
class Raport:
"""
Generates LaTeX reports with analysis results and visualizations.
"""
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super(Raport, cls).__new__(cls, *args, **kwargs)
cls._instance._initialize()
return cls._instance
def _initialize(self):
logger.info("Initializing ReportGenerator with title: %s", config.raport_title)
try:
self.doc = Document(
default_filepath=config.root_dir, geometry_options=config.tex_geomatry
)
self.title = config.raport_title
# Add necessary packages
self.doc.packages.append(Package("graphicx"))
self.doc.packages.append(Package("float"))
self.doc.packages.append(Package("booktabs"))
self.doc.packages.append(Package("hyperref"))
self.doc.packages.append(Package("caption"))
self.doc.packages.append(Package("subcaption"))
self.doc.packages.append(Package("amsmath"))
logger.debug("Initialized LaTeX document with packages")
except Exception as e:
logger.error("Failed to initialize ReportGenerator: %s", str(e))
raise
[docs]
def add_section(self, title: str, description: str = "") -> Section:
"""
Adds a new section to the LaTeX document.
Args:
title (str): The title of the section.
description (str, optional): The description of the section. Defaults to "".
Returns:
Section: The newly created section.
"""
section = Section(title)
if description:
section.append(description)
self.doc.append(section)
return section
[docs]
def add_subsection(self, title: str) -> Subsection:
"""
Adds a new subsection to the LaTeX document.
Args:
title (str): The title of the subsection.
Returns:
Subsection: The newly created subsection.
"""
subsection = Subsection(title)
self.doc.append(subsection)
return subsection
[docs]
def add_table(
self,
data: dict | list,
caption: str = None,
header: List[dict] = ["Category", "Value"],
widths: list = None,
label: str = None,
) -> None:
"""Add a table to the document with wrapped text, dividing columns equally across an A4 page.
Args:
data (dict | list): Data to convert to table.
caption (str): Table caption. If None, no caption will be set.
header (List[dict]): Table header. Defaults to ["Category", "Value"].
If None, no header will be set.
widths (list) - column widths
"""
decimal_precision = config.raport_decimal_precision
try:
if isinstance(data, dict):
data = list(data.items())
num_columns = len(header) if header is not None else len(data[0])
with self.doc.create(Table(position="H")) as table:
with table.create(Center()) as centered:
centered.append(
NoEscape(r"\renewcommand{\arraystretch}{1.5}")
) # Adjust 1.5 as needed
if widths is not None:
# Use provided widths for columns
alignment = " ".join([f"p{{{w}mm}}" for w in widths])
else:
alignment = " ".join(["l" for _ in range(num_columns)])
with centered.create(Tabular(alignment)) as tabular:
tabular.add_hline()
if header is not None:
if len(header) != num_columns:
raise ValueError(
f"Header length ({len(header)}) does not match "
f"number of columns ({num_columns})."
)
tabular.add_row(
[NoEscape(f"\\textbf{{{c}}}") for c in header]
)
tabular.add_hline()
for row in data:
# Check row length matches the number of columns
if len(row) != num_columns:
raise ValueError(
f"Row length ({len(row)}) does not match number of "
f"columns ({num_columns}): {row}"
)
# Format row elements
formatted_row = [
(
f"{value:.{decimal_precision}f}"
if isinstance(value, float)
else str(value)
)
for value in row
]
tabular.add_row(formatted_row)
tabular.add_hline()
if caption is not None:
table.add_caption(caption)
if label is not None:
# Add label for referencing
table.append(NoEscape(f"\\label{{{label}}}"))
except Exception as e:
logger.error(f"Failed to add table {caption}: {str(e)}")
raise
[docs]
def add_text(self, text: str) -> None:
"""Adds plain text to the LaTeX document.
Args: text (str): The text to add to the document."""
try:
self.doc.append(NoEscape(text))
except Exception as e:
logger.error(f"Failed to add text: {str(e)}")
raise
[docs]
def add_reference(
self, label: str, prefix: str = "Table", add_space: bool = True
) -> None:
"""Adds a reference to a labeled element in the document.
Args:
label (str): The label of the element to reference.
prefix (str): Text to prepend before the reference. Defaults to "Table".
add_space (bool): If True, adds a space after the reference. Defaults to True.
"""
try:
reference = NoEscape(f"{prefix}~\\ref{{{label}}}")
if add_space:
reference += NoEscape(" ")
self.doc.append(reference)
except Exception as e:
logger.error(f"Failed to add reference to {label}: {str(e)}")
raise
[docs]
def add_list(self, items: list, caption: str = None) -> None:
"""
Adds a bullet-point list to the document.
Args:
items (list): List of items to include in the bullet-point list.
caption (str, optional): Optional caption or description above the list.
"""
try:
if caption:
self.doc.append(NoEscape(r"\text{" + caption + r"}"))
with self.doc.create(Itemize()) as itemize:
for item in items:
itemize.add_item(str(item))
except Exception as e:
logger.error(f"Failed to add list: {str(e)}")
raise
[docs]
def add_verbatim(self, content: str) -> str:
"""Add verbatim text to the document.
Args:
content (str): Text to add in verbatim environment.
Returns:
str: Formatted text.
"""
try:
self.doc.append(
NoEscape(
r"\begin{verbatim}" + "\n" + content + "\n" + r"\end{verbatim}"
)
)
return content
except Exception as e:
logger.error("Failed to add verbatim content: %s", str(e))
raise
[docs]
def generate(self) -> None:
"""
Generate the final PDF report.
Args:
output_path (str): Path where to save the PDF.
"""
try:
logger.debug(f"Generating PDF at {config.raport_path}")
self.doc.generate_pdf(
config.raport_path, clean=False, clean_tex=False, compiler="pdflatex"
)
self.doc.generate_pdf(
config.raport_path, clean_tex=config.return_tex_, compiler="pdflatex"
)
logger.info("PDF generation complete")
except Exception as e:
logger.error(f"Failed to generate PDF: {str(e)}")
raise