""" 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"{h}", 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"Total hives: {len(hives)}", f"Total inspections: {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("No inspections recorded.", 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