Skip to content

Reports

Report templates made of blocks, rendered to PDF.

report

Paginated report documents.

Two halves, deliberately separable. blocks is the data model (what a report is made of) and needs nothing beyond the standard library, so a client can build templates and persist them through DocumentStore with no PDF library installed. pdf renders one, and is the only part behind the pdf extra:

pip install "py-beacon-kit[pdf]"

Complementary to the Excel reporting in beacon.portfolio.reporting rather than a replacement for it: a spreadsheet is for numbers someone will pick up and work with, a PDF is a paginated document that looks the same everywhere and is meant to be read as it was laid out.

Importing beacon.report.pdf without reportlab raises MissingDependencyError naming the extra; importing beacon.report.blocks always works.

BarChart dataclass

BarChart(
    categories: list[str],
    values: list[float],
    title: str = "",
    height: float = 140.0,
)

Bases: Block

A simple horizontal bar chart, drawn natively rather than as an image.

Kept separate from :class:Chart because a handful of labelled bars (top holdings, sector weights, per-factor contributions) is most of what a factsheet actually shows, and routing that through an image pipeline would mean a report could not be produced without the plotting extra.

Attributes:

Name Type Description
categories list[str]

Bar labels, top to bottom.

values list[float]

One value per category. Negative values are drawn to the left of the axis, so a contribution chart reads correctly.

title str

Optional caption.

height float

Drawing height in points.

from_dict classmethod

from_dict(data: dict[str, Any]) -> BarChart

Rebuild from stored form.

Block dataclass

Block()

Base for everything that can appear in a report.

Subclasses set kind and are registered in BLOCK_TYPES so a stored template can be read back.

to_dict

to_dict() -> dict[str, Any]

JSON-ready form, carrying the discriminator.

Chart dataclass

Chart(
    image_path: str | None = None,
    title: str = "",
    height: float = 200.0,
)

Bases: Block

A rendered chart image, or a placeholder for one.

Attributes:

Name Type Description
image_path str | None

Path to a rendered image. None, or a path that does not exist, draws a labelled placeholder instead of failing: a template is designed before the charts it will hold are built, and a missing image should not stop a layout being reviewed.

title str

Optional caption.

height float

Drawing height in points.

from_dict classmethod

from_dict(data: dict[str, Any]) -> Chart

Rebuild from stored form.

Header dataclass

Header(title: str, subtitle: str = '', as_of: str = '')

Bases: Block

The title band at the top of a report.

Attributes:

Name Type Description
title str

Main line.

subtitle str

Optional second line.

as_of str

Optional date string. Kept as text rather than a date because a report's as-of label is presentation: "31 Dec 2024" and "2024-12-31" are the same date and a different report.

PageSetup dataclass

PageSetup(
    size: str = "A4",
    orientation: str = PORTRAIT,
    margin: float = DEFAULT_MARGIN,
)

The sheet a report is drawn on.

Attributes:

Name Type Description
size str

A key of PAGE_SIZES.

orientation str

PORTRAIT or LANDSCAPE.

margin float

Blank border in points, applied on all four sides.

dimensions property

dimensions: tuple[float, float]

(width, height) in points, after orientation.

content_width property

content_width: float

Drawable width between the margins.

content_height property

content_height: float

Drawable height between the margins.

to_dict

to_dict() -> dict[str, Any]

JSON-ready form.

from_dict classmethod

from_dict(data: dict[str, Any]) -> PageSetup

Rebuild from stored form.

ReportTemplate dataclass

ReportTemplate(
    template_id: str,
    name: str,
    page: PageSetup = PageSetup(),
    blocks: list[Block] = list(),
)

A page setup and the blocks to draw on it.

Attributes:

Name Type Description
template_id str

Stable identifier, used as the DocumentStore key.

name str

Human-readable name.

page PageSetup

Sheet setup.

blocks list[Block]

Content, drawn top to bottom in order.

to_dict

to_dict() -> dict[str, Any]

JSON-ready form, suitable for DocumentStore.

from_dict classmethod

from_dict(data: dict[str, Any]) -> ReportTemplate

Rebuild a stored template.

Parameters:

Name Type Description Default
data dict[str, Any]

The stored form.

required

Returns:

Name Type Description
ReportTemplate ReportTemplate

The template.

Raises:

Type Description
ReportingError

If a block cannot be read.

Stat dataclass

Stat(label: str, value: str, change: str = '')

One labelled number in a StatGrid.

Attributes:

Name Type Description
label str

What it is.

value str

Preformatted for display. The block model does not format numbers: a percentage, a currency amount and a ratio all need different treatment, and the caller knows which this is.

change str

Optional secondary line, e.g. a period change.

StatGrid dataclass

StatGrid(stats: list[Stat], columns: int = 4)

Bases: Block

A row of headline figures.

Attributes:

Name Type Description
stats list[Stat]

The figures, laid out left to right.

