angee.parties.backends
Directory backend contract — sync a contacts source into parties.
A :class:~angee.parties.models.Directory (an integrate.Integration child + Bridge) selects one DirectoryBackend by registry key. The backend does the per-source transport + parse in two steps: :meth:DirectoryBackend.discover enumerates the address books (each becomes a :class:~angee.parties.models.Folder) and :meth:DirectoryBackend.fetch_contacts returns one address book's contacts as neutral :class:ParsedContact rows. The map onto parties — the idempotent (folder, source_uid) upsert and the purge of vanished contacts — is owned by Directory.sync + the parties managers, so every source shares one write path. The parties_integrate_carddav addon contributes the carddav backend; the manual null-object keeps the registry non-empty when no source is installed.
ParsedPhoto
@dataclass(frozen=True)
class ParsedPhoto()A contact photo parsed from a source — inline bytes or a remote URI.
The pure parse step decodes inline (base64 / data-URI) photos to data and records a uri for remote ones; the backend's transport step resolves any uri to data before the map ingests it through the storage File owner.
ParsedAddressbook
@dataclass(frozen=True)
class ParsedAddressbook()One address-book collection discovered on a source.
href is its stable collection URL (the folder dedup key). ctag is the collection-version cursor used today — an unchanged ctag lets the sync skip the whole collection. sync_token is reserved for a future RFC 6578 sync-collection delta fetch; the carddav backend does not yet populate or use it (every sync is a full list + multiget), so it stays "" for now.
ParsedAddress
@dataclass(frozen=True)
class ParsedAddress()One physical address parsed from a directory source.
ParsedContact
@dataclass(frozen=True)
class ParsedContact()One contact parsed from a directory source, neutral of the wire format.
uid is the source's stable id (a vCard UID); it is the per-folder idempotency key, so it must be stable across syncs. etag is the per-resource version (for change detection) and raw_vcard is kept for lossless round-trip. Emails/phones are (value, label, is_preferred) triples. organization/department/title/role carry the affiliation; birthday/anniversary are resolved dates; photo is the avatar.
DirectoryBackend
class DirectoryBackend(BridgeImpl, HttpClientMixin)Abstract backend that discovers and fetches a contacts source.
self.bridge is the Directory row — its config carries the server URL and self.bridge.credential authenticates — and self.http is the shared SSRF-pinned client (a self-hosted source passes allow_private=True).
probe
def probe() -> NoneValidate the source connection before a directory persists (no-op by default).
A source backend overrides this to fail fast on a bad URL or rejected credentials, so the connect mutation never saves a directory that can never sync. It must raise on a bad connection and return None on success.
discover
def discover() -> list[ParsedAddressbook]Return every address-book collection the source exposes.
fetch_contacts
def fetch_contacts(addressbook: ParsedAddressbook) -> list[ParsedContact]Return every contact in one address book as neutral dataclasses.
ManualDirectoryBackend
class ManualDirectoryBackend(DirectoryBackend)The null-object default: a directory with no source backend syncs nothing.
Keeps ANGEE_DIRECTORY_BACKEND_CLASSES non-empty when no source addon is installed (ImplClassField requires a non-empty registry), so the GraphQL enum is never empty and a draft directory always has a selectable backend.
discover
def discover() -> list[ParsedAddressbook]Return no address books — a manual directory is populated by hand.
fetch_contacts
def fetch_contacts(addressbook: ParsedAddressbook) -> list[ParsedContact]Return no contacts — a manual directory is populated by hand.