| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 |
- # -*- coding: utf-8 -*-
- """
- Calendar Sync Manager.
- Periodische Synchronisation der Kalender-Events von Cloud-Providern.
- Cache-Verwaltung und Event-Abfragen.
- """
- import asyncio
- from datetime import datetime, timedelta
- from trixy_core.utils.debug import pinfo, pdebug, perror, pwarn
- from plugins.calendar.models import CalendarEvent, EventStatus
- from plugins.calendar.providers.base import CalendarProvider
- class CalendarSyncManager:
- """
- Verwaltet die Synchronisation von Kalender-Events.
- - In-Memory Cache pro Kalender-Key
- - Lock gegen parallele Syncs
- - Fehlerbehandlung pro Kalender (ein Fehler blockiert nicht die anderen)
- """
- def __init__(
- self,
- providers: dict[str, CalendarProvider],
- calendars_config: dict,
- lookahead_hours: int = 48,
- max_retries: int = 3,
- ) -> None:
- """
- Args:
- providers: Provider-Instanzen {name: provider}
- calendars_config: Kalender-Konfiguration aus config.json
- lookahead_hours: Zeitfenster fuer Event-Abfrage in Stunden
- max_retries: Maximale Wiederholungen bei Fehlern
- """
- self._providers = providers
- self._calendars_config = calendars_config
- self._lookahead_hours = lookahead_hours
- self._max_retries = max_retries
- self._event_cache: dict[str, list[CalendarEvent]] = {}
- self._sync_lock = asyncio.Lock()
- self._last_sync: datetime | None = None
- @property
- def last_sync(self) -> datetime | None:
- """Zeitpunkt der letzten Synchronisation."""
- return self._last_sync
- async def sync_all(self) -> dict[str, int]:
- """
- Synchronisiert alle konfigurierten Kalender.
- Returns:
- Dict mit {calendar_key: anzahl_events}
- """
- async with self._sync_lock:
- results: dict[str, int] = {}
- for cal_key, cal_config in self._calendars_config.items():
- try:
- events = await self.sync_calendar(cal_key)
- results[cal_key] = len(events)
- except Exception as e:
- perror(f"CalendarSync: Fehler bei '{cal_key}': {e}")
- results[cal_key] = -1
- self._last_sync = datetime.now()
- total = sum(v for v in results.values() if v >= 0)
- pinfo(f"CalendarSync: {total} Events aus {len(results)} Kalendern synchronisiert")
- return results
- async def sync_calendar(self, calendar_key: str) -> list[CalendarEvent]:
- """
- Synchronisiert einen einzelnen Kalender.
- Args:
- calendar_key: Kalender-Schluessel aus Config
- Returns:
- Liste der synchronisierten Events
- """
- cal_config = self._calendars_config.get(calendar_key, {})
- if not cal_config:
- pwarn(f"CalendarSync: Kalender '{calendar_key}' nicht in Config gefunden")
- return []
- provider_name = cal_config.get("provider", "")
- provider = self._providers.get(provider_name)
- if not provider:
- pwarn(f"CalendarSync: Provider '{provider_name}' fuer '{calendar_key}' nicht verfuegbar")
- return []
- if not provider.is_connected:
- # Reconnect-Versuch
- pdebug(f"CalendarSync: Provider '{provider_name}' nicht verbunden, versuche erneut...")
- connected = await provider.connect()
- if not connected:
- pwarn(f"CalendarSync: Provider '{provider_name}' weiterhin nicht erreichbar")
- return self._event_cache.get(calendar_key, [])
- calendar_id = cal_config.get("calendar_id", "")
- user = cal_config.get("user", "")
- # User-Email aus Provider-Config aufloeser
- user_email = self._resolve_user_email(provider_name, user)
- now = datetime.now()
- end = now + timedelta(hours=self._lookahead_hours)
- # Retry-Logik
- last_error: Exception | None = None
- for attempt in range(self._max_retries):
- try:
- events = await provider.fetch_events(
- calendar_id=calendar_id,
- start=now,
- end=end,
- user=user_email,
- )
- # Abgesagte Termine filtern
- events = [e for e in events if e.status != EventStatus.CANCELLED]
- # Cache aktualisieren
- self._event_cache[calendar_key] = events
- pdebug(f"CalendarSync: '{calendar_key}' -> {len(events)} Events")
- return events
- except Exception as e:
- last_error = e
- if attempt < self._max_retries - 1:
- pdebug(f"CalendarSync: Retry {attempt + 1}/{self._max_retries} fuer '{calendar_key}'")
- await asyncio.sleep(2 ** attempt)
- perror(f"CalendarSync: '{calendar_key}' fehlgeschlagen nach {self._max_retries} Versuchen: {last_error}")
- return self._event_cache.get(calendar_key, [])
- def get_events(
- self,
- calendar_keys: list[str],
- start: datetime | None = None,
- end: datetime | None = None,
- ) -> list[CalendarEvent]:
- """
- Gibt gecachte Events fuer die angegebenen Kalender zurueck.
- Args:
- calendar_keys: Liste von Kalender-Schluesseln
- start: Optionaler Start-Filter
- end: Optionaler End-Filter
- Returns:
- Sortierte Liste von CalendarEvent
- """
- all_events: list[CalendarEvent] = []
- for key in calendar_keys:
- events = self._event_cache.get(key, [])
- all_events.extend(events)
- # Zeitfilter anwenden
- if start:
- all_events = [e for e in all_events if e.end >= start]
- if end:
- all_events = [e for e in all_events if e.start <= end]
- # Nach Startzeit sortieren
- all_events.sort(key=lambda e: e.start)
- return all_events
- def get_next_event(self, calendar_keys: list[str]) -> CalendarEvent | None:
- """
- Gibt den naechsten bevorstehenden Termin zurueck.
- Args:
- calendar_keys: Liste von Kalender-Schluesseln
- Returns:
- Naechstes CalendarEvent oder None
- """
- now = datetime.now()
- events = self.get_events(calendar_keys, start=now)
- return events[0] if events else None
- def search_events(
- self,
- calendar_keys: list[str],
- query: str,
- ) -> list[CalendarEvent]:
- """
- Sucht Events anhand eines Suchbegriffs.
- Durchsucht Titel, Beschreibung und Ort.
- Args:
- calendar_keys: Liste von Kalender-Schluesseln
- query: Suchbegriff
- Returns:
- Gefundene Events, sortiert nach Startzeit
- """
- now = datetime.now()
- events = self.get_events(calendar_keys, start=now)
- query_lower = query.lower()
- results: list[CalendarEvent] = []
- for event in events:
- if (
- query_lower in event.title.lower()
- or query_lower in event.description.lower()
- or query_lower in event.location.lower()
- ):
- results.append(event)
- return results
- def get_all_cached_events(self) -> list[CalendarEvent]:
- """Gibt alle gecachten Events zurueck (fuer Notifications)."""
- all_events: list[CalendarEvent] = []
- for events in self._event_cache.values():
- all_events.extend(events)
- return all_events
- def get_calendar_key_for_event(self, event: CalendarEvent) -> str:
- """Gibt den Kalender-Key fuer ein Event zurueck."""
- for key, events in self._event_cache.items():
- if any(e.event_id == event.event_id for e in events):
- return key
- return ""
- def _resolve_user_email(self, provider_name: str, user: str) -> str:
- """Loest einen User-Key in eine Email-Adresse auf."""
- if not user:
- return ""
- provider = self._providers.get(provider_name)
- if not provider:
- return user
- user_mapping = provider.config.get("user_mapping", {})
- return user_mapping.get(user, user)
|