Source code for regstack.sms.base

from __future__ import annotations

import re
from abc import ABC, abstractmethod
from dataclasses import dataclass

# E.164: leading '+', then 1-15 digits, no leading zero in the country code.
_E164 = re.compile(r"^\+[1-9]\d{1,14}$")


[docs] def is_valid_e164(phone: str) -> bool: """Whether ``phone`` is a valid E.164 international phone number. E.164 means a leading ``+``, then a country code starting with a non-zero digit, then up to 15 total digits. Used to validate user-supplied phone numbers before storing them or handing them to an SMS provider. Args: phone: A candidate phone number string (e.g. ``+15551234567``). Returns: ``True`` if ``phone`` matches the E.164 grammar. """ return bool(_E164.fullmatch(phone))
[docs] @dataclass(frozen=True, slots=True) class SmsMessage: """A rendered SMS ready to hand to an :class:`SmsService`.""" to: str """Recipient phone number in E.164 format.""" body: str """The SMS body. Implementations are not required to enforce the 160-character GSM-7 limit; long messages may be split by the upstream provider.""" from_number: str | None = None """Sender phone number in E.164. ``None`` lets the backend fall back to a configured default (e.g. ``config.sms.from_number``)."""
[docs] class SmsService(ABC): """Pluggable transport for sending an :class:`SmsMessage`. Bundled implementations: - :class:`~regstack.sms.null.NullSmsService` — discards messages, the default when SMS 2FA is off. - Amazon SNS (``aioboto3``) — needs the ``sns`` extra. - Twilio — needs the ``twilio`` extra. To plug in a different provider implement :meth:`send` and pass the instance to :meth:`RegStack.set_sms_backend <regstack.app.RegStack.set_sms_backend>`. """
[docs] @abstractmethod async def send(self, message: SmsMessage) -> None: """Deliver one SMS. No retries; caller decides on failure. Args: message: The pre-rendered message. """ ...