""" main.py — Entry point for the Factors.AI account-export automation. Usage ───── python main.py # uses settings in config.py / .env python main.py --visible # opens a real browser window (debug) python main.py --dry-run # skip email; just download & compare Schedule (cron example — runs every day at 8 AM) ───────────────────────────────────────────────── 0 8 * * * /path/to/venv/bin/python /path/to/factors_automation/main.py """ import argparse import logging import shutil import sys from pathlib import Path # ── Logging (set up before any other import) ────────────────────────────────── from config import LOG_PATH # noqa: E402 – needs to be early logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)-8s] %(name)s — %(message)s", datefmt="%Y-%m-%d %H:%M:%S", handlers=[ logging.StreamHandler(sys.stdout), logging.FileHandler(str(LOG_PATH), encoding="utf-8"), ], ) logger = logging.getLogger(__name__) # ── Application imports ─────────────────────────────────────────────────────── from config import ( # noqa: E402 FACTORS_EMAIL, FACTORS_PASSWORD, TARGET_PROJECT, TEMP_DOWNLOAD_DIR, LAST_CSV_PATH, SMTP_HOST, SMTP_PORT, SENDER_EMAIL, SENDER_PASSWORD, RECIPIENT_EMAILS, FIELDS_TO_SELECT, HEADLESS, ) from scraper import FactorsAIScraper from comparator import find_new_entries from mailer import send_alert # ── CLI arguments ───────────────────────────────────────────────────────────── def _parse_args() -> argparse.Namespace: p = argparse.ArgumentParser(description="Factors.AI account-export automation") p.add_argument( "--visible", action="store_true", help="Open a real browser window instead of running headless", ) p.add_argument( "--dry-run", action="store_true", help="Download and compare but do NOT send the email", ) return p.parse_args() # ── Main flow ───────────────────────────────────────────────────────────────── def main() -> None: args = _parse_args() headless = HEADLESS and not args.visible logger.info("=" * 65) logger.info(" Factors.AI Account Export Automation — START") logger.info(f" headless={headless} dry_run={args.dry_run}") logger.info("=" * 65) # ── Step 1 ─ Download CSV via Selenium ──────────────────────────────────── logger.info("STEP 1: Downloading CSV …") scraper = FactorsAIScraper( download_dir=str(TEMP_DOWNLOAD_DIR), headless=headless, ) downloaded_csv = scraper.run( email=FACTORS_EMAIL, password=FACTORS_PASSWORD, target_project=TARGET_PROJECT, fields_to_select=FIELDS_TO_SELECT, ) logger.info(f" Downloaded → {downloaded_csv}") # ── Step 2 ─ Compare with last export ──────────────────────────────────── logger.info("STEP 2: Comparing with previous export …") last_csv = str(LAST_CSV_PATH) if LAST_CSV_PATH.exists() else None new_entries, is_first_run = find_new_entries(downloaded_csv, last_csv) if new_entries.empty and not is_first_run: logger.info(" ✓ No new entries found — nothing to mail.") else: count = len(new_entries) tag = "(first run — all entries)" if is_first_run else f"({count} new)" logger.info(f" ✓ New entries detected {tag}") # ── Step 3 ─ Send email ─────────────────────────────────────────────── if args.dry_run: logger.info("STEP 3: [DRY RUN] Email skipped.") logger.info(new_entries.to_string()) else: logger.info("STEP 3: Sending alert email …") send_alert( smtp_host=SMTP_HOST, smtp_port=SMTP_PORT, sender_email=SENDER_EMAIL, sender_password=SENDER_PASSWORD, recipients=RECIPIENT_EMAILS, new_entries=new_entries, is_first_run=is_first_run, downloaded_csv=downloaded_csv ) logger.info(" ✓ Email sent.") # ── Step 4 ─ Persist the new CSV as last_export.csv ───────────────────── logger.info(f"STEP 4: Saving export as last_export.csv → {LAST_CSV_PATH}") shutil.copy2(downloaded_csv, str(LAST_CSV_PATH)) logger.info("=" * 65) logger.info(" Automation complete ✓") logger.info("=" * 65) if __name__ == "__main__": main()