Skip to main content

sr.py — structural reporting

A banded report formatter. Apply a template to a sequence of records and get a paginated document: bands, groups, page and column headers and footers, running aggregates, and page footers that know the final page count.

A Python library with a CLI over it, counterpart to the parallel Go project. Template plus JSON in, PDF out.

Documentation

doc/template.md Template format: nodes, properties, geometry, ordering rules, font resolution
doc/expressions.md Expression language, predefined names, formatting, variable semantics
doc/layout.md Layout and pagination
doc/printout.md The intermediate document a renderer consumes
doc/barcode.md Barcode encoding: which symbol each type draws for a value
doc/render.md PDF rendering: what a renderer decides, and what it must not
doc/cli.md The command line: subcommands, flags, streams, exit codes
example/minimal/ The smallest complete report, and the printout doc/printout.md shows
example/sakila/ Reference template and dataset
example/invoices/ Second example: both kinds of subreport, region grouping, the remaining variable modes
example/fonts/ Fonts committed so examples resolve identically everywhere

How it works

Three artifacts and one language:

  • A template is a KDL v2 document describing page geometry and a tree of bands. Bands are title, summary, header, footer, and detail; groups nest around the detail band.
  • Data is JSON or NDJSON. Field types are declared in the template, which is what turns "19.99" into an exact decimal and "2005-05-24T22:53:30Z" into a time value.
  • A printout is the engine's output: pages of absolutely-positioned marks with nothing evaluable left in them. Renderers do no layout.
  • Expressions are Starlark — sandboxed, with no imports, filesystem, or network.

Layout is speculative: every band is measured before it is committed, then placed, split, or deferred to the next frame. Band splitting, keep-together, orphan and widow control, and correct deferred page counts all follow from that.

Command line

python sr.py build --template sakila.kdl --data payments.jsonl --out report.pdf

That is the launcher at the root of a checkout, which runs the program without installing the package. Installed with pip install s-reports, the same program is the s-reports command, and python -m sr where the interpreter's scripts directory is not on the path.

Flag
-t, --template Template file. Required.
-d, --data JSON array or NDJSON file, - for standard input. Omit for a template with no records.
-o, --out Output path, - for standard output. Extension selects the format: .pdf, .srp.jsonl, .srp.cbor.
--format pdf, jsonl or cbor, when the extension does not say.
--param NAME=VALUE Supply a report parameter. Repeatable. VALUE is text, parsed per the parameter's declared type. A name the template does not declare is an error, not a value that goes nowhere.
--build-time RFC 3339. Fixes BUILD_TIME for reproducible output.
--strict-fonts Resolve only explicitly named font files; fail otherwise.
--strict-names Refuse a node or property the format does not define, instead of warning about it.
--accept NAME Take one such name without a warning. Repeatable.
--allow-overflow Downgrade oversized-band errors to warnings recorded in the printout.
--uncompressed Leave PDF streams uncompressed, to read the file in an editor.
-v, --verbose Report host font diagnostics.

Other subcommands:

python sr.py validate sakila.kdl             # check a template without data
python sr.py inspect report.srp.jsonl        # dump a printout as readable text
python sr.py render report.srp.jsonl -o report.pdf

build writing a printout and render reading one back produce the same PDF as build writing PDF directly, which is what makes printouts worth archiving.

The document goes to standard output and everything about the run to standard error, so -o - pipes. Exit 0 is success, 1 a run that failed, 2 a mistake in the command line; warnings never change it, because they travel in the printout header where archiving keeps them. A one-letter flag takes one dash and a long one two, so -t and --template, but not --t or -template. A value is attached with = or separate, a flag may follow the positional arguments, and a switch also takes =true or =false. Full reference: doc/cli.md.

Library

from pathlib import Path

from sr.api import Options, build
from sr.printout.write import write_jsonl

result = build(
    Path("sakila.kdl"),
    Path("payments.jsonl"),
    Options(params={"period_start": "2005-06-01"}, strict_fonts=True),
)
out = Path("sakila.srp.jsonl")
with out.open("w", encoding="utf-8", newline="") as handle:
    write_jsonl(result.printout, handle, out.parent)

build returns the printout as an object, which is the primary artifact: serializing it is a separate step, and the directory it is written to is what its font and image paths are made relative to. Every flag on the command line is a field of Options, because the command line decides nothing.

Rendering is a separate step too, and takes the printout in memory or one read back from a file. Both give the same bytes:

from sr.pdf import render
from sr.printout.read import read

Path("sakila.pdf").write_bytes(render(result.printout))
Path("again.pdf").write_bytes(render(read(out)))

Running the examples

python sr.py build -t example/minimal/minimal.kdl -d example/minimal/films.jsonl -o minimal.srp.jsonl

minimal.kdl is the smallest report there is: one font, a header with a rule, and one field per record. Its printout is the example at the end of doc/printout.md, which the test suite builds and compares byte for byte, so that example is a document rather than an illustration of one.

python sr.py build -t example/sakila/sakila.kdl -d example/sakila/payments.jsonl -o sakila.pdf

Narrowed to one month, using the template's date parameters:

python sr.py build -t example/sakila/sakila.kdl -d example/sakila/payments.jsonl -o june.pdf --param period_start=2005-06-01 --param period_end=2005-07-01

The second example, which uses both kinds of subreport:

python sr.py build -t example/invoices/invoices.kdl -d example/invoices/invoices.jsonl -o invoices.pdf

sakila.kdl — a payment list grouped by customer in two columns: every band type, a group with its own title and summary, column header and footer, stretch fields, a floating element, six barcode types, an embedded image and a referenced one, both kinds of cross-reference, conditional outline entries, a deferred page count, justified text, and typed parameters.

