fx Capstone
ENहिन्दी

Capstone: end-to-end pipeline — CSV फ़ोल्डर → साफ़ करना → जोड़ना → फ़ॉर्मेटेड xlsx → ईमेल → शेड्यूल

⏱ 40 min

आप क्या सीखेंगे

  • इस काम के लिए Python क्यों?
  • प्रोजेक्ट की बनावट
  • इनपुट का नियम

समझिए

 inbox/*.csv ─► load ─► clean ─► aggregate ─► formatted .xlsx ─► email ─► archive
                         │                                          │
                         └─► Rejected Rows sheet                    └─► logs/pipeline.log
                                    (runs daily via Task Scheduler / cron)

यह मॉड्यूल 1 (टूल चुनना), मॉड्यूल 5 (pandas + openpyxl) और मॉड्यूल 6 (ईमेल + शेड्यूलिंग) को एक साथ लाता है।

1. इस काम के लिए Python क्यों?

मॉड्यूल 1 का डिसीज़न ट्री चलाएँ: बहुत-सी फ़ाइलें, बिना किसी के Excel खोले चलना है, फ़ाइल ईमेल करनी है, लॉग चाहिए → Python। Power Query CSV जोड़ सकता है, पर ईमेल नहीं कर सकता और बिना निगरानी नहीं चल सकता; VBA को Excel खुला और लॉग-इन यूज़र चाहिए।

2. प्रोजेक्ट की बनावट

sales-pipeline/
    .venv/                      ← virtual environment
    inbox/                      ← branches drop CSVs here
    reports/                    ← generated reports
    archive/                    ← processed CSVs, one folder per run
    logs/pipeline.log           ← created automatically
    monthly_report.py           ← from Python for Excel, Lesson 6 (load, clean, summarise, write, format)
    send_mail.py                ← from Scheduling & CI, Lesson 2
    pipeline.py                 ← the orchestrator (this lesson)
    run_pipeline.bat            ← Windows launcher for Task Scheduler
    requirements.txt            ← pandas, openpyxl

सेटअप:

cd sales-pipeline
python -m venv .venv
# Windows: .venv\Scripts\activate     Mac/Linux: source .venv/bin/activate
pip install pandas openpyxl
mkdir inbox reports archive

3. इनपुट का नियम

हर CSV में ये कॉलम होने चाहिए: Date, Salesperson, Region, Product, Amount, और तारीख़ें dd/mm/yyyy में। इसे लिखकर ब्रांच के साथ शेयर करें — ज़्यादातर पाइपलाइन फ़ेलियर इनपुट के अचानक बदलाव से होते हैं। सफ़ाई वाला स्टेप स्पेस हटाता है, कैपिटलाइज़ेशन ठीक करता है, डुप्लिकेट हटाता है और गलत तारीख़/रकम को फ़ेल होने की जगह Rejected Rows शीट में भेजता है।

4. ऑर्केस्ट्रेटर: pipeline.py

monthly_report.py और send_mail.py काम करते हैं; pipeline.py स्टेप क्रम से चलाता है, चार्ट जोड़ता है, तय करता है कि ईमेल करना है या नहीं, इनपुट archive करता है और सब लॉग करता है।

"""
pipeline.py — ExcelWalaa Automation capstone
CSV folder -> clean -> aggregate -> formatted xlsx -> email -> archive (run it on a schedule)

Usage:
    python pipeline.py --inbox inbox --outdir reports --archive archive --to owner@example.com
    python pipeline.py --inbox inbox --outdir reports --archive archive --no-email   # for testing
"""
import argparse
import logging
import os
import shutil
import sys
from datetime import datetime
from pathlib import Path
from uuid import uuid4

from openpyxl import load_workbook
from openpyxl.chart import BarChart, Reference

from monthly_report import load_data, clean, summarise, write_report, format_report
from send_mail import send_file

log = logging.getLogger("pipeline")


def setup_logging(log_dir: Path) -> None:
    log_dir.mkdir(parents=True, exist_ok=True)
    fmt = "%(asctime)s | %(levelname)-7s | %(message)s"
    logging.basicConfig(level=logging.INFO, format=fmt,
                        handlers=[logging.FileHandler(log_dir / "pipeline.log", encoding="utf-8"),
                                  logging.StreamHandler(sys.stdout)])


def add_region_chart(path: Path) -> None:
    wb = load_workbook(path)
    ws = wb["By Region"]
    last = ws.max_row
    chart = BarChart()
    chart.title = "Sales by Region"
    chart.add_data(Reference(ws, min_col=2, min_row=1, max_row=last), titles_from_data=True)
    chart.set_categories(Reference(ws, min_col=1, min_row=2, max_row=last))
    chart.width, chart.height = 16, 8
    ws.add_chart(chart, "D2")
    wb.save(path)


def archive_files(files: list[Path], archive_root: Path, stamp: str) -> Path:
    target = archive_root / stamp
    target.mkdir(parents=True, exist_ok=False)
    for f in files:
        shutil.move(str(f), target / f.name)
    return target


def run(args: argparse.Namespace) -> int:
    inbox, outdir, archive = Path(args.inbox), Path(args.outdir), Path(args.archive)
    stamp = datetime.now().strftime("%Y-%m-%d_%H%M%S_%f") + "_" + uuid4().hex[:8]
    if not inbox.is_dir():
        raise FileNotFoundError(f"Inbox does not exist: {inbox}")

    files = sorted(inbox.glob("*.csv"))
    if not files:
        log.info("Inbox %s is empty - nothing to do.", inbox)
        return 0
    log.info("Found %d CSV file(s): %s", len(files), ", ".join(f.name for f in files))

    if not args.no_email and not (args.to and os.environ.get("SMTP_USER") and os.environ.get("SMTP_PASSWORD")):
        raise ValueError("Email requires --to, SMTP_USER and SMTP_PASSWORD; use --no-email for a local run")

    # Process exactly the snapshot that will be archived; later arrivals wait for the next run.
    raw = load_data(inbox, files=files)
    good, rejected = clean(raw)
    summaries = summarise(good)
    log.info("Rows read: %d | clean: %d | rejected: %d", len(raw), len(good), len(rejected))
    if good.empty:
        log.error("No valid rows - report not created. Check the Rejected rows in the source files.")
        return 1

    # 4: formatted workbook
    outdir.mkdir(parents=True, exist_ok=True)
    report = outdir / f"Sales_Report_{stamp}.xlsx"
    write_report(good, rejected, summaries, report)
    format_report(report)
    add_region_chart(report)
    log.info("Report saved: %s", report.resolve())

    # 5: email
    if args.no_email:
        log.info("Email skipped (--no-email).")
    else:
        total = good["Amount"].sum()
        body = (f"Hello,\n\nAttached is the sales report ({stamp}).\n"
                f"Clean rows: {len(good)} | Rejected rows: {len(rejected)}\n"
                f"Total sales: Rs {total:,.0f}\n\nThis email was sent automatically.")
        send_file(report, args.to, f"Sales report {stamp}", body)
        log.info("Email sent to %s", args.to)

    # 6: archive processed inputs so the next run starts clean
    moved_to = archive_files(files, archive, stamp)
    log.info("Inputs archived to %s", moved_to)
    return 0


def main() -> None:
    p = argparse.ArgumentParser(description="CSV -> Excel report -> email pipeline")
    p.add_argument("--inbox", default="inbox")
    p.add_argument("--outdir", default="reports")
    p.add_argument("--archive", default="archive")
    p.add_argument("--to", default=os.environ.get("REPORT_TO"))
    p.add_argument("--no-email", action="store_true")
    args = p.parse_args()

    setup_logging(Path("logs"))
    try:
        sys.exit(run(args))
    except Exception:
        log.exception("Pipeline failed")
        sys.exit(1)


if __name__ == "__main__":
    main()

5. डिज़ाइन के फ़ैसले (और क्यों)

फ़ैसला क्यों
import से फ़ंक्शन दोबारा इस्तेमाल एक ही जाँचा हुआ सफ़ाई/फ़ॉर्मेटिंग लॉजिक; बग एक जगह ठीक करें
खाली inbox → लॉग लाइन के साथ exit 0 शांत दिन कोई एरर नहीं है
कोई सही रो नहीं → exit 1 टूटा इनपुट ज़ोर से फ़ेल होना चाहिए
टाइमस्टैम्प वाले रिपोर्ट और archive नाम कभी ओवरराइट नहीं; किसी भी दिन का रन ढूँढना आसान
सफल होने के बाद ही archive रन फ़ेल हो तो फ़ाइलें दोबारा चलाने के लिए inbox में रहती हैं
ईमेल सेटिंग environment से कोड में कोई पासवर्ड नहीं
--no-email फ़्लैग सुरक्षित टेस्टिंग
try/except + log.exception लॉग में एरर का पूरा विवरण, शेड्यूलर के लिए non-zero exit code

6. कदम-दर-कदम जाँचें

a) बिना ईमेल ड्राई रन। मॉड्यूल 5 लेसन 6 की तीनों सैंपल CSV inbox/ में कॉपी करें और चलाएँ:

python pipeline.py --no-email

अपेक्षित लॉग:

INFO    | Found 3 CSV file(s): north.csv, south.csv, west.csv
INFO    | Rows read: 10 | clean: 7 | rejected: 2
INFO    | Report saved: .../reports/Sales_Report_2026-10-01_0900.xlsx
INFO    | Email skipped (--no-email).
INFO    | Inputs archived to archive/2026-10-01_0900

रिपोर्ट खोलें: By Region में बार चार्ट के साथ South 96,000, North 92,000, West 61,000 दिखना चाहिए; Rejected Rows में "bad date" वाली रो और "NA" रकम वाली रो दिखनी चाहिए।

b) तुरंत दोबारा चलाएँ। अब inbox खाली है → "nothing to do", exit code 0।

c) जानबूझकर तोड़ें। inbox में सिर्फ़ गलत रो वाली CSV रखें → "No valid rows", exit code 1, फ़ाइल inbox में ही रहती है।

d) ईमेल जाँचें। environment variables सेट करें (Gmail App Password इस्तेमाल करें) और अपने आप को भेजें:

# Windows (PowerShell)
$env:SMTP_USER="reports@yourshop.com"; $env:SMTP_PASSWORD="app-password"
python pipeline.py --to you@example.com
# Mac/Linux
export SMTP_USER=reports@yourshop.com SMTP_PASSWORD=app-password
python pipeline.py --to you@example.com

7. शेड्यूल करें

Windows — run_pipeline.bat:

@echo off
cd /d "%~dp0"
if not exist logs mkdir logs
rem Set SMTP_USER, SMTP_PASSWORD and REPORT_TO in the scheduler account environment.
".venv\Scripts\python.exe" pipeline.py %* >> logs\scheduler.log 2>&1
exit /b %errorlevel%

SMTP_PASSWORD को .bat फ़ाइल की जगह user environment variable के रूप में रखें (Start → "Edit environment variables for your account")। फिर Task Scheduler टास्क बनाएँ (मॉड्यूल 6 लेसन 1): रोज़ सुबह 9:00, action = .bat, Start in = प्रोजेक्ट फ़ोल्डर, "Run task as soon as possible after a scheduled start is missed"।

Mac/Linux — cron:

0 9 * * 1-6 cd /home/divyesh/sales-pipeline && SMTP_USER=reports@yourshop.com SMTP_PASSWORD='app-password' REPORT_TO=owner@yourshop.com .venv/bin/python pipeline.py >> logs/scheduler.log 2>&1

(crontab आपके यूज़र अकाउंट तक निजी है, पर सिर्फ़ आपके पढ़ने लायक पासवर्ड फ़ाइल और भी बेहतर है।)

GitHub Actions? रिपोर्ट और ईमेल के लिए चलता है, पर GitHub की मशीन पर archive/ में भेजी फ़ाइलें रन के बाद गायब हो जाती हैं। इसे तब इस्तेमाल करें जब इनपुट डाउनलोड स्टेप (API, क्लाउड स्टोरेज) से आएँ, और archive स्टेप छोड़ दें — या archive को वापस repo में commit करें।

8. आख़िरी चेकलिस्ट

  • बिल्कुल उसी शेड्यूल कमांड से नए टर्मिनल में चलता है
  • शेड्यूलर में venv का Python और पूरे पाथ
  • खाली inbox → exit 0; गलत डेटा → Rejected Rows; सब गलत → exit 1
  • रिपोर्ट में 5 शीट, फ़ॉर्मेटेड हेडर, नंबर/तारीख़ फ़ॉर्मेट और एक चार्ट
  • अटैचमेंट और सही टोटल के साथ ईमेल पहुँचता है
  • प्रोसेस हुई CSV archive/<timestamp>/ में जाती हैं
  • logs/pipeline.log हर रन समझाता है
  • किसी कोड फ़ाइल में पासवर्ड नहीं
  • एक पेज की README: क्या करता है, इनपुट फ़ॉर्मेट, कैसे चलाएँ, किसे फ़ोन करें

9. आगे बढ़ाने के आइडिया (एक-दो चुनें)

  • फ़ेलियर अलर्ट: main() के except ब्लॉक में लॉग का आख़िरी हिस्सा खुद को ईमेल करें।
  • महीने-दर-महीने: पिछले महीने की रिपोर्ट लोड करें और By Region में % बदलाव कॉलम जोड़ें।
  • Config फ़ाइल: पाथ, प्राप्तकर्ता और reorder सीमा config.json में ले जाएँ।
  • और आउटपुट: हर रीजन की एक फ़ाइल (मॉड्यूल 5 लेसन 3), हर रीजनल मैनेजर के ईमेल में अटैच।
  • Google Sheets स्रोत: CSV की जगह Sheets API से डेटा लाएँ — या उल्टा, पूरा फ़्लो Apps Script (मॉड्यूल 4) में करें।
  • टेस्ट: एक छोटी pytest फ़ाइल जो clean() को गड़बड़ रो देकर नतीजा जाँचे।

आम गलतियाँ

सिर्फ़ परफ़ेक्ट डेटा से जाँचना। रिपोर्ट सुरक्षित लिखे जाने से पहले archive करना। महीना या प्राप्तकर्ता कोड में फ़िक्स लिखना। हाथ से चलाना भरोसेमंद होने से पहले शेड्यूल करना। README न होना, जिससे कोई और इसे नहीं चला पाता।

अभ्यास

mediumअपने (या सैंपल) डेटा से पाइपलाइन बनाएँ, सेक्शन 6 के चारों टेस्ट चलाएँ, कल सुबह 9 बजे के लिए शेड्यूल करें, और अगली सुबह लॉग, रिपोर्ट, ईमेल और archive जाँचें। फिर एक एक्सटेंशन जोड़ें और README लिखें।
sample-data को inbox में copy करके pipeline.py --no-email चलाएँ। कुल 249000, 7 clean, 2 मूल rejected rows, 1 हटाया duplicate और region chart मिलें; inputs अलग archive folder में जाएँ। अगला खाली run exit 0 दे। All-bad input, missing SMTP या simulated send failure पर inputs inbox में रहें। जाँच के बाद recipient और schedule सेट करें। README में paths, schema, secret names, recovery और duplicate key लिखें।

प्रश्नोत्तरी

रिपोर्ट और ईमेल सफल होने के बाद ही इनपुट archive क्यों?
ताकि फ़ेल रन उन्हीं फ़ाइलों से दोबारा चल सके
खाली inbox पर क्या लौटना चाहिए?
"nothing to do" लॉग लाइन के साथ exit code 0
SMTP पासवर्ड कहाँ रहना चाहिए?
environment variable या secret store में — कभी कोड में नहीं
Capstone: end-to-end pipeline — CSV फ़ोल्डर → साफ़ करना → जोड़ना → फ़ॉर्मेटेड xlsx → ईमेल → शेड्यूल · हिंदी | ExcelWalaa