Files
claudetools/temp/bardach_main_dupes_fix.py
Mike Swanson fa15b03180 sync: Auto-sync from ACG-M-L5090 at 2026-03-10 19:11:00
Synced files:
- Quote wizard frontend (all components, hooks, types, config)
- API updates (config, models, routers, schemas, services)
- Client work (bg-builders, gurushow)
- Scripts (BGB Lesley termination, CIPP, Datto, migration)
- Temp files (Bardach contacts, VWP investigation, misc)
- Credentials and session logs
- Email service, PHP API, session logs

Machine: ACG-M-L5090
Timestamp: 2026-03-10 19:11:00

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 19:59:08 -07:00

246 lines
9.0 KiB
Python

"""
Merge and delete duplicate contacts in Barbara Bardach's main Contacts folder.
Reads analysis from bardach_main_dupes_analysis.json, merges data from delete
contacts into keepers, then deletes the duplicates.
"""
import json
import subprocess
import sys
import urllib.parse
TENANT_ID = "dd4a82e8-85a3-44ac-8800-07945ab4d95f"
CLIENT_ID = "fabb3421-8b34-484b-bc17-e46de9703418"
CLIENT_SECRET = "~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO"
USER_EMAIL = "barbara@bardach.net"
GRAPH_BASE = f"https://graph.microsoft.com/v1.0/users/{USER_EMAIL}/contacts"
ANALYSIS_FILE = r"D:\ClaudeTools\temp\bardach_main_dupes_analysis.json"
def get_token():
url = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token"
data = (
f"grant_type=client_credentials"
f"&client_id={CLIENT_ID}"
f"&client_secret={urllib.parse.quote(CLIENT_SECRET, safe='')}"
f"&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default"
)
result = subprocess.run(
["curl", "-s", "-X", "POST", url,
"-H", "Content-Type: application/x-www-form-urlencoded",
"-d", data],
capture_output=True, text=True
)
resp = json.loads(result.stdout)
if "access_token" not in resp:
print(f"[ERROR] Failed to get token: {resp}")
sys.exit(1)
return resp["access_token"]
def get_contact(token, contact_id):
"""GET full contact details from Graph API."""
url = f"{GRAPH_BASE}/{contact_id}"
select = "$select=displayName,givenName,surname,emailAddresses,homePhones,businessPhones,personalNotes,companyName,jobTitle,homeAddress,businessAddress,birthday"
result = subprocess.run(
["curl", "-s", "-X", "GET", f"{url}?{select}",
"-H", f"Authorization: Bearer {token}",
"-H", "Content-Type: application/json"],
capture_output=True, text=True
)
return json.loads(result.stdout)
def patch_contact(token, contact_id, payload):
"""PATCH a contact with the given payload."""
payload_json = json.dumps(payload)
url = f"{GRAPH_BASE}/{contact_id}"
result = subprocess.run(
["curl", "-s", "-X", "PATCH", url,
"-H", f"Authorization: Bearer {token}",
"-H", "Content-Type: application/json",
"-d", payload_json,
"-w", "\n%{http_code}"],
capture_output=True, text=True
)
lines = result.stdout.strip().rsplit("\n", 1)
status = int(lines[-1]) if len(lines) > 1 else 0
return status, lines[0] if len(lines) > 1 else result.stdout
def delete_contact(token, contact_id):
"""DELETE a contact. Returns HTTP status code."""
url = f"{GRAPH_BASE}/{contact_id}"
result = subprocess.run(
["curl", "-s", "-X", "DELETE", url,
"-H", f"Authorization: Bearer {token}",
"-o", "/dev/null",
"-w", "%{http_code}"],
capture_output=True, text=True
)
return int(result.stdout.strip())
def emails_equal(a, b):
"""Case-insensitive email comparison."""
return (a or "").lower().strip() == (b or "").lower().strip()
def has_address_data(addr):
"""Check if an address dict has any actual data."""
if not addr or not isinstance(addr, dict):
return False
return any(v for v in addr.values() if v)
def build_merge_payload(keeper, delete_contact_data):
"""Compare keeper and delete contact, return PATCH payload for fields to merge."""
payload = {}
merge_notes = []
# --- emailAddresses ---
keeper_emails = keeper.get("emailAddresses") or []
delete_emails = delete_contact_data.get("emailAddresses") or []
keeper_addrs = {e.get("address", "").lower().strip() for e in keeper_emails if e.get("address", "").strip()}
new_emails = []
for e in delete_emails:
addr = (e.get("address") or "").strip()
if addr and addr.lower() not in keeper_addrs:
new_emails.append(e)
if new_emails:
# Filter out empty entries from keeper too
clean_keeper = [e for e in keeper_emails if (e.get("address") or "").strip()]
payload["emailAddresses"] = clean_keeper + new_emails
merge_notes.append(f"emails: +{[e['address'] for e in new_emails]}")
# --- homePhones ---
keeper_hp = keeper.get("homePhones") or []
delete_hp = delete_contact_data.get("homePhones") or []
keeper_hp_set = {p.strip() for p in keeper_hp if p.strip()}
new_hp = [p for p in delete_hp if p.strip() and p.strip() not in keeper_hp_set]
if new_hp:
payload["homePhones"] = list(keeper_hp) + new_hp
merge_notes.append(f"homePhones: +{new_hp}")
# --- businessPhones ---
keeper_bp = keeper.get("businessPhones") or []
delete_bp = delete_contact_data.get("businessPhones") or []
keeper_bp_set = {p.strip() for p in keeper_bp if p.strip()}
new_bp = [p for p in delete_bp if p.strip() and p.strip() not in keeper_bp_set]
if new_bp:
payload["businessPhones"] = list(keeper_bp) + new_bp
merge_notes.append(f"businessPhones: +{new_bp}")
# --- personalNotes ---
keeper_notes = (keeper.get("personalNotes") or "").strip()
delete_notes = (delete_contact_data.get("personalNotes") or "").strip()
if delete_notes and not keeper_notes:
payload["personalNotes"] = delete_notes
merge_notes.append(f"personalNotes: set")
elif delete_notes and keeper_notes and delete_notes.lower() != keeper_notes.lower():
payload["personalNotes"] = keeper_notes + "\n" + delete_notes
merge_notes.append(f"personalNotes: appended")
# --- companyName ---
keeper_co = (keeper.get("companyName") or "").strip()
delete_co = (delete_contact_data.get("companyName") or "").strip()
if delete_co and not keeper_co:
payload["companyName"] = delete_co
merge_notes.append(f"companyName: '{delete_co}'")
# --- jobTitle ---
keeper_jt = (keeper.get("jobTitle") or "").strip()
delete_jt = (delete_contact_data.get("jobTitle") or "").strip()
if delete_jt and not keeper_jt:
payload["jobTitle"] = delete_jt
merge_notes.append(f"jobTitle: '{delete_jt}'")
# --- homeAddress ---
if has_address_data(delete_contact_data.get("homeAddress")) and not has_address_data(keeper.get("homeAddress")):
payload["homeAddress"] = delete_contact_data["homeAddress"]
merge_notes.append("homeAddress: set")
# --- businessAddress ---
if has_address_data(delete_contact_data.get("businessAddress")) and not has_address_data(keeper.get("businessAddress")):
payload["businessAddress"] = delete_contact_data["businessAddress"]
merge_notes.append("businessAddress: set")
# --- birthday ---
keeper_bday = keeper.get("birthday")
delete_bday = delete_contact_data.get("birthday")
if delete_bday and not keeper_bday:
payload["birthday"] = delete_bday
merge_notes.append(f"birthday: '{delete_bday}'")
return payload, merge_notes
def main():
with open(ANALYSIS_FILE, "r", encoding="utf-8") as f:
analysis = json.load(f)
groups = analysis["groups"]
print(f"Loaded {len(groups)} duplicate groups to process.\n")
token = get_token()
print("[OK] Got access token.\n")
success_count = 0
error_count = 0
for i, group in enumerate(groups, 1):
name = group["name"]
keeper_id = group["keeper_id"]
delete_ids = group["delete_ids"]
print(f"--- Group {i}/{len(groups)}: {name} ---")
# GET keeper details
keeper = get_contact(token, keeper_id)
if "error" in keeper:
print(f" [ERROR] Failed to GET keeper: {keeper['error']}")
error_count += 1
continue
for did in delete_ids:
# GET delete contact details
del_data = get_contact(token, did)
if "error" in del_data:
print(f" [ERROR] Failed to GET delete contact: {del_data['error']}")
error_count += 1
continue
# Build merge payload
payload, merge_notes = build_merge_payload(keeper, del_data)
if payload:
status, resp_body = patch_contact(token, keeper_id, payload)
if 200 <= status < 300:
print(f" [OK] PATCH keeper - merged: {', '.join(merge_notes)}")
# Update our local keeper data with the patched fields
keeper.update(payload)
else:
print(f" [ERROR] PATCH keeper failed (HTTP {status}): {resp_body[:200]}")
error_count += 1
continue
else:
print(f" [INFO] No data to merge from duplicate.")
# DELETE the duplicate
del_status = delete_contact(token, did)
if del_status == 204:
print(f" [OK] DELETE duplicate (HTTP 204)")
success_count += 1
else:
print(f" [ERROR] DELETE failed (HTTP {del_status})")
error_count += 1
print()
print(f"=== DONE: {success_count} deleted successfully, {error_count} errors ===")
if __name__ == "__main__":
main()