Featured image of post DuckDB End-to-End Automated Report Pipeline: From Data Source to Email Delivery

DuckDB End-to-End Automated Report Pipeline: From Data Source to Email Delivery

Build a complete DuckDB + Python automated reporting pipeline from scratch. Covers batch CSV ingestion, SQL aggregation, formatted Excel output, scheduled execution, and email delivery.

DuckDB End-to-End Automated Report Pipeline: From Data Source to Email Delivery

Toolchain | For: Data analysts who manually compile reports every week


The Pain Point

You spend 3 hours every Monday morning manually compiling Excel reports — repetitive, boring, and error-prone. With a DuckDB-based automated reporting system, this workflow compresses to 5 minutes, or even runs fully autonomously.

This article walks you through building a complete DuckDB + Python automated report pipeline, covering data ingestion, analytical queries, formatted output, scheduled execution, and email delivery.


System Architecture Overview

Data Sources (Excel/CSV/SQL)
    ↓
DuckDB (Unified Query Layer)
    ↓
Python Processing (Cleaning/Aggregation)
    ↓
Output (Excel/PDF/Email Delivery)

Key advantages: DuckDB reads Excel and CSV files natively without intermediate conversion; SQL表达能力 strong for complex aggregations in a single query; combined with Python’s ecosystem, it can string together an entire workflow.


Step 1: Environment Setup

pip install duckdb openpyxl pandas

Verify installation:

import duckdb
print(duckdb.__version__)  # Should print the version number

Step 2: Connect to Data Sources

Assume you have an e-commerce sales data folder sales_data/ containing multiple CSV files, each representing one month of sales records.

import duckdb
import pandas as pd
from pathlib import Path

# Connect to a persistent DuckDB database (data saved to disk)
con = duckdb.connect('sales_automation.duckdb')

# Batch-read all CSV files (auto-merge, no need to read individually)
con.execute("""
    CREATE TABLE all_sales AS
    SELECT * FROM read_csv_auto('sales_data/*.csv', hive_partitioning=true)
""")

# Preview the data structure
result = con.execute("DESCRIBE all_sales").fetchdf()
print(result)

Key techniques:

  • read_csv_auto automatically infers column types — no need to manually specify schemas
  • hive_partitioning=true supports automatic partitioning by directory structure; dates in filenames are extracted as partition columns
  • DuckDB’s columnar storage makes aggregation queries 10x faster than Pandas

Step 3: Core Analytical Queries

3.1 Monthly Sales Summary

monthly_summary = con.execute("""
    SELECT 
        DATE_TRUNC('month', sale_date) AS month,
        SUM(revenue) AS total_revenue,
        COUNT(DISTINCT customer_id) AS active_customers,
        AVG(order_value) AS avg_order_value,
        SUM(CASE WHEN region = 'East' THEN revenue ELSE 0 END) AS east_region_revenue
    FROM all_sales
    GROUP BY DATE_TRUNC('month', sale_date)
    ORDER BY month DESC
""").fetchdf()

print(monthly_summary)

3.2 Top Products Ranking (with window functions)

top_products = con.execute("""
    SELECT 
        product_name,
        category,
        SUM(quantity) AS total_qty,
        SUM(revenue) AS total_revenue,
        RANK() OVER (ORDER BY SUM(revenue) DESC) AS revenue_rank
    FROM all_sales
    GROUP BY product_name, category
    HAVING SUM(revenue) > 1000
    ORDER BY total_revenue DESC
    LIMIT 20
""").fetchdf()

3.3 Customer Segmentation

customer_segment = con.execute("""
    SELECT 
        customer_id,
        COUNT(*) AS order_count,
        SUM(revenue) AS total_spend,
        MAX(sale_date) AS last_purchase,
        CASE 
            WHEN SUM(revenue) > 10000 THEN 'VIP'
            WHEN SUM(revenue) > 5000 THEN 'High-Value'
            WHEN SUM(revenue) > 1000 THEN 'Regular'
            ELSE 'Potential'
        END AS segment
    FROM all_sales
    GROUP BY customer_id
    ORDER BY total_spend DESC
""").fetchdf()

Step 4: Generate Formatted Reports

4.1 Excel Format (Styled)

import openpyxl
from openpyxl.styles import Font, PatternFill, Alignment

def generate_excel_report(df_dict, output_path='report.xlsx'):
    with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
        for sheet_name, df in df_dict.items():
            df.to_excel(writer, sheet_name=sheet_name, index=False)
            
            # Style the header row
            ws = writer.sheets[sheet_name]
            header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
            header_font = Font(bold=True, color='FFFFFF')
            
            for cell in ws[1]:
                cell.fill = header_fill
                cell.font = header_font
                cell.alignment = Alignment(horizontal='center')
            
            # Auto-fit column widths
            for column in ws.columns:
                max_length = 0
                column_letter = column[0].column_letter
                for cell in column:
                    try:
                        if len(str(cell.value)) > max_length:
                            max_length = len(str(cell.value))
                    except:
                        pass
                adjusted_width = min(max_length + 2, 50)
                ws.column_dimensions[column_letter].width = adjusted_width
    
    print(f'Report generated: {output_path}')

