from __future__ import annotations from datetime import datetime from typing import Any, Literal from pydantic import BaseModel, Field, model_validator VCardPlanAction = Literal["create", "update", "ignore", "unchanged", "conflict"] VCardCommitAction = Literal["create", "update", "ignore"] class VCardBatchFilePayload(BaseModel): filename: str = Field(min_length=1, max_length=500) content_base64: str = Field(min_length=1, max_length=14_000_000) class VCardBatchPreviewRequest(BaseModel): files: list[VCardBatchFilePayload] = Field(min_length=1, max_length=50) duplicate_card_policy: Literal["reject", "first", "last"] = "reject" existing_contact_policy: Literal["update", "ignore", "reject"] = "update" class VCardDuplicateSuggestion(BaseModel): contact_id: str display_name: str reasons: list[str] = Field(default_factory=list) class VCardBatchPlanItemResponse(BaseModel): source_key: str source_filename: str card_index: int action: VCardPlanAction allowed_actions: list[VCardCommitAction] = Field(default_factory=list) contact_id: str | None = None display_name: str | None = None changed_fields: list[str] = Field(default_factory=list) duplicate_suggestions: list[VCardDuplicateSuggestion] = Field(default_factory=list) message: str | None = None class VCardBatchDiagnosticResponse(BaseModel): severity: Literal["info", "warning", "error"] code: str message: str source_filename: str | None = None card_index: int | None = None field: str | None = None details: dict[str, Any] = Field(default_factory=dict) class VCardBatchProgressResponse(BaseModel): total: int completed: int created: int = 0 updated: int = 0 ignored: int = 0 failed: int = 0 class VCardBatchRunResponse(BaseModel): id: str address_book_id: str status: str input_hash: str plan_hash: str parser_version: str execution_mode: Literal["bounded_sync", "persisted_batch"] file_count: int card_count: int statistics: dict[str, int | str] = Field(default_factory=dict) diagnostics: list[VCardBatchDiagnosticResponse] = Field(default_factory=list) plan: list[VCardBatchPlanItemResponse] = Field(default_factory=list) progress: VCardBatchProgressResponse can_apply: bool can_cancel: bool commit_hash: str | None = None created_at: datetime updated_at: datetime applied_at: datetime | None = None class VCardBatchSelection(BaseModel): source_key: str = Field(min_length=1, max_length=1000) action: VCardCommitAction class VCardBatchCommitRequest(BaseModel): expected_plan_hash: str = Field(min_length=64, max_length=64) selections: list[VCardBatchSelection] = Field( default_factory=list, max_length=10_000 ) class VCardBatchCancelRequest(BaseModel): expected_plan_hash: str = Field(min_length=64, max_length=64) reason: str = Field(min_length=3, max_length=2000) class VCardExportRequest(BaseModel): scope: Literal["address_book", "address_list", "contacts"] = "address_book" address_list_id: str | None = Field(default=None, max_length=36) contact_ids: list[str] = Field(default_factory=list, max_length=10_000) version: Literal["3.0", "4.0"] = "4.0" @model_validator(mode="after") def validate_scope(self) -> "VCardExportRequest": if self.scope == "address_list" and not self.address_list_id: raise ValueError("Address-list export requires address_list_id.") if self.scope == "contacts" and not self.contact_ids: raise ValueError( "Selected-contact export requires at least one contact id." ) if self.scope != "address_list" and self.address_list_id: raise ValueError("address_list_id is only valid for address-list export.") if self.scope != "contacts" and self.contact_ids: raise ValueError("contact_ids are only valid for selected-contact export.") if len(set(self.contact_ids)) != len(self.contact_ids): raise ValueError("Selected contact ids must be unique.") return self class VCardExportResponse(BaseModel): filename: str media_type: str = "text/vcard" scope: str version: str ordering: str contact_count: int content_hash: str content: str __all__ = [ "VCardBatchCancelRequest", "VCardBatchCommitRequest", "VCardBatchFilePayload", "VCardBatchPreviewRequest", "VCardBatchRunResponse", "VCardBatchSelection", "VCardExportRequest", "VCardExportResponse", ]