Loading
Data Automation • 5 min read

Turn Any Python Script into an Automated PDF Report — Emailed on Schedule (with WeasyPrint)

By Ken Muriithi • Published on September 23, 2026
CyberOak Deployment Blueprint Ready

You can load this entire automation pipeline script directly into your runtime console environment.

Deploy to Workspace

Turn Any Python Script into an Automated PDF Report — Emailed on Schedule

Most reporting workflows end the same way: someone runs a query, formats the output, and emails it to a stakeholder. It's a five-minute task that somehow always happens at the worst possible time — right before a meeting, right after someone's already asked twice.

This tutorial shows how to generate a clean PDF report from your data using WeasyPrint, then email it automatically with smtplib. Then you'll see how to schedule the whole thing on CyberOak so it just shows up in the right inbox, on time, every time.


What This Automation Does

The script takes tabular data, renders it into a styled HTML report, converts that HTML to a PDF with WeasyPrint, and emails the PDF as an attachment.

For every run, it:

  • Builds an HTML report from your data (a simple table, but easy to style further with real CSS)
  • Converts the HTML to a pixel-accurate PDF using WeasyPrint
  • Sends the PDF as an email attachment via smtplib
  • Logs success or failure, so you know if the report actually went out

Why WeasyPrint?

WeasyPrint renders HTML and CSS using an actual browser-grade layout engine, which means it handles modern CSS — flexbox, grid, @page rules, custom fonts, page headers/footers — far more faithfully than older HTML-to-PDF converters. If you want a report that actually looks designed rather than like a plain table dump, WeasyPrint gives you much more control over the final output.


Why Build This?

A report that sits in a dashboard only helps the people who remember to check the dashboard. A report that lands in someone's inbox gets read.

This pattern covers a lot of common recurring needs:

  • Weekly sales or KPI summaries to a manager
  • Daily error/exception digests to an engineering team
  • Monthly finance close reports to stakeholders
  • Client-facing status reports for an agency

The report generation and the delivery are both simple — the part people actually skip is doing it reliably, on schedule, without a human in the loop.


Why Run It on CyberOak?

Sending email from a script usually means hardcoding SMTP credentials somewhere you'd rather not, and remembering to actually trigger the script.

CyberOak removes both problems:

  • Upload the script — WeasyPrint and smtplib run out of the box
  • Store your SMTP host, port, username, password, and recipient as Key-Value Parameters — encrypted at rest, injected only at runtime, never hardcoded
  • Schedule it — daily, weekly, monthly, or anything from every 10 minutes to once a year
  • Get alerted — a dashboard and email alert if the automation itself fails, separate from the report email it sends
  • Review logs and outputs per run — if a report didn't go out, you'll know exactly why

How the Script Works

  1. Reads SMTP and recipient details from CyberOak's Key-Value Parameters.
  2. Builds a styled HTML report from the report data.
  3. Converts that HTML into a PDF file using WeasyPrint.
  4. Attaches the PDF to an email message.
  5. Sends it via smtplib over a secure connection.
  6. Logs the outcome — success, or the specific error if something failed.

Output Example

Each run produces a styled PDF like this, attached directly to an email:

| Metric | Value | |--------|-------| | New Signups | 128 | | Active Trials | 342 | | Churned Accounts | 7 |

Swap the data source for a database query, an API call, or a CSV upload, and the same script becomes almost any recurring report.


Customizing the Report

This script is intentionally simple so it's easy to extend:

  • Pull report_data from PostgreSQL, an API, or an uploaded Excel file instead of hardcoding it
  • Add a real logo, custom fonts, or a multi-page layout using WeasyPrint's @page and CSS support
  • Send to multiple recipients, or a different recipient per report type
  • Attach the raw CSV alongside the PDF for recipients who want the underlying data
  • Add conditional logic — only send the email if a metric crosses a threshold

Scheduling Your Report Emails

This is the step that turns a "nice script" into something your team actually relies on.

