Skip to content

Commit

Permalink
Merge branch 'main' into 2041-implement-datadog-apm-for-celery
Browse files Browse the repository at this point in the history
  • Loading branch information
MackHalliday authored Nov 21, 2024
2 parents 5893819 + 1e3e3ae commit d94f79a
Show file tree
Hide file tree
Showing 10 changed files with 479 additions and 111 deletions.
172 changes: 120 additions & 52 deletions app/dao/service_sms_sender_dao.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
from typing import Optional
from uuid import UUID

from sqlalchemy import desc, select, update

from app import db
from app.dao.dao_utils import transactional
from app.exceptions import ArchiveValidationError
from app.models import ServiceSmsSender, InboundNumber
from app.models import ProviderDetails, ServiceSmsSender, InboundNumber
from app.service.exceptions import (
SmsSenderDefaultValidationException,
SmsSenderProviderValidationException,
SmsSenderInboundNumberIntegrityException,
SmsSenderRateLimitIntegrityException,
)
from sqlalchemy import desc, select, update
from typing import Optional


def insert_service_sms_sender(
Expand Down Expand Up @@ -66,11 +70,13 @@ def dao_add_sms_sender_for_service(
service_id,
sms_sender,
is_default,
provider_id,
description,
inbound_number_id=None,
rate_limit=None,
rate_limit_interval=None,
sms_sender_specifics={},
):
) -> ServiceSmsSender:
default_sms_sender = _get_default_sms_sender_for_service(service_id=service_id)

