119 lines
3.9 KiB
Python
119 lines
3.9 KiB
Python
"""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)."""
|
||
|
|
...
|