Add a summary report set from Markdown

from-markdown takes a hand-written summary such as SUMMARY.md and sets it
in the full report's dress: the same cover, header, footers, table of
contents, headings and grids. The shared macros move to macros.html.j2 so
both templates draw from them, and layout.render_pdf takes the template and
document title to use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
alexanderandClaude Opus 5.5 committed 2026-09-25 12:22:45 +02:00
1 parent 0ace2e1803
commit 864e2ff824
10 files changed
+573 -92

No files matched your search

+94
View File
@@ -0,0 +1,94 @@
"""The summary report: a Markdown summary, set as a PDF in the style of the full report."""
from pathlib import Path
from uuid import UUID
from pydantic import BaseModel, Field
from ..vendor.cc_common.api import PipelineError, context
from ..vendor.cc_common.utils import print
from ..summary import render_summary
from .generate import _destination
class SummaryProperties(BaseModel):
"""Configuration properties for the from-markdown CLI command.
Attributes:
markdown: The Markdown summary to set.
scan_id: The scan it is about. Named on the cover and in the footers
when given, and left out when not.
path: Where to write the PDF. A directory is written into under the name
the renderer chose; leaving it out writes into ``reports/``.
project: The name of the scanned project. Taken from the summary's title
when not given.
manufacturer: Who makes the scanned product, as the summary states it.
"""
markdown: Path = Field(description="The Markdown summary to build the PDF from.")
scan_id: UUID | None = Field(default=None, description="The scan the summary is about.")
path: Path | None = Field(default=None, description="Where to write the PDF summary.")
project: str | None = Field(
default=None, description="The name of the scanned project. Read from the title when omitted."
)
manufacturer: str | None = Field(default=None, description="The manufacturer of the scanned product.")
class GeneratedSummary(BaseModel):
"""The summary that was written.
Attributes:
source: The Markdown it was set from.
path: The file written.
size: How large it came out, in bytes.
"""
source: Path
path: Path
size: int
def print_as_cc_common_result(self) -> None:
"""Say what was written and where."""
print(f"[bold]Summary from {self.source}[/bold]")
print(f" {self.size / 1024:.0f} kB written to {self.path}")
def from_markdown(properties: SummaryProperties) -> GeneratedSummary:
"""Build the summary report from a Markdown file.
Args:
properties: The Markdown file, what to say about it, and the destination.
Returns:
The summary that was written.
Raises:
PipelineError: The Markdown cannot be read, or the destination cannot be written.
"""
try:
markdown = properties.markdown.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError) as error:
message = f"Cannot read the summary '{properties.markdown}': {error}"
raise PipelineError(message) from error
name, content = render_summary(
markdown,
scan_id=properties.scan_id,
project=properties.project,
manufacturer=properties.manufacturer,
)
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 summary to '{destination}': {error}"
raise PipelineError(message) from error
context.logger.info(f"Summary written to {destination} ✔")
return GeneratedSummary(source=properties.markdown, path=destination, size=len(content))