objects: Values the core takes and returns#

The values the core takes and returns.

A change set, whole or one page of it.

class calendaring_sync.objects.change_set.ChangeSet(scope: str, since: str | None, token: str, changes: Mapping[str, Changed | Removed | Unchanged], full_listing: bool = False, truncated: bool = False)[source]#

Bases: object

One change set, or one truncated page of it, for one scope.

scope#

The id of the scope it covers.

Type:

str

since#

The sync token it was fetched from, None for a scope that has none. Every page of a full listing carries the scope’s token from before the listing.

Type:

str | None

token#

The token the scope moves to. On a truncated page of an incremental change set that’s the page’s checkpoint. Every page of a full listing carries the same token, the one the listing read before it started, and the scope moves to it when the last page is ingested.

Type:

str

changes#

Each object id mapped to how the object changed. An object appears once. The change set keeps a read-only copy, so changing the mapping it was built from afterwards changes nothing.

Type:

collections.abc.Mapping[str, calendaring_sync.objects.change.Changed | calendaring_sync.objects.change.Removed | calendaring_sync.objects.change.Unchanged]

full_listing#

Whether it lists every object in the scope, rather than only what changed.

Type:

bool

truncated#

Whether more pages follow it.

Type:

bool

Raises:
  • TypeError – If a field has the wrong type.

  • LoneSurrogateError – If the scope id, a token or an object id holds a lone surrogate.

  • ValueError – If an incremental change set reports an object Unchanged, which only a full listing can.

The three ways an object can appear in a change set.

calendaring_sync.objects.change.Change: TypeAlias = calendaring_sync.objects.change.Changed | calendaring_sync.objects.change.Removed | calendaring_sync.objects.change.Unchanged#

Any one of the three.

class calendaring_sync.objects.change.Changed(version: str | None, fingerprint: str, content: bytes, containers: frozenset[str] = frozenset({}), logical_key: str | None = None)[source]#

Bases: object

The object changed on the server, or is new there.

version#

The object’s version, or None if the server gives none.

Type:

str | None

fingerprint#

The object’s fingerprint, computed by the adapter.

Type:

str

content#

The object’s content, the bytes exactly as the server sent them.

Type:

bytes

containers#

The containers the object belongs to.

Type:

frozenset[str]

logical_key#

The object’s logical key, if it has one.

Type:

str | None

Raises:

Examples

An adapter reports an object the server changed, and one it removed, in a change set from token t1 to t2:

>>> from calendaring_sync import Changed, ChangeSet, Removed
>>> changes = {"a": Changed("v2", "fp-a2", b"Team meeting, moved"), "b": Removed()}
>>> change_set = ChangeSet("work", "t1", "t2", changes)
>>> change_set.changes["a"].content
b'Team meeting, moved'
class calendaring_sync.objects.change.Removed[source]#

Bases: object

The object is gone from the server.

class calendaring_sync.objects.change.Unchanged[source]#

Bases: object

The object hasn’t changed since the version the store holds.

Only a full listing reports this, so the object isn’t fetched again.

A record: everything the core stores about one object.

class calendaring_sync.objects.record.Record(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)[source]#

Bases: object

What the store keeps about one object on the server.

From version to logical_key, the fields hold the synced copy: the object as your application and the server last agreed on it. Your pending edit is kept beside it in edit, and the server’s newer copy, when it conflicts with your edit, in conflict. A record in conflict therefore holds all three versions of the object: the synced copy, yours, and the server’s.

local_key#

The local key the core gave the record. It never changes, and is never given to another record.

Type:

int

scope#

The id of the scope the record belongs to.

Type:

str

object_id#

The object id, or None until the server assigns one to an object you created.

Type:

str | None

state#

The record’s state.

Type:

calendaring_sync.objects.record.RecordState

version#

The 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; server_version gives whichever is current.

Type:

str | None

fingerprint#

The fingerprint of content, or None after a fingerprint scheme change until the object is listed again.

Type:

str | None

content#

The content of the synced copy, the bytes the server sent, in the scope’s media type. None for an object you created that the server doesn’t have yet.

Type:

bytes | None

containers#

The containers of the synced copy.

Type:

frozenset[str]

logical_key#

The logical key of the synced copy, if it has one.

Type:

str | None

synced_at#

When an ingest last applied a change to the record, or None if none has.

Type:

datetime.datetime | None

last_entry#

The number of the last change feed entry about the record, 0 if there’s none.

Type:

int

edit#

The record’s pending edit, held while it’s pending, in flight or in conflict, and None otherwise.

Type:

calendaring_sync.objects.edit.LocalEdit | None

conflict#

The server’s copy in an open conflict, held while the record is in conflict, and None otherwise.

Type:

calendaring_sync.objects.conflict.Conflict | None

claims#

How many times a push of the record was claimed. How a push ended only counts while no later claim has been made.

Type:

int

property is_create: bool#

Whether the record is an object you created that the server doesn’t have yet.

property is_refused: bool#

Whether the server refused this record’s create or change for its logical key.

property is_refused_change: bool#

Whether the server refused this record’s change for its logical key.

The server then still holds the synced copy, and the conflict holds no copy of its own.

classmethod new(scope_id: str, object_id: str | None = None) → Self[source]#

Return a record the store doesn’t hold yet, with nothing synced.

Its local_key is 0 until a store’s insert_record() gives it one; fill in the rest with synced_to(), or with an edit.

Examples

>>> from calendaring_sync import Record
>>> record = Record.new("work", "a")
>>> record.local_key, record.object_id, record.content
(0, 'a', None)
removed_at(at: datetime) → Self[source]#

Return the record once its object is gone from the server, with nothing left to push.

It keeps its synced copy, so a reader of the change feed can still see what was removed.

Examples

>>> from datetime import UTC, datetime
>>> from calendaring_sync import Record
>>> Record.new("work", "a").removed_at(datetime(2026, 11, 2, tzinfo=UTC)).state
<RecordState.REMOVED: 'removed'>
property server_logical_key: str | None#

The logical key of the server’s copy, None if the server holds none.

As with server_version, that’s the conflict’s while the record is in conflict, and the synced one for a change the server refused. A create the server doesn’t have has none.

property server_version: str | None#

The version of the server’s copy, None if the store knows none.

That’s the conflict’s while the record is in conflict, but the synced one for a change the server refused, and None once the server removed the object.

synced_to(copy: Changed | LocalEdit | Conflict, at: datetime) → Self[source]#

Return the record synced to copy, with no edit or conflict left.

copy is what with_copy() takes.

Examples

>>> from datetime import UTC, datetime
>>> from calendaring_sync import Changed, Record
>>> change = Changed("v1", "fp-a", b"Team meeting")
>>> Record.new("work", "a").synced_to(change, datetime(2026, 11, 2, tzinfo=UTC)).state
<RecordState.SYNCED: 'synced'>
with_copy(copy: Changed | LocalEdit | Conflict, at: datetime) → Self[source]#

Return the record whose synced copy is copy, synced at at.

Its state, edit and conflict stay as they are. copy is one of:

  • a change from the server, at its version

  • your edit, once the server applied it, with no version: the server may store it rewritten, so the next ingest brings its version

  • a conflict’s server side, at its version

Examples

>>> from datetime import UTC, datetime
>>> from calendaring_sync import Changed, Record
>>> change = Changed("v2", "fp-a2", b"Team meeting, moved")
>>> moved = Record.new("work", "a").with_copy(change, datetime(2026, 11, 2, tzinfo=UTC))
>>> moved.version, moved.content
('v2', b'Team meeting, moved')
class calendaring_sync.objects.record.RecordState(*values)[source]#

Bases: StrEnum

The five states a record can be in.

CONFLICT = 'conflict'#

Holds an open conflict.

IN_FLIGHT = 'in_flight'#

Its push was claimed, and how it ended isn’t known yet.

PENDING = 'pending'#

Holds a pending edit.

REMOVED = 'removed'#

Gone from the server, kept until it’s purged or the object comes back, on the server or as a create at its id.

SYNCED = 'synced'#

Matches the server’s copy.

calendaring_sync.objects.record.SyncedCopy: TypeAlias = calendaring_sync.objects.change.Changed | calendaring_sync.objects.edit.LocalEdit | calendaring_sync.objects.conflict.Conflict#

A copy of an object that a record can be synced to.

A local edit your application recorded and the server doesn’t have yet.

class calendaring_sync.objects.edit.EditFields(fingerprint: str, containers: frozenset[str] = frozenset({}), logical_key: str | None = None)[source]#

Bases: object

What an edit records about an object beside its content.

An adapter reads these from content the same way for the server’s copy and for an edit, so an edit back to the synced copy cancels, and the server’s copy of your own edit isn’t a conflict. To build them for content you made, such as a Merge, call the adapter’s edit_fields().

fingerprint#

The content’s fingerprint.

Type:

str

containers#

The containers the object belongs to.

Type:

frozenset[str]

logical_key#

The object’s logical key, if it has one.

Type:

str | None

Raises:

TypeError – If a field has the wrong type.

Examples

>>> from calendaring_sync import EditFields
>>> fields = EditFields("fp-a2", frozenset({"work"}), "meeting-1")
>>> fields.logical_key
'meeting-1'
class calendaring_sync.objects.edit.EditKind(*values)[source]#

Bases: StrEnum

What a local edit does to its object.

CHANGE = 'change'#

Changes an object the server has.

CREATE = 'create'#

Creates an object the server doesn’t have yet.

DELETE = 'delete'#

Deletes an object the server has.

class calendaring_sync.objects.edit.LocalEdit(kind: EditKind, content: bytes | None, fingerprint: str | None, containers: frozenset[str], logical_key: str | None, recorded_at: datetime)[source]#

Bases: object

One pending edit of a record.

kind#

What the edit does.

Type:

calendaring_sync.objects.edit.EditKind

content#

The object’s content after the edit, None for a delete.

Type:

bytes | None

fingerprint#

The fingerprint of content, None for a delete, or after a fingerprint scheme change until the object is edited again.

Type:

str | None

containers#

The containers the object belongs to after the edit.

Type:

frozenset[str]

logical_key#

The object’s logical key after the edit, if it has one.

Type:

str | None

recorded_at#

When the edit was last recorded.

Type:

datetime.datetime

The server’s copy in a conflict, kept on its record until resolved.

class calendaring_sync.objects.conflict.Conflict(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)[source]#

Bases: object

The server’s copy of an object, kept where it conflicts with your edit.

Together with its record’s synced copy and Record.edit, it makes up the conflict record: the record’s content is the object as both sides last agreed on it, edit.content is yours, and 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.

kind#

How the two sides disagree.

Type:

calendaring_sync.objects.conflict.ConflictKind

remote_version#

The version of the server’s copy, None if it has none, the server removed the object, or after a fingerprint scheme change until the server’s copy is fetched again.

Type:

str | None

remote_fingerprint#

The fingerprint of the server’s copy, None if the server removed the object, or after a fingerprint scheme change until the server’s copy is fetched again.

Type:

str | None

remote_content#

The server’s copy, None if the server removed the object.

Type:

bytes | None

remote_containers#

The containers of the server’s copy.

Type:

frozenset[str]

remote_logical_key#

The logical key of the server’s copy, if it has one.

Type:

str | None

opened_at#

When the conflict was first stored.

Type:

datetime.datetime

holder#

For a create refused because another object holds its logical key, that object’s object id when the server named it; None otherwise.

Type:

str | None

classmethod from_change(kind: ConflictKind, change: Changed | Removed, opened_at: datetime) → Self[source]#

Return a conflict whose server side is what a change reported.

class calendaring_sync.objects.conflict.ConflictKind(*values)[source]#

Bases: StrEnum

How the server’s copy and the local edit disagree.

BOTH_CHANGED = 'both_changed'#

Both sides changed the object.

BOTH_CREATED = 'both_created'#

The server has a new object at the id your unsent create chose.

DELETED_LOCALLY = 'deleted_locally'#

You deleted the object, and the server changed it.

LOGICAL_KEY_TAKEN = 'logical_key_taken'#

The server refused your create, or your change, because another object holds its logical key. Once that object stops holding it, the edit is pending again.

REMOVED_REMOTELY = 'removed_remotely'#

The server removed the object, and you changed it.

How a push ended, as the adapter reports it.

class calendaring_sync.objects.push.Applied(object_id: str | None = None)[source]#

Bases:

The server applied the edit.

object_id#

The object id the server gave the object, for a create whose id the server picks, or when the server moved the object to another id. None keeps the record’s id.

Type:

str | None

Raises:

Examples

An adapter reports a create the server stored under an id it picked:

>>> from calendaring_sync import Applied
>>> Applied("event-17").object_id
'event-17'
class calendaring_sync.objects.push.Gone[source]#

Bases:

The object no longer exists on the server.

A delete is then done; a change becomes a conflict.

class calendaring_sync.objects.push.LogicalKeyTaken(holder: str | None = None)[source]#

Bases:

The server refused a create or a change because another object holds its logical key.

holder#

The object id of the object that holds the key, when the server names it; None when it doesn’t.

Type:

str | None

Raises:
calendaring_sync.objects.push.PushOutcome: TypeAlias = calendaring_sync.objects.push.Applied | calendaring_sync.objects.push.Rejected | calendaring_sync.objects.push.Gone | calendaring_sync.objects.push.LogicalKeyTaken | calendaring_sync.objects.push.Unknown#

Any one of the five.

class calendaring_sync.objects.push.Rejected[source]#

Bases:

The server refused the write because something changed since your last sync.

The edit stays pending. The next ingest makes it a conflict only if the object the edit is for changed, and otherwise it’s pushed again.

class calendaring_sync.objects.push.Unknown[source]#

Bases:

The request may or may not have reached the server.

The push stays in flight, and the next complete ingest settles it from what the server then holds.

How your application resolves a conflict.

class calendaring_sync.objects.resolution.Delete[source]#

Bases:

Delete the object, whatever the server’s copy holds.

Where the server has the object, the delete goes out over its current copy, and for a change the server refused for its logical key, that’s the object the change was for. Where the server doesn’t have it, the record is removed, or deleted if the server never had it.

class calendaring_sync.objects.resolution.KeepLocal[source]#

Bases:

Push your local edit as it is, over the server’s current copy.

Where the server removed the object, a change goes out as a create. An edit the server refused because another object holds its logical key can’t go out unchanged, so this refuses it.

class calendaring_sync.objects.resolution.KeepServer[source]#

Bases:

Keep the server’s copy, and drop your local edit.

Where the server has the object, the record takes the server’s copy. Where it doesn’t, because the server removed the object or refused your create, the record is removed, or deleted if the server never had it.

class calendaring_sync.objects.resolution.Merge(content: bytes, fields: EditFields)[source]#

Bases:

Push content you merged from the conflict’s versions, over the server’s current copy.

A record in conflict holds three versions of its object: the synced copy both sides last agreed on, in content; your local edit, in edit; and the server’s copy, in conflict. Your application merges them into new content, and reads its fields with the adapter’s edit_fields(), so they match what a sync reads from the server’s copy. Where the server removed the object, the merge goes out as a create.

content#

The merged content.

Type:

bytes

fields#

Its fingerprint, containers and logical key.

Type:

calendaring_sync.objects.edit.EditFields

Raises:

Examples

Your application keeps the room your edit set and the time the server’s copy moved to, in one copy. An adapter would read its fields; these are made up:

>>> from calendaring_sync import EditFields, Merge
>>> content = b"Team meeting, room 3, moved"
>>> merged = Merge(content, EditFields("fp-merged"))
>>> merged.fields.fingerprint
'fp-merged'
calendaring_sync.objects.resolution.Resolution: TypeAlias = calendaring_sync.objects.resolution.KeepServer | calendaring_sync.objects.resolution.KeepLocal | calendaring_sync.objects.resolution.Merge | calendaring_sync.objects.resolution.Delete | calendaring_sync.objects.resolution.UpdateHolder#

Any one of the five.

