Apiarist / report.py
Apiarist Dev
fix: PDF notes wrap inside table; hide trend chart until 2+ inspections (no empty box)
b378fd6
Raw
History Blame Contribute Delete
5.8 kB
"""
PDF report generator for an apiary's weekly inspections.
Pulls inspection rows from SQLite (db.py) and renders a clean one-page
report per hive plus a summary page. Uses reportlab, pure Python, no
system deps, ships fine on HF Spaces.
"""
from __future__ import annotations
import time
from io import BytesIO
from pathlib import Path
from reportlab.lib import colors
from reportlab.lib.pagesizes import LETTER
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.platypus import (
SimpleDocTemplate,
Paragraph,
Spacer,
Table,
TableStyle,
PageBreak,
)
import db
def _styles():
base = getSampleStyleSheet()
base.add(
ParagraphStyle(
name="ApiaristTitle",
parent=base["Heading1"],
fontSize=22,
textColor=colors.HexColor("#f4a300"),
spaceAfter=12,
)
)
base.add(
ParagraphStyle(
name="ApiaristH2",
parent=base["Heading2"],
fontSize=14,
textColor=colors.HexColor("#1a1410"),
spaceBefore=10,
spaceAfter=6,
)
)
base.add(
ParagraphStyle(
name="ApiaristSmall",
parent=base["BodyText"],
fontSize=9,
textColor=colors.HexColor("#555"),
)
)
return base
def _date(epoch: float | None) -> str:
if not epoch:
return "-"
return time.strftime("%Y-%m-%d %H:%M", time.localtime(epoch))
def _hive_table(inspections: list[dict]) -> Table:
# Wrap the long Notes text in a Paragraph so it wraps inside the column
# instead of overflowing the table edge.
cell_style = ParagraphStyle(
name="cell", fontSize=8, leading=10,
textColor=colors.HexColor("#1a1410"),
)
hdr = [
Paragraph(f"<b>{h}</b>", ParagraphStyle("h", fontSize=9, textColor=colors.HexColor("#1a1410")))
for h in ["Date", "Queen?", "Mites", "Swarm?", "Health", "Notes"]
]
rows = [hdr]
for i in inspections:
rows.append(
[
Paragraph(_date(i["created_at"]), cell_style),
"Y" if i["queen_detected"] else "N",
str(i["varroa_mites_visible"] or 0),
"Y" if i["swarm_cells_detected"] else "N",
i["frame_health"] or "-",
Paragraph((i["notes"] or "")[:160], cell_style),
]
)
t = Table(rows, repeatRows=1, colWidths=[1.05 * inch] + [0.6 * inch] * 4 + [2.9 * inch])
t.setStyle(
TableStyle(
[
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#f4a300")),
("TEXTCOLOR", (0, 0), (-1, 0), colors.HexColor("#1a1410")),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 9),
("BOTTOMPADDING", (0, 0), (-1, 0), 6),
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f7f1e0")]),
("GRID", (0, 0), (-1, -1), 0.25, colors.HexColor("#cccccc")),
("VALIGN", (0, 0), (-1, -1), "TOP"),
]
)
)
return t
def generate_report() -> bytes:
"""Build a PDF report covering every hive in the registry. Returns bytes."""
hives = db.list_hives()
styles = _styles()
buf = BytesIO()
doc = SimpleDocTemplate(
buf,
pagesize=LETTER,
rightMargin=0.6 * inch,
leftMargin=0.6 * inch,
topMargin=0.6 * inch,
bottomMargin=0.6 * inch,
title="Apiarist Inspection Report",
author="Apiarist",
)
story = []
story.append(Paragraph(" Apiarist Inspection Report", styles["ApiaristTitle"]))
story.append(
Paragraph(
f"Generated {time.strftime('%Y-%m-%d %H:%M')}",
styles["ApiaristSmall"],
)
)
story.append(Spacer(1, 0.2 * inch))
total_inspections = sum((h.get("inspection_count") or 0) for h in hives)
summary_lines = [
f"<b>Total hives:</b> {len(hives)}",
f"<b>Total inspections:</b> {total_inspections}",
]
for line in summary_lines:
story.append(Paragraph(line, styles["BodyText"]))
story.append(Spacer(1, 0.3 * inch))
if not hives:
story.append(
Paragraph(
"No hives have been registered yet. Add one in the Hives tab "
"and inspect a frame to populate this report.",
styles["BodyText"],
)
)
doc.build(story)
return buf.getvalue()
for idx, h in enumerate(hives):
story.append(Paragraph(h["name"], styles["ApiaristH2"]))
meta_bits = []
if h.get("location"):
meta_bits.append(f"Location: {h['location']}")
if h.get("queen_marker"):
meta_bits.append(f"Queen marker: {h['queen_marker']}")
meta_bits.append(f"Inspections: {h.get('inspection_count') or 0}")
meta_bits.append(f"Last inspected: {_date(h.get('last_inspected'))}")
story.append(Paragraph(" • ".join(meta_bits), styles["ApiaristSmall"]))
story.append(Spacer(1, 0.1 * inch))
inspections = db.get_inspections_for_hive(h["id"], limit=20)
if inspections:
story.append(_hive_table(inspections))
else:
story.append(
Paragraph("<i>No inspections recorded.</i>", styles["BodyText"])
)
if idx != len(hives) - 1:
story.append(PageBreak())
doc.build(story)
return buf.getvalue()
def save_report(path: Path) -> Path:
pdf_bytes = generate_report()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(pdf_bytes)
return path