#!/usr/bin/env python3 """Step 4: Delete duplicate contacts in batches. Usage: python bardach_dedup_step4_delete_batch.py [start_offset] Processes 500 deletes per run. Saves progress.""" import json import subprocess import sys import time import os 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" PROGRESS_FILE = "D:/ClaudeTools/temp/bardach_dedup_delete_progress.json" RESULTS_FILE = "D:/ClaudeTools/temp/bardach_dedup_delete_results.json" BATCH_SIZE = 500 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}", flush=True) sys.exit(1) return data["access_token"] def delete_contact(token, contact_id): url = f"https://graph.microsoft.com/v1.0/users/{USER}/contacts/{contact_id}" result = subprocess.run( ["curl", "-s", "-X", "DELETE", url, "-H", f"Authorization: Bearer {token}", "-w", "%{http_code}"], capture_output=True, text=True ) output = result.stdout.strip() try: status_code = int(output[-3:]) except (ValueError, IndexError): status_code = 0 return status_code def load_progress(): if os.path.exists(PROGRESS_FILE): with open(PROGRESS_FILE, "r", encoding="utf-8") as f: return json.load(f) return {"completed_index": 0, "successes": 0, "failures": []} def save_progress(progress): with open(PROGRESS_FILE, "w", encoding="utf-8") as f: json.dump(progress, f, indent=2, ensure_ascii=False) def main(): start_offset = int(sys.argv[1]) if len(sys.argv) > 1 else None with open(PLAN_FILE, "r", encoding="utf-8") as f: plan_data = json.load(f) all_deletes = [] for entry in plan_data["plan"]: for did in entry["delete_ids"]: all_deletes.append({"id": did, "display_name": entry["display_name"]}) total = len(all_deletes) progress = load_progress() start = start_offset if start_offset is not None else progress["completed_index"] end = min(start + BATCH_SIZE, total) print(f"DELETE BATCH: {start}-{end} of {total} (batch size {BATCH_SIZE})", flush=True) if start >= total: print(f"[OK] All {total} deletes already processed!", flush=True) print(f" Successes: {progress['successes']}", flush=True) print(f" Failures: {len(progress['failures'])}", flush=True) # Save final results with open(RESULTS_FILE, "w", encoding="utf-8") as f: json.dump({ "total_attempted": total, "successes": progress["successes"], "failures": len(progress["failures"]), "failure_details": progress["failures"] }, f, indent=2, ensure_ascii=False) return token = get_token() print("[OK] Token acquired", flush=True) successes = progress["successes"] failures = list(progress["failures"]) batch_start_time = time.time() for i in range(start, end): item = all_deletes[i] status_code = delete_contact(token, item["id"]) if status_code == 204 or status_code == 200: successes += 1 elif status_code == 404: # Already deleted (maybe from previous run) successes += 1 else: failures.append({"display_name": item["display_name"], "id": item["id"], "status": status_code}) if (i - start + 1) % 100 == 0: elapsed = time.time() - batch_start_time rate = (i - start + 1) / elapsed if elapsed > 0 else 0 print(f" {i + 1}/{total} | Batch: {i - start + 1}/{end - start} | OK: {successes} | Fail: {len(failures)} | {rate:.1f}/sec", flush=True) if (i - start + 1) % 50 == 0: time.sleep(0.3) # Save progress progress = {"completed_index": end, "successes": successes, "failures": failures} save_progress(progress) elapsed = time.time() - batch_start_time print(f"\nBatch complete: {start}-{end} in {elapsed:.0f}s", flush=True) print(f" Total successes so far: {successes}", flush=True) print(f" Total failures so far: {len(failures)}", flush=True) print(f" Next batch starts at: {end}", flush=True) if end < total: print(f" Remaining: {total - end}", flush=True) else: print(f"[OK] ALL DELETES COMPLETE!", flush=True) with open(RESULTS_FILE, "w", encoding="utf-8") as f: json.dump({ "total_attempted": total, "successes": successes, "failures": len(failures), "failure_details": failures }, f, indent=2, ensure_ascii=False) print(f"[OK] Results saved to {RESULTS_FILE}", flush=True) if __name__ == "__main__": main()