Expand audit_calendar_sync.py to comprehensive management tool with pagination, duplicate detection/deletion, and orphaned calendar cleanup
This commit is contained in:
@@ -57,8 +57,21 @@ async def ensure_google_calendar(service, employee_kuerzel):
|
|||||||
"""Ensure Google Calendar exists for employee."""
|
"""Ensure Google Calendar exists for employee."""
|
||||||
calendar_name = f"AW-{employee_kuerzel}"
|
calendar_name = f"AW-{employee_kuerzel}"
|
||||||
try:
|
try:
|
||||||
calendar_list = service.calendarList().list().execute()
|
# Fetch all calendars with pagination
|
||||||
for calendar in calendar_list.get('items', []):
|
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:
|
if calendar['summary'] == calendar_name:
|
||||||
return calendar['id']
|
return calendar['id']
|
||||||
return None # Calendar doesn't exist
|
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."""
|
"""Delete Google Calendar for employee if it exists."""
|
||||||
calendar_name = f"AW-{employee_kuerzel}"
|
calendar_name = f"AW-{employee_kuerzel}"
|
||||||
try:
|
try:
|
||||||
calendar_list = service.calendarList().list().execute()
|
# Fetch all calendars with pagination
|
||||||
for calendar in calendar_list.get('items', []):
|
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:
|
if calendar['summary'] == calendar_name:
|
||||||
calendar_id = calendar['id']
|
calendar_id = calendar['id']
|
||||||
primary = calendar.get('primary', False)
|
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}")
|
logger.error(f"Failed to check/delete Google calendar for {employee_kuerzel}: {e}")
|
||||||
raise
|
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():
|
async def main():
|
||||||
if len(sys.argv) < 3 or len(sys.argv) > 5:
|
if len(sys.argv) < 2:
|
||||||
print("Usage: python audit_calendar_sync.py <employee_kuerzel> <google|advoware> [--delete-orphaned-google] [--delete-calendar]")
|
print("Usage: python audit_calendar_sync.py <command> [options]")
|
||||||
print(" --delete-orphaned-google: Delete Google events that exist in Google but not in the DB")
|
print("\nCommands:")
|
||||||
print(" --delete-calendar: Delete the Google calendar for the employee")
|
print(" audit <employee_kuerzel> <google|advoware> [--delete-orphaned-google]")
|
||||||
print("Example: python audit_calendar_sync.py SB google --delete-orphaned-google")
|
print(" Audit sync entries for a specific employee")
|
||||||
print("Example: python audit_calendar_sync.py SB google --delete-calendar")
|
print(" delete-calendar <employee_kuerzel>")
|
||||||
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
employee_kuerzel = sys.argv[1].upper()
|
command = sys.argv[1].lower()
|
||||||
check_system = sys.argv[2].lower()
|
|
||||||
delete_orphaned_google = '--delete-orphaned-google' in sys.argv
|
|
||||||
delete_calendar = '--delete-calendar' in sys.argv
|
|
||||||
|
|
||||||
if delete_calendar:
|
try:
|
||||||
# Delete calendar mode
|
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 <employee_kuerzel> <google|advoware> [--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 <employee_kuerzel>")
|
||||||
|
sys.exit(1)
|
||||||
|
employee_kuerzel = sys.argv[2].upper()
|
||||||
deleted = await delete_google_calendar(service, employee_kuerzel)
|
deleted = await delete_google_calendar(service, employee_kuerzel)
|
||||||
if deleted:
|
if deleted:
|
||||||
print(f"Successfully deleted Google calendar for {employee_kuerzel}")
|
print(f"Successfully deleted Google calendar for {employee_kuerzel}")
|
||||||
else:
|
else:
|
||||||
print(f"No calendar deleted for {employee_kuerzel}")
|
print(f"No calendar deleted for {employee_kuerzel}")
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Failed to delete calendar: {e}")
|
elif command == 'list-all':
|
||||||
sys.exit(1)
|
await list_all_calendars(service)
|
||||||
else:
|
|
||||||
# Audit mode
|
elif command == 'find-duplicates':
|
||||||
try:
|
await find_duplicates(service)
|
||||||
await audit_calendar_sync(employee_kuerzel, check_system, delete_orphaned_google)
|
|
||||||
except Exception as e:
|
elif command == 'delete-duplicates':
|
||||||
logger.error(f"Audit failed: {e}")
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Command failed: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
Reference in New Issue
Block a user