"""What a plugin implements to serve its collections over DAV (SPEC App. E.1). The core (app/dav/routes.py) speaks the protocol; a provider only knows its own data: collections (calendars, address books) and the resources in them (one iCalendar object / one vCard each), named, with an ETag. Every method gets the signed-in user's id; providers open the user's RLS session (``sdk(name).session(user_id)``) themselves. """ from __future__ import annotations import uuid from dataclasses import dataclass, field from datetime import datetime from typing import Literal, Protocol class DavError(Exception): """A request the provider refuses: ``status`` is the HTTP answer (403 read-only, 404 missing, 409 conflict, 412 precondition failed, 415 unsupported data); ``condition`` an optional DAV precondition element (e.g. ``{urn:ietf:params:xml:ns:caldav}valid-calendar-data``).""" def __init__(self, status: int, message: str = "", condition: str | None = None) -> None: super().__init__(message or str(status)) self.status = status self.condition = condition @dataclass class Collection: id: str displayname: str sync_token: int # the latest change number; also the CTag description: str = "" color: str | None = None # "#rrggbb" read_only: bool = False components: list[str] = field(default_factory=lambda: ["VEVENT"]) # calendars only @dataclass class Resource: name: str # last path segment, e.g. "abc.ics" etag: str # without quotes data: str | None = None # the iCalendar / vCard text, when asked for @dataclass class Changes: sync_token: int changed: list[str] # resource names changed or added since the old token deleted: list[str] # resource names gone since the old token class DavProvider(Protocol): kind: Literal["calendar", "addressbook"] content_type: str # "text/calendar; charset=utf-8" / "text/vcard; charset=utf-8" extension: str # ".ics" / ".vcf" async def collections(self, user_id: uuid.UUID) -> list[Collection]: ... async def resources(self, user_id: uuid.UUID, collection_id: str) -> list[Resource]: ... async def get( self, user_id: uuid.UUID, collection_id: str, names: list[str] | None ) -> list[Resource]: ... async def put( self, user_id: uuid.UUID, collection_id: str, name: str, data: str, *, if_match: str | None, if_none_match: bool, ) -> tuple[str, bool]: """Store ``data`` as ``name``; returns (new ETag, created).""" ... async def delete( self, user_id: uuid.UUID, collection_id: str, name: str, *, if_match: str | None ) -> None: ... async def changes(self, user_id: uuid.UUID, collection_id: str, since: int) -> Changes | None: """What changed after change number ``since``; None when that token is too old to answer (the client then resyncs from scratch).""" ... async def create_collection( self, user_id: uuid.UUID, collection_id: str, displayname: str, color: str | None ) -> None: """Create a collection at the id the client chose (MKCALENDAR / MKCOL); DavError(403) when that id cannot be used.""" ... async def update_collection( self, user_id: uuid.UUID, collection_id: str, *, displayname: str | None = None, color: str | None = None, description: str | None = None, ) -> None: ... async def delete_collection(self, user_id: uuid.UUID, collection_id: str) -> None: ... async def query( self, user_id: uuid.UUID, collection_id: str, start: datetime | None, end: datetime | None, ) -> list[str] | None: """Calendars: names of resources with an occurrence overlapping the range; None when the provider cannot filter (the core then returns all).""" ...