Files
claudetools/temp/bardach_merge_analysis.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

218 lines
7.5 KiB
Python

"""Analyze Temp vs Contacts folders for merge strategy."""
import subprocess, json, sys
TENANT_ID = "dd4a82e8-85a3-44ac-8800-07945ab4d95f"
CLAUDE_APP = "fabb3421-8b34-484b-bc17-e46de9703418"
CLAUDE_SECRET = "~QJ8Q~NyQSs4OcGqHZyPrA2CVnq9KBfKiimntbMO"
USER = "barbara@bardach.net"
# All contact fields we want to preserve
SELECT_FIELDS = (
"id,displayName,givenName,surname,middleName,nickName,"
"emailAddresses,homePhones,businessPhones,"
"companyName,jobTitle,department,"
"homeAddress,businessAddress,otherAddress,"
"birthday,personalNotes,"
"categories,title,generation,imAddresses,"
"parentFolderId"
)
def get_token():
r = subprocess.run([
'curl', '-s', '-X', 'POST',
f'https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token',
'-d', f'client_id={CLAUDE_APP}&client_secret={CLAUDE_SECRET}&scope=https://graph.microsoft.com/.default&grant_type=client_credentials'
], capture_output=True, text=True)
return json.loads(r.stdout)['access_token']
def pull_contacts(token, folder_id=None, folder_name="default"):
if folder_id:
base = f"https://graph.microsoft.com/v1.0/users/{USER}/contactFolders/{folder_id}/contacts"
else:
base = f"https://graph.microsoft.com/v1.0/users/{USER}/contacts"
url = f"{base}?$top=100&$select={SELECT_FIELDS}"
all_contacts = []
page = 0
while url:
page += 1
r = subprocess.run(['curl', '-s', '-H', f'Authorization: Bearer {token}', url],
capture_output=True, text=True)
data = json.loads(r.stdout)
if 'error' in data:
print(f" Error: {data['error'].get('message','')[:200]}")
break
items = data.get('value', [])
all_contacts.extend(items)
url = data.get('@odata.nextLink')
if page % 10 == 0:
print(f" {folder_name}: page {page}, {len(all_contacts)} contacts...")
if not items:
break
print(f" {folder_name}: {len(all_contacts)} total")
return all_contacts
token = get_token()
print("[OK] Token acquired\n")
# Get folder IDs
print("Getting contact folders...")
r = subprocess.run(['curl', '-s', '-H', f'Authorization: Bearer {token}',
f'https://graph.microsoft.com/v1.0/users/{USER}/contactFolders?$select=displayName,id'],
capture_output=True, text=True)
folders = json.loads(r.stdout).get('value', [])
temp_id = None
contacts_id = None
for f in folders:
print(f" {f['displayName']}: {f['id'][:40]}...")
if f['displayName'] == 'Temp':
temp_id = f['id']
elif f['displayName'] == 'Contacts':
contacts_id = f['id']
if not temp_id:
print("[ERROR] Temp folder not found!")
sys.exit(1)
# Pull both folders
print("\nPulling Contacts folder...")
main_contacts = pull_contacts(token, contacts_id, "Contacts")
print("\nPulling Temp folder...")
temp_contacts = pull_contacts(token, temp_id, "Temp")
# Save raw data
with open('D:/ClaudeTools/temp/bardach_main_contacts.json', 'w') as f:
json.dump(main_contacts, f, indent=2)
with open('D:/ClaudeTools/temp/bardach_temp_contacts.json', 'w') as f:
json.dump(temp_contacts, f, indent=2)
print(f"\nSaved raw data files")
# Build matching keys
def make_key(c):
"""Create a matching key from name."""
name = (c.get('displayName') or '').strip().lower()
return name
def make_email_keys(c):
"""Get all email addresses as keys."""
return set(e.get('address', '').strip().lower()
for e in c.get('emailAddresses', [])
if e.get('address'))
# Index main contacts
main_by_name = {}
main_by_email = {}
for c in main_contacts:
key = make_key(c)
if key:
main_by_name.setdefault(key, []).append(c)
for email in make_email_keys(c):
main_by_email.setdefault(email, []).append(c)
# Categorize temp contacts
matched_by_name = []
matched_by_email = []
unmatched = []
blank = []
for c in temp_contacts:
key = make_key(c)
emails = make_email_keys(c)
if not key and not emails:
blank.append(c)
continue
if key and key in main_by_name:
matched_by_name.append((c, main_by_name[key]))
continue
email_match = None
for email in emails:
if email in main_by_email:
email_match = main_by_email[email]
break
if email_match:
matched_by_email.append((c, email_match))
else:
unmatched.append(c)
print(f"\n{'='*60}")
print(f"MERGE ANALYSIS")
print(f"{'='*60}")
print(f"Main Contacts folder: {len(main_contacts)}")
print(f"Temp folder (iCloud): {len(temp_contacts)}")
print(f"")
print(f"Matched by name: {len(matched_by_name)}")
print(f"Matched by email: {len(matched_by_email)}")
print(f"Unmatched (new): {len(unmatched)}")
print(f"Blank (no name/email):{len(blank)}")
print(f"Total categorized: {len(matched_by_name)+len(matched_by_email)+len(unmatched)+len(blank)}")
# Analyze what fields the temp contacts have that main doesn't
print(f"\n{'='*60}")
print(f"FIELD ANALYSIS - What Temp contacts add")
print(f"{'='*60}")
fields_to_check = ['personalNotes', 'companyName', 'jobTitle', 'birthday',
'homeAddress', 'businessAddress', 'homePhones', 'businessPhones',
'nickName', 'categories']
for field in fields_to_check:
temp_has = sum(1 for c in temp_contacts if c.get(field) and
(isinstance(c[field], str) and c[field].strip() or
isinstance(c[field], list) and len(c[field]) > 0 or
isinstance(c[field], dict) and any(v for v in c[field].values())))
main_has = sum(1 for c in main_contacts if c.get(field) and
(isinstance(c[field], str) and c[field].strip() or
isinstance(c[field], list) and len(c[field]) > 0 or
isinstance(c[field], dict) and any(v for v in c[field].values())))
print(f" {field:25s}: Temp={temp_has:5d} Main={main_has:5d}")
# For matched contacts, how many have notes in temp but not in main?
notes_to_merge = 0
for temp_c, main_matches in matched_by_name + matched_by_email:
temp_notes = (temp_c.get('personalNotes') or '').strip()
if temp_notes:
main_notes = (main_matches[0].get('personalNotes') or '').strip()
if not main_notes:
notes_to_merge += 1
print(f"\n Matched contacts where Temp has notes but Main doesn't: {notes_to_merge}")
# Sample unmatched
print(f"\n{'='*60}")
print(f"SAMPLE UNMATCHED (first 30 - these would be ADDED to main)")
print(f"{'='*60}")
for c in unmatched[:30]:
name = c.get('displayName', '(no name)')
emails = ', '.join(e.get('address','') for e in c.get('emailAddresses',[]))
company = c.get('companyName', '')
detail = emails or company or ''
print(f" {name}" + (f" - {detail}" if detail else ""))
if len(unmatched) > 30:
print(f" ... and {len(unmatched)-30} more")
# Sample blank
if blank:
print(f"\n{'='*60}")
print(f"BLANK CONTACTS ({len(blank)} - no displayName or email)")
print(f"{'='*60}")
for c in blank[:10]:
# Show whatever fields they have
non_empty = {k: v for k, v in c.items()
if v and k not in ('id', 'parentFolderId', '@odata.etag', 'changeKey',
'createdDateTime', 'lastModifiedDateTime', 'categories',
'flag', 'emailAddresses', 'homePhones', 'businessPhones',
'imAddresses')
and not k.startswith('@')}
print(f" Fields: {list(non_empty.keys())}")