Skip to main content
COSMICBYTEZLABS
NewsSecurityHOWTOsToolsStudyTraining
ProjectsChecklistsAI RankingsNewsletterStatusTagsAbout
Subscribe

Press Enter to search or Esc to close

News
Security
HOWTOs
Tools
Study
Training
Projects
Checklists
AI Rankings
Newsletter
Status
Tags
About
RSS Feed
Reading List
Subscribe

Stay in the Loop

Get the latest security alerts, tutorials, and tech insights delivered to your inbox.

Subscribe NowFree forever. No spam.
COSMICBYTEZLABS

Your trusted source for IT intelligence, cybersecurity insights, and hands-on technical guides.

980+ Articles
124+ Guides

CONTENT

  • Latest News
  • Security Alerts
  • HOWTOs
  • Projects
  • Exam Prep

RESOURCES

  • Search
  • Browse Tags
  • Newsletter Archive
  • Reading List
  • RSS Feed

COMPANY

  • About Us
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CosmicBytez Labs. All rights reserved.

System Status: Operational
  1. Home
  2. HOWTOs
  3. Automating Report Generation with Python and Jinja2
Automating Report Generation with Python and Jinja2
HOWTOIntermediate

Automating Report Generation with Python and Jinja2

Build an automated report generation system using Python, Jinja2 templates, and data extraction from multiple sources. Covers multi-tenant data...

Dylan H.

Software Engineering

February 7, 2026
5 min read

Prerequisites

  • Python 3.10+
  • Basic HTML/CSS
  • Understanding of data formats (CSV, JSON, Excel)

Overview

Manual report generation is tedious and error-prone. This guide builds a system that extracts data from multiple sources, processes it through templates, and generates professional reports automatically. The pattern works for QBRs, security scorecards, compliance reports, and more.

Architecture

┌───────────────┐    ┌──────────────┐    ┌──────────────┐
│  Data Sources │    │  Processing  │    │   Output     │
│               │    │              │    │              │
│  CSV Files    │───►│  Extract     │───►│  HTML Report │
│  Excel        │    │  Transform   │    │  PDF Export  │
│  APIs         │    │  Template    │    │  Email       │
│  Databases    │    │              │    │              │
└───────────────┘    └──────────────┘    └──────────────┘

Requirements

pip install jinja2 pandas openpyxl weasyprint
PackagePurpose
jinja2HTML template rendering
pandasData extraction and transformation
openpyxlExcel file reading
weasyprintHTML to PDF conversion

Process

Step 1: Project Structure

report-generator/
├── templates/
│   ├── base.html        # Base layout
│   └── qbr.html         # QBR report template
├── data/
│   └── clients/         # Per-client data files
├── output/              # Generated reports
├── extractors.py        # Data extraction logic
├── generator.py         # Report generation engine
└── main.py              # Entry point

Step 2: Data Extraction

# extractors.py
import pandas as pd
from pathlib import Path
from typing import Any
 
class DataExtractor:
    """Extract and normalize data from various sources."""
 
    def __init__(self, data_dir: Path):
        self.data_dir = data_dir
 
    def extract_client_data(self, client_id: str) -> dict[str, Any]:
        """Extract all data for a specific client."""
        client_dir = self.data_dir / "clients" / client_id
 
        return {
            "client_info": self._read_client_info(client_dir),
            "tickets": self._read_tickets(client_dir),
            "security_score": self._calculate_security_score(client_dir),
            "recommendations": self._read_recommendations(client_dir),
        }
 
    def _read_client_info(self, path: Path) -> dict:
        df = pd.read_excel(path / "info.xlsx")
        return df.iloc[0].to_dict()
 
    def _read_tickets(self, path: Path) -> list[dict]:
        df = pd.read_csv(path / "tickets.csv")
        return df.to_dict(orient="records")
 
    def _calculate_security_score(self, path: Path) -> dict:
        df = pd.read_csv(path / "security-audit.csv")
        total = len(df)
        passed = len(df[df["status"] == "pass"])
        score = round((passed / total) * 100) if total > 0 else 0
 
        return {
            "score": score,
            "total_checks": total,
            "passed": passed,
            "failed": total - passed,
            "grade": self._score_to_grade(score),
        }
 
    @staticmethod
    def _score_to_grade(score: int) -> str:
        if score >= 90: return "A"
        if score >= 80: return "B"
        if score >= 70: return "C"
        if score >= 60: return "D"
        return "F"
 
    def _read_recommendations(self, path: Path) -> list[dict]:
        rec_file = path / "recommendations.json"
        if rec_file.exists():
            import json
            return json.loads(rec_file.read_text())
        return []

Step 3: Jinja2 Template

