from __future__ import annotations from dataclasses import dataclass from datetime import datetime from typing import Any, Literal from pydantic import BaseModel, Field ChangeAction = Literal["upsert", "delete"] ChangeDomain = Literal["global", "report"] EnterpriseRole = Literal["admin", "zootech", "viewer"] ConflictResolution = Literal["keep_orchestrator", "keep_hub"] class ChangeEventIn(BaseModel): event_id: str seq: int domain: ChangeDomain = "global" table: str = Field(alias="table") record_id: str action: ChangeAction version: int = 1 content_hash: str = "" payload: dict[str, Any] = Field(default_factory=dict) emitted_at: datetime origin_site_id: str | None = None model_config = {"populate_by_name": True} class ChangeEventOut(BaseModel): event_id: str seq: int domain: ChangeDomain table: str record_id: str action: ChangeAction version: int content_hash: str payload: dict[str, Any] emitted_at: datetime origin_site_id: str | None = None class PushChangesRequest(BaseModel): events: list[ChangeEventIn] class PushChangesResponse(BaseModel): applied_event_ids: list[str] held_event_ids: list[str] conflicts: list[str] class PullChangesRequest(BaseModel): cursor: int = 0 limit: int = 100 class PullChangesResponse(BaseModel): events: list[ChangeEventOut] next_cursor: int class AckChangesRequest(BaseModel): event_ids: list[str] direction: Literal["inbound", "outbound"] = "inbound" class AckChangesResponse(BaseModel): last_acked_seq: int class SyncCapabilitiesResponse(BaseModel): protocol_version: str = "1.0" domains: list[str] = Field(default_factory=lambda: ["global", "report"]) class HeartbeatRequest(BaseModel): wesp_version: str | None = None lag_seconds: int | None = None class HeartbeatResponse(BaseModel): status: str = "ok" server_time: datetime class PairStartRequest(BaseModel): enterprise_id: str farm_name: str class PairStartResponse(BaseModel): session_id: str code: str expires_at: datetime class PairConfirmRequest(BaseModel): code: str hub_site_id: str hub_name: str | None = None hub_url: str | None = None class PairConfirmResponse(BaseModel): farm_hub_id: str hub_site_id: str api_key: str enterprise_id: str class ConflictSummary(BaseModel): id: str table_name: str record_id: str farm_hub_id: str | None status: str created_at: datetime class ConflictDetail(ConflictSummary): orchestrator_snapshot: dict[str, Any] hub_snapshot: dict[str, Any] held_event_ids: list[str] class ResolveConflictRequest(BaseModel): resolution: ConflictResolution class EnterpriseOut(BaseModel): id: str name: str slug: str status: str class FarmHubOut(BaseModel): id: str enterprise_id: str name: str hub_site_id: str url: str | None status: str last_seen: datetime | None wesp_version: str | None @dataclass class HubAuthContext: farm_hub_id: str hub_site_id: str enterprise_id: str farm_hub: Any @dataclass class TenantContext: enterprise_id: str enterprise_role: EnterpriseRole | None user_id: str | None farm_hub_ids: list[str] | None