Source code for terminusgps.aws.notifications

import asyncio
import os
import uuid
from collections.abc import Sequence
from contextlib import AsyncExitStack

from aiobotocore.session import AioSession

from terminusgps.django.validators import validate_e164_phone_number


class AsyncPollyManager:
    def __init__(
        self, bucket_name: str | None = None, region_name: str = "us-east-1"
    ) -> None:
        self._exit_stack = AsyncExitStack()
        self._polly_client = None
        self._region_name = region_name
        self._bucket_name = bucket_name or os.getenv("AWS_POLLY_BUCKET_NAME", "")

    @property
    def region_name(self) -> str:
        """
        An AWS region name.

        :type: :py:obj:`str`

        """
        return self._region_name

    @property
    def bucket_name(self) -> str:
        """
        The AWS bucket that will host audio files.

        :type: :py:obj:`str`

        """
        return self._bucket_name

    async def __aenter__(self) -> "AsyncPollyManager":
        """
        Creates an asyncronous session and client.

        :returns: The polly manager.
        :rtype: :py:obj:`~terminusgps.aws.notifications.AsyncPollyManager`

        """
        session = AioSession()
        self._polly_client = await self._exit_stack.enter_async_context(
            session.create_client("polly", region_name=self.region_name)
        )
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        """
        Destroys the asyncronous session.

        :param exc_type: Exception type.
        :param exc_val: Exception value.
        :param exc_tb: Exception traceback.
        :returns: Nothing.
        :rtype: :py:obj:`None`

        """
        await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb)

    async def synthesize_speech(
        self,
        message: str,
        format: str = "ogg_vorbis",
        sample_rate: int = 22050,
        language_code: str = "en-US",
    ) -> None:
        """
        Synthesizes the message into an audio file and saves it to the S3 bucket.

        :param message: A message to be read aloud.
        :type message: :py:obj:`str`
        :param format: A file format. Default is ``"ogg_vorbis"``.
        :type format: :py:obj:`str`
        :param sample_rate: The audio frequency in Hz. Default is ``22050``.
        :type sample_rate: :py:obj:`int`
        :param language_code: The language to use in the synthesis. Default is ``"en-US"``.
        :type language_code: :py:obj:`str`
        :returns: Nothing.
        :rtype: :py:obj:`None`

        """
        await self._polly_client.start_speech_synthesis_task(
            **{
                "Engine": "standard",
                "LanguageCode": language_code,
                "OutputFormat": format,
                "OutputS3BucketName": self.bucket_name,
                "SampleRate": str(sample_rate),
                "Text": f"<speak>{message}</speak>",
                "TextType": "ssml",
                "VoiceId": "Joanna",
            }
        )

    async def synthesize_speech_batch(
        self,
        messages: Sequence[str],
        format: str = "ogg_vorbis",
        sample_rate: int = 22050,
        language_code: str = "en-US",
    ) -> None:
        """
        Synthesizes a sequence of messages into audio files and saves them to the S3 bucket.

        :param messages: A sequence of messages to be read aloud.
        :type messages: :py:obj:`Sequence`
        :param format: A file format. Default is ``"ogg_vorbis"``.
        :type format: :py:obj:`str`
        :param sample_rate: The audio frequency in Hz. Default is ``22050``.
        :type sample_rate: :py:obj:`int`
        :param language_code: The language to use in the synthesis. Default is ``"en-US"``.
        :type language_code: :py:obj:`str`
        :returns: Nothing.
        :rtype: :py:obj:`None`

        """
        await asyncio.gather(
            *[
                self.synthesize_speech(
                    message=message,
                    format=format,
                    sample_rate=sample_rate,
                    language_code=language_code,
                )
                for message in messages
            ]
        )


