Source code for calendaring_sync.objects.record

# SPDX-FileCopyrightText: 2026 calendaring-sync contributors
# SPDX-License-Identifier: AGPL-3.0-or-later

"""A :term:`record`: everything the core stores about one :term:`object`."""

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum

from calendaring_sync.objects.conflict import Conflict
from calendaring_sync.objects.edit import EditKind, LocalEdit


[docs] class RecordState(StrEnum): """The five states a record can be in.""" #: Matches the server's copy. SYNCED = "synced" #: Holds a :term:`pending edit`. PENDING = "pending" #: Its :term:`push` was :term:`claimed <claim>`, and how it ended isn't known yet. IN_FLIGHT = "in_flight" #: Holds an open :term:`conflict`. CONFLICT = "conflict" #: Gone from the server, kept until it's :term:`purged <purge>` or the #: object comes back, on the server or as a create at its id. REMOVED = "removed"
[docs] @dataclass(frozen=True) class Record: """What the store keeps about one object on the server. From :attr:`version` to :attr:`logical_key`, the fields hold the synced copy: the object as your application and the server last agreed on it. Your :term:`pending edit` is kept beside it in :attr:`edit`, and the server's newer copy, when it conflicts with your edit, in :attr:`conflict`. A record in conflict therefore holds all three versions of the object: the synced copy, yours, and the server's. Attributes: local_key: The :term:`local key` the core gave the record. It never changes, and is never given to another record. scope: The id of the :term:`scope` the record belongs to. object_id: The :term:`object id`, or ``None`` until the server assigns one to an object you created. state: The record's state. version: The :term:`version` the server gave the synced copy, or ``None``. While the record is in conflict, the server holds a newer copy, whose version is ``conflict.remote_version``; :attr:`server_version` gives whichever is current. fingerprint: The :term:`fingerprint` of :attr:`content`, or ``None`` after a :term:`fingerprint scheme` change until the object is listed again. content: The :term:`synced content`, the bytes the server sent, in the scope's media type. ``None`` for an object you created that the server doesn't have yet. containers: The :term:`containers <container>` of the synced copy. logical_key: The :term:`logical key` of the synced copy, if it has one. synced_at: When an :term:`ingest` last applied a change to the record, or ``None`` if none has. last_entry: The number of the last :term:`change feed` entry about the record, ``0`` if there's none. edit: The record's :term:`pending edit`, held while it's pending, in flight or in conflict, and ``None`` otherwise. conflict: The server's side of an open :term:`conflict`, held while the record is in conflict, and ``None`` otherwise. claims: How many times a :term:`push` of the record was :term:`claimed <claim>`. How a push ended only counts while no later claim has been made. """ local_key: int scope: str object_id: str | None state: RecordState version: str | None fingerprint: str | None content: bytes | None containers: frozenset[str] logical_key: str | None synced_at: datetime | None last_entry: int edit: LocalEdit | None = None conflict: Conflict | None = None claims: int = 0 @property def is_create(self) -> bool: """Whether the record is an object you created that the server doesn't have yet.""" return self.edit is not None and self.edit.kind is EditKind.CREATE @property def server_version(self) -> str | None: """The :term:`version` of the server's copy, ``None`` if the store knows none. That's the conflict's while the record is in conflict, and ``None`` once the server removed the object. """ if self.state is RecordState.REMOVED: return None if self.conflict is not None: return self.conflict.remote_version return self.version