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
¶
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. |
Block
dataclass
¶
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.
Chart
dataclass
¶
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. |
Header
dataclass
¶
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
¶
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. |
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. |
from_dict
classmethod
¶
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
¶
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
¶
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. |
Text
dataclass
¶
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 ¶
Rebuild one block from its stored form.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
dict[str, Any]
|
A block's dict, carrying its |
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
¶
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. |
Block
dataclass
¶
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.
Header
dataclass
¶
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
¶
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
¶
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
¶
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. |
BarChart
dataclass
¶
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. |
Chart
dataclass
¶
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. |
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. |
from_dict
classmethod
¶
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 ¶
Rebuild one block from its stored form.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
dict[str, Any]
|
A block's dict, carrying its |
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 ¶
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. |