from collections.abc import Iterator from typing import Any from fansifter_common.httpclient import HTTPClient from .exceptions import TwilioClientError from .models import ( AccountResponse, APIKeyResponse, MessageCountryPricing, MessagingPhoneNumberResponse, MessagingService, PhoneNumber, SendMessageRequest, SendMessageResponse, ) class TwilioClient(HTTPClient): exception_class = TwilioClientError DEFAULT_REQUEST_TIMEOUT = 10 def __init__( self, account_sid: str | None = None, auth_token: str | None = None, default_timeout: int | None = None, ) -> None: self.account_sid = account_sid self.auth_token = auth_token super().__init__( client_options={"timeout": default_timeout or self.DEFAULT_REQUEST_TIMEOUT} ) def _require_auth(self, auth: tuple[str, str] | None) -> tuple[str, str]: if auth is None: if not self.account_sid or not self.auth_token: raise self.exception_class( message="Both account_sid and auth_token must be provided.", ) return self.account_sid, self.auth_token return auth def get_message_country_pricing( self, country_iso2: str, *, auth: tuple[str, str] | None = None ) -> MessageCountryPricing: return self.request( "GET", f"https://pricing.twilio.com/v1/Messaging/Countries/{country_iso2}", type=MessageCountryPricing, auth=self._require_auth(auth), ) def send_message( self, request: SendMessageRequest, *, auth: tuple[str, str] | None = None, ) -> SendMessageResponse: account_sid = request.account_sid or self.account_sid return self.request( "POST", f"https://api.twilio.com/2010-04-01/Accounts/{account_sid}/Messages.json", auth=self._require_auth(auth), data=self._build_send_message_payload(request), type=SendMessageResponse, ) def get_account( self, *, account_sid: str, auth: tuple[str, str] | None = None ) -> AccountResponse: return self.request( "GET", f"https://api.twilio.com/2010-04-01/Accounts/{account_sid}.json", auth=self._require_auth(auth), type=AccountResponse, ) def iter_messaging_phone_numbers( self, *, messaging_service_sid: str | None = None, next_page_url: str | None = None, auth: tuple[str, str] | None = None, ) -> Iterator[PhoneNumber]: auth = self._require_auth(auth) if next_page_url: url = f"https://api.twilio.com{next_page_url}" else: url = f"https://messaging.twilio.com/v1/Services/{messaging_service_sid}/PhoneNumbers" response = self.request( "GET", url, auth=auth, type=MessagingPhoneNumberResponse, ) yield from response.phone_numbers if response.meta and response.meta.next_page_url: yield from self.iter_messaging_phone_numbers( next_page_url=response.meta.next_page_url, auth=auth, ) def get_messaging_phone_numbers( self, *, messaging_service_sid: str, auth: tuple[str, str] | None = None, ) -> list[PhoneNumber]: return list( self.iter_messaging_phone_numbers( messaging_service_sid=messaging_service_sid, auth=auth, ) ) def get_messaging_service( self, *, messaging_service_sid: str | None = None, auth: tuple[str, str] | None = None, ) -> MessagingService: return self.request( "GET", f"https://messaging.twilio.com/v1/Services/{messaging_service_sid}", auth=self._require_auth(auth), type=MessagingService, ) def create_api_key( self, *, account_sid: str, friendly_name: str, auth: tuple[str, str] | None = None, ) -> APIKeyResponse: return self.request( "POST", "https://iam.twilio.com/v1/Keys", auth=self._require_auth(auth), data={ "AccountSid": account_sid, "FriendlyName": friendly_name, }, type=APIKeyResponse, ) @classmethod def _build_send_message_payload(cls, request: SendMessageRequest) -> dict[str, Any]: data = { "To": request.to, "Body": request.body, } if request.messaging_service_sid: data["MessagingServiceSid"] = request.messaging_service_sid if request.from_: data["From"] = request.from_ if request.status_callback: data["StatusCallback"] = request.status_callback if request.media_url: if isinstance(request.media_url, list): if len(request.media_url) == 1: data["MediaUrl"] = request.media_url[0] else: for i, url in enumerate(request.media_url): data[f"MediaUrl[{i}]"] = url else: data["MediaUrl"] = request.media_url return data