import datetime import decimal import logging from dataclasses import dataclass from typing import NamedTuple from zoneinfo import ZoneInfo, ZoneInfoNotFoundError from pydantic import AwareDatetime, BaseModel, Field logger = logging.getLogger(__name__) class CampaignSendTimezone(BaseModel): iana_tz_id: str = Field(alias="ianaTzId") utc_offset_std: decimal.Decimal = Field(alias="utcOffsetStd") state_provinces: list[str] | None = Field(alias="stateProvinces") quiet_hours_min: str = Field(alias="quietHoursMin") quiet_hours_max: str = Field(alias="quietHoursMax") next_safe_time: AwareDatetime | None = Field(alias="nextSafeTime") is_in_safe_hours: bool = Field(alias="isInSafeHours") is_adjusted: bool = Field(default=False, alias="isAdjusted") send_at_local: datetime.datetime = Field(alias="sendAtLocal") @property def is_sendable(self) -> bool: """Safe to send now, either naturally or via an explicit marketer override.""" return self.is_in_safe_hours or self.is_adjusted @property def safe_until(self) -> datetime.datetime | None: if not self.is_in_safe_hours: return None try: hour, minute = (int(part) for part in self.quiet_hours_max.split(":")[:2]) tzinfo = ZoneInfo(self.iana_tz_id) local_close = datetime.datetime.combine( self.send_at_local.date(), datetime.time(hour, minute) ) except (ValueError, ZoneInfoNotFoundError) as exc: logger.warning( "Could not resolve safe-hours end for timezone `%s`: %s", self.iana_tz_id, exc, extra={"iana_tz_id": self.iana_tz_id}, ) return None return local_close.replace(tzinfo=tzinfo).astimezone(datetime.UTC) class CampaignSendCountry(BaseModel): country_code: str = Field(alias="countryCode") timezones: list[CampaignSendTimezone] class CampaignSendTimezonesResponse(BaseModel): countries: list[CampaignSendCountry] @dataclass(frozen=True, slots=True) class BatchGate: is_sendable: bool safe_until: datetime.datetime | None class FanGroupCount(NamedTuple): country_code: str state_province: str | None recipient_count: int class BatchSend(NamedTuple): batch_id: int campaign_id: str country_code: str state_province: str | None messages_to_send: int safe_until: datetime.datetime | None