From 1539c26be6a6280504c1480569ce0790ecb8f443 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 25 Oct 2025 07:48:35 +0000 Subject: [PATCH] Expand audit_calendar_sync.py to comprehensive management tool with pagination, duplicate detection/deletion, and orphaned calendar cleanup --- .../advoware_cal_sync/audit_calendar_sync.py | 294 ++++++++++++++++-- 1 file changed, 267 insertions(+), 27 deletions(-) diff --git a/bitbylaw/steps/advoware_cal_sync/audit_calendar_sync.py b/bitbylaw/steps/advoware_cal_sync/audit_calendar_sync.py index 235cf4f3..ad72afa4 100644 --- a/bitbylaw/steps/advoware_cal_sync/audit_calendar_sync.py +++ b/bitbylaw/steps/advoware_cal_sync/audit_calendar_sync.py @@ -57,8 +57,21 @@ async def ensure_google_calendar(service, employee_kuerzel): """Ensure Google Calendar exists for employee.""" calendar_name = f"AW-{employee_kuerzel}" try: - calendar_list = service.calendarList().list().execute() - for calendar in calendar_list.get('items', []): + # Fetch all calendars with pagination + all_calendars = [] + page_token = None + while True: + calendar_list = service.calendarList().list( + pageToken=page_token, + maxResults=250 + ).execute() + calendars = calendar_list.get('items', []) + all_calendars.extend(calendars) + page_token = calendar_list.get('nextPageToken') + if not page_token: + break + + for calendar in all_calendars: if calendar['summary'] == calendar_name: return calendar['id'] return None # Calendar doesn't exist @@ -290,8 +303,21 @@ async def delete_google_calendar(service, employee_kuerzel): """Delete Google Calendar for employee if it exists.""" calendar_name = f"AW-{employee_kuerzel}" try: - calendar_list = service.calendarList().list().execute() - for calendar in calendar_list.get('items', []): + # Fetch all calendars with pagination + all_calendars = [] + page_token = None + while True: + calendar_list = service.calendarList().list( + pageToken=page_token, + maxResults=250 + ).execute() + calendars = calendar_list.get('items', []) + all_calendars.extend(calendars) + page_token = calendar_list.get('nextPageToken') + if not page_token: + break + + for calendar in all_calendars: if calendar['summary'] == calendar_name: calendar_id = calendar['id'] primary = calendar.get('primary', False) @@ -317,39 +343,253 @@ async def delete_google_calendar(service, employee_kuerzel): logger.error(f"Failed to check/delete Google calendar for {employee_kuerzel}: {e}") raise +async def list_all_calendars(service): + """List all Google Calendars.""" + try: + # Fetch all calendars with pagination + all_calendars = [] + page_token = None + while True: + calendar_list = service.calendarList().list( + pageToken=page_token, + maxResults=250 + ).execute() + calendars = calendar_list.get('items', []) + all_calendars.extend(calendars) + page_token = calendar_list.get('nextPageToken') + if not page_token: + break + + print(f"\n=== All Google Calendars ({len(all_calendars)}) ===") + for cal in sorted(all_calendars, key=lambda x: x.get('summary', '')): + summary = cal.get('summary', 'Unnamed') + cal_id = cal['id'] + primary = cal.get('primary', False) + access_role = cal.get('accessRole', 'unknown') + print(f" {summary} (ID: {cal_id}, Primary: {primary}, Access: {access_role})") + + return all_calendars + except Exception as e: + logger.error(f"Failed to list calendars: {e}") + raise + +async def find_duplicates(service): + """Find duplicate calendars by name.""" + all_calendars = await list_all_calendars(service) + + from collections import defaultdict + name_groups = defaultdict(list) + for cal in all_calendars: + summary = cal.get('summary', 'Unnamed') + name_groups[summary].append(cal) + + duplicates = {name: cals for name, cals in name_groups.items() if len(cals) > 1} + + if duplicates: + print(f"\n=== Duplicate Calendars Found ({len(duplicates)} unique names with duplicates) ===") + total_duplicates = sum(len(cals) - 1 for cals in duplicates.values()) + print(f"Total duplicate calendars: {total_duplicates}") + + for name, cals in duplicates.items(): + print(f"\nCalendar Name: '{name}' - {len(cals)} instances") + for cal in cals: + cal_id = cal['id'] + primary = cal.get('primary', False) + access_role = cal.get('accessRole', 'unknown') + print(f" ID: {cal_id}, Primary: {primary}, Access Role: {access_role}") + else: + print("\nNo duplicate calendars found!") + + return duplicates + +async def delete_duplicates(service, duplicates): + """Delete duplicate calendars, keeping one per name.""" + if not duplicates: + print("No duplicates to delete.") + return + + print(f"\n=== Deleting Duplicate Calendars ===") + total_deleted = 0 + + for name, cals in duplicates.items(): + # Keep the first one, delete the rest + keep_cal = cals[0] + to_delete = cals[1:] + + print(f"\nKeeping: '{name}' (ID: {keep_cal['id']})") + for cal in to_delete: + cal_id = cal['id'] + try: + service.calendars().delete(calendarId=cal_id).execute() + print(f" Deleted: {cal_id}") + total_deleted += 1 + except HttpError as e: + print(f" Failed to delete {cal_id}: {e}") + except Exception as e: + print(f" Error deleting {cal_id}: {e}") + + print(f"\nTotal calendars deleted: {total_deleted}") + +async def get_all_employees_from_db(): + """Get all employee kuerzel from DB.""" + conn = await connect_db() + try: + rows = await conn.fetch( + """ + SELECT DISTINCT employee_kuerzel + FROM calendar_sync + WHERE deleted = FALSE + ORDER BY employee_kuerzel + """, + # No params + ) + employees = [row['employee_kuerzel'] for row in rows] + logger.info(f"Found {len(employees)} distinct employees in DB") + return employees + finally: + await conn.close() + +async def find_orphaned_calendars(service): + """Find AW-* calendars that don't have corresponding employees in DB.""" + all_calendars = await list_all_calendars(service) + employees = await get_all_employees_from_db() + + # Create set of expected calendar names + expected_names = {f"AW-{emp}" for emp in employees} + + orphaned = [] + for cal in all_calendars: + summary = cal.get('summary', '') + if summary.startswith('AW-') and summary not in expected_names: + orphaned.append(cal) + + if orphaned: + print(f"\n=== Orphaned AW-* Calendars ({len(orphaned)}) ===") + for cal in sorted(orphaned, key=lambda x: x.get('summary', '')): + summary = cal.get('summary', '') + cal_id = cal['id'] + primary = cal.get('primary', False) + access_role = cal.get('accessRole', 'unknown') + print(f" {summary} (ID: {cal_id}, Primary: {primary}, Access: {access_role})") + else: + print("\nNo orphaned AW-* calendars found!") + + return orphaned + +async def cleanup_orphaned_calendars(service, orphaned): + """Delete orphaned AW-* calendars.""" + if not orphaned: + print("No orphaned calendars to delete.") + return + + print(f"\n=== Deleting Orphaned AW-* Calendars ===") + total_deleted = 0 + + for cal in orphaned: + summary = cal.get('summary', '') + cal_id = cal['id'] + primary = cal.get('primary', False) + + if primary: + print(f" Skipping primary calendar: {summary}") + continue + + try: + service.calendars().delete(calendarId=cal_id).execute() + print(f" Deleted: {summary} (ID: {cal_id})") + total_deleted += 1 + except HttpError as e: + print(f" Failed to delete {summary} ({cal_id}): {e}") + except Exception as e: + print(f" Error deleting {summary} ({cal_id}): {e}") + + print(f"\nTotal orphaned calendars deleted: {total_deleted}") + async def main(): - if len(sys.argv) < 3 or len(sys.argv) > 5: - print("Usage: python audit_calendar_sync.py [--delete-orphaned-google] [--delete-calendar]") - print(" --delete-orphaned-google: Delete Google events that exist in Google but not in the DB") - print(" --delete-calendar: Delete the Google calendar for the employee") - print("Example: python audit_calendar_sync.py SB google --delete-orphaned-google") - print("Example: python audit_calendar_sync.py SB google --delete-calendar") + if len(sys.argv) < 2: + print("Usage: python audit_calendar_sync.py [options]") + print("\nCommands:") + print(" audit [--delete-orphaned-google]") + print(" Audit sync entries for a specific employee") + print(" delete-calendar ") + print(" Delete the Google calendar for a specific employee") + print(" list-all") + print(" List all Google calendars") + print(" find-duplicates") + print(" Find duplicate calendars by name") + print(" delete-duplicates") + print(" Find and delete duplicate calendars (keeps one per name)") + print(" find-orphaned") + print(" Find AW-* calendars without corresponding employees in DB") + print(" cleanup-orphaned") + print(" Find and delete orphaned AW-* calendars") + print("\nOptions:") + print(" --delete-orphaned-google: Delete Google events that exist in Google but not in the DB (for audit command)") + print("\nExamples:") + print(" python audit_calendar_sync.py audit SB google --delete-orphaned-google") + print(" python audit_calendar_sync.py delete-calendar SB") + print(" python audit_calendar_sync.py list-all") + print(" python audit_calendar_sync.py find-duplicates") + print(" python audit_calendar_sync.py delete-duplicates") + print(" python audit_calendar_sync.py find-orphaned") + print(" python audit_calendar_sync.py cleanup-orphaned") sys.exit(1) - employee_kuerzel = sys.argv[1].upper() - check_system = sys.argv[2].lower() - delete_orphaned_google = '--delete-orphaned-google' in sys.argv - delete_calendar = '--delete-calendar' in sys.argv + command = sys.argv[1].lower() - if delete_calendar: - # Delete calendar mode - try: - service = await get_google_service() + try: + service = await get_google_service() + + if command == 'audit': + if len(sys.argv) < 4: + print("Usage: python audit_calendar_sync.py audit [--delete-orphaned-google]") + sys.exit(1) + employee_kuerzel = sys.argv[2].upper() + check_system = sys.argv[3].lower() + delete_orphaned_google = '--delete-orphaned-google' in sys.argv + await audit_calendar_sync(employee_kuerzel, check_system, delete_orphaned_google) + + elif command == 'delete-calendar': + if len(sys.argv) < 3: + print("Usage: python audit_calendar_sync.py delete-calendar ") + sys.exit(1) + employee_kuerzel = sys.argv[2].upper() deleted = await delete_google_calendar(service, employee_kuerzel) if deleted: print(f"Successfully deleted Google calendar for {employee_kuerzel}") else: print(f"No calendar deleted for {employee_kuerzel}") - except Exception as e: - logger.error(f"Failed to delete calendar: {e}") - sys.exit(1) - else: - # Audit mode - try: - await audit_calendar_sync(employee_kuerzel, check_system, delete_orphaned_google) - except Exception as e: - logger.error(f"Audit failed: {e}") + + elif command == 'list-all': + await list_all_calendars(service) + + elif command == 'find-duplicates': + await find_duplicates(service) + + elif command == 'delete-duplicates': + duplicates = await find_duplicates(service) + if duplicates: + await delete_duplicates(service, duplicates) + else: + print("No duplicates to delete.") + + elif command == 'find-orphaned': + await find_orphaned_calendars(service) + + elif command == 'cleanup-orphaned': + orphaned = await find_orphaned_calendars(service) + if orphaned: + await cleanup_orphaned_calendars(service, orphaned) + else: + print("No orphaned calendars to delete.") + + else: + print(f"Unknown command: {command}") sys.exit(1) + except Exception as e: + logger.error(f"Command failed: {e}") + sys.exit(1) + if __name__ == "__main__": asyncio.run(main()) \ No newline at end of file