diff --git a/docs/MODULE_ARCHITECTURE.md b/docs/MODULE_ARCHITECTURE.md index 9ce6ee1..a21b6de 100644 --- a/docs/MODULE_ARCHITECTURE.md +++ b/docs/MODULE_ARCHITECTURE.md @@ -208,7 +208,8 @@ Other stable runtime capabilities currently include: - `identity.directory` and `identity.search` - `organizations.directory` - `idm.directory` -- `calendar.outbox` and `calendar.scheduling` +- `calendar.outbox`, `calendar.scheduling`, `calendar.invitations`, and + `calendar.externalProfiles` - `poll.scheduling` - `notifications.dispatch` - `workflow.definitionContributions` and `workflow.runtimeWorker` @@ -286,7 +287,8 @@ Current named interfaces, generated from the source manifests by the workspace contract checks, are: - `addresses.contact_writer`, `addresses.lookup`, `addresses.recipient_source` -- `calendar.outbox`, `calendar.scheduling` +- `calendar.external_profiles`, `calendar.invitations`, `calendar.outbox`, + `calendar.scheduling` - `campaigns.access`, `campaigns.delivery_tasks`, `campaigns.mail_policy_context`, `campaigns.policy_context`, `campaigns.retention` diff --git a/src/govoplan_core/core/calendar.py b/src/govoplan_core/core/calendar.py index 5d88544..045673d 100644 --- a/src/govoplan_core/core/calendar.py +++ b/src/govoplan_core/core/calendar.py @@ -9,6 +9,7 @@ from typing import Protocol, runtime_checkable CAPABILITY_CALENDAR_SCHEDULING = "calendar.scheduling" CAPABILITY_CALENDAR_OUTBOX = "calendar.outbox" CAPABILITY_CALENDAR_INVITATIONS = "calendar.invitations" +CAPABILITY_CALENDAR_EXTERNAL_PROFILES = "calendar.externalProfiles" CALENDAR_AVAILABILITY_READ_SCOPE = "calendar:availability:read" CALENDAR_EVENT_WRITE_SCOPE = "calendar:event:write" @@ -90,6 +91,38 @@ class CalendarInvitationRef: degraded_reasons: tuple[str, ...] = () +@dataclass(frozen=True, slots=True) +class CalendarExternalProfileRequest: + """Connector-neutral request for a Calendar-owned external profile.""" + + profile_kind: str + calendar_id: str + endpoint_url: str + display_name: str | None = None + auth_type: str = "none" + username: str | None = None + credential_ref: str | None = None + sync_enabled: bool = True + sync_interval_seconds: int = 900 + sync_direction: str = "two_way" + conflict_policy: str = "etag" + connector_profile_ref: str | None = None + identity_mapping_ref: str | None = None + resource_calendar_ref: str | None = None + metadata: Mapping[str, object] = field(default_factory=dict) + + +@dataclass(frozen=True, slots=True) +class CalendarExternalProfileRef: + source_id: str + calendar_id: str + profile_kind: str + transport_kind: str + connector_profile_ref: str | None = None + identity_mapping_ref: str | None = None + resource_calendar_ref: str | None = None + + @runtime_checkable class CalendarSchedulingProvider(Protocol): def list_freebusy( @@ -165,6 +198,25 @@ class CalendarInvitationProvider(Protocol): ) -> CalendarInvitationRef: ... + +@runtime_checkable +class CalendarExternalProfileProvider(Protocol): + """Optional connector route for Calendar-owned groupware adapters.""" + + def supported_profiles(self) -> Sequence[Mapping[str, object]]: + ... + + def configure_profile( + self, + session: object, + *, + tenant_id: str, + user_id: str | None, + request: CalendarExternalProfileRequest, + ) -> CalendarExternalProfileRef: + ... + + def calendar_scheduling_provider(registry: object | None) -> CalendarSchedulingProvider | None: if registry is None or not hasattr(registry, "has_capability"): return None @@ -192,3 +244,18 @@ def calendar_invitation_provider( return None capability = registry.capability(CAPABILITY_CALENDAR_INVITATIONS) return capability if isinstance(capability, CalendarInvitationProvider) else None + + +def calendar_external_profile_provider( + registry: object | None, +) -> CalendarExternalProfileProvider | None: + if registry is None or not hasattr(registry, "has_capability"): + return None + if not registry.has_capability(CAPABILITY_CALENDAR_EXTERNAL_PROFILES): + return None + capability = registry.capability(CAPABILITY_CALENDAR_EXTERNAL_PROFILES) + return ( + capability + if isinstance(capability, CalendarExternalProfileProvider) + else None + )