columns int

How many per row. Extra stats wrap onto further rows.

to_dict

to_dict() -> dict[str, Any]

JSON-ready form.

from_dict classmethod

from_dict(data: dict[str, Any]) -> StatGrid

Rebuild from stored form.

Table dataclass

Table(
    columns: list[str],
    rows: list[list[str]],
    title: str = "",
    align_right: list[int] = list(),
)

Bases: Block

A grid of values.

Attributes:

Name Type Description
columns list[str]

Header labels.

rows list[list[str]]

Row values, already formatted for display.

title str

Optional caption above the table.

align_right list[int]

Column indices to right-align. Numbers read far better right-aligned, and the block model cannot tell which columns hold them because every cell is already a string.

from_dict classmethod

from_dict(data: dict[str, Any]) -> Table

Rebuild from stored form.

Text dataclass

Text(body: str, size: float = 9.0, muted: bool = False)

Bases: Block

A paragraph.

Attributes:

Name Type Description
body str

The text. Wrapped to the content width by the renderer.

size float

Font size in points.

muted bool

Whether to draw in the muted ink rather than the primary.

block_from_dict

block_from_dict(data: dict[str, Any]) -> Block

Rebuild one block from its stored form.

Parameters:

Name Type Description Default
data dict[str, Any]

A block's dict, carrying its kind.

required

Returns:

Name Type Description
Block Block

The reconstructed block.

Raises:

Type Description
ReportingError

If the kind is missing or unknown. Skipping an unreadable block would silently drop content from a report, which is worse than refusing to render it.

blocks

The block model: what a report is made of, as plain data.

A report template is a page setup and an ordered list of blocks. Nothing here knows how to draw anything: these are dataclasses that round-trip through JSON so a template can be stored, edited by a client, and rendered later by a process that never saw the one that created it. The drawing lives in beacon.report.pdf behind the pdf extra, which means a client can build and persist templates with no PDF library installed at all.

Blocks carry a kind discriminator so a stored template can be read back into the right class. That is the one piece of ceremony in here, and it is what lets DocumentStore hold a heterogeneous list without the reader having to guess.

Why the chart block holds a path rather than a figure

A chart is rendered by the plotting layer (beacon.plot) into an image file, and this block points at it. Holding a live matplotlib figure would drag an optional dependency into the data model and make a template unserialisable, the exact coupling the split above exists to avoid. A chart block with no image, or with a path that does not exist, renders as a labelled placeholder, so a template can be designed and reviewed before the charts it will hold are built.

PageSetup dataclass

PageSetup(
    size: str = "A4",
    orientation: str = PORTRAIT,
    margin: float = DEFAULT_MARGIN,
)

The sheet a report is drawn on.

Attributes:

Name Type Description
size str

A key of PAGE_SIZES.

orientation str

PORTRAIT or LANDSCAPE.

margin float

Blank border in points, applied on all four sides.

dimensions property
dimensions: tuple[float, float]

(width, height) in points, after orientation.

content_width property
content_width: float

Drawable width between the margins.

content_height property
content_height: float

Drawable height between the margins.

to_dict
to_dict() -> dict[str, Any]

JSON-ready form.

from_dict classmethod
from_dict(data: dict[str, Any]) -> PageSetup

Rebuild from stored form.

Block dataclass

Block()

Base for everything that can appear in a report.

Subclasses set kind and are registered in BLOCK_TYPES so a stored template can be read back.

to_dict
to_dict() -> dict[str, Any]

JSON-ready form, carrying the discriminator.

Header dataclass

Header(title: str, subtitle: str = '', as_of: str = '')

Bases: Block

The title band at the top of a report.

Attributes:

Name Type Description
title str

Main line.

subtitle str

Optional second line.

as_of str

Optional date string. Kept as text rather than a date because a report's as-of label is presentation: "31 Dec 2024" and "2024-12-31" are the same date and a different report.

Text dataclass

Text(body: str, size: float = 9.0, muted: bool = False)

Bases: Block

A paragraph.

Attributes:

Name Type Description
body str

The text. Wrapped to the content width by the renderer.

size float

Font size in points.

muted bool

Whether to draw in the muted ink rather than the primary.

Stat dataclass

Stat(label: str, value: str, change: str = '')

One labelled number in a StatGrid.

Attributes:

Name Type Description
label str

What it is.

value str

Preformatted for display. The block model does not format numbers: a percentage, a currency amount and a ratio all need different treatment, and the caller knows which this is.

change str

Optional secondary line, e.g. a period change.

StatGrid dataclass

StatGrid(stats: list[Stat], columns: int = 4)

Bases: Block

A row of headline figures.

Attributes:

Name Type Description
stats list[Stat]

The figures, laid out left to right.

columns int

How many per row. Extra stats wrap onto further rows.

to_dict
to_dict() -> dict[str, Any]

JSON-ready form.

