#!/usr/bin/env python3 """Step 3b: Retry failed merges with phone number limits applied.""" import json import subprocess import sys import time TENANT_ID = "dd4a82e8-85a3-44ac-8800-07945ab4d95f" CLIENT_ID = "fabb3421-8b34-484b-bc17-e46de9703418" CLIENT_SECRET = "~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO" SCOPE = "https://graph.microsoft.com/.default" USER = "barbara@bardach.net" PLAN_FILE = "D:/ClaudeTools/temp/bardach_dedup_plan.json" MERGE_RESULTS = "D:/ClaudeTools/temp/bardach_dedup_merge_results.json" RESULTS_FILE = "D:/ClaudeTools/temp/bardach_dedup_merge_results_retry.json" PHONE_MAX = 2 # Graph API limit def get_token(): url = f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token" result = subprocess.run( ["curl", "-s", "-X", "POST", url, "-H", "Content-Type: application/x-www-form-urlencoded", "-d", f"client_id={CLIENT_ID}&scope={SCOPE}&client_secret={CLIENT_SECRET}&grant_type=client_credentials"], capture_output=True, text=True ) data = json.loads(result.stdout) if "access_token" not in data: print(f"[ERROR] Failed to get token: {data}") sys.exit(1) return data["access_token"] def patch_contact(token, contact_id, body): url = f"https://graph.microsoft.com/v1.0/users/{USER}/contacts/{contact_id}" body_json = json.dumps(body) result = subprocess.run( ["curl", "-s", "-X", "PATCH", url, "-H", f"Authorization: Bearer {token}", "-H", "Content-Type: application/json", "-d", body_json, "-w", "\n%{http_code}"], capture_output=True, text=True ) lines = result.stdout.strip().rsplit("\n", 1) status_code = int(lines[-1]) if len(lines) > 1 else 0 response_body = lines[0] if len(lines) > 1 else result.stdout return status_code, response_body def truncate_phones(updates): """Truncate phone arrays to max allowed by Graph API.""" for field in ["homePhones", "businessPhones"]: if field in updates and len(updates[field]) > PHONE_MAX: updates[field] = updates[field][:PHONE_MAX] return updates def main(): print("=" * 60) print("STEP 3b: Retry failed merges with phone limits") print("=" * 60) # Load the original results to get failed contact names with open(MERGE_RESULTS, "r", encoding="utf-8") as f: merge_results = json.load(f) failed_names = {f["display_name"] for f in merge_results["failure_details"]} print(f"[INFO] Failed contacts to retry: {len(failed_names)}") # Load the plan to get updates for failed contacts with open(PLAN_FILE, "r", encoding="utf-8") as f: plan_data = json.load(f) to_retry = [e for e in plan_data["plan"] if e["display_name"] in failed_names and e["updates_to_apply"]] print(f"[INFO] Entries with updates to retry: {len(to_retry)}") token = get_token() print("[OK] Token acquired") successes = [] failures = [] for i, entry in enumerate(to_retry): contact_id = entry["keeper_id"] updates = truncate_phones(dict(entry["updates_to_apply"])) name = entry["display_name"] status_code, response = patch_contact(token, contact_id, updates) if 200 <= status_code < 300: successes.append({"display_name": name, "contact_id": contact_id, "status": status_code}) else: failures.append({"display_name": name, "contact_id": contact_id, "status": status_code, "error": response[:500]}) print(f" [WARNING] Retry PATCH failed for '{name}': HTTP {status_code} - {response[:200]}") if (i + 1) % 50 == 0: print(f" Progress: {i + 1}/{len(to_retry)}") results = { "total_retried": len(to_retry), "successes": len(successes), "failures": len(failures), "failure_details": failures } with open(RESULTS_FILE, "w", encoding="utf-8") as f: json.dump(results, f, indent=2, ensure_ascii=False) print(f"\n{'=' * 60}") print(f"RETRY MERGE RESULTS") print(f"{'=' * 60}") print(f" Retried: {len(to_retry)}") print(f" Successes: {len(successes)}") print(f" Failures: {len(failures)}") print(f"\nCombined totals (original + retry):") print(f" Total merges succeeded: {merge_results['successes'] + len(successes)}") print(f" Total merges failed: {len(failures)}") if __name__ == "__main__": main()