from __future__ import annotations from datetime import datetime from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field, model_validator PollKind = Literal["single_choice", "multiple_choice", "yes_no", "yes_no_maybe", "ranked_choice", "availability"] PollStatus = Literal["draft", "open", "closed", "decided", "archived"] PollTransitionAction = Literal["open", "draft", "close", "decide", "archive", "unarchive"] PollVisibility = Literal["private", "tenant", "public", "unlisted"] PollResultVisibility = Literal["organizer", "after_response", "after_close", "public"] AvailabilityValue = Literal["available", "maybe", "unavailable"] class PollOptionInput(BaseModel): model_config = ConfigDict(extra="forbid") key: str | None = Field(default=None, min_length=1, max_length=120) label: str = Field(min_length=1, max_length=500) description: str | None = None value: dict[str, Any] | None = None metadata: dict[str, Any] = Field(default_factory=dict) class PollCreateRequest(BaseModel): model_config = ConfigDict(extra="forbid") title: str = Field(min_length=1, max_length=500) slug: str | None = Field(default=None, max_length=120) description: str | None = None kind: PollKind = "single_choice" status: PollStatus = "draft" visibility: PollVisibility = "private" result_visibility: PollResultVisibility = "after_close" context_module: str | None = Field(default=None, max_length=100) context_resource_type: str | None = Field(default=None, max_length=100) context_resource_id: str | None = Field(default=None, max_length=255) workflow_state: str | None = Field(default=None, max_length=100) workflow_steps: list[dict[str, Any]] = Field(default_factory=list) allow_anonymous: bool = False allow_response_update: bool = True min_choices: int = Field(default=1, ge=0) max_choices: int | None = Field(default=None, ge=1) opens_at: datetime | None = None closes_at: datetime | None = None options: list[PollOptionInput] = Field(default_factory=list) metadata: dict[str, Any] = Field(default_factory=dict) @model_validator(mode="after") def validate_window(self) -> "PollCreateRequest": if self.opens_at is not None and self.closes_at is not None and self.closes_at <= self.opens_at: raise ValueError("closes_at must be after opens_at") return self class PollUpdateRequest(BaseModel): model_config = ConfigDict(extra="forbid") title: str | None = Field(default=None, min_length=1, max_length=500) description: str | None = None visibility: PollVisibility | None = None result_visibility: PollResultVisibility | None = None context_module: str | None = Field(default=None, max_length=100) context_resource_type: str | None = Field(default=None, max_length=100) context_resource_id: str | None = Field(default=None, max_length=255) workflow_state: str | None = Field(default=None, max_length=100) workflow_steps: list[dict[str, Any]] | None = None allow_anonymous: bool | None = None allow_response_update: bool | None = None min_choices: int | None = Field(default=None, ge=0) max_choices: int | None = Field(default=None, ge=1) opens_at: datetime | None = None closes_at: datetime | None = None metadata: dict[str, Any] | None = None class PollOptionResponse(BaseModel): id: str key: str label: str description: str | None = None position: int value: dict[str, Any] | None = None metadata: dict[str, Any] = Field(default_factory=dict) class PollTransitionAvailabilityResponse(BaseModel): action: PollTransitionAction target_status: PollStatus | None = None available: bool reason: str | None = None class PollResponseItem(BaseModel): id: str respondent_id: str | None = None respondent_label: str | None = None answers: list[dict[str, Any]] = Field(default_factory=list) submitted_at: datetime created_at: datetime updated_at: datetime metadata: dict[str, Any] = Field(default_factory=dict) class PollResponse(BaseModel): id: str tenant_id: str slug: str title: str description: str | None = None kind: str status: str visibility: str result_visibility: str context_module: str | None = None context_resource_type: str | None = None context_resource_id: str | None = None workflow_state: str | None = None workflow_steps: list[dict[str, Any]] = Field(default_factory=list) allow_anonymous: bool allow_response_update: bool min_choices: int max_choices: int | None = None opens_at: datetime | None = None closes_at: datetime | None = None closed_at: datetime | None = None decided_at: datetime | None = None decided_option_id: str | None = None archived_from_status: str | None = None created_by_user_id: str | None = None created_at: datetime updated_at: datetime metadata: dict[str, Any] = Field(default_factory=dict) options: list[PollOptionResponse] = Field(default_factory=list) lifecycle_actions: list[PollTransitionAvailabilityResponse] = Field(default_factory=list) class PollListResponse(BaseModel): polls: list[PollResponse] = Field(default_factory=list) class PollAnswerInput(BaseModel): model_config = ConfigDict(extra="forbid") option_id: str | None = None option_key: str | None = None value: Any = None rank: int | None = Field(default=None, ge=1) class PollSubmitResponseRequest(BaseModel): model_config = ConfigDict(extra="forbid") respondent_id: str | None = Field(default=None, max_length=255) respondent_label: str | None = Field(default=None, max_length=500) answers: list[PollAnswerInput] = Field(default_factory=list) metadata: dict[str, Any] = Field(default_factory=dict) class PollOptionResultResponse(BaseModel): option_id: str option_key: str label: str count: int = 0 score: int = 0 values: dict[str, int] = Field(default_factory=dict) ranks: dict[int, int] = Field(default_factory=dict) class PollResultSummaryResponse(BaseModel): poll_id: str kind: str status: str response_count: int option_results: list[PollOptionResultResponse] = Field(default_factory=list) leading_option_ids: list[str] = Field(default_factory=list) class PollDecisionRequest(BaseModel): model_config = ConfigDict(extra="forbid") option_id: str | None = None option_key: str | None = None class PollTransitionRequest(BaseModel): model_config = ConfigDict(extra="forbid") action: PollTransitionAction option_id: str | None = None option_key: str | None = None class PollLifecycleTransitionResponse(BaseModel): id: str action: PollTransitionAction from_status: PollStatus to_status: PollStatus decision_option_id: str | None = None previous_decision_option_id: str | None = None idempotency_key: str | None = None actor_user_id: str | None = None actor_api_key_id: str | None = None created_at: datetime metadata: dict[str, Any] = Field(default_factory=dict) class PollStatusResponse(BaseModel): poll: PollResponse transition: PollLifecycleTransitionResponse | None = None replayed: bool = False class PollLifecycleResponse(BaseModel): poll_id: str status: PollStatus archived_from_status: str | None = None actions: list[PollTransitionAvailabilityResponse] = Field(default_factory=list) history: list[PollLifecycleTransitionResponse] = Field(default_factory=list) class PollResponseListResponse(BaseModel): responses: list[PollResponseItem] = Field(default_factory=list) class PollInvitationCreateRequest(BaseModel): model_config = ConfigDict(extra="forbid") respondent_id: str | None = Field(default=None, max_length=255) respondent_label: str | None = Field(default=None, max_length=500) email: str | None = Field(default=None, max_length=320) expires_at: datetime | None = None metadata: dict[str, Any] = Field(default_factory=dict) class PollInvitationResponse(BaseModel): id: str poll_id: str respondent_id: str | None = None respondent_label: str | None = None email: str | None = None token: str | None = None expires_at: datetime | None = None revoked_at: datetime | None = None last_used_at: datetime | None = None created_at: datetime updated_at: datetime metadata: dict[str, Any] = Field(default_factory=dict) class PollInvitationListResponse(BaseModel): invitations: list[PollInvitationResponse] = Field(default_factory=list)