#!/usr/bin/env python3 """Step 3: Execute merges - PATCH updates to keeper contacts.""" 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" RESULTS_FILE = "D:/ClaudeTools/temp/bardach_dedup_merge_results.json" 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 main(): print("=" * 60) print("STEP 3: Execute merges (PATCH updates to keepers)") print("=" * 60) with open(PLAN_FILE, "r", encoding="utf-8") as f: plan_data = json.load(f) plan = plan_data["plan"] to_update = [e for e in plan if e["updates_to_apply"]] print(f"[INFO] Keepers needing updates: {len(to_update)}") token = get_token() print("[OK] Token acquired") successes = [] failures = [] op_count = 0 for i, entry in enumerate(to_update): contact_id = entry["keeper_id"] updates = entry["updates_to_apply"] name = entry["display_name"] status_code, response = patch_contact(token, contact_id, updates) op_count += 1 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] PATCH failed for '{name}': HTTP {status_code}") if (i + 1) % 50 == 0: print(f" Progress: {i + 1}/{len(to_update)} (success: {len(successes)}, fail: {len(failures)})") # Re-acquire token every 500 ops if op_count % 500 == 0: print(" Re-acquiring token...") token = get_token() # Small delay to avoid throttling if op_count % 50 == 0: time.sleep(1) # Save results results = { "total_attempted": len(to_update), "successes": len(successes), "failures": len(failures), "success_details": successes, "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"MERGE RESULTS") print(f"{'=' * 60}") print(f" Total attempted: {len(to_update)}") print(f" Successes: {len(successes)}") print(f" Failures: {len(failures)}") print(f"[OK] Results saved to {RESULTS_FILE}") if __name__ == "__main__": main()