#!/usr/bin/env python3 """ contacts_from_legacy.py — Build a Giftbit-compatible contacts CSV from the legacy "adresses postales" export (selectionAdressesMap*.csv). The legacy export is pipe-delimited with a 1-line title preamble. It contains messy fields (multi-email cells, mixed-case addresses, etc.). This script extracts the columns Giftbit needs, normalizes them, and writes a clean comma-separated CSV ready for either: - Upload to Giftbit's UI for English delivery, or - Pairing with this folder's `send_gift_campaign.js` for French delivery. Multi-email handling (when a cell contains "a@x.com;b@y.com" — typically a couple living at the same address): --multi=first (default) — keep the first email, 1 gift per household --multi=split — split into multiple rows, 1 gift per person (note: both rows share the same name) --multi=skip — drop those rows for manual review Usage: python3 contacts_from_legacy.py \\ --src ~/Downloads/selectionAdressesMap9-12.csv \\ --out ~/Downloads/giftbit-contacts.csv \\ --multi first """ import argparse import csv import re import sys from pathlib import Path LOWER_WORDS = {"de", "du", "des", "la", "le", "les", "au", "aux", "à", "et", "sur", "en"} EMAIL_SPLIT = re.compile(r"\s*[;,]\s*") def split_name(full): """First whitespace-delimited token = firstname, rest = lastname.""" full = (full or "").strip() if not full: return "", "" parts = full.split(None, 1) return (parts[0], parts[1].strip()) if len(parts) > 1 else (parts[0], "") def title_address(addr): """Title-case an address with French article rules. '25 Rue Des Hirondelles ' -> '25 Rue des Hirondelles' 'chemin de la 1Re-Concession' -> 'Chemin de la 1re-Concession' """ if not addr: return "" out = [] for i, word in enumerate(addr.split()): lw = word.lower() if i > 0 and lw in LOWER_WORDS: out.append(lw) elif "-" in word: chunks = [] for c in word.split("-"): cl = c.lower() if cl in LOWER_WORDS: chunks.append(cl) else: chunks.append(c[:1].upper() + c[1:].lower()) out.append("-".join(chunks)) else: out.append(word[:1].upper() + word[1:].lower()) return " ".join(out) def is_email(s): return bool(s and "@" in s and "." in s.split("@")[-1]) def pick_emails(row): """Pull clean email list from the first non-empty source field.""" for k in ("email au compte", "email à l'adresse"): raw = (row.get(k) or "").strip() if not raw: continue emails = [e.strip().lower() for e in EMAIL_SPLIT.split(raw) if is_email(e.strip())] if emails: return emails return [] def main(): p = argparse.ArgumentParser() p.add_argument("--src", required=True, help="Source legacy CSV (pipe-delimited).") p.add_argument("--out", required=True, help="Output Giftbit-compatible CSV.") p.add_argument("--multi", choices=("first", "split", "skip"), default="first", help="How to handle cells with multiple emails.") args = p.parse_args() src = Path(args.src).expanduser() out = Path(args.out).expanduser() rows = [] with open(src, encoding="utf-8") as f: next(f) # skip "adresses postales" title line reader = csv.DictReader(f, delimiter="|") reader.fieldnames = [fn.strip() for fn in reader.fieldnames] for r in reader: rows.append({(k.strip() if k else k): (v.strip() if v else v) for k, v in r.items()}) contacts, seen = [], set() skipped_no_email = skipped_no_name = skipped_multi = 0 for r in rows: emails = pick_emails(r) if not emails: skipped_no_email += 1 continue if len(emails) > 1: if args.multi == "skip": skipped_multi += 1 continue if args.multi == "first": emails = emails[:1] full = (r.get("nom au compte") or r.get("nom à l'adresse") or "").strip() if not full: skipped_no_name += 1 continue firstname, lastname = split_name(full) desc = title_address(r.get("adresse dans F") or "") for em in emails: if em in seen: continue seen.add(em) contacts.append({ "firstname": firstname, "lastname": lastname, "email": em, "description": desc, }) with open(out, "w", encoding="utf-8", newline="") as f: w = csv.DictWriter(f, fieldnames=["firstname", "lastname", "email", "description"], quoting=csv.QUOTE_MINIMAL) w.writeheader() for c in contacts: w.writerow(c) print(f" source rows: {len(rows)}") print(f" contacts written: {len(contacts)}") print(f" skipped (no email): {skipped_no_email}") print(f" skipped (multi-email, --skip): {skipped_multi}") print(f" skipped (no name): {skipped_no_name}") print(f" → {out}") if __name__ == "__main__": sys.exit(main() or 0)