[docs] class AsyncNotificationManager: def __init__( self, group_id: str | None = None, region_name: str = "us-east-1" ) -> None: """ Sets :py:attr:`group_id` and :py:attr:`region_name`. :returns: Nothing. :rtype: :py:obj:`None` """ self._exit_stack = AsyncExitStack() self._sns_client = None self._region_name = region_name self.group_id = group_id async def __aenter__(self) -> "AsyncNotificationManager": """ Creates an asyncronous session and client. :returns: The notification manager. :rtype: :py:obj:`~terminusgps.aws.notifications.AsyncNotificationManager` """ session = AioSession() self._sns_client = await self._exit_stack.enter_async_context( session.create_client("sns", region_name=self.region_name) ) return self async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: """ Destroys the asyncronous session. :param exc_type: Exception type. :param exc_val: Exception value. :param exc_tb: Exception traceback. :returns: Nothing. :rtype: :py:obj:`None` """ await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb) @property def region_name(self) -> str: """ An AWS region name. :type: :py:obj:`str` """ return self._region_name @property def group_id(self) -> str: """ A message group id. :type: :py:obj:`str` """ return str(self._group_id) @group_id.setter def group_id(self, other: str | None) -> None: """ Sets :py:attr:`group_id` to ``other``. If ``other`` isn't provided, instead set :py:attr:`group_id` to :py:func:`~uuid.uuid4`. :param other: A message group id. :type other: :py:obj:`str` | :py:obj:`None` :returns: Nothing. :rtype: :py:obj:`None` """ if not other: other = str(uuid.uuid4()) self._group_id = other
[docs] async def send_sms( self, to_number: str, message: str, message_id: str | None = None ) -> None: """ Sends an sms to ``to_number``. :param to_number: A destination phone number. :type to_number: :py:obj:`str` :param message: A message body. :type message: :py:obj:`str` :param message_id: A message deduplication id. :py:func:`~uuid.uuid4` is used if ``message_id`` was not provided. :type message_id: :py:obj:`str` | :py:obj:`None` :raises ValidationError: If ``to_number`` wasn't a valid E.164 formatted phone number. :returns: Nothing. :rtype: :py:obj:`None` """ validate_e164_phone_number(to_number) if not message_id: message_id = str(uuid.uuid4()) await self._sns_client.publish( **{ "PhoneNumber": to_number, "Message": message, "MessageDeduplicationId": message_id, "MessageGroupId": self.group_id, } )
[docs] async def send_push( self, target_arn: str, message: str, message_id: str | None = None ) -> None: """ Sends a push notification to ``target_arn``. :param target_arn: An AWS resource ARN. :type target_arn: :py:obj:`str` :param message: A message body. :type message: :py:obj:`str` :param message_id: A message deduplication id. :py:func:`~uuid.uuid4` is used if ``message_id`` was not provided. :type message_id: :py:obj:`str` | :py:obj:`None` :returns: Nothing. :rtype: :py:obj:`None` """ if not message_id: message_id = str(uuid.uuid4()) await self._sns_client.publish( **{ "TargetArn": target_arn, "Message": message, "MessageDeduplicationId": message_id, "MessageGroupId": self.group_id, } )
[docs] async def send_sms_batch(self, to_numbers: Sequence[str], message: str) -> None: """ Sends an sms message to each phone number in ``to_numbers``. :param to_numbers: A sequence of phone numbers. :type to_numbers: :py:obj:`~collections.abc.Sequence` :param message: A message body. :type message: :py:obj:`str` :raises ValidationError: If a ``to_number`` wasn't a valid E.164 formatted phone number. :returns: Nothing. :rtype: :py:obj:`None` """ for to_number in to_numbers: validate_e164_phone_number(to_number) await asyncio.gather( *[self.send_sms(to_number=num, message=message) for num in to_numbers] )
[docs] async def send_push_batch(self, target_arns: Sequence[str], message: str) -> None: """ Sends a push notification to each AWS resource by ARN in ``target_arns``. :param target_arns: A sequence of AWS resource ARNs. :type target_arns: :py:obj:`~collections.abc.Sequence` :param message: A message body. :type message: :py:obj:`str` :returns: Nothing. :rtype: :py:obj:`None` """ await asyncio.gather( *[self.send_push(target_arn=arn, message=message) for arn in target_arns] )