Schedule it on CyberOak to run:

  • Every Monday morning, before your team's weekly sync
  • Daily, for an overnight error digest
  • Monthly, for a recurring stakeholder update
  • On-demand, whenever someone needs a fresh copy immediately

If the SMTP connection fails or the PDF generation breaks, you'll get a CyberOak dashboard and email alert about the automation — separate from the report it's supposed to send — so a silent failure never happens.


Who Can Use This?

This automation fits anyone who owns a recurring "send this report" task:

  • Founders and managers who want a weekly metrics digest without asking for one
  • Ops and finance teams sending recurring stakeholder updates
  • Agencies sending client status reports
  • Engineering teams wanting a daily error/exception summary
  • Anyone tired of manually attaching a file to an email every week

Conclusion

Generating and emailing a report doesn't need a reporting platform or a scheduled task on someone's laptop. WeasyPrint handles the PDF with real CSS control, smtplib handles the delivery, and together they cover most "send this report" needs in under 100 lines.

Upload it to CyberOak, store your SMTP credentials once, set a schedule, and the report starts showing up in the right inbox — on its own, from now on.

preview.py
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.base import MIMEBase
from email.mime.text import MIMEText
from email import encoders
from weasyprint import HTML

# Email and report parameters are supplied by CyberOak's
# Key-Value Parameters section.
SMTP_HOST = SMTP_HOST
SMTP_PORT = SMTP_PORT
SMTP_USER = SMTP_USER
SMTP_PASSWORD = SMTP_PASSWORD
REPORT_RECIPIENT = REPORT_RECIPIENT


def build_html_report(rows: list[dict], title: str) -> str:
    table_rows = "".join(
        f"<tr><td>{r['name']}</td><td>{r['value']}</td></tr>"
        for r in rows
    )

    return f"""
    <html>
    <head>
        <style>
            @page {{
                size: A4;
                margin: 2cm;
                @bottom-center {{
                    content: "Generated automatically by CyberOak";
                    font-size: 9px;
                    color: #999;
                }}
            }}
            body {{ font-family: Helvetica, sans-serif; }}
            h1 {{ color: #212832; }}
            table {{ width: 100%; border-collapse: collapse; }}
            th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
            th {{ background-color: #00ba94; color: white; }}
        </style>
    </head>
    <body>
        <h1>{title}</h1>
        <table>
            <tr><th>Metric</th><th>Value</th></tr>
            {table_rows}
        </table>
    </body>
    </html>
    """


def html_to_pdf(html_content: str, output_path: str):
    HTML(string=html_content).write_pdf(output_path)


def send_email_with_attachment(
    subject: str,
    body: str,
    attachment_path: str,
    recipient: str,
):
    message = MIMEMultipart()
    message["From"] = SMTP_USER
    message["To"] = recipient
    message["Subject"] = subject
    message.attach(MIMEText(body, "plain"))

    with open(attachment_path, "rb") as f:
        part = MIMEBase("application", "octet-stream")
        part.set_payload(f.read())
        encoders.encode_base64(part)
        part.add_header(
            "Content-Disposition",
            f"attachment; filename={attachment_path}",
        )
        message.attach(part)

    print("Connecting to SMTP server...")
    with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server:
        server.starttls()
        server.login(SMTP_USER, SMTP_PASSWORD)
        server.sendmail(SMTP_USER, recipient, message.as_string())

    print(f"Report emailed to {recipient}")


# Example usage
if __name__ == "__main__":
    report_data = [
        {"name": "New Signups", "value": 128},
        {"name": "Active Trials", "value": 342},
        {"name": "Churned Accounts", "value": 7},
    ]

    html = build_html_report(report_data, "Weekly Growth Report")
    pdf_path = "weekly_growth_report.pdf"

    print("Generating PDF report...")
    html_to_pdf(html, pdf_path)

    send_email_with_attachment(
        subject="Weekly Growth Report",
        body="Attached is this week's automated growth report.",
        attachment_path=pdf_path,
        recipient=REPORT_RECIPIENT,
    )
Related Articles

No other articles are categorized under this module layout yet.

Need Custom Orchestration?
Explore Runtime Documentation