from_dict classmethod
from_dict(data: dict[str, Any]) -> StatGrid

Rebuild from stored form.

Table dataclass

Table(
    columns: list[str],
    rows: list[list[str]],
    title: str = "",
    align_right: list[int] = list(),
)

Bases: Block

A grid of values.

Attributes:

Name Type Description
columns list[str]

Header labels.

rows list[list[str]]

Row values, already formatted for display.

title str

Optional caption above the table.

align_right list[int]

Column indices to right-align. Numbers read far better right-aligned, and the block model cannot tell which columns hold them because every cell is already a string.

from_dict classmethod
from_dict(data: dict[str, Any]) -> Table

Rebuild from stored form.

BarChart dataclass

BarChart(
    categories: list[str],
    values: list[float],
    title: str = "",
    height: float = 140.0,
)

Bases: Block

A simple horizontal bar chart, drawn natively rather than as an image.

Kept separate from :class:Chart because a handful of labelled bars (top holdings, sector weights, per-factor contributions) is most of what a factsheet actually shows, and routing that through an image pipeline would mean a report could not be produced without the plotting extra.

Attributes:

Name Type Description
categories list[str]

Bar labels, top to bottom.

values list[float]

One value per category. Negative values are drawn to the left of the axis, so a contribution chart reads correctly.

title str

Optional caption.

height float

Drawing height in points.

from_dict classmethod
from_dict(data: dict[str, Any]) -> BarChart

Rebuild from stored form.

Chart dataclass

Chart(
    image_path: str | None = None,
    title: str = "",
    height: float = 200.0,
)

Bases: Block

A rendered chart image, or a placeholder for one.

Attributes:

Name Type Description
image_path str | None

Path to a rendered image. None, or a path that does not exist, draws a labelled placeholder instead of failing: a template is designed before the charts it will hold are built, and a missing image should not stop a layout being reviewed.

title str

Optional caption.

height float

Drawing height in points.

from_dict classmethod
from_dict(data: dict[str, Any]) -> Chart

Rebuild from stored form.

ReportTemplate dataclass

ReportTemplate(
    template_id: str,
    name: str,
    page: PageSetup = PageSetup(),
    blocks: list[Block] = list(),
)

A page setup and the blocks to draw on it.

Attributes:

Name Type Description
template_id str

Stable identifier, used as the DocumentStore key.

name str

Human-readable name.

page PageSetup

Sheet setup.

blocks list[Block]

Content, drawn top to bottom in order.

to_dict
to_dict() -> dict[str, Any]

JSON-ready form, suitable for DocumentStore.

from_dict classmethod
from_dict(data: dict[str, Any]) -> ReportTemplate

Rebuild a stored template.

Parameters:

Name Type Description Default
data dict[str, Any]

The stored form.

required

Returns:

Name Type Description
ReportTemplate ReportTemplate

The template.

Raises:

Type Description
ReportingError

If a block cannot be read.

block_from_dict

block_from_dict(data: dict[str, Any]) -> Block

Rebuild one block from its stored form.

Parameters:

Name Type Description Default
data dict[str, Any]

A block's dict, carrying its kind.

required

Returns:

Name Type Description
Block Block

The reconstructed block.

Raises:

Type Description
ReportingError

If the kind is missing or unknown. Skipping an unreadable block would silently drop content from a report, which is worse than refusing to render it.

pdf

Rendering a report template to PDF.

Separate from the block model so a template can be built and stored without reportlab installed; this module is the only thing behind the pdf extra.

Deterministic by construction

Two renders of the same template produce byte-identical files. That is not free: a PDF normally carries a creation timestamp and a random document id, so the default behaviour is for every render to differ. reportlab's invariant mode pins both, and it is switched on here rather than offered as an option:

  • a report is a record, and two people running the same template on the same data should be able to confirm they got the same thing by comparing hashes;
  • the alternative is a diffing story where every rerender looks like a change, which makes review worthless.
Colours come from the design tokens

Specifically from the raw.paper-* values, which are mode-independent. A factsheet is a print artefact: it must look the same whichever theme the surrounding application is wearing, and identical on screen to the PDF it becomes. Using the themed tokens here would produce a dark-mode PDF, which is not a thing anyone wants.

One page

The renderer produces a single page. It lays blocks out top to bottom and raises ReportingError when they do not fit, naming the block that overflowed. Pagination is real work (headers repeating, tables splitting, blocks that must not break across a boundary), and guessing at it would produce documents that look almost right.

render

render(
    template: ReportTemplate, output_path: str | Path
) -> Path

Draw a template to a PDF file.

Parameters:

Name Type Description Default
template ReportTemplate

What to draw.

required
output_path str | Path

Where to write it. Parent directories are created.

required

Returns:

Name Type Description
Path Path

The written file.

Raises:

Type Description
ReportingError

If the content does not fit on one page.

MissingDependencyError

If reportlab is not installed.