EasyPour — Markdown Reports to HTML/PDF (with a smile)
Turn tidy Python objects into Markdown, then to beautiful HTML and PDF — all with a tiny, friendly API and a simple CLI.
What Is It?
- Build a
Reportof nestedSections with text,Tables, andImages. - Render Markdown to HTML with tasteful defaults, or to PDF via ReportLab.
- Drive it from Python or from the command line using
python -m easypour.cli.
Install
Pick one of the options below.
- From PyPI (if/when published):
pip install EasyPour
- Include optional WeasyPrint dependency (for Markdown→PDF via CLI):
pip install "EasyPour[weasy]"
- From source (this repo):
pip install .(or editable:pip install -e .)
PDF rendering uses ReportLab (pure Python wheels available on PyPI). If you don’t already have it: pip install reportlab.
Quick Start (Library)
Create a small report, write Markdown, and export HTML/PDF.
from easypour import Report, Table, Image, b, i, code
# Build a report
rpt = Report(title="Weekly Model Analysis", author="ESPR3SS0", meta={"draft": True})
sec = rpt.add_section("Summary")
sec.add_text(
f"This week was {b('great')} — latency down, accuracy steady.",
f"We also verified {code('predict()')} on fresh data.",
)
# Add a table
metrics = Table(headers=["Metric", "Value"], rows=[["Accuracy", "92.8%"], ["F1", "91.5%"]])
rpt.add_section("Metrics").add_table(metrics)
# Add an image (caption and width use a <figure> wrapper)
rpt.add_section("Artifacts").add_image(Image("./charts/latency.png", alt="Latency", caption="P95 latency", width="60%"))
# Write Markdown
md_path = rpt.write_markdown("report.md")
print("Wrote:", md_path)
# Render to HTML (inline CSS included)
from easypour import markdown_to_html
html = markdown_to_html(rpt.to_markdown(), title=rpt.title)
open("report.html", "w", encoding="utf-8").write(html)
# Render to PDF (ReportLab backend)
rpt.write_pdf("report.pdf")
# Configure PDF defaults in code (page size, margins, fonts, captions)
rpt.configure_pdf(
page_size=(8.5 * 72, 11 * 72),
margins=(54, 54, 72, 72),
layout="two",
column_gap=22,
font="Times-Roman",
figure_caption_style={"font": "Times-Italic"},
)
rpt.write_pdf("report_two_col.pdf")
# Use the IEEE preset for two-column output (optional)
from easypour.ieee import IEEETemplate
rpt.write_pdf("report_ieee.pdf", template=IEEETemplate())
Quick Start (CLI)
You can also use the CLI module.
- From an existing Markdown file to HTML/PDF (PDF requires
EasyPour[weasy]):python -m easypour.cli --from-md report.md --html report.htmlpython -m easypour.cli --from-md report.md --pdf report.pdf
- From a Python builder to Markdown/HTML/PDF:
- Create
builder.pywith abuild_report()function that returns either aReportor a Markdownstr:# builder.py from easypour import Report def build_report(): r = Report("CLI Report", author="You") r.add_section("Hello").add_text("This was generated via the CLI.") return r
- Run it:
- Markdown:
python -m easypour.cli --builder builder.py --md out.md - HTML:
python -m easypour.cli --builder builder.py --html out.html - PDF (requires
build_report()to return aReport):python -m easypour.cli --builder builder.py --pdf out.pdf
- Markdown:
- Create
Tip: --builder and --from-md are mutually exclusive.
Little Things That Delight
- Inline helpers:
b("bold"),i("italic"),u("underline"),code("snippet"),link("text", "https://…"). - Tables:
Table.from_dicts([{...}, {...}])or explicit headers/rows. - Images: pass
captionand/orwidth(like"60%"or"380px") for a<figure>wrapper. - Math snippets:
Section.add_math(r"\int_0^1 ...", caption="Integral")renders TeX-like formulas via matplotlib. - Figures/tables with numbering:
Section.add_figure(...)/Section.add_table(..., numbered=True)auto-generate IEEE-style captions. - Citations:
Report.add_reference(...)+report.cite("smith19")give you[1]references and an auto-built References section. - Layout control:
PDFTemplate(layout="two", column_gap=24)or eventemplate.register_layout("cover", builder)let you define single/two/custom column frames and caption styles without touching ReportLab internals. - Global PDF tuning without templates:
report.configure_pdf(page_size=..., margins=..., font="Times-Roman", header_fn=...)sets default page size, margins, fonts, column layouts, headers/footers, and caption styles. If you also pass a custom template, EasyPour will warn when your code-level choices override template values so you always know which settings win. - Interactive plots:
Section.add_matplotlib(fig, interactive=True)keeps the PDF static while upgrading the Streamlit/Dash view to Plotly (zoom/pan/hover). - Cross references: Label figures/tables (
label="fig:latency") and dropreport.ref("fig:latency")anywhere to emitFigure N. - Sensible HTML defaults: readable fonts, clean tables, page numbers for PDF.
- Extra styling:
markdown_to_html(md, extra_css="body { color: #333; }")or pass a customPDFTemplatetoReport.write_pdf.
Why EasyPour?
- Small surface area, batteries included.
- Markdown first; HTML/PDF are just a render away.
- Works great in scripts, notebooks, and CI.
Troubleshooting
- PDF export fails or looks odd:
- Ensure ReportLab is installed:
pip install reportlab. - Check that any referenced images exist on disk and are reachable from your working directory.
- Ensure ReportLab is installed:
- CLI says the builder is missing:
- Your
builder.pymust definebuild_report().
- Your
Contributing & Tests
- Run all tests:
pytest -q - Quick, no-PDF/CLI tests:
pytest -q -m "not pdf and not cli" - Lint/format:
ruff check .andruff format .
Enjoy making many marks! ✨
Documentation
The docs site lives under docs/ and is built with MkDocs Material. Install the optional extras and run:
pip install ".[docs]"
mkdocs serve
Commits to main automatically build and publish the static site to GitHub Pages.
More Examples
All examples below are pure Python — you can drop them in a builder.py and run them via the CLI too.
Bullets, Checklists, Code Blocks, Strikethrough
from easypour.core import Report
r = Report("Lists + Code")
s = r.add_section("Goodies")
s.add_bullets(["Item A", "Item B", "Item C"]) # - bullets
s.add_checklist([("Do A", False), ("Done B", True)]) # - [ ] / [x]
s.add_codeblock("print('hello world')", language="python") # ```python
s.add_strikethrough("old text") # ~~old text~~
open("lists.md", "w").write(r.to_markdown())
Tables (from dicts) and Images (by path)
from easypour.core import Report, Table
r = Report("Data + Image")
data = [
{"Metric": "Accuracy", "Value": "92.8%"},
{"Metric": "F1", "Value": "91.5%"},
]
r.add_section("Results").add_table(Table.from_dicts(data))
r.add_section("Plot").add_image_path("./charts/acc.png", alt="acc", caption="Accuracy", width="50%")
Matplotlib → Image (inline convenience)
from easypour.core import Report
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(3, 2))
ax.plot([0, 1, 2], [0, 1, 0])
ax.set_title("Curve")
r = Report("Matplotlib Demo")
r.add_section("Figure").add_matplotlib(fig, out_dir="assets", filename="curve.png", caption="A curve", width="60%")
r.write_pdf("mpl.pdf")
Two-Column Layout + References
from easypour import Report
from easypour.core import Table
from easypour.render import PDFTemplate
r = Report("Two-Column Demo", author="EasyPour Labs")
r.add_reference("smith19", "Smith et al., 'Cool Paper', IEEE, 2019.")
sec = r.add_section("Results")
sec.add_text(
f"This result improves latency by 20% {r.cite('smith19')} as illustrated in {r.ref('fig:latency')}."
)
sec.add_table(
Table(headers=["Metric", "Value"], rows=[["Latency", "12ms"], ["AUC", "0.962"]]),
caption="Key performance metrics",
numbered=True,
label="tab:metrics",
)
fig_sec = r.add_section("Figures")
fig_sec.add_figure("./charts/latency.png", caption="Latency over input rate", label="fig:latency", width="60%")
r.ensure_references_section()
template = PDFTemplate(layout="two", column_gap=24)
r.write_pdf("two_column.pdf", template=template)
Use r.ref("fig:latency") (or any label you assign) anywhere in your narrative to produce the resolved text (e.g., Figure 1), keeping captions and references consistent across Markdown, HTML, PDF, Streamlit, and Dash.
Math Formulas → Image
from easypour.core import Report
r = Report("Math Demo")
sec = r.add_section("Equations")
sec.add_math(r"\int_0^1 x^2\,dx = \frac{1}{3}", caption="Simple integral", width="220px")
r.write_pdf("math.pdf")
IEEE-Style Sample
python examples/pdf/ieee_builder.py
Generates Markdown and an IEEE-style PDF (two columns, captions, references) in examples/out/ using IEEETemplate.
Simple Full Workflow (Markdown + PDF + Streamlit/Dash)
- Generate Markdown/PDF:
python examples/playbook/simple_full.py - Preview in Streamlit:
streamlit run examples/playbook/simple_full.py -- --preview streamlit - Preview in Dash:
python examples/playbook/simple_full.py --preview dash
Advanced Full Workflow (IEEE template + interactive figures)
- Generate Markdown/PDF:
python examples/playbook/advanced_full.py - Preview in Streamlit:
streamlit run examples/playbook/advanced_full.py -- --preview streamlit - Preview in Dash:
python examples/playbook/advanced_full.py --preview dash
Streamlit Example
Run an interactive Streamlit app that builds a report, previews Markdown/HTML, and lets you download a PDF.
- Command:
streamlit run examples/streamlit/app.py - Minimal gist:
# examples/streamlit/app.py (excerpt)
import streamlit as st
from easypour.core import Report, Table
from easypour import markdown_to_html
def build_report(include_table: bool) -> Report:
rpt = Report("Streamlit Demo", author="Examples")
rpt.add_section("Summary").add_text("Generated inside Streamlit.")
if include_table:
rpt.add_section("Metrics").add_table(Table.from_dicts([
{"Metric": "Accuracy", "Value": "92.8%"},
{"Metric": "F1", "Value": "91.5%"},
]))
return rpt
st.sidebar.header("Options")
opt_table = st.sidebar.checkbox("Include table", value=True)
rpt = build_report(opt_table)
md = rpt.to_markdown()
st.code(md, language="markdown")
html = markdown_to_html(md, title=rpt.title)
st.components.v1.html(html, height=400, scrolling=True)
Tip: Report.show_streamlit() now defaults to four tabs—Report (native Streamlit rendering), Markdown, HTML, and PDF (download button)—and you can reorder or trim them via configure_streamlit(tabs=[...]).
Streamlit Interactive Plots Example
- Command:
streamlit run examples/streamlit/interactive_plots.py - Demonstrates how
Section.add_matplotlib(fig, interactive=True, ...)preserves the static PNG for Markdown/PDF while automatically upgrading the Streamlit view to Plotly charts (zoom, pan, hover).
Dash Example
Run a Dash app that renders the report HTML inside the app.
- Command:
python examples/dash/app.pythen open http://127.0.0.1:8050/ - Minimal gist:
# examples/dash/app.py (excerpt)
from dash import Dash, dcc, html, Input, Output
from easypour.core import Report, Table
from easypour import markdown_to_html
def build_report(include_table: bool) -> Report:
r = Report("Dash Demo", author="Examples")
r.add_section("Summary").add_text("Generated inside Dash.")
if include_table:
r.add_section("Metrics").add_table(Table.from_dicts([
{"Metric": "Accuracy", "Value": "92.8%"},
{"Metric": "F1", "Value": "91.5%"},
]))
return r
app = Dash(__name__)
app.layout = html.Div([
dcc.Checklist(id="opts", options=[{"label": "Include table", "value": "table"}], value=["table"]),
html.Div(id="content"),
])
@app.callback(Output("content", "children"), [Input("opts", "value")])
def update_content(values):
rpt = build_report("table" in (values or []))
html_doc = markdown_to_html(rpt.to_markdown(), title=rpt.title)
return html.Iframe(srcDoc=html_doc, style={"width": "100%", "height": "70vh"})
Tip: Dash apps created via report.to_dash_app() default to tabs for Report, Markdown, HTML, and PDF (download button powered by ReportLab). Customize the subset/order with report.configure_dash(tabs=["Report", "PDF"]), or add your own layout around the returned Dash app.
Dash Interactive Plots Example
- Command:
python examples/dash/interactive_plots.py - Mirrors the Streamlit demo but inside Dash: Matplotlib figures remain PNGs in Markdown/PDF yet show up as fully interactive Plotly graphs in the Dash app.
Release files for EasyPour 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| easypour-0.1.0.tar.gz | 32.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| easypour-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 67.5 kB
Release files / easypour-0.1.0.tar.gz
| Download URL | easypour-0.1.0.tar.gz |
|---|---|
| Size | 32.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
09564bc083ecb79393598be49c1c51a2ee74b21a5b67aeb556d8a268b9d9b2f4
|
|
BLAKE2b-256 checksum How to use checksums |
3cdf161c0e889dd903796fa9d948fd8f9d9a9d76b793375ad7b1cb8bd73a303b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.0
|
Release files / easypour-0.1.0-py3-none-any.whl
| Download URL | easypour-0.1.0-py3-none-any.whl |
|---|---|
| Size | 35.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5afea0e6bc32d96a182ab171a926d1b6d7745137697aa73c458b5a262409286e
|
|
BLAKE2b-256 checksum How to use checksums |
63783b7fd14994c6064c033148dcc2fc0cf1ceec8397eba9aaf5c716ec3e7b75
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.0
|