Source code for calendaring_sync.objects.conflict
# SPDX-FileCopyrightText: 2026 calendaring-sync contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
"""The server's side of a :term:`conflict`, kept on its :term:`record` until it's resolved."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from enum import StrEnum
from typing import Self
from calendaring_sync.objects._data import NO_DATA, data
from calendaring_sync.objects.change import Changed, Removed
[docs]
class ConflictKind(StrEnum):
"""How the server's side and the :term:`local edit` disagree."""
#: Both sides changed the object.
BOTH_CHANGED = "both_changed"
#: The server removed the object, and you changed it.
REMOVED_REMOTELY = "removed_remotely"
#: You deleted the object, and the server changed it.
DELETED_LOCALLY = "deleted_locally"
#: The server has a new object at the id your unsent create chose.
BOTH_CREATED = "both_created"
#: The server refused your create because another object holds its
#: :term:`logical key`.
LOGICAL_KEY_TAKEN = "logical_key_taken"
[docs]
@dataclass(frozen=True)
class Conflict:
"""The server's copy of an object, kept where it conflicts with your edit.
Together with its record's synced copy and :attr:`Record.edit
<calendaring_sync.objects.record.Record.edit>`, it makes up the
:term:`conflict record`: the record's
:attr:`~calendaring_sync.objects.record.Record.content` is the object
as both sides last agreed on it, ``edit.content`` is yours, and
:attr:`remote_content` is the server's. A resolution can compare all
three, for example to keep both sides where they changed different
parts of the object.
Attributes:
kind: How the two sides disagree.
remote_version: The :term:`version` of the server's copy, ``None`` if
it has none, the server removed the object, or after a
:term:`fingerprint scheme` change until the server's copy is
fetched again.
remote_fingerprint: The :term:`fingerprint` of the server's copy,
``None`` if the server removed the object, or after a
:term:`fingerprint scheme` change until the server's copy is
fetched again.
remote_content: The server's copy, ``None`` if the server removed the
object.
remote_containers: The :term:`containers <container>` of the
server's copy.
remote_logical_key: The :term:`logical key` of the server's copy, if
it has one.
opened_at: When the conflict was first stored.
holder: For a create refused because another object holds its
:term:`logical key`, that object's :term:`object id` when the
server named it; ``None`` otherwise.
"""
kind: ConflictKind
remote_version: str | None
remote_fingerprint: str | None
remote_content: bytes | None
remote_containers: frozenset[str]
remote_logical_key: str | None
opened_at: datetime
holder: str | None = None
[docs]
@classmethod
def from_change(
cls, kind: ConflictKind, change: Changed | Removed, opened_at: datetime
) -> Self:
"""Return a conflict whose server side is what a change reported."""
if isinstance(change, Removed):
return cls(kind, None, *NO_DATA, opened_at)
return cls(kind, change.version, *data(change), opened_at)