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>
This commit is contained in:
245
temp/bardach_main_dupes_fix.py
Normal file
245
temp/bardach_main_dupes_fix.py
Normal file
@@ -0,0 +1,245 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user