fix(responses): reject stale scheduling answers

This commit is contained in:
2026-07-20 18:35:57 +02:00
parent 0f33e25c83
commit bbf503a7d7
5 changed files with 311 additions and 39 deletions

View File

@@ -2,8 +2,9 @@ from __future__ import annotations
from datetime import datetime
from typing import Any, Literal
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from pydantic import BaseModel, ConfigDict, Field, model_validator
from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, field_validator, model_validator
SchedulingStatus = Literal["draft", "collecting", "closed", "decided", "handed_off", "cancelled", "archived"]
@@ -13,6 +14,16 @@ SchedulingResultVisibility = Literal["organizer", "after_response", "after_close
SchedulingAvailabilityValue = Literal["available", "maybe", "unavailable"]
def _known_timezone(value: str | None) -> str | None:
if value is None:
return None
try:
ZoneInfo(value)
except ZoneInfoNotFoundError as exc:
raise ValueError("timezone must be a valid IANA timezone") from exc
return value
class SchedulingCalendarPreferences(BaseModel):
model_config = ConfigDict(extra="forbid")
@@ -28,12 +39,14 @@ class SchedulingCandidateSlotInput(BaseModel):
label: str | None = Field(default=None, max_length=500)
description: str | None = None
start_at: datetime
end_at: datetime
start_at: AwareDatetime
end_at: AwareDatetime
timezone: str | None = Field(default=None, max_length=100)
location: str | None = Field(default=None, max_length=500)
metadata: dict[str, Any] = Field(default_factory=dict)
_validate_timezone = field_validator("timezone")(_known_timezone)
@model_validator(mode="after")
def validate_range(self) -> "SchedulingCandidateSlotInput":
if self.end_at <= self.start_at:
@@ -52,12 +65,14 @@ class SchedulingCandidateSlotUpdateRequest(BaseModel):
label: str | None = Field(default=None, min_length=1, max_length=500)
description: str | None = None
start_at: datetime | None = None
end_at: datetime | None = None
start_at: AwareDatetime | None = None
end_at: AwareDatetime | None = None
timezone: str | None = Field(default=None, min_length=1, max_length=100)
location: str | None = Field(default=None, max_length=500)
metadata: dict[str, Any] | None = None
_validate_timezone = field_validator("timezone")(_known_timezone)
@model_validator(mode="after")
def validate_range(self) -> "SchedulingCandidateSlotUpdateRequest":
if self.start_at is not None and self.end_at is not None and self.end_at <= self.start_at:
@@ -94,6 +109,8 @@ class SchedulingRequestCreateRequest(BaseModel):
create_participant_invitations: bool = True
metadata: dict[str, Any] = Field(default_factory=dict)
_validate_timezone = field_validator("timezone")(_known_timezone)
class SchedulingRequestUpdateRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
@@ -119,6 +136,7 @@ class SchedulingCandidateSlotResponse(BaseModel):
timezone: str
location: str | None = None
position: int
revision: str
freebusy_checked_at: datetime | None = None
freebusy_status: str | None = None
freebusy_conflicts: list[dict[str, Any]] = Field(default_factory=list)
@@ -192,6 +210,7 @@ class SchedulingAvailabilityAnswerInput(BaseModel):
slot_id: str
value: SchedulingAvailabilityValue
option_revision: str = Field(min_length=64, max_length=64, pattern=r"^[0-9a-f]{64}$")
class SchedulingAvailabilityResponseRequest(BaseModel):