Run black on moto & test directories.
This commit is contained in:
parent
c820395dbf
commit
96e5b1993d
507 changed files with 52541 additions and 47814 deletions
|
|
@ -2,6 +2,6 @@ from __future__ import unicode_literals
|
|||
from .models import sns_backends
|
||||
from ..core.models import base_decorator, deprecated_base_decorator
|
||||
|
||||
sns_backend = sns_backends['us-east-1']
|
||||
sns_backend = sns_backends["us-east-1"]
|
||||
mock_sns = base_decorator(sns_backends)
|
||||
mock_sns_deprecated = deprecated_base_decorator(sns_backends)
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@ class SNSNotFoundError(RESTError):
|
|||
code = 404
|
||||
|
||||
def __init__(self, message):
|
||||
super(SNSNotFoundError, self).__init__(
|
||||
"NotFound", message)
|
||||
super(SNSNotFoundError, self).__init__("NotFound", message)
|
||||
|
||||
|
||||
class ResourceNotFoundError(RESTError):
|
||||
|
|
@ -15,39 +14,36 @@ class ResourceNotFoundError(RESTError):
|
|||
|
||||
def __init__(self):
|
||||
super(ResourceNotFoundError, self).__init__(
|
||||
'ResourceNotFound', 'Resource does not exist')
|
||||
"ResourceNotFound", "Resource does not exist"
|
||||
)
|
||||
|
||||
|
||||
class DuplicateSnsEndpointError(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, message):
|
||||
super(DuplicateSnsEndpointError, self).__init__(
|
||||
"DuplicateEndpoint", message)
|
||||
super(DuplicateSnsEndpointError, self).__init__("DuplicateEndpoint", message)
|
||||
|
||||
|
||||
class SnsEndpointDisabled(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, message):
|
||||
super(SnsEndpointDisabled, self).__init__(
|
||||
"EndpointDisabled", message)
|
||||
super(SnsEndpointDisabled, self).__init__("EndpointDisabled", message)
|
||||
|
||||
|
||||
class SNSInvalidParameter(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, message):
|
||||
super(SNSInvalidParameter, self).__init__(
|
||||
"InvalidParameter", message)
|
||||
super(SNSInvalidParameter, self).__init__("InvalidParameter", message)
|
||||
|
||||
|
||||
class InvalidParameterValue(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, message):
|
||||
super(InvalidParameterValue, self).__init__(
|
||||
"InvalidParameterValue", message)
|
||||
super(InvalidParameterValue, self).__init__("InvalidParameterValue", message)
|
||||
|
||||
|
||||
class TagLimitExceededError(RESTError):
|
||||
|
|
@ -55,12 +51,13 @@ class TagLimitExceededError(RESTError):
|
|||
|
||||
def __init__(self):
|
||||
super(TagLimitExceededError, self).__init__(
|
||||
'TagLimitExceeded', 'Could not complete request: tag quota of per resource exceeded')
|
||||
"TagLimitExceeded",
|
||||
"Could not complete request: tag quota of per resource exceeded",
|
||||
)
|
||||
|
||||
|
||||
class InternalError(RESTError):
|
||||
code = 500
|
||||
|
||||
def __init__(self, message):
|
||||
super(InternalError, self).__init__(
|
||||
"InternalFailure", message)
|
||||
super(InternalError, self).__init__("InternalFailure", message)
|
||||
|
|
|
|||
|
|
@ -12,13 +12,22 @@ from boto3 import Session
|
|||
|
||||
from moto.compat import OrderedDict
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.core.utils import iso_8601_datetime_with_milliseconds, camelcase_to_underscores
|
||||
from moto.core.utils import (
|
||||
iso_8601_datetime_with_milliseconds,
|
||||
camelcase_to_underscores,
|
||||
)
|
||||
from moto.sqs import sqs_backends
|
||||
from moto.awslambda import lambda_backends
|
||||
|
||||
from .exceptions import (
|
||||
SNSNotFoundError, DuplicateSnsEndpointError, SnsEndpointDisabled, SNSInvalidParameter,
|
||||
InvalidParameterValue, InternalError, ResourceNotFoundError, TagLimitExceededError
|
||||
SNSNotFoundError,
|
||||
DuplicateSnsEndpointError,
|
||||
SnsEndpointDisabled,
|
||||
SNSInvalidParameter,
|
||||
InvalidParameterValue,
|
||||
InternalError,
|
||||
ResourceNotFoundError,
|
||||
TagLimitExceededError,
|
||||
)
|
||||
from .utils import make_arn_for_topic, make_arn_for_subscription
|
||||
|
||||
|
|
@ -28,7 +37,6 @@ MAXIMUM_MESSAGE_LENGTH = 262144 # 256 KiB
|
|||
|
||||
|
||||
class Topic(BaseModel):
|
||||
|
||||
def __init__(self, name, sns_backend):
|
||||
self.name = name
|
||||
self.sns_backend = sns_backend
|
||||
|
|
@ -36,27 +44,33 @@ class Topic(BaseModel):
|
|||
self.display_name = ""
|
||||
self.delivery_policy = ""
|
||||
self.effective_delivery_policy = json.dumps(DEFAULT_EFFECTIVE_DELIVERY_POLICY)
|
||||
self.arn = make_arn_for_topic(
|
||||
self.account_id, name, sns_backend.region_name)
|
||||
self.arn = make_arn_for_topic(self.account_id, name, sns_backend.region_name)
|
||||
|
||||
self.subscriptions_pending = 0
|
||||
self.subscriptions_confimed = 0
|
||||
self.subscriptions_deleted = 0
|
||||
|
||||
self._policy_json = self._create_default_topic_policy(sns_backend.region_name, self.account_id, name)
|
||||
self._policy_json = self._create_default_topic_policy(
|
||||
sns_backend.region_name, self.account_id, name
|
||||
)
|
||||
self._tags = {}
|
||||
|
||||
def publish(self, message, subject=None, message_attributes=None):
|
||||
message_id = six.text_type(uuid.uuid4())
|
||||
subscriptions, _ = self.sns_backend.list_subscriptions(self.arn)
|
||||
for subscription in subscriptions:
|
||||
subscription.publish(message, message_id, subject=subject,
|
||||
message_attributes=message_attributes)
|
||||
subscription.publish(
|
||||
message,
|
||||
message_id,
|
||||
subject=subject,
|
||||
message_attributes=message_attributes,
|
||||
)
|
||||
return message_id
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
if attribute_name == 'TopicName':
|
||||
|
||||
if attribute_name == "TopicName":
|
||||
return self.name
|
||||
raise UnformattedGetAttTemplateException()
|
||||
|
||||
|
|
@ -73,52 +87,47 @@ class Topic(BaseModel):
|
|||
self._policy_json = json.loads(policy)
|
||||
|
||||
@classmethod
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
|
||||
def create_from_cloudformation_json(
|
||||
cls, resource_name, cloudformation_json, region_name
|
||||
):
|
||||
sns_backend = sns_backends[region_name]
|
||||
properties = cloudformation_json['Properties']
|
||||
properties = cloudformation_json["Properties"]
|
||||
|
||||
topic = sns_backend.create_topic(
|
||||
properties.get("TopicName")
|
||||
)
|
||||
topic = sns_backend.create_topic(properties.get("TopicName"))
|
||||
for subscription in properties.get("Subscription", []):
|
||||
sns_backend.subscribe(topic.arn, subscription[
|
||||
'Endpoint'], subscription['Protocol'])
|
||||
sns_backend.subscribe(
|
||||
topic.arn, subscription["Endpoint"], subscription["Protocol"]
|
||||
)
|
||||
return topic
|
||||
|
||||
def _create_default_topic_policy(self, region_name, account_id, name):
|
||||
return {
|
||||
"Version": "2008-10-17",
|
||||
"Id": "__default_policy_ID",
|
||||
"Statement": [{
|
||||
"Effect": "Allow",
|
||||
"Sid": "__default_statement_ID",
|
||||
"Principal": {
|
||||
"AWS": "*"
|
||||
},
|
||||
"Action": [
|
||||
"SNS:GetTopicAttributes",
|
||||
"SNS:SetTopicAttributes",
|
||||
"SNS:AddPermission",
|
||||
"SNS:RemovePermission",
|
||||
"SNS:DeleteTopic",
|
||||
"SNS:Subscribe",
|
||||
"SNS:ListSubscriptionsByTopic",
|
||||
"SNS:Publish",
|
||||
"SNS:Receive",
|
||||
],
|
||||
"Resource": make_arn_for_topic(
|
||||
self.account_id, name, region_name),
|
||||
"Condition": {
|
||||
"StringEquals": {
|
||||
"AWS:SourceOwner": str(account_id)
|
||||
}
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Sid": "__default_statement_ID",
|
||||
"Principal": {"AWS": "*"},
|
||||
"Action": [
|
||||
"SNS:GetTopicAttributes",
|
||||
"SNS:SetTopicAttributes",
|
||||
"SNS:AddPermission",
|
||||
"SNS:RemovePermission",
|
||||
"SNS:DeleteTopic",
|
||||
"SNS:Subscribe",
|
||||
"SNS:ListSubscriptionsByTopic",
|
||||
"SNS:Publish",
|
||||
"SNS:Receive",
|
||||
],
|
||||
"Resource": make_arn_for_topic(self.account_id, name, region_name),
|
||||
"Condition": {"StringEquals": {"AWS:SourceOwner": str(account_id)}},
|
||||
}
|
||||
}]
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class Subscription(BaseModel):
|
||||
|
||||
def __init__(self, topic, endpoint, protocol):
|
||||
self.topic = topic
|
||||
self.endpoint = endpoint
|
||||
|
|
@ -128,39 +137,54 @@ class Subscription(BaseModel):
|
|||
self._filter_policy = None # filter policy as a dict, not json.
|
||||
self.confirmed = False
|
||||
|
||||
def publish(self, message, message_id, subject=None,
|
||||
message_attributes=None):
|
||||
def publish(self, message, message_id, subject=None, message_attributes=None):
|
||||
if not self._matches_filter_policy(message_attributes):
|
||||
return
|
||||
|
||||
if self.protocol == 'sqs':
|
||||
if self.protocol == "sqs":
|
||||
queue_name = self.endpoint.split(":")[-1]
|
||||
region = self.endpoint.split(":")[3]
|
||||
if self.attributes.get('RawMessageDelivery') != 'true':
|
||||
enveloped_message = json.dumps(self.get_post_data(message, message_id, subject, message_attributes=message_attributes), sort_keys=True, indent=2, separators=(',', ': '))
|
||||
if self.attributes.get("RawMessageDelivery") != "true":
|
||||
enveloped_message = json.dumps(
|
||||
self.get_post_data(
|
||||
message,
|
||||
message_id,
|
||||
subject,
|
||||
message_attributes=message_attributes,
|
||||
),
|
||||
sort_keys=True,
|
||||
indent=2,
|
||||
separators=(",", ": "),
|
||||
)
|
||||
else:
|
||||
enveloped_message = message
|
||||
sqs_backends[region].send_message(queue_name, enveloped_message)
|
||||
elif self.protocol in ['http', 'https']:
|
||||
elif self.protocol in ["http", "https"]:
|
||||
post_data = self.get_post_data(message, message_id, subject)
|
||||
requests.post(self.endpoint, json=post_data, headers={'Content-Type': 'text/plain; charset=UTF-8'})
|
||||
elif self.protocol == 'lambda':
|
||||
requests.post(
|
||||
self.endpoint,
|
||||
json=post_data,
|
||||
headers={"Content-Type": "text/plain; charset=UTF-8"},
|
||||
)
|
||||
elif self.protocol == "lambda":
|
||||
# TODO: support bad function name
|
||||
# http://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html
|
||||
arr = self.endpoint.split(":")
|
||||
region = arr[3]
|
||||
qualifier = None
|
||||
if len(arr) == 7:
|
||||
assert arr[5] == 'function'
|
||||
assert arr[5] == "function"
|
||||
function_name = arr[-1]
|
||||
elif len(arr) == 8:
|
||||
assert arr[5] == 'function'
|
||||
assert arr[5] == "function"
|
||||
qualifier = arr[-1]
|
||||
function_name = arr[-2]
|
||||
else:
|
||||
assert False
|
||||
|
||||
lambda_backends[region].send_sns_message(function_name, message, subject=subject, qualifier=qualifier)
|
||||
lambda_backends[region].send_sns_message(
|
||||
function_name, message, subject=subject, qualifier=qualifier
|
||||
)
|
||||
|
||||
def _matches_filter_policy(self, message_attributes):
|
||||
# TODO: support Anything-but matching, prefix matching and
|
||||
|
|
@ -177,10 +201,10 @@ class Subscription(BaseModel):
|
|||
if isinstance(rule, six.string_types):
|
||||
if field not in message_attributes:
|
||||
return False
|
||||
if message_attributes[field]['Value'] == rule:
|
||||
if message_attributes[field]["Value"] == rule:
|
||||
return True
|
||||
try:
|
||||
json_data = json.loads(message_attributes[field]['Value'])
|
||||
json_data = json.loads(message_attributes[field]["Value"])
|
||||
if rule in json_data:
|
||||
return True
|
||||
except (ValueError, TypeError):
|
||||
|
|
@ -188,11 +212,13 @@ class Subscription(BaseModel):
|
|||
if isinstance(rule, (six.integer_types, float)):
|
||||
if field not in message_attributes:
|
||||
return False
|
||||
if message_attributes[field]['Type'] == 'Number':
|
||||
attribute_values = [message_attributes[field]['Value']]
|
||||
elif message_attributes[field]['Type'] == 'String.Array':
|
||||
if message_attributes[field]["Type"] == "Number":
|
||||
attribute_values = [message_attributes[field]["Value"]]
|
||||
elif message_attributes[field]["Type"] == "String.Array":
|
||||
try:
|
||||
attribute_values = json.loads(message_attributes[field]['Value'])
|
||||
attribute_values = json.loads(
|
||||
message_attributes[field]["Value"]
|
||||
)
|
||||
if not isinstance(attribute_values, list):
|
||||
attribute_values = [attribute_values]
|
||||
except (ValueError, TypeError):
|
||||
|
|
@ -208,29 +234,32 @@ class Subscription(BaseModel):
|
|||
if isinstance(rule, dict):
|
||||
keyword = list(rule.keys())[0]
|
||||
attributes = list(rule.values())[0]
|
||||
if keyword == 'exists':
|
||||
if keyword == "exists":
|
||||
if attributes and field in message_attributes:
|
||||
return True
|
||||
elif not attributes and field not in message_attributes:
|
||||
return True
|
||||
return False
|
||||
|
||||
return all(_field_match(field, rules, message_attributes)
|
||||
for field, rules in six.iteritems(self._filter_policy))
|
||||
return all(
|
||||
_field_match(field, rules, message_attributes)
|
||||
for field, rules in six.iteritems(self._filter_policy)
|
||||
)
|
||||
|
||||
def get_post_data(
|
||||
self, message, message_id, subject, message_attributes=None):
|
||||
def get_post_data(self, message, message_id, subject, message_attributes=None):
|
||||
post_data = {
|
||||
"Type": "Notification",
|
||||
"MessageId": message_id,
|
||||
"TopicArn": self.topic.arn,
|
||||
"Subject": subject or "my subject",
|
||||
"Message": message,
|
||||
"Timestamp": iso_8601_datetime_with_milliseconds(datetime.datetime.utcnow()),
|
||||
"Timestamp": iso_8601_datetime_with_milliseconds(
|
||||
datetime.datetime.utcnow()
|
||||
),
|
||||
"SignatureVersion": "1",
|
||||
"Signature": "EXAMPLElDMXvB8r9R83tGoNn0ecwd5UjllzsvSvbItzfaMpN2nk5HVSw7XnOn/49IkxDKz8YrlH2qJXj2iZB0Zo2O71c4qQk1fMUDi3LGpij7RCW7AW9vYYsSqIKRnFS94ilu7NFhUzLiieYr4BKHpdTmdD6c0esKEYBpabxDSc=",
|
||||
"SigningCertURL": "https://sns.us-east-1.amazonaws.com/SimpleNotificationService-f3ecfb7224c7233fe7bb5f59f96de52f.pem",
|
||||
"UnsubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456789012:some-topic:2bcfbf39-05c3-41de-beaa-fcfcc21c8f55"
|
||||
"UnsubscribeURL": "https://sns.us-east-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:123456789012:some-topic:2bcfbf39-05c3-41de-beaa-fcfcc21c8f55",
|
||||
}
|
||||
if message_attributes:
|
||||
post_data["MessageAttributes"] = message_attributes
|
||||
|
|
@ -238,7 +267,6 @@ class Subscription(BaseModel):
|
|||
|
||||
|
||||
class PlatformApplication(BaseModel):
|
||||
|
||||
def __init__(self, region, name, platform, attributes):
|
||||
self.region = region
|
||||
self.name = name
|
||||
|
|
@ -248,14 +276,11 @@ class PlatformApplication(BaseModel):
|
|||
@property
|
||||
def arn(self):
|
||||
return "arn:aws:sns:{region}:123456789012:app/{platform}/{name}".format(
|
||||
region=self.region,
|
||||
platform=self.platform,
|
||||
name=self.name,
|
||||
region=self.region, platform=self.platform, name=self.name
|
||||
)
|
||||
|
||||
|
||||
class PlatformEndpoint(BaseModel):
|
||||
|
||||
def __init__(self, region, application, custom_user_data, token, attributes):
|
||||
self.region = region
|
||||
self.application = application
|
||||
|
|
@ -269,14 +294,14 @@ class PlatformEndpoint(BaseModel):
|
|||
def __fixup_attributes(self):
|
||||
# When AWS returns the attributes dict, it always contains these two elements, so we need to
|
||||
# automatically ensure they exist as well.
|
||||
if 'Token' not in self.attributes:
|
||||
self.attributes['Token'] = self.token
|
||||
if 'Enabled' not in self.attributes:
|
||||
self.attributes['Enabled'] = 'True'
|
||||
if "Token" not in self.attributes:
|
||||
self.attributes["Token"] = self.token
|
||||
if "Enabled" not in self.attributes:
|
||||
self.attributes["Enabled"] = "True"
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return json.loads(self.attributes.get('Enabled', 'true').lower())
|
||||
return json.loads(self.attributes.get("Enabled", "true").lower())
|
||||
|
||||
@property
|
||||
def arn(self):
|
||||
|
|
@ -298,7 +323,6 @@ class PlatformEndpoint(BaseModel):
|
|||
|
||||
|
||||
class SNSBackend(BaseBackend):
|
||||
|
||||
def __init__(self, region_name):
|
||||
super(SNSBackend, self).__init__()
|
||||
self.topics = OrderedDict()
|
||||
|
|
@ -307,7 +331,16 @@ class SNSBackend(BaseBackend):
|
|||
self.platform_endpoints = {}
|
||||
self.region_name = region_name
|
||||
self.sms_attributes = {}
|
||||
self.opt_out_numbers = ['+447420500600', '+447420505401', '+447632960543', '+447632960028', '+447700900149', '+447700900550', '+447700900545', '+447700900907']
|
||||
self.opt_out_numbers = [
|
||||
"+447420500600",
|
||||
"+447420505401",
|
||||
"+447632960543",
|
||||
"+447632960028",
|
||||
"+447700900149",
|
||||
"+447700900550",
|
||||
"+447700900545",
|
||||
"+447700900907",
|
||||
]
|
||||
|
||||
def reset(self):
|
||||
region_name = self.region_name
|
||||
|
|
@ -318,13 +351,19 @@ class SNSBackend(BaseBackend):
|
|||
self.sms_attributes.update(attrs)
|
||||
|
||||
def create_topic(self, name, attributes=None, tags=None):
|
||||
fails_constraints = not re.match(r'^[a-zA-Z0-9_-]{1,256}$', name)
|
||||
fails_constraints = not re.match(r"^[a-zA-Z0-9_-]{1,256}$", name)
|
||||
if fails_constraints:
|
||||
raise InvalidParameterValue("Topic names must be made up of only uppercase and lowercase ASCII letters, numbers, underscores, and hyphens, and must be between 1 and 256 characters long.")
|
||||
raise InvalidParameterValue(
|
||||
"Topic names must be made up of only uppercase and lowercase ASCII letters, numbers, underscores, and hyphens, and must be between 1 and 256 characters long."
|
||||
)
|
||||
candidate_topic = Topic(name, self)
|
||||
if attributes:
|
||||
for attribute in attributes:
|
||||
setattr(candidate_topic, camelcase_to_underscores(attribute), attributes[attribute])
|
||||
setattr(
|
||||
candidate_topic,
|
||||
camelcase_to_underscores(attribute),
|
||||
attributes[attribute],
|
||||
)
|
||||
if tags:
|
||||
candidate_topic._tags = tags
|
||||
if candidate_topic.arn in self.topics:
|
||||
|
|
@ -337,8 +376,7 @@ class SNSBackend(BaseBackend):
|
|||
if next_token is None or not next_token:
|
||||
next_token = 0
|
||||
next_token = int(next_token)
|
||||
values = list(values_map.values())[
|
||||
next_token: next_token + DEFAULT_PAGE_SIZE]
|
||||
values = list(values_map.values())[next_token : next_token + DEFAULT_PAGE_SIZE]
|
||||
if len(values) == DEFAULT_PAGE_SIZE:
|
||||
next_token = next_token + DEFAULT_PAGE_SIZE
|
||||
else:
|
||||
|
|
@ -366,9 +404,9 @@ class SNSBackend(BaseBackend):
|
|||
|
||||
def get_topic_from_phone_number(self, number):
|
||||
for subscription in self.subscriptions.values():
|
||||
if subscription.protocol == 'sms' and subscription.endpoint == number:
|
||||
if subscription.protocol == "sms" and subscription.endpoint == number:
|
||||
return subscription.topic.arn
|
||||
raise SNSNotFoundError('Could not find valid subscription')
|
||||
raise SNSNotFoundError("Could not find valid subscription")
|
||||
|
||||
def set_topic_attribute(self, topic_arn, attribute_name, attribute_value):
|
||||
topic = self.get_topic(topic_arn)
|
||||
|
|
@ -382,11 +420,11 @@ class SNSBackend(BaseBackend):
|
|||
topic = self.get_topic(topic_arn)
|
||||
subscription = Subscription(topic, endpoint, protocol)
|
||||
attributes = {
|
||||
'PendingConfirmation': 'false',
|
||||
'Endpoint': endpoint,
|
||||
'TopicArn': topic_arn,
|
||||
'Protocol': protocol,
|
||||
'SubscriptionArn': subscription.arn
|
||||
"PendingConfirmation": "false",
|
||||
"Endpoint": endpoint,
|
||||
"TopicArn": topic_arn,
|
||||
"Protocol": protocol,
|
||||
"SubscriptionArn": subscription.arn,
|
||||
}
|
||||
subscription.attributes = attributes
|
||||
self.subscriptions[subscription.arn] = subscription
|
||||
|
|
@ -394,7 +432,11 @@ class SNSBackend(BaseBackend):
|
|||
|
||||
def _find_subscription(self, topic_arn, endpoint, protocol):
|
||||
for subscription in self.subscriptions.values():
|
||||
if subscription.topic.arn == topic_arn and subscription.endpoint == endpoint and subscription.protocol == protocol:
|
||||
if (
|
||||
subscription.topic.arn == topic_arn
|
||||
and subscription.endpoint == endpoint
|
||||
and subscription.protocol == protocol
|
||||
):
|
||||
return subscription
|
||||
return None
|
||||
|
||||
|
|
@ -405,7 +447,8 @@ class SNSBackend(BaseBackend):
|
|||
if topic_arn:
|
||||
topic = self.get_topic(topic_arn)
|
||||
filtered = OrderedDict(
|
||||
[(sub.arn, sub) for sub in self._get_topic_subscriptions(topic)])
|
||||
[(sub.arn, sub) for sub in self._get_topic_subscriptions(topic)]
|
||||
)
|
||||
return self._get_values_nexttoken(filtered, next_token)
|
||||
else:
|
||||
return self._get_values_nexttoken(self.subscriptions, next_token)
|
||||
|
|
@ -413,15 +456,18 @@ class SNSBackend(BaseBackend):
|
|||
def publish(self, arn, message, subject=None, message_attributes=None):
|
||||
if subject is not None and len(subject) > 100:
|
||||
# Note that the AWS docs around length are wrong: https://github.com/spulec/moto/issues/1503
|
||||
raise ValueError('Subject must be less than 100 characters')
|
||||
raise ValueError("Subject must be less than 100 characters")
|
||||
|
||||
if len(message) > MAXIMUM_MESSAGE_LENGTH:
|
||||
raise InvalidParameterValue("An error occurred (InvalidParameter) when calling the Publish operation: Invalid parameter: Message too long")
|
||||
raise InvalidParameterValue(
|
||||
"An error occurred (InvalidParameter) when calling the Publish operation: Invalid parameter: Message too long"
|
||||
)
|
||||
|
||||
try:
|
||||
topic = self.get_topic(arn)
|
||||
message_id = topic.publish(message, subject=subject,
|
||||
message_attributes=message_attributes)
|
||||
message_id = topic.publish(
|
||||
message, subject=subject, message_attributes=message_attributes
|
||||
)
|
||||
except SNSNotFoundError:
|
||||
endpoint = self.get_endpoint(arn)
|
||||
message_id = endpoint.publish(message)
|
||||
|
|
@ -436,8 +482,7 @@ class SNSBackend(BaseBackend):
|
|||
try:
|
||||
return self.applications[arn]
|
||||
except KeyError:
|
||||
raise SNSNotFoundError(
|
||||
"Application with arn {0} not found".format(arn))
|
||||
raise SNSNotFoundError("Application with arn {0} not found".format(arn))
|
||||
|
||||
def set_application_attributes(self, arn, attributes):
|
||||
application = self.get_application(arn)
|
||||
|
|
@ -450,18 +495,23 @@ class SNSBackend(BaseBackend):
|
|||
def delete_platform_application(self, platform_arn):
|
||||
self.applications.pop(platform_arn)
|
||||
|
||||
def create_platform_endpoint(self, region, application, custom_user_data, token, attributes):
|
||||
if any(token == endpoint.token for endpoint in self.platform_endpoints.values()):
|
||||
def create_platform_endpoint(
|
||||
self, region, application, custom_user_data, token, attributes
|
||||
):
|
||||
if any(
|
||||
token == endpoint.token for endpoint in self.platform_endpoints.values()
|
||||
):
|
||||
raise DuplicateSnsEndpointError("Duplicate endpoint token: %s" % token)
|
||||
platform_endpoint = PlatformEndpoint(
|
||||
region, application, custom_user_data, token, attributes)
|
||||
region, application, custom_user_data, token, attributes
|
||||
)
|
||||
self.platform_endpoints[platform_endpoint.arn] = platform_endpoint
|
||||
return platform_endpoint
|
||||
|
||||
def list_endpoints_by_platform_application(self, application_arn):
|
||||
return [
|
||||
endpoint for endpoint
|
||||
in self.platform_endpoints.values()
|
||||
endpoint
|
||||
for endpoint in self.platform_endpoints.values()
|
||||
if endpoint.application.arn == application_arn
|
||||
]
|
||||
|
||||
|
|
@ -469,8 +519,7 @@ class SNSBackend(BaseBackend):
|
|||
try:
|
||||
return self.platform_endpoints[arn]
|
||||
except KeyError:
|
||||
raise SNSNotFoundError(
|
||||
"Endpoint with arn {0} not found".format(arn))
|
||||
raise SNSNotFoundError("Endpoint with arn {0} not found".format(arn))
|
||||
|
||||
def set_endpoint_attributes(self, arn, attributes):
|
||||
endpoint = self.get_endpoint(arn)
|
||||
|
|
@ -481,8 +530,7 @@ class SNSBackend(BaseBackend):
|
|||
try:
|
||||
del self.platform_endpoints[arn]
|
||||
except KeyError:
|
||||
raise SNSNotFoundError(
|
||||
"Endpoint with arn {0} not found".format(arn))
|
||||
raise SNSNotFoundError("Endpoint with arn {0} not found".format(arn))
|
||||
|
||||
def get_subscription_attributes(self, arn):
|
||||
_subscription = [_ for _ in self.subscriptions.values() if _.arn == arn]
|
||||
|
|
@ -493,8 +541,8 @@ class SNSBackend(BaseBackend):
|
|||
return subscription.attributes
|
||||
|
||||
def set_subscription_attributes(self, arn, name, value):
|
||||
if name not in ['RawMessageDelivery', 'DeliveryPolicy', 'FilterPolicy']:
|
||||
raise SNSInvalidParameter('AttributeName')
|
||||
if name not in ["RawMessageDelivery", "DeliveryPolicy", "FilterPolicy"]:
|
||||
raise SNSInvalidParameter("AttributeName")
|
||||
|
||||
# TODO: should do validation
|
||||
_subscription = [_ for _ in self.subscriptions.values() if _.arn == arn]
|
||||
|
|
@ -504,7 +552,7 @@ class SNSBackend(BaseBackend):
|
|||
|
||||
subscription.attributes[name] = value
|
||||
|
||||
if name == 'FilterPolicy':
|
||||
if name == "FilterPolicy":
|
||||
filter_policy = json.loads(value)
|
||||
self._validate_filter_policy(filter_policy)
|
||||
subscription._filter_policy = filter_policy
|
||||
|
|
@ -517,7 +565,9 @@ class SNSBackend(BaseBackend):
|
|||
# Even the offical documentation states the total combination of values must not exceed 100, in reality it is 150
|
||||
# https://docs.aws.amazon.com/sns/latest/dg/sns-subscription-filter-policies.html#subscription-filter-policy-constraints
|
||||
if combinations > 150:
|
||||
raise SNSInvalidParameter("Invalid parameter: FilterPolicy: Filter policy is too complex")
|
||||
raise SNSInvalidParameter(
|
||||
"Invalid parameter: FilterPolicy: Filter policy is too complex"
|
||||
)
|
||||
|
||||
for field, rules in six.iteritems(value):
|
||||
for rule in rules:
|
||||
|
|
@ -534,57 +584,74 @@ class SNSBackend(BaseBackend):
|
|||
if isinstance(rule, dict):
|
||||
keyword = list(rule.keys())[0]
|
||||
attributes = list(rule.values())[0]
|
||||
if keyword == 'anything-but':
|
||||
if keyword == "anything-but":
|
||||
continue
|
||||
elif keyword == 'exists':
|
||||
elif keyword == "exists":
|
||||
if not isinstance(attributes, bool):
|
||||
raise SNSInvalidParameter("Invalid parameter: FilterPolicy: exists match pattern must be either true or false.")
|
||||
raise SNSInvalidParameter(
|
||||
"Invalid parameter: FilterPolicy: exists match pattern must be either true or false."
|
||||
)
|
||||
continue
|
||||
elif keyword == 'numeric':
|
||||
elif keyword == "numeric":
|
||||
continue
|
||||
elif keyword == 'prefix':
|
||||
elif keyword == "prefix":
|
||||
continue
|
||||
else:
|
||||
raise SNSInvalidParameter("Invalid parameter: FilterPolicy: Unrecognized match type {type}".format(type=keyword))
|
||||
raise SNSInvalidParameter(
|
||||
"Invalid parameter: FilterPolicy: Unrecognized match type {type}".format(
|
||||
type=keyword
|
||||
)
|
||||
)
|
||||
|
||||
raise SNSInvalidParameter("Invalid parameter: FilterPolicy: Match value must be String, number, true, false, or null")
|
||||
raise SNSInvalidParameter(
|
||||
"Invalid parameter: FilterPolicy: Match value must be String, number, true, false, or null"
|
||||
)
|
||||
|
||||
def add_permission(self, topic_arn, label, aws_account_ids, action_names):
|
||||
if topic_arn not in self.topics:
|
||||
raise SNSNotFoundError('Topic does not exist')
|
||||
raise SNSNotFoundError("Topic does not exist")
|
||||
|
||||
policy = self.topics[topic_arn]._policy_json
|
||||
statement = next((statement for statement in policy['Statement'] if statement['Sid'] == label), None)
|
||||
statement = next(
|
||||
(
|
||||
statement
|
||||
for statement in policy["Statement"]
|
||||
if statement["Sid"] == label
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if statement:
|
||||
raise SNSInvalidParameter('Statement already exists')
|
||||
raise SNSInvalidParameter("Statement already exists")
|
||||
|
||||
if any(action_name not in VALID_POLICY_ACTIONS for action_name in action_names):
|
||||
raise SNSInvalidParameter('Policy statement action out of service scope!')
|
||||
raise SNSInvalidParameter("Policy statement action out of service scope!")
|
||||
|
||||
principals = ['arn:aws:iam::{}:root'.format(account_id) for account_id in aws_account_ids]
|
||||
actions = ['SNS:{}'.format(action_name) for action_name in action_names]
|
||||
principals = [
|
||||
"arn:aws:iam::{}:root".format(account_id) for account_id in aws_account_ids
|
||||
]
|
||||
actions = ["SNS:{}".format(action_name) for action_name in action_names]
|
||||
|
||||
statement = {
|
||||
'Sid': label,
|
||||
'Effect': 'Allow',
|
||||
'Principal': {
|
||||
'AWS': principals[0] if len(principals) == 1 else principals
|
||||
},
|
||||
'Action': actions[0] if len(actions) == 1 else actions,
|
||||
'Resource': topic_arn
|
||||
"Sid": label,
|
||||
"Effect": "Allow",
|
||||
"Principal": {"AWS": principals[0] if len(principals) == 1 else principals},
|
||||
"Action": actions[0] if len(actions) == 1 else actions,
|
||||
"Resource": topic_arn,
|
||||
}
|
||||
|
||||
self.topics[topic_arn]._policy_json['Statement'].append(statement)
|
||||
self.topics[topic_arn]._policy_json["Statement"].append(statement)
|
||||
|
||||
def remove_permission(self, topic_arn, label):
|
||||
if topic_arn not in self.topics:
|
||||
raise SNSNotFoundError('Topic does not exist')
|
||||
raise SNSNotFoundError("Topic does not exist")
|
||||
|
||||
statements = self.topics[topic_arn]._policy_json['Statement']
|
||||
statements = [statement for statement in statements if statement['Sid'] != label]
|
||||
statements = self.topics[topic_arn]._policy_json["Statement"]
|
||||
statements = [
|
||||
statement for statement in statements if statement["Sid"] != label
|
||||
]
|
||||
|
||||
self.topics[topic_arn]._policy_json['Statement'] = statements
|
||||
self.topics[topic_arn]._policy_json["Statement"] = statements
|
||||
|
||||
def list_tags_for_resource(self, resource_arn):
|
||||
if resource_arn not in self.topics:
|
||||
|
|
@ -613,34 +680,34 @@ class SNSBackend(BaseBackend):
|
|||
|
||||
|
||||
sns_backends = {}
|
||||
for region in Session().get_available_regions('sns'):
|
||||
for region in Session().get_available_regions("sns"):
|
||||
sns_backends[region] = SNSBackend(region)
|
||||
|
||||
|
||||
DEFAULT_EFFECTIVE_DELIVERY_POLICY = {
|
||||
'http': {
|
||||
'disableSubscriptionOverrides': False,
|
||||
'defaultHealthyRetryPolicy': {
|
||||
'numNoDelayRetries': 0,
|
||||
'numMinDelayRetries': 0,
|
||||
'minDelayTarget': 20,
|
||||
'maxDelayTarget': 20,
|
||||
'numMaxDelayRetries': 0,
|
||||
'numRetries': 3,
|
||||
'backoffFunction': 'linear'
|
||||
}
|
||||
"http": {
|
||||
"disableSubscriptionOverrides": False,
|
||||
"defaultHealthyRetryPolicy": {
|
||||
"numNoDelayRetries": 0,
|
||||
"numMinDelayRetries": 0,
|
||||
"minDelayTarget": 20,
|
||||
"maxDelayTarget": 20,
|
||||
"numMaxDelayRetries": 0,
|
||||
"numRetries": 3,
|
||||
"backoffFunction": "linear",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
VALID_POLICY_ACTIONS = [
|
||||
'GetTopicAttributes',
|
||||
'SetTopicAttributes',
|
||||
'AddPermission',
|
||||
'RemovePermission',
|
||||
'DeleteTopic',
|
||||
'Subscribe',
|
||||
'ListSubscriptionsByTopic',
|
||||
'Publish',
|
||||
'Receive'
|
||||
"GetTopicAttributes",
|
||||
"SetTopicAttributes",
|
||||
"AddPermission",
|
||||
"RemovePermission",
|
||||
"DeleteTopic",
|
||||
"Subscribe",
|
||||
"ListSubscriptionsByTopic",
|
||||
"Publish",
|
||||
"Receive",
|
||||
]
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,10 +1,6 @@
|
|||
from __future__ import unicode_literals
|
||||
from .responses import SNSResponse
|
||||
|
||||
url_bases = [
|
||||
"https?://sns.(.+).amazonaws.com",
|
||||
]
|
||||
url_bases = ["https?://sns.(.+).amazonaws.com"]
|
||||
|
||||
url_paths = {
|
||||
'{0}/$': SNSResponse.dispatch,
|
||||
}
|
||||
url_paths = {"{0}/$": SNSResponse.dispatch}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
|||
import re
|
||||
import uuid
|
||||
|
||||
E164_REGEX = re.compile(r'^\+?[1-9]\d{1,14}$')
|
||||
E164_REGEX = re.compile(r"^\+?[1-9]\d{1,14}$")
|
||||
|
||||
|
||||
def make_arn_for_topic(account_id, name, region_name):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue