"""Offline-first sync schemas (delta pull + idempotent push). The client keeps an opaque ``cursor`` (a server-clock timestamp). It pulls everything changed in the half-open window ``(since, cursor]`` and pushes the append-only events it accumulated offline. Events carry a client-generated ``id`` so a replay is idempotent. """ import datetime as dt import uuid from typing import Literal from pydantic import BaseModel, Field from app.api.schemas.track import TrackOut LikeValue = Literal["like", "dislike", "neutral"] # -- pull (server -> client) -------------------------------------------------- class LikeEventOut(BaseModel): id: uuid.UUID track_id: uuid.UUID value: str created_at: dt.datetime class PlayEventOut(BaseModel): id: uuid.UUID track_id: uuid.UUID played_at: dt.datetime play_duration_seconds: int | None completed: bool class PlaylistSyncOut(BaseModel): id: uuid.UUID name: str description: str | None version: int updated_at: dt.datetime track_ids: list[uuid.UUID] class SyncChangesOut(BaseModel): """Everything that changed for the caller since their last cursor. Feed ``cursor`` back as ``?since=`` on the next pull.""" cursor: dt.datetime likes: list[LikeEventOut] plays: list[PlayEventOut] playlists: list[PlaylistSyncOut] tracks: list[TrackOut] # -- push (client -> server) -------------------------------------------------- class LikeEventIn(BaseModel): id: uuid.UUID track_id: uuid.UUID value: LikeValue created_at: dt.datetime class PlayEventIn(BaseModel): id: uuid.UUID track_id: uuid.UUID played_at: dt.datetime play_duration_seconds: int | None = None completed: bool = False class SyncPushIn(BaseModel): likes: list[LikeEventIn] = Field(default_factory=list) plays: list[PlayEventIn] = Field(default_factory=list) class SyncPushOut(BaseModel): """How many events were newly stored (a replay reports 0) + a fresh cursor.""" cursor: dt.datetime accepted_likes: int accepted_plays: int