# Generate multi-sheet report
report_data = {
    'Monthly Summary': monthly_summary,
    'Top Products': top_products,
    'Customer Segments': customer_segment
}

generate_excel_report(report_data, 'weekly_report.xlsx')

4.2 Plain Text Format (Minimal)

def generate_text_report(df_dict, output_path='report.txt'):
    with open(output_path, 'w', encoding='utf-8') as f:
        f.write('=' * 60 + '\n')
        f.write('Weekly Data Report\n')
        f.write('=' * 60 + '\n\n')
        
        for sheet_name, df in df_dict.items():
            f.write(f'\n[{sheet_name}]\n')
            f.write(df.to_string(index=False))
            f.write('\n' + '-' * 60 + '\n')
    
    print(f'Text report generated: {output_path}')

Step 5: Scheduled Execution (Three Options)

Option A: Python schedule library (local execution)

import schedule
import time
from datetime import datetime

def run_weekly_report():
    print(f'{datetime.now()} - Starting weekly report...')
    
    # Reload latest data
    con.execute("DROP TABLE IF EXISTS all_sales")
    con.execute("""
        CREATE TABLE all_sales AS
        SELECT * FROM read_csv_auto('sales_data/*.csv', hive_partitioning=true)
    """)
    
    # Execute analysis queries
    monthly_summary = con.execute("""
        SELECT DATE_TRUNC('month', sale_date) AS month,
               SUM(revenue) AS total_revenue, COUNT(DISTINCT customer_id) AS active_customers
        FROM all_sales GROUP BY DATE_TRUNC('month', sale_date)
        ORDER BY month DESC
    """).fetchdf()
    
    # Output report
    report_data = {'Monthly Summary': monthly_summary}
    generate_excel_report(report_data, f'report_{datetime.now().strftime("%Y%m%d")}.xlsx')
    print('Weekly report generated!')

# Run every Monday at 9:00 AM
schedule.every().monday.at("09:00").do(run_weekly_report)

while True:
    schedule.run_pending()
    time.sleep(60)

Option B: System Cron (Linux/Mac)

crontab -e

Add this line (runs every Monday at 9:00 AM):

0 9 * * 1 cd /path/to/project && python3 weekly_report.py

Option C: GitHub Actions (cloud-based, free)

Create .github/workflows/report.yml:

name: Weekly Report
on:
  schedule:
    - cron: '0 9 * * 1'  # Monday 9:00 UTC
  workflow_dispatch:

jobs:
  build-report:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      - name: Install dependencies
        run: pip install duckdb openpyxl pandas
      - name: Generate report
        run: python3 weekly_report.py
      - name: Upload artifact
        uses: actions/upload-artifact@v3
        with:
          name: weekly-report
          path: report_*.xlsx

Step 6: Email Delivery (Optional)

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.base import MIMEBase
from email import encoders

def send_email_report(to_email, report_path, subject='Weekly Report'):
    msg = MIMEMultipart()
    msg['From'] = '[email protected]'
    msg['To'] = to_email
    msg['Subject'] = subject
    
    body = 'Please find this week\'s data report attached.'
    msg.attach(MIMEText(body, 'plain', 'utf-8'))
    
    with open(report_path, 'rb') as f:
        attachment = MIMEBase('application', 'octet-stream')
        attachment.set_payload(f.read())
        encoders.encode_base64(attachment)
        attachment.add_header('Content-Disposition', f'attachment; filename={report_path}')
        msg.attach(attachment)
    
    with smtplib.SMTP('smtp.example.com', 587) as server:
        server.starttls()
        server.login('[email protected]', 'your_password')
        server.send_message(msg)
    
    print(f'Email sent to {to_email}')

Comparison with Traditional Approaches

DimensionTraditional Pandas ApproachDuckDB + Python Approach
Multi-file readingMust read_csv + concat one by oneread_csv_auto('*.csv') in one line
Memory usageAll data loaded into memoryColumnar storage, compute-on-demand
Query performanceNoticeably slow above 1M rowsSub-second response for millions of rows
File supportRequires additional librariesNative support for CSV/Parquet/JSON/Excel/SQLite/PostgreSQL
Deployment complexityDependencies on Pandas + multiple libsSingle DuckDB dependency

Monetization Suggestions

The value of this automated reporting pipeline goes far beyond saving time:

  1. SaaS Product: Package the reporting capability as an API service, charge per call — monthly income from thousands to tens of thousands of dollars
  2. Data Product: Provide industry data report subscription services for SMEs with annual billing for stable income
  3. Internal Tooling: Promote this system across your team, become the data infrastructure lead, and accelerate your career
  4. Consulting & Training: Convert your实战 experience into courses or consulting services, charging 500-5000 RMB per client

The core idea: transform one-off reporting work into reusable data products.


Learn more DuckDB实战 tips → duckdblab.org

📺 Watch video tutorials → Olap Studio YouTube

Subscribe for more DuckDB & AI automation tutorials

Built with Hugo
Theme Stack designed by Jimmy

⚠️ This site is an independent community project, not affiliated with, endorsed by, or sponsored by the DuckDB Foundation or official DuckDB project.

"DuckDB" is a registered trademark of the DuckDB Foundation. This site uses the name solely for factual description purposes.

All content is for educational and community promotion purposes only and does not constitute any commercial service.