invoices.kdl — invoices by region with line items: an inline embedded subreport with an arg and its own records, a template= subreport in region_sheet.kdl that paginates itself onto landscape pages of its own, a group using keeptogether with minrows and mintailrows, a summary with swapfooter, an image with embed=#false, a compressed data blob, iter="item" against iter="detail", and the calc modes sakila leaves out.

Between them the two templates use every node in the format but accept, and all twelve calc modes. They do not exhaust every property. What they leave out, so that reading them as a reference does not mislead:

Nodes accept
Properties layout width / height / landscape, font typeface / data / bold / italic, line backslant, image type, barcode data, data expr, maxheight, format as a date-parse layout on either parameter or member, subreport ownpageno, columns balance
Spellings the x / y aliases for left / top — both examples use left and top throughout
Enumerations barcode types Code93, DataMatrix, QR-M; image scale="cut" and "grow"; dash="dash"
Scopes iter="report" / "page" / "column"; reset="detail" / "item"

Both reference the committed fonts by path, so they build identically on any machine and work under --strict-fonts. Swap file= for typeface= to use whatever the machine has instead.

Reproducible output

The same template over the same data produces byte-identical printouts when --build-time is fixed and --strict-fonts is set -- on any machine, not just across runs on one. Rendering adds no timestamp of its own. Without the first, the run timestamp differs; without the second, output depends on which fonts are installed.

Nothing machine-specific survives in a strict printout. Strict mode admits only fonts the template named by path, and every path the template named — fonts and embed=#false images alike — is written relative to the printout rather than absolute. So a printout and the files it points at move as one tree. The header records which font file each typeface resolved to and by which step, so a difference is diagnosable from the artifact.

Development

make install    # a venv, the dependencies, and the test and lint tools
make check      # ruff, mypy, pytest

Everything generated goes under build/: the pytest, ruff and mypy caches, setuptools' metadata, and compiled bytecode. The first three come from pyproject.toml and the fourth from PYTHONPYCACHEPREFIX, which the GNUmakefile exports — a pytest typed straight into a shell still writes __pycache__ beside the sources unless that variable is exported there too.

Tests are pytest. tests/unit/ covers a module at a time, tests/golden/ holds this engine to documents that are printed in the specification, and tests/differential/ compares this engine against the Go one, byte for byte, over the examples and over a corpus of probes. It compares the renderers too, by drawing each page of both PDFs with pdfium, from the oracles extra, and comparing the pixels; a PDF is also held to its structure by the tests' own reader, and by qpdf --check where qpdf is on the path. And it compares the command line: what validate reports on every template in the corpus, what inspect dumps of every printout, and the exit code, the output, and the summary line of a table of command lines, each asking one question of doc/cli.md.

The engine was built a milestone at a time, and every command and output format is here now. Every node the format has is built: it paginates reports of field, line, rectangle, image, barcode, and xref elements and outline entries, with groups, columns, band splitting, eject nodes, keep-together rules, balanced columns, deferred values, and subreports of both kinds, inline and with pages of their own, and writes them as NDJSON, CBOR, or PDF. Each printout is held to its own invariants before it is written, and s-reports inspect holds any printout it reads to them. Every barcode type is encoded here, from the standards, rather than by a library, and an image is interpreted only as far as the header that states its size. The PDF writer is written here too: the object table and the cross reference, the drawing, the outline and the links. fontTools subsets the faces and Pillow decodes the images the file does not carry as they stand. Each rendered page is drawn by pdfium and compared, pixel by pixel, with the same printout rendered by the reference. The command line and its two reports are held to the reference's too. The cases the differential corpus cannot yet compare are listed, with what each waits for, in tests/differential/pending.toml.

The Go implementation is the oracle: a reference binary whose outputs settle questions the specification leaves open. Its source is not read -- this is a second implementation from the specification, not a port. The harness builds it from a tree beside this repository, and skips with a reason where there is no Go toolchain to build it with. See tests/differential/README.md.

Where the two engines disagree and doc/ does not say who is right, the answer is a sentence in doc/, not a workaround in either engine.

License

MIT. See LICENSE.

Metadata

Release files for s-reports 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for s-reports 0.1.0
File Size Uploaded Provenance Actions
s_reports-0.1.0.tar.gz 309.4 kB With provenance Details for s_reports-0.1.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for s-reports 0.1.0
File Interpreter ABI Platform Actions
s_reports-0.1.0-py3-none-any.whl Python 3 none any Details for s_reports-0.1.0-py3-none-any.whl

Total release size: 653.4 kB

Release files / s_reports-0.1.0.tar.gz

Download URL s_reports-0.1.0.tar.gz
Size 309.4 kB
Tags Source
SHA-256 checksum
How to use checksums
4396b7d978e3cffdde0943d83534ea35d4e41c7fbbdafe63a270f2af0a470eb2
BLAKE2b-256 checksum
How to use checksums
188b3f43041680f366838f1cedebfa4a9d32602174522acb594226ea9f84ce19
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

Release files / s_reports-0.1.0-py3-none-any.whl

Download URL s_reports-0.1.0-py3-none-any.whl
Size 344.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3137d139b52732aba0756e4bf5b01918da270ebc87eb5fe8901329c9d693dd3a
BLAKE2b-256 checksum
How to use checksums
733ea05b0ea8650ebc424917639cb037a2ee57aa6165600b86b5dd21480b8a09
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

Security for s-reports 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for s_reports-0.1.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page