if not default_sms_sender and not is_default:
Expand All @@ -79,17 +85,7 @@ def dao_add_sms_sender_for_service(
if is_default:
_set_default_sms_sender_to_not_default(default_sms_sender)

if rate_limit is not None and rate_limit < 1:
raise SmsSenderRateLimitIntegrityException('rate_limit cannot be less than 1.')

if rate_limit_interval is not None and rate_limit_interval < 1:
raise SmsSenderRateLimitIntegrityException('rate_limit_interval cannot be less than 1.')

# TODO - Refactor validation after merging inbound number & sms_sender
if (rate_limit is not None and rate_limit_interval is None) or (
rate_limit_interval is not None and rate_limit is None
):
raise SmsSenderRateLimitIntegrityException('Provide both rate_limit and rate_limit_interval.')
_validate_rate_limit(None, rate_limit, rate_limit_interval)

if inbound_number_id is not None:
inbound_number = _allocate_inbound_number_for_service(service_id, inbound_number_id)
Expand All @@ -100,78 +96,150 @@ def dao_add_sms_sender_for_service(
f"and the Inbound Number '{inbound_number.id}' ('{inbound_number.number}')."
)

provider_details = _validate_provider(provider_id)

new_sms_sender = ServiceSmsSender(
service_id=service_id,
sms_sender=sms_sender,
description=description,
is_default=is_default,
inbound_number_id=inbound_number_id,
provider=provider_details,
provider_id=provider_id,
rate_limit=rate_limit,
rate_limit_interval=rate_limit_interval,
service_id=service_id,
sms_sender=sms_sender,
sms_sender_specifics=sms_sender_specifics,
)

db.session.add(new_sms_sender)
return new_sms_sender


def _validate_provider(provider_id: UUID) -> ProviderDetails:
"""Validate the provider_details. This is a helper function when adding or updating an SMS sender.
It checks the provider exists and raises an Exception if it doesn't.
Args:
provider_id (UUID): The ID of the provider to validate.
Returns:
ProviderDetails: The provider details.
Raises:
SmsSenderProviderValidationException: If the provider doesn't exist.
"""
# this causes a circular import, so it's being imported here...
from app.dao.provider_details_dao import get_provider_details_by_id

provider_details = get_provider_details_by_id(provider_id)

if provider_details is None:
raise SmsSenderProviderValidationException(f'No provider details found for id {provider_id}')

return provider_details


@transactional
def dao_update_service_sms_sender( # noqa: C901
def dao_update_service_sms_sender(
service_id,
service_sms_sender_id,
**kwargs,
):
) -> ServiceSmsSender:
if 'is_default' in kwargs:
default_sms_sender = _get_default_sms_sender_for_service(service_id)
is_default = kwargs['is_default']

if service_sms_sender_id == default_sms_sender.id and not is_default:
raise SmsSenderDefaultValidationException('You must have at least one SMS sender as the default.')

if is_default:
_set_default_sms_sender_to_not_default(default_sms_sender)
_handle_default_sms_sender(service_id, service_sms_sender_id, kwargs['is_default'])

if 'inbound_number_id' in kwargs:
_allocate_inbound_number_for_service(service_id, kwargs['inbound_number_id'])

sms_sender_to_update = db.session.get(ServiceSmsSender, service_sms_sender_id)

if 'rate_limit' in kwargs and kwargs['rate_limit'] is not None and kwargs['rate_limit'] < 1:
raise SmsSenderRateLimitIntegrityException('rate_limit cannot be less than 1.')
if 'rate_limit' in kwargs and kwargs['rate_limit_interval'] is not None and kwargs['rate_limit_interval'] < 1:
raise SmsSenderRateLimitIntegrityException('rate_limit_interval cannot be less than 1.')

if (
'rate_limit' in kwargs
and kwargs['rate_limit']
and ('rate_limit_interval' not in kwargs or not kwargs['rate_limit_interval'])
):
if not sms_sender_to_update.rate_limit_interval:
raise SmsSenderRateLimitIntegrityException(
'Cannot update sender to have only one of rate limit value and interval.'
)
sms_sender_to_update: ServiceSmsSender = db.session.get(ServiceSmsSender, service_sms_sender_id)

if (
'rate_limit_interval' in kwargs
and kwargs['rate_limit_interval']
and ('rate_limit' not in kwargs or not kwargs['rate_limit'])
):
if not sms_sender_to_update.rate_limit:
raise SmsSenderRateLimitIntegrityException(
'Cannot update sender to have only one of rate limit value and interval.'
)
_validate_rate_limit(sms_sender_to_update, kwargs.get('rate_limit'), kwargs.get('rate_limit_interval'))

if 'sms_sender' in kwargs and sms_sender_to_update.inbound_number_id:
raise SmsSenderInboundNumberIntegrityException(
'You cannot update the number for this SMS sender because it has an associated Inbound Number.'
)

if 'provider_id' in kwargs:
_validate_provider(kwargs['provider_id'])

for key, value in kwargs.items():
setattr(sms_sender_to_update, key, value)

db.session.add(sms_sender_to_update)
return sms_sender_to_update


def _handle_default_sms_sender(service_id: UUID, service_sms_sender_id: UUID, is_default: bool) -> None:
"""Check the default SMS sender.
This is a helper function when updating an SMS sender. It ensures there is a default SMS sender for the service and
raises an exception if there won't be a default sender after the update.
Args:
service_id (UUID): The ID of the service.
service_sms_sender_id (UUID): The ID of the SMS sender.
is_default (bool): Whether the SMS sender should be updated to be the default.
Raises:
SmsSenderDefaultValidationException: If there is no default SMS sender for the service.
"""
default_sms_sender = _get_default_sms_sender_for_service(service_id)

# ensure there will still be a default sender on the service, else raise an exception
if service_sms_sender_id == default_sms_sender.id and not is_default:
raise SmsSenderDefaultValidationException('You must have at least one SMS sender as the default.')

if is_default:
_set_default_sms_sender_to_not_default(default_sms_sender)


def _validate_rate_limit(
sms_sender_to_update: ServiceSmsSender | None,
rate_limit: int | None,
rate_limit_interval: int | None,
) -> None:
"""Validate the rate limit and rate limit interval.
This is a helper function when adding or updating a SMS sender.
It ensures the rate limit and rate limit interval are valid.
Args:
sms_sender_to_update (ServiceSmsSender | None): The SMS sender to update, or None if adding a new SMS sender.
rate_limit (int | None): The sms sender's rate limit.
rate_limit_interval (int | None): The sms sender's rate limit interval.
Raises:
SmsSenderRateLimitIntegrityException: If the rate limit or rate limit interval is invalid.
"""
# ensure rate_limit is a positive integer, when included in kwargs
if rate_limit is not None and rate_limit < 1:
raise SmsSenderRateLimitIntegrityException('rate_limit cannot be less than 1.')

# ensure rate_limit_interval is a positive integer, when included in kwargs
if rate_limit_interval is not None and rate_limit_interval < 1:
raise SmsSenderRateLimitIntegrityException('rate_limit_interval cannot be less than 1.')

# only run these checks when updating an existing SMS sender
if sms_sender_to_update:
# if rate limit is being updated, ensure rate limit interval is also being updated, or is already valid
if rate_limit and not rate_limit_interval:
if not sms_sender_to_update.rate_limit_interval:
raise SmsSenderRateLimitIntegrityException(
'Cannot update sender to have only one of rate limit value and interval.'
)

# if rate limit interval is being updated, ensure rate limit is also being updated, or is already valid
if not rate_limit and rate_limit_interval:
if not sms_sender_to_update.rate_limit:
raise SmsSenderRateLimitIntegrityException(
'Cannot update sender to have only one of rate limit value and interval.'
)
else:
# when adding a new sender ensure both rate limit and rate limit interval are provided, or neither
if (rate_limit is None) != (rate_limit_interval is None):
raise SmsSenderRateLimitIntegrityException('Provide both rate_limit and rate_limit_interval, or neither.')


@transactional
def archive_sms_sender(
service_id,
Expand Down
5 changes: 3 additions & 2 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,15 +549,16 @@ class ServiceSmsSender(db.Model):
def get_reply_to_text(self):
return try_validate_and_format_phone_number(self.sms_sender)

def serialize(self):
def serialize(self) -> dict[str, bool | int | str | None]:
return {
'archived': self.archived,
'created_at': self.created_at.strftime(DATETIME_FORMAT),
'description': self.description,
'id': str(self.id),
'inbound_number_id': str(self.inbound_number_id) if self.inbound_number_id else None,
'is_default': self.is_default,
'provider_id': str(self.provider_id),
'provider_id': str(self.provider_id) if self.provider_id else None,
'provider_name': self.provider.display_name if self.provider else None,
'rate_limit': self.rate_limit if self.rate_limit else None,
'rate_limit_interval': self.rate_limit_interval if self.rate_limit_interval else None,
'service_id': str(self.service_id),
Expand Down
4 changes: 4 additions & 0 deletions app/service/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@ class SmsSenderInboundNumberIntegrityException(Exception):
pass


class SmsSenderProviderValidationException(Exception):
pass


class SmsSenderRateLimitIntegrityException(Exception):
pass
31 changes: 13 additions & 18 deletions app/service/service_senders_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,31 @@
'required': ['contact_block', 'is_default'],
}

service_sms_sender_request_properties = {
'description': {'type': 'string'},
'inbound_number_id': uuid,
'is_default': {'type': 'boolean'},
'provider_id': uuid,
'rate_limit': {'type': ['integer', 'null'], 'minimum': 1},
'rate_limit_interval': {'type': ['integer', 'null'], 'minimum': 1},
'sms_sender': {'type': 'string'},
'sms_sender_specifics': {'type': ['object', 'null']},
}

add_service_sms_sender_request = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'description': 'POST add service SMS sender',
'type': 'object',
'title': 'Add new SMS sender for service',
'properties': {
'inbound_number_id': uuid,
'is_default': {'type': 'boolean'},
'rate_limit': {'type': ['integer', 'null'], 'minimum': 1},
'rate_limit_interval': {'type': ['integer', 'null'], 'minimum': 1},
'sms_sender': {'type': 'string'},
'sms_sender_specifics': {'type': ['object', 'null']},
},
'required': ['is_default', 'sms_sender'],
'properties': service_sms_sender_request_properties,
'required': ['description', 'is_default', 'provider_id', 'sms_sender'],
}


update_service_sms_sender_request = {
'$schema': 'http://json-schema.org/draft-04/schema#',
'description': 'POST update service SMS sender',
'type': 'object',
'title': 'Update SMS sender for service',
'properties': {
'inbound_number_id': uuid,
'is_default': {'type': 'boolean'},
'rate_limit': {'type': ['integer', 'null'], 'minimum': 1},
'rate_limit_interval': {'type': ['integer', 'null'], 'minimum': 1},
'sms_sender': {'type': 'string'},
'sms_sender_specifics': {'type': ['object', 'null']},
},
'properties': service_sms_sender_request_properties,
'minProperties': 1,
}
2 changes: 2 additions & 0 deletions app/service/sms_sender_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from app.service.exceptions import (
SmsSenderDefaultValidationException,
SmsSenderInboundNumberIntegrityException,
SmsSenderProviderValidationException,
SmsSenderRateLimitIntegrityException,
)
from app.service.service_senders_schema import (
Expand All @@ -35,6 +36,7 @@ def _validate_service_exists():
@service_sms_sender_blueprint.errorhandler(SmsSenderRateLimitIntegrityException)
@service_sms_sender_blueprint.errorhandler(SmsSenderDefaultValidationException)
@service_sms_sender_blueprint.errorhandler(SmsSenderInboundNumberIntegrityException)
@service_sms_sender_blueprint.errorhandler(SmsSenderProviderValidationException)
def handle_errors(error):
current_app.logger.info(error)
return jsonify(result='error', message=str(error)), 400
Expand Down
Loading

0 comments on commit d94f79a

Please sign in to comment.