"""report_generator generate command.""" from pathlib import Path from uuid import UUID from pydantic import BaseModel, Field from cc_common.api import PipelineError, context from cc_common.utils import print from persistent_storage.shared import StorageConfiguration from ..rendering import render from ..sources import collect # Where the report goes when the caller names no path. DEFAULT_DIRECTORY = Path("reports") class GenerateProperties(BaseModel): """Configuration properties for the generate CLI command. Attributes: scan_id: The scan to report on. path: Where to write the PDF. A directory is written into under the name the renderer chose; leaving it out writes into ``reports/``. storage_config: The store the scan's artifacts are read from. """ scan_id: UUID = Field(description="The scan the report should be generated for.") path: Path | None = Field(default=None, description="Where to write the PDF report.") storage_config: StorageConfiguration class GeneratedReport(BaseModel): """The report that was written. Attributes: scan_id: The scan it is about. path: The file written. size: How large it came out, in bytes. """ scan_id: UUID path: Path size: int def print_as_cc_common_result(self) -> None: """Say what was written and where.""" print(f"[bold]Report for scan {self.scan_id}[/bold]") print(f" {self.size / 1024:.0f} kB written to {self.path}") def generate(properties: GenerateProperties) -> GeneratedReport: """Build the PDF report of one scan and write it. Args: properties: Parsed command properties naming the scan and the destination. Returns: The report that was written. Raises: PipelineError: The scan cannot be read, is missing an input the report needs, or the destination cannot be written. """ sources = collect(properties.storage_config, properties.scan_id) name, content = render(sources) destination = _destination(properties.path, name) try: destination.parent.mkdir(parents=True, exist_ok=True) destination.write_bytes(content) except OSError as error: message = f"Cannot write the report to '{destination}': {error}" raise PipelineError(message) from error context.logger.info(f"Report written to {destination} ✔") return GeneratedReport(scan_id=properties.scan_id, path=destination, size=len(content)) def _destination(path: Path | None, name: str) -> Path: """Where the report goes, treating a directory as a place to put it rather than as the file.""" if path is None: return DEFAULT_DIRECTORY / name return path / name if path.is_dir() or not path.suffix else path