{# templates/qbr.html #}
<!DOCTYPE html>
<html>
<head>
  <style>
    body { font-family: 'Segoe UI', sans-serif; color: #1a1a2e; margin: 40px; }
    .header { background: linear-gradient(135deg, #0f3460, #16213e); color: white;
              padding: 30px; border-radius: 8px; margin-bottom: 30px; }
    .metric-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px;
                   margin: 20px 0; }
    .metric-card { background: #f8f9fa; border-radius: 8px; padding: 20px;
                   text-align: center; border-left: 4px solid #0f3460; }
    .metric-value { font-size: 2em; font-weight: bold; color: #0f3460; }
    .metric-label { color: #666; font-size: 0.9em; }
    table { width: 100%; border-collapse: collapse; margin: 20px 0; }
    th { background: #0f3460; color: white; padding: 12px; text-align: left; }
    td { padding: 10px 12px; border-bottom: 1px solid #e0e0e0; }
    tr:nth-child(even) { background: #f8f9fa; }
    .score-badge { display: inline-block; padding: 8px 16px; border-radius: 20px;
                   font-weight: bold; font-size: 1.5em; }
    .grade-A { background: #d4edda; color: #155724; }
    .grade-B { background: #cce5ff; color: #004085; }
    .grade-C { background: #fff3cd; color: #856404; }
    .grade-D, .grade-F { background: #f8d7da; color: #721c24; }
  </style>
</head>
<body>
  <div class="header">
    <h1>Quarterly Business Review</h1>
    <p>{{ client.name }} — {{ report_date }}</p>
  </div>
 
  <h2>Security Scorecard</h2>
  <div class="metric-grid">
    <div class="metric-card">
      <div class="metric-value">
        <span class="score-badge grade-{{ security.grade }}">
          {{ security.grade }}
        </span>
      </div>
      <div class="metric-label">Security Grade</div>
    </div>
    <div class="metric-card">
      <div class="metric-value">{{ security.score }}%</div>
      <div class="metric-label">Compliance Score</div>
    </div>
    <div class="metric-card">
      <div class="metric-value">{{ security.passed }}/{{ security.total_checks }}</div>
      <div class="metric-label">Checks Passed</div>
    </div>
  </div>
 
  <h2>Support Summary</h2>
  <div class="metric-grid">
    <div class="metric-card">
      <div class="metric-value">{{ tickets | length }}</div>
      <div class="metric-label">Total Tickets</div>
    </div>
    <div class="metric-card">
      <div class="metric-value">
        {{ tickets | selectattr("priority", "equalto", "high") | list | length }}
      </div>
      <div class="metric-label">High Priority</div>
    </div>
    <div class="metric-card">
      <div class="metric-value">
        {{ tickets | selectattr("status", "equalto", "resolved") | list | length }}
      </div>
      <div class="metric-label">Resolved</div>
    </div>
  </div>
 
  {% if recommendations %}
  <h2>Recommendations</h2>
  <table>
    <tr><th>Priority</th><th>Recommendation</th><th>Impact</th></tr>
    {% for rec in recommendations %}
    <tr>
      <td>{{ rec.priority }}</td>
      <td>{{ rec.description }}</td>
      <td>{{ rec.impact }}</td>
    </tr>
    {% endfor %}
  </table>
  {% endif %}
</body>
</html>

Step 4: Report Generator

# generator.py
from pathlib import Path
from datetime import datetime
from jinja2 import Environment, FileSystemLoader
from weasyprint import HTML
 
class ReportGenerator:
    def __init__(self, template_dir: Path, output_dir: Path):
        self.env = Environment(loader=FileSystemLoader(template_dir))
        self.output_dir = output_dir
        self.output_dir.mkdir(parents=True, exist_ok=True)
 
    def generate(self, template_name: str, data: dict, filename: str) -> Path:
        """Render template with data and export to PDF."""
        template = self.env.get_template(template_name)
 
        # Add common variables
        data["report_date"] = datetime.now().strftime("%B %d, %Y")
        data["generated_at"] = datetime.now().isoformat()
 
        # Render HTML
        html_content = template.render(**data)
 
        # Save HTML (useful for debugging)
        html_path = self.output_dir / f"{filename}.html"
        html_path.write_text(html_content)
 
        # Convert to PDF
        pdf_path = self.output_dir / f"{filename}.pdf"
        HTML(string=html_content).write_pdf(str(pdf_path))
 
        return pdf_path

Step 5: Batch Processing

# main.py
from pathlib import Path
from extractors import DataExtractor
from generator import ReportGenerator
 
def generate_all_reports():
    data_dir = Path("data")
    extractor = DataExtractor(data_dir)
    generator = ReportGenerator(Path("templates"), Path("output"))
 
    # Process each client
    clients_dir = data_dir / "clients"
    for client_dir in clients_dir.iterdir():
        if not client_dir.is_dir():
            continue
 
        client_id = client_dir.name
        print(f"Generating report for {client_id}...")
 
        data = extractor.extract_client_data(client_id)
        pdf = generator.generate(
            template_name="qbr.html",
            data={
                "client": data["client_info"],
                "tickets": data["tickets"],
                "security": data["security_score"],
                "recommendations": data["recommendations"],
            },
            filename=f"QBR-{client_id}-{datetime.now().strftime('%Y-%m')}"
        )
        print(f"  -> {pdf}")
 
if __name__ == "__main__":
    from datetime import datetime
    generate_all_reports()

Key Takeaways

  • Separate data extraction from presentation — makes templates reusable
  • Jinja2 filters (selectattr, length) handle data aggregation in templates
  • WeasyPrint produces print-quality PDFs from HTML/CSS
  • Process clients in a loop for multi-tenant batch generation
  • Store HTML alongside PDF for debugging template issues

Related Reading

  • Tech & Security News Roundup
  • Building Event-Driven Systems with Python asyncio
  • Python for Security Automation: Essential Scripting
#Python#automation#Jinja2#Reporting#Data Processing

Related Articles

Automated News Aggregation with Deduplication Algorithms

Build a news aggregation system that fetches from multiple RSS feeds, detects duplicate articles using Jaccard similarity, and generates clean...

6 min read

How to Configure Microsoft Sentinel Analytics Rules

End-to-end SOC guide for Microsoft Sentinel: build KQL-based scheduled and NRT analytics rules, wire automation rules for incident triage, and deploy...

15 min read

SentinelOne Health Check: Agent Status Monitoring and

Organizations deploying SentinelOne endpoint protection require continuous monitoring of agent health to ensure comprehensive threat coverage across their...

17 min read
Back to all HOWTOs