Compare commits
4 Commits
b4c4bf0a9e
...
c5600b42ec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5600b42ec | ||
|
|
c5ddd02307 | ||
|
|
1539c26be6 | ||
|
|
62f57bb035 |
@@ -1,19 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import asyncpg
|
|
||||||
from config import Config
|
|
||||||
|
|
||||||
async def check_db():
|
|
||||||
conn = await asyncpg.connect(
|
|
||||||
host=Config.POSTGRES_HOST or 'localhost',
|
|
||||||
user=Config.POSTGRES_USER,
|
|
||||||
password=Config.POSTGRES_PASSWORD,
|
|
||||||
database=Config.POSTGRES_DB_NAME,
|
|
||||||
timeout=10
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
row = await conn.fetchrow('SELECT * FROM calendar_sync WHERE sync_id = $1', '1329fa1f-9de5-49dc-95c6-a13525f315c5')
|
|
||||||
print('DB Row:', dict(row) if row else 'No row found')
|
|
||||||
finally:
|
|
||||||
await conn.close()
|
|
||||||
|
|
||||||
asyncio.run(check_db())
|
|
||||||
@@ -177,13 +177,254 @@ Cron-Step läuft täglich.
|
|||||||
- Recurring-Events: Begrenzte Unterstützung.
|
- Recurring-Events: Begrenzte Unterstützung.
|
||||||
- Performance: Bei vielen Terminen Paginierung prüfen.
|
- Performance: Bei vielen Terminen Paginierung prüfen.
|
||||||
|
|
||||||
## Letzte Änderungen
|
## Audit und Management Tool (`audit_calendar_sync.py`)
|
||||||
|
|
||||||
- Refaktorierung zu event-driven Design ohne PostgreSQL Hub.
|
Das `audit_calendar_sync.py` Tool bietet umfassende Audit-, Management- und Debugging-Funktionen für die Calendar-Synchronisation. Es ermöglicht die Überprüfung der Sync-Integrität, das Aufräumen von Duplikaten und verwaisten Einträgen sowie detaillierte Abfragen einzelner Termine.
|
||||||
- Fixes für mehrtägige Termine: Korrekte Verwendung von `datumBis`.
|
|
||||||
- Entfernung 24h-Limit; Google Calendar unterstützt lange Events.
|
### Verwendung
|
||||||
- Per-Employee Locking mit Redis.
|
|
||||||
- Logging via context.logger für Workbench.
|
```bash
|
||||||
- Neue Schritte: calendar_sync_all_step.py, calendar_sync_cron_step.py.
|
cd /opt/motia-app/bitbylaw
|
||||||
- Workbench-Gruppierung: "advoware-calendar-sync".
|
source python_modules/bin/activate
|
||||||
|
python steps/advoware_cal_sync/audit_calendar_sync.py <command> [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Befehle
|
||||||
|
|
||||||
|
#### `audit <employee_kuerzel> <google|advoware> [--delete-orphaned-google]`
|
||||||
|
|
||||||
|
Auditiert Sync-Einträge für einen spezifischen Mitarbeiter und prüft deren Existenz in beiden Systemen.
|
||||||
|
|
||||||
|
**Parameter:**
|
||||||
|
- `employee_kuerzel`: Mitarbeiter-Kürzel (z.B. "SB", "UR")
|
||||||
|
- `google|advoware`: System, das auditiert werden soll
|
||||||
|
- `--delete-orphaned-google`: Optional, löscht Google-Events die in Google existieren aber nicht in der DB
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```bash
|
||||||
|
# Audit Google Calendar für Mitarbeiter SB
|
||||||
|
python audit_calendar_sync.py audit SB google
|
||||||
|
|
||||||
|
# Audit Advoware für Mitarbeiter UR mit Löschung verwaister Google-Events
|
||||||
|
python audit_calendar_sync.py audit UR google --delete-orphaned-google
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ausgabe:**
|
||||||
|
- Anzahl der DB-Einträge
|
||||||
|
- Anzahl der Events im Zielsystem
|
||||||
|
- Anzahl existierender/verwaiste Einträge
|
||||||
|
- Details zu verwaisten Einträgen
|
||||||
|
|
||||||
|
#### `delete-calendar <employee_kuerzel>`
|
||||||
|
|
||||||
|
Löscht den Google Calendar für einen spezifischen Mitarbeiter (falls vorhanden).
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```bash
|
||||||
|
python audit_calendar_sync.py delete-calendar SB
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `list-all`
|
||||||
|
|
||||||
|
Listet alle Google Calendars auf, einschließlich Name, ID, Primary-Status und Access-Role.
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```bash
|
||||||
|
python audit_calendar_sync.py list-all
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ausgabe:**
|
||||||
|
```
|
||||||
|
=== All Google Calendars (27) ===
|
||||||
|
AW-SB (ID: abc123@group.calendar.google.com, Primary: False, Access: owner)
|
||||||
|
AW-UR (ID: def456@group.calendar.google.com, Primary: False, Access: owner)
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `find-duplicates`
|
||||||
|
|
||||||
|
Findet duplizierte Google Calendars nach Namen.
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```bash
|
||||||
|
python audit_calendar_sync.py find-duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ausgabe:**
|
||||||
|
```
|
||||||
|
=== Duplicate Calendars Found (2 unique names with duplicates) ===
|
||||||
|
Total duplicate calendars: 3
|
||||||
|
|
||||||
|
Calendar Name: 'AW-SB' - 2 instances
|
||||||
|
ID: abc123@group.calendar.google.com, Primary: False, Access Role: owner
|
||||||
|
ID: xyz789@group.calendar.google.com, Primary: False, Access Role: owner
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `delete-duplicates`
|
||||||
|
|
||||||
|
Findet und löscht duplizierte Calendars (behält jeweils einen pro Namen).
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```bash
|
||||||
|
python audit_calendar_sync.py delete-duplicates
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `find-orphaned`
|
||||||
|
|
||||||
|
Findet AW-* Calendars ohne entsprechende Mitarbeiter in der Datenbank.
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```bash
|
||||||
|
python audit_calendar_sync.py find-orphaned
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `cleanup-orphaned`
|
||||||
|
|
||||||
|
Findet und löscht verwaiste AW-* Calendars.
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```bash
|
||||||
|
python audit_calendar_sync.py cleanup-orphaned
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `query-frnr <frnr>`
|
||||||
|
|
||||||
|
Zeigt alle Sync-Informationen für eine spezifische Advoware frNr.
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```bash
|
||||||
|
python audit_calendar_sync.py query-frnr 79291
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ausgabe:**
|
||||||
|
```
|
||||||
|
=== Sync Information for frNr: 79291 ===
|
||||||
|
Found 1 sync entry
|
||||||
|
|
||||||
|
Sync ID: 6ee9ba95-8aff-4868-9171-c10a8789427c
|
||||||
|
Employee: UR
|
||||||
|
Advoware frNr: 79291
|
||||||
|
Google Event ID: jao7r00j26lt1i0chk454bi9as
|
||||||
|
Source System: advoware
|
||||||
|
Sync Strategy: source_system_wins
|
||||||
|
Sync Status: synced
|
||||||
|
Last Sync: 2025-10-24 23:30:17.692668+00:00
|
||||||
|
Created: 2025-10-24 07:22:41.729295+00:00
|
||||||
|
Updated: 2025-10-24 07:22:41.729295+00:00
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `query-event <event_id>`
|
||||||
|
|
||||||
|
Zeigt Sync-Informationen für eine spezifische Google Event ID.
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```bash
|
||||||
|
python audit_calendar_sync.py query-event jao7r00j26lt1i0chk454bi9as
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `verify-sync <frnr>`
|
||||||
|
|
||||||
|
Vollständige Sync-Verifikation: Prüft einen Termin in beiden Systemen (Advoware und Google Calendar).
|
||||||
|
|
||||||
|
**Beispiel:**
|
||||||
|
```bash
|
||||||
|
python audit_calendar_sync.py verify-sync 79291
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ausgabe:**
|
||||||
|
```
|
||||||
|
=== Sync Verification for frNr: 79291 ===
|
||||||
|
Employee: UR
|
||||||
|
Sync Status: synced
|
||||||
|
Last Sync: 2025-10-24 23:30:17.692668+00:00
|
||||||
|
|
||||||
|
--- Checking Advoware ---
|
||||||
|
✅ Found in Advoware:
|
||||||
|
Subject: Jour fixe iS Neomi - Teilnahme im Einzelfall
|
||||||
|
Date: 2024-06-04T17:00:00
|
||||||
|
Time: N/A
|
||||||
|
End Time: 19:00:00
|
||||||
|
End Date: 2026-02-03T00:00:00
|
||||||
|
Last Modified: 2025-09-29T11:55:43.624
|
||||||
|
frNr: 79291
|
||||||
|
|
||||||
|
--- Checking Google Calendar ---
|
||||||
|
✅ Found in Google Calendar:
|
||||||
|
Summary: Advoware (frNr: 79291)
|
||||||
|
Start: 2024-06-04T17:00:00+02:00
|
||||||
|
End: 2024-06-04T19:00:00+02:00
|
||||||
|
|
||||||
|
--- Sync Status Summary ---
|
||||||
|
✅ Synchronized: Exists in both systems
|
||||||
|
```
|
||||||
|
|
||||||
|
### Technische Details
|
||||||
|
|
||||||
|
#### Datenbank-Integration
|
||||||
|
- Verwendet PostgreSQL-Verbindung aus `config.py`
|
||||||
|
- Tabelle: `calendar_sync`
|
||||||
|
- Felder: `sync_id`, `employee_kuerzel`, `advoware_frnr`, `google_event_id`, etc.
|
||||||
|
|
||||||
|
#### API-Integration
|
||||||
|
- **Google Calendar API**: `calendarList().list()` mit Paginierung (maxResults=250)
|
||||||
|
- **Advoware API**: `GET /api/v1/advonet/Termine` mit `frnr` Filter
|
||||||
|
- Automatische Token-Verwaltung und Fehlerbehandlung
|
||||||
|
|
||||||
|
#### Sicherheit
|
||||||
|
- Verwendet bestehende Service-Account und API-Credentials
|
||||||
|
- Keine zusätzlichen Berechtigungen erforderlich
|
||||||
|
|
||||||
|
### Häufige Anwendungsfälle
|
||||||
|
|
||||||
|
#### 1. Nach der Erstinstallation
|
||||||
|
```bash
|
||||||
|
# Alle Calendars auflisten
|
||||||
|
python audit_calendar_sync.py list-all
|
||||||
|
|
||||||
|
# Duplikate finden und entfernen
|
||||||
|
python audit_calendar_sync.py find-duplicates
|
||||||
|
python audit_calendar_sync.py delete-duplicates
|
||||||
|
|
||||||
|
# Verwaiste Calendars entfernen
|
||||||
|
python audit_calendar_sync.py find-orphaned
|
||||||
|
python audit_calendar_sync.py cleanup-orphaned
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Bei Sync-Problemen
|
||||||
|
```bash
|
||||||
|
# Sync-Status für einen Mitarbeiter prüfen
|
||||||
|
python audit_calendar_sync.py audit SB google
|
||||||
|
|
||||||
|
# Einzelnen Termin verifizieren
|
||||||
|
python audit_calendar_sync.py verify-sync 79291
|
||||||
|
|
||||||
|
# Sync-Informationen abfragen
|
||||||
|
python audit_calendar_sync.py query-frnr 79291
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Regelmäßige Wartung
|
||||||
|
```bash
|
||||||
|
# Wöchentliche Überprüfung auf Duplikate
|
||||||
|
python audit_calendar_sync.py find-duplicates
|
||||||
|
|
||||||
|
# Monatliche Bereinigung verwaister Einträge
|
||||||
|
python audit_calendar_sync.py cleanup-orphaned
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fehlerbehandlung
|
||||||
|
|
||||||
|
- **API-Fehler**: Automatische Retry-Logik mit Backoff
|
||||||
|
- **Berechtigungsfehler**: Klare Fehlermeldungen mit Lösungsvorschlägen
|
||||||
|
- **Netzwerkprobleme**: Timeout-Handling und Wiederholungen
|
||||||
|
- **Dateninkonsistenzen**: Detaillierte Logging für Debugging
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- **Paginierung**: Automatische Handhabung großer Resultsets
|
||||||
|
- **Batch-Verarbeitung**: Effiziente API-Calls mit minimalen Requests
|
||||||
|
- **Caching**: Wiederverwendung von API-Verbindungen wo möglich
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
|
||||||
|
Alle Operationen werden über `context.logger` geloggt und sind in der Motia Workbench sichtbar. Zusätzliche Debug-Informationen werden auf der Konsole ausgegeben.
|
||||||
|
|
||||||
|
|||||||
@@ -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,38 +343,470 @@ 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 query_frnr(frnr):
|
||||||
|
"""Query sync information for a specific Advoware frNr."""
|
||||||
|
conn = await connect_db()
|
||||||
|
try:
|
||||||
|
# Find all sync entries for this frNr
|
||||||
|
rows = await conn.fetch(
|
||||||
|
"""
|
||||||
|
SELECT sync_id, employee_kuerzel, advoware_frnr, google_event_id,
|
||||||
|
source_system, sync_strategy, sync_status, last_sync, created_at
|
||||||
|
FROM calendar_sync
|
||||||
|
WHERE advoware_frnr = $1
|
||||||
|
ORDER BY sync_id
|
||||||
|
""",
|
||||||
|
int(frnr)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
print(f"\nNo sync entries found for frNr: {frnr}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n=== Sync Information for frNr: {frnr} ===")
|
||||||
|
print(f"Found {len(rows)} sync entr{'y' if len(rows) == 1 else 'ies'}")
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
print(f"\nSync ID: {row['sync_id']}")
|
||||||
|
print(f" Employee: {row['employee_kuerzel']}")
|
||||||
|
print(f" Advoware frNr: {row['advoware_frnr']}")
|
||||||
|
print(f" Google Event ID: {row['google_event_id']}")
|
||||||
|
print(f" Source System: {row['source_system']}")
|
||||||
|
print(f" Sync Strategy: {row['sync_strategy']}")
|
||||||
|
print(f" Sync Status: {row['sync_status']}")
|
||||||
|
print(f" Last Sync: {row['last_sync']}")
|
||||||
|
print(f" Created: {row['created_at']}")
|
||||||
|
print(f" Updated: {row['created_at']}") # Using created_at as updated_at doesn't exist
|
||||||
|
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
|
async def query_event(event_id):
|
||||||
|
"""Query sync information for a specific Google Event ID."""
|
||||||
|
conn = await connect_db()
|
||||||
|
try:
|
||||||
|
# Find sync entry for this event ID
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
"""
|
||||||
|
SELECT sync_id, employee_kuerzel, advoware_frnr, google_event_id,
|
||||||
|
source_system, sync_strategy, sync_status, last_sync, created_at
|
||||||
|
FROM calendar_sync
|
||||||
|
WHERE google_event_id = $1
|
||||||
|
""",
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
print(f"\nNo sync entry found for Google Event ID: {event_id}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n=== Sync Information for Google Event ID: {event_id} ===")
|
||||||
|
print(f"Sync ID: {row['sync_id']}")
|
||||||
|
print(f" Employee: {row['employee_kuerzel']}")
|
||||||
|
print(f" Advoware frNr: {row['advoware_frnr']}")
|
||||||
|
print(f" Google Event ID: {row['google_event_id']}")
|
||||||
|
print(f" Source System: {row['source_system']}")
|
||||||
|
print(f" Sync Strategy: {row['sync_strategy']}")
|
||||||
|
print(f" Sync Status: {row['sync_status']}")
|
||||||
|
print(f" Last Sync: {row['last_sync']}")
|
||||||
|
print(f" Created: {row['created_at']}")
|
||||||
|
print(f" Updated: {row['created_at']}") # Using created_at as updated_at doesn't exist
|
||||||
|
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
|
async def verify_sync(frnr, service, advoware_api):
|
||||||
|
"""Verify sync status for a frNr by checking both systems."""
|
||||||
|
conn = await connect_db()
|
||||||
|
try:
|
||||||
|
# Get sync entry
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
"""
|
||||||
|
SELECT sync_id, employee_kuerzel, advoware_frnr, google_event_id,
|
||||||
|
source_system, sync_strategy, sync_status, last_sync
|
||||||
|
FROM calendar_sync
|
||||||
|
WHERE advoware_frnr = $1 AND deleted = FALSE
|
||||||
|
""",
|
||||||
|
int(frnr)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
print(f"\nNo active sync entry found for frNr: {frnr}")
|
||||||
|
return
|
||||||
|
|
||||||
|
employee_kuerzel = row['employee_kuerzel']
|
||||||
|
google_event_id = row['google_event_id']
|
||||||
|
|
||||||
|
print(f"\n=== Sync Verification for frNr: {frnr} ===")
|
||||||
|
print(f"Employee: {employee_kuerzel}")
|
||||||
|
print(f"Sync Status: {row['sync_status']}")
|
||||||
|
print(f"Last Sync: {row['last_sync']}")
|
||||||
|
|
||||||
|
# Check Advoware
|
||||||
|
print(f"\n--- Checking Advoware ---")
|
||||||
|
try:
|
||||||
|
# Use frNr with a broad date range to query the appointment
|
||||||
|
advoware_result = await advoware_api.api_call(
|
||||||
|
'api/v1/advonet/Termine',
|
||||||
|
method='GET',
|
||||||
|
params={
|
||||||
|
'kuerzel': employee_kuerzel,
|
||||||
|
'frnr': int(frnr), # Use lowercase 'frnr' as per API docs
|
||||||
|
'from': '2000-01-01T00:00:00',
|
||||||
|
'to': '2030-12-31T23:59:59'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# API returns a list, find the specific appointment
|
||||||
|
target_appointment = None
|
||||||
|
if isinstance(advoware_result, list):
|
||||||
|
for appointment in advoware_result:
|
||||||
|
if str(appointment.get('frNr', '')) == str(frnr):
|
||||||
|
target_appointment = appointment
|
||||||
|
break
|
||||||
|
|
||||||
|
if target_appointment:
|
||||||
|
print("✅ Found in Advoware:")
|
||||||
|
print(f" Subject: {target_appointment.get('text', 'N/A')}")
|
||||||
|
print(f" Date: {target_appointment.get('datum', 'N/A')}")
|
||||||
|
print(f" Time: {target_appointment.get('uhrzeitVon', 'N/A')}")
|
||||||
|
print(f" End Time: {target_appointment.get('uhrzeitBis', 'N/A')}")
|
||||||
|
print(f" End Date: {target_appointment.get('datumBis', 'N/A')}")
|
||||||
|
print(f" Last Modified: {target_appointment.get('zuletztGeaendertAm', 'N/A')}")
|
||||||
|
print(f" frNr: {target_appointment.get('frNr', 'N/A')}")
|
||||||
|
advoware_exists = True
|
||||||
|
else:
|
||||||
|
print(f"❌ Not found in Advoware (checked {len(advoware_result) if isinstance(advoware_result, list) else 0} appointments)")
|
||||||
|
# Show first few appointments for debugging (limited to 5)
|
||||||
|
if isinstance(advoware_result, list) and len(advoware_result) > 0:
|
||||||
|
print(" First few appointments returned:")
|
||||||
|
for i, app in enumerate(advoware_result[:5]):
|
||||||
|
print(f" [{i}] Subject: {app.get('text', 'N/A')}")
|
||||||
|
print(f" Date: {app.get('datum', 'N/A')}")
|
||||||
|
print(f" frNr: {app.get('frNr', 'N/A')}")
|
||||||
|
advoware_exists = False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error checking Advoware: {e}")
|
||||||
|
advoware_exists = False
|
||||||
|
|
||||||
|
# Check Google Calendar
|
||||||
|
print(f"\n--- Checking Google Calendar ---")
|
||||||
|
try:
|
||||||
|
# Find the calendar
|
||||||
|
calendar_id = await ensure_google_calendar(service, employee_kuerzel)
|
||||||
|
if not calendar_id:
|
||||||
|
print(f"❌ Google calendar for {employee_kuerzel} not found")
|
||||||
|
google_exists = False
|
||||||
|
else:
|
||||||
|
# Get the event
|
||||||
|
event = service.events().get(calendarId=calendar_id, eventId=google_event_id).execute()
|
||||||
|
print("✅ Found in Google Calendar:")
|
||||||
|
print(f" Summary: {event.get('summary', 'N/A')}")
|
||||||
|
print(f" Start: {event.get('start', {}).get('dateTime', event.get('start', {}).get('date', 'N/A'))}")
|
||||||
|
print(f" End: {event.get('end', {}).get('dateTime', event.get('end', {}).get('date', 'N/A'))}")
|
||||||
|
google_exists = True
|
||||||
|
except HttpError as e:
|
||||||
|
if e.resp.status == 404:
|
||||||
|
print("❌ Not found in Google Calendar")
|
||||||
|
google_exists = False
|
||||||
|
else:
|
||||||
|
print(f"❌ Error checking Google Calendar: {e}")
|
||||||
|
google_exists = False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error checking Google Calendar: {e}")
|
||||||
|
google_exists = False
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print(f"\n--- Sync Status Summary ---")
|
||||||
|
if advoware_exists and google_exists:
|
||||||
|
print("✅ Synchronized: Exists in both systems")
|
||||||
|
elif advoware_exists and not google_exists:
|
||||||
|
print("⚠️ Out of sync: Exists in Advoware, missing in Google")
|
||||||
|
elif not advoware_exists and google_exists:
|
||||||
|
print("⚠️ Out of sync: Exists in Google, missing in Advoware")
|
||||||
|
else:
|
||||||
|
print("❌ Orphaned: Missing in both systems")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
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(" query-frnr <frnr>")
|
||||||
|
print(" Show sync information for a specific Advoware frNr")
|
||||||
|
print(" query-event <event_id>")
|
||||||
|
print(" Show sync information for a specific Google Event ID")
|
||||||
|
print(" verify-sync <frnr>")
|
||||||
|
print(" Verify sync status by checking both Advoware and Google")
|
||||||
|
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")
|
||||||
|
print(" python audit_calendar_sync.py query-frnr 12345")
|
||||||
|
print(" python audit_calendar_sync.py query-event abc123@google.com")
|
||||||
|
print(" python audit_calendar_sync.py verify-sync 12345")
|
||||||
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:
|
|
||||||
# Delete calendar mode
|
|
||||||
try:
|
try:
|
||||||
service = await get_google_service()
|
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)
|
||||||
|
|
||||||
|
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:
|
else:
|
||||||
# Audit mode
|
print("No duplicates to delete.")
|
||||||
try:
|
|
||||||
await audit_calendar_sync(employee_kuerzel, check_system, delete_orphaned_google)
|
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.")
|
||||||
|
|
||||||
|
elif command == 'query-frnr':
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
print("Usage: python audit_calendar_sync.py query-frnr <frnr>")
|
||||||
|
sys.exit(1)
|
||||||
|
frnr = sys.argv[2]
|
||||||
|
await query_frnr(frnr)
|
||||||
|
|
||||||
|
elif command == 'query-event':
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
print("Usage: python audit_calendar_sync.py query-event <event_id>")
|
||||||
|
sys.exit(1)
|
||||||
|
event_id = sys.argv[2]
|
||||||
|
await query_event(event_id)
|
||||||
|
|
||||||
|
elif command == 'verify-sync':
|
||||||
|
if len(sys.argv) < 3:
|
||||||
|
print("Usage: python audit_calendar_sync.py verify-sync <frnr>")
|
||||||
|
sys.exit(1)
|
||||||
|
frnr = sys.argv[2]
|
||||||
|
advoware_api = AdvowareAPI({})
|
||||||
|
await verify_sync(frnr, service, advoware_api)
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"Unknown command: {command}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Audit failed: {e}")
|
logger.error(f"Command failed: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -167,9 +167,20 @@ async def ensure_google_calendar(service, employee_kuerzel, context=None):
|
|||||||
try:
|
try:
|
||||||
# Enforce rate limiting for calendar list fetch
|
# Enforce rate limiting for calendar list fetch
|
||||||
await enforce_global_rate_limit(context)
|
await enforce_global_rate_limit(context)
|
||||||
calendar_list = service.calendarList().list().execute()
|
|
||||||
|
# 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
|
||||||
|
|
||||||
calendar_id = None
|
calendar_id = None
|
||||||
for calendar in calendar_list.get('items', []):
|
for calendar in all_calendars:
|
||||||
if calendar['summary'] == calendar_name:
|
if calendar['summary'] == calendar_name:
|
||||||
calendar_id = calendar['id']
|
calendar_id = calendar['id']
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -28,5 +28,31 @@
|
|||||||
"sourceHandlePosition": "right"
|
"sourceHandlePosition": "right"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "advoware-calendar-sync",
|
||||||
|
"config": {
|
||||||
|
"steps/advoware_cal_sync/calendar_sync_cron_step.py": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"sourceHandlePosition": "right"
|
||||||
|
},
|
||||||
|
"steps/advoware_cal_sync/calendar_sync_api_step.py": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 100,
|
||||||
|
"sourceHandlePosition": "right"
|
||||||
|
},
|
||||||
|
"steps/advoware_cal_sync/calendar_sync_all_step.py": {
|
||||||
|
"x": 300,
|
||||||
|
"y": 50,
|
||||||
|
"targetHandlePosition": "left",
|
||||||
|
"sourceHandlePosition": "right"
|
||||||
|
},
|
||||||
|
"steps/advoware_cal_sync/calendar_sync_event_step.py": {
|
||||||
|
"x": 600,
|
||||||
|
"y": 50,
|
||||||
|
"targetHandlePosition": "left"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
0
query_db.py
Normal file
0
query_db.py
Normal file
Reference in New Issue
Block a user