class calendaring_sync.objects.resolution.ResolvedConflicts(resolved: tuple[int, ...] = (), left_open: tuple[int, ...] = (), changed: tuple[int, ...] = ())[source]#

Bases: object

What resolve_conflicts() did with each conflict.

Each field holds local keys, in the order the conflicts were passed to your resolver.

resolved#

The conflicts resolved as your resolver answered.

Type:

tuple[int, …]

left_open#

The conflicts your resolver answered None for.

Type:

tuple[int, …]

changed#

The conflicts that changed while your resolver ran, so its answer wasn’t applied. The next call passes them again.

Type:

tuple[int, …]

class calendaring_sync.objects.resolution.UpdateHolder[source]#

Bases:

Push a refused edit as a change to the object that holds its logical key.

Only for a create or a change refused because another object holds its logical key. A refused create then leaves nothing, and the object a refused change was for stays as it was synced.

A scope as the store keeps it.

class calendaring_sync.objects.scope.Scope(id: str, token: str | None, fingerprint_scheme: str, media_type: str, last_sync_at: datetime | None = None, last_full_listing_at: datetime | None = None, listing_token: str | None = None)[source]#

Bases: object

One scope.

id#

The id the adapter gave the scope.

Type:

str

token#

The scope’s sync token, or None before its first full listing finishes and after a reset.

Type:

str | None

fingerprint_scheme#

The fingerprint scheme of the scope’s records.

Type:

str

media_type#

The media type of the scope’s content.

Type:

str

last_sync_at#

When an ingest of the scope last finished, or None.

Type:

datetime.datetime | None

last_full_listing_at#

When a full listing of the scope last finished, or None.

Type:

datetime.datetime | None

listing_token#

The token of the full listing in progress, or None. A page of any other listing is refused, so two processes listing the scope at once can’t mix their results.

Type:

str | None

An entry in the change feed.

class calendaring_sync.objects.feed.FeedEntry(number: int, scope: str, kind: FeedEntryKind, local_key: int | None, object_id: str | None, at: datetime)[source]#

Bases: object

One feed entry.

number#

The entry’s number. Each entry’s is larger than the one before it.

Type:

int

scope#

The id of the scope the entry is about.

Type:

str

kind#

What happened.

Type:

calendaring_sync.objects.feed.FeedEntryKind

local_key#

The local key of the record the entry is about, None for a dropped scope. The record may since have been purged, or deleted if it was a create the server never had.

Type:

int | None

object_id#

The record’s object id when the entry was written, None for a dropped scope.

Type:

str | None

at#

When the entry was written.

Type:

datetime.datetime

class calendaring_sync.objects.feed.FeedEntryKind(*values)[source]#

Bases: StrEnum

What a feed entry records.

CHANGED = 'changed'#

A remote change was applied, to an object that’s new, changed, or back on the server.

CONFLICT_OPENED = 'conflict_opened'#

A conflict was stored.

CONFLICT_RESOLVED = 'conflict_resolved'#

An open conflict ended, and the record holds what it ended with.

CONFLICT_UPDATED = 'conflict_updated'#

The server’s copy in an open conflict changed.

PUSHED = 'pushed'#

the record holds your edit as synced, or is removed for a delete.

Type:

A push reached the server

PUSH_FAILED = 'push_failed'#

A push didn’t go through, and the edit is pending again for a later sync.

REMOVED = 'removed'#

A remote removal was applied.

SCOPE_DROPPED = 'scope_dropped'#

A scope was dropped, with every record in it.

What drop_scope() discards.

class calendaring_sync.objects.dropped_scope.DroppedScope(scope: str, pending: tuple[Record, ...], conflicts: tuple[Record, ...])[source]#

Bases: object

The local work a drop discarded, for your application to save or show.

scope#

The id of the dropped scope.

Type:

str

pending#

The records that held a pending edit, including ones in flight.

Type:

tuple[calendaring_sync.objects.record.Record, …]

conflicts#

The records that held an open conflict.

Type:

tuple[calendaring_sync.objects.record.Record, …]

Last updated on 2026-10-27.