clear-skies-aws 1.10.1__py3-none-any.whl → 2.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clear_skies_aws-1.10.1.dist-info → clear_skies_aws-2.0.1.dist-info}/METADATA +36 -35
- clear_skies_aws-2.0.1.dist-info/RECORD +4 -0
- {clear_skies_aws-1.10.1.dist-info → clear_skies_aws-2.0.1.dist-info}/WHEEL +1 -1
- clear_skies_aws-2.0.1.dist-info/licenses/LICENSE +21 -0
- clear_skies_aws-1.10.1.dist-info/LICENSE +0 -7
- clear_skies_aws-1.10.1.dist-info/RECORD +0 -71
- clearskies_aws/__init__.py +0 -2
- clearskies_aws/actions/__init__.py +0 -108
- clearskies_aws/actions/action_aws.py +0 -118
- clearskies_aws/actions/assume_role.py +0 -102
- clearskies_aws/actions/assume_role_test.py +0 -72
- clearskies_aws/actions/ses.py +0 -194
- clearskies_aws/actions/ses_test.py +0 -89
- clearskies_aws/actions/sns.py +0 -64
- clearskies_aws/actions/sns_test.py +0 -77
- clearskies_aws/actions/sqs.py +0 -82
- clearskies_aws/actions/sqs_test.py +0 -127
- clearskies_aws/actions/step_function.py +0 -66
- clearskies_aws/actions/step_function_test.py +0 -103
- clearskies_aws/backends/__init__.py +0 -12
- clearskies_aws/backends/dynamo_db_backend.py +0 -614
- clearskies_aws/backends/dynamo_db_backend_test.py +0 -300
- clearskies_aws/backends/dynamo_db_condition_parser.py +0 -365
- clearskies_aws/backends/dynamo_db_condition_parser_test.py +0 -266
- clearskies_aws/backends/dynamo_db_parti_ql_backend.py +0 -1123
- clearskies_aws/backends/dynamo_db_parti_ql_backend_test.py +0 -544
- clearskies_aws/backends/sqs_backend.py +0 -80
- clearskies_aws/backends/sqs_backend_test.py +0 -31
- clearskies_aws/contexts/__init__.py +0 -10
- clearskies_aws/contexts/cli.py +0 -19
- clearskies_aws/contexts/cli_websocket_mock.py +0 -33
- clearskies_aws/contexts/lambda_api_gateway.py +0 -30
- clearskies_aws/contexts/lambda_api_gateway_web_socket.py +0 -30
- clearskies_aws/contexts/lambda_elb.py +0 -30
- clearskies_aws/contexts/lambda_http_gateway.py +0 -30
- clearskies_aws/contexts/lambda_invocation.py +0 -48
- clearskies_aws/contexts/lambda_sns.py +0 -43
- clearskies_aws/contexts/lambda_sqs_standard_partial_batch.py +0 -51
- clearskies_aws/contexts/lambda_sqs_standard_partial_batch_test.py +0 -66
- clearskies_aws/contexts/wsgi.py +0 -19
- clearskies_aws/di/__init__.py +0 -1
- clearskies_aws/di/standard_dependencies.py +0 -60
- clearskies_aws/handlers/__init__.py +0 -2
- clearskies_aws/handlers/secrets_manager_rotation.py +0 -174
- clearskies_aws/handlers/simple_body_routing.py +0 -39
- clearskies_aws/input_outputs/__init__.py +0 -8
- clearskies_aws/input_outputs/cli_websocket_mock.py +0 -12
- clearskies_aws/input_outputs/lambda_api_gateway.py +0 -105
- clearskies_aws/input_outputs/lambda_api_gateway_test.py +0 -87
- clearskies_aws/input_outputs/lambda_api_gateway_web_socket.py +0 -8
- clearskies_aws/input_outputs/lambda_elb.py +0 -21
- clearskies_aws/input_outputs/lambda_http_gateway.py +0 -12
- clearskies_aws/input_outputs/lambda_invocation.py +0 -34
- clearskies_aws/input_outputs/lambda_sns.py +0 -52
- clearskies_aws/input_outputs/lambda_sqs_standard.py +0 -54
- clearskies_aws/mocks/__init__.py +0 -1
- clearskies_aws/mocks/actions/__init__.py +0 -6
- clearskies_aws/mocks/actions/ses.py +0 -28
- clearskies_aws/mocks/actions/sns.py +0 -23
- clearskies_aws/mocks/actions/sqs.py +0 -23
- clearskies_aws/mocks/actions/step_function.py +0 -26
- clearskies_aws/secrets/__init__.py +0 -7
- clearskies_aws/secrets/additional_configs/__init__.py +0 -54
- clearskies_aws/secrets/additional_configs/iam_db_auth.py +0 -29
- clearskies_aws/secrets/additional_configs/iam_db_auth_with_ssm.py +0 -92
- clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +0 -81
- clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssm_bastion.py +0 -141
- clearskies_aws/secrets/akeyless_with_ssm_cache.py +0 -38
- clearskies_aws/secrets/parameter_store.py +0 -50
- clearskies_aws/secrets/parameter_store_test.py +0 -18
- clearskies_aws/secrets/secrets_manager.py +0 -75
- clearskies_aws/secrets/secrets_manager_test.py +0 -18
- clearskies_aws/web_socket_connection_model.py +0 -43
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
from unittest.mock import MagicMock, call
|
|
3
|
-
|
|
4
|
-
from .assume_role import AssumeRole
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class AssumeRoleTest(unittest.TestCase):
|
|
8
|
-
def test_with_external_id(self):
|
|
9
|
-
sts = MagicMock()
|
|
10
|
-
sts.assume_role = MagicMock(
|
|
11
|
-
return_value={
|
|
12
|
-
"Credentials": {
|
|
13
|
-
"AccessKeyId": "access-key",
|
|
14
|
-
"SecretAccessKey": "secret-key",
|
|
15
|
-
"SessionToken": "session-token",
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
)
|
|
19
|
-
boto3 = MagicMock()
|
|
20
|
-
boto3.client = MagicMock(return_value=sts)
|
|
21
|
-
boto3.Session = MagicMock(return_value='MOAR BOTO')
|
|
22
|
-
|
|
23
|
-
assume_role = AssumeRole(role_arn='aws:arn:role/name', external_id='12345')
|
|
24
|
-
self.assertEqual("MOAR BOTO", assume_role(boto3))
|
|
25
|
-
boto3.client.assert_called_with("sts")
|
|
26
|
-
boto3.Session.assert_called_with(
|
|
27
|
-
aws_access_key_id="access-key",
|
|
28
|
-
aws_secret_access_key="secret-key",
|
|
29
|
-
aws_session_token="session-token",
|
|
30
|
-
)
|
|
31
|
-
sts.assume_role.assert_called_with(
|
|
32
|
-
RoleArn='aws:arn:role/name',
|
|
33
|
-
RoleSessionName="clearkies-aws",
|
|
34
|
-
DurationSeconds=3600,
|
|
35
|
-
ExternalId="12345",
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
def test_with_source(self):
|
|
39
|
-
sts = MagicMock()
|
|
40
|
-
sts.assume_role = MagicMock(
|
|
41
|
-
return_value={
|
|
42
|
-
"Credentials": {
|
|
43
|
-
"AccessKeyId": "access-key",
|
|
44
|
-
"SecretAccessKey": "secret-key",
|
|
45
|
-
"SessionToken": "session-token",
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
)
|
|
49
|
-
boto3 = MagicMock()
|
|
50
|
-
boto3.client = MagicMock(return_value=sts)
|
|
51
|
-
boto3.Session = MagicMock(return_value='MOAR BOTO')
|
|
52
|
-
source = MagicMock(return_value=boto3)
|
|
53
|
-
|
|
54
|
-
assume_role = AssumeRole(
|
|
55
|
-
role_arn='aws:arn:role/name',
|
|
56
|
-
source=source,
|
|
57
|
-
role_session_name="sup",
|
|
58
|
-
duration=7200,
|
|
59
|
-
)
|
|
60
|
-
self.assertEqual("MOAR BOTO", assume_role("not-boto3"))
|
|
61
|
-
boto3.client.assert_called_with("sts")
|
|
62
|
-
boto3.Session.assert_called_with(
|
|
63
|
-
aws_access_key_id="access-key",
|
|
64
|
-
aws_secret_access_key="secret-key",
|
|
65
|
-
aws_session_token="session-token",
|
|
66
|
-
)
|
|
67
|
-
sts.assume_role.assert_called_with(
|
|
68
|
-
RoleArn='aws:arn:role/name',
|
|
69
|
-
RoleSessionName="sup",
|
|
70
|
-
DurationSeconds=7200,
|
|
71
|
-
)
|
|
72
|
-
source.assert_called_with("not-boto3")
|
clearskies_aws/actions/ses.py
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import boto3
|
|
2
|
-
import clearskies
|
|
3
|
-
import datetime
|
|
4
|
-
|
|
5
|
-
from botocore.exceptions import ClientError
|
|
6
|
-
from clearskies.environment import Environment
|
|
7
|
-
from clearskies.models import Models
|
|
8
|
-
from collections.abc import Sequence
|
|
9
|
-
from typing import Any, Callable, List, Optional, Union
|
|
10
|
-
from types import ModuleType
|
|
11
|
-
|
|
12
|
-
from ..di import StandardDependencies
|
|
13
|
-
from .assume_role import AssumeRole
|
|
14
|
-
from .action_aws import ActionAws
|
|
15
|
-
class SES(ActionAws):
|
|
16
|
-
_name = "ses"
|
|
17
|
-
|
|
18
|
-
def __init__(self, environment: Environment, boto3: boto3, di: StandardDependencies) -> None:
|
|
19
|
-
"""Setup action."""
|
|
20
|
-
super().__init__(environment, boto3, di)
|
|
21
|
-
|
|
22
|
-
def configure(
|
|
23
|
-
self,
|
|
24
|
-
sender,
|
|
25
|
-
to: Optional[Union[list, str, Callable]] = None,
|
|
26
|
-
cc: Optional[Union[list, str, Callable]] = None,
|
|
27
|
-
bcc: Optional[Union[list, str, Callable]] = None,
|
|
28
|
-
subject: Optional[str] = None,
|
|
29
|
-
message: Optional[str] = None,
|
|
30
|
-
subject_template: Optional[str] = None,
|
|
31
|
-
message_template: Optional[str] = None,
|
|
32
|
-
subject_template_file: Optional[str] = None,
|
|
33
|
-
message_template_file: Optional[str] = None,
|
|
34
|
-
assume_role: Optional[AssumeRole] = None,
|
|
35
|
-
dependencies_for_template: Optional[list[Any]] = None,
|
|
36
|
-
when: Optional[Callable] = None,
|
|
37
|
-
) -> None:
|
|
38
|
-
"""Configure the rules for this email notification."""
|
|
39
|
-
super().configure(message_callable=None, when=when, assume_role=assume_role)
|
|
40
|
-
self.destinations = {
|
|
41
|
-
"to": [],
|
|
42
|
-
"cc": [],
|
|
43
|
-
"bcc": [],
|
|
44
|
-
}
|
|
45
|
-
# this just moves the data from the various "to" inputs (to, cc, bcc) into the self.destinations
|
|
46
|
-
# dictionary, after normalizing it so that it is always a list.
|
|
47
|
-
for key in self.destinations.keys():
|
|
48
|
-
destination_values = locals()[key]
|
|
49
|
-
if not destination_values:
|
|
50
|
-
continue
|
|
51
|
-
if type(destination_values) == str or callable(destination_values):
|
|
52
|
-
self.destinations[key] = [destination_values]
|
|
53
|
-
else:
|
|
54
|
-
self.destinations[key] = destination_values
|
|
55
|
-
self.subject = subject
|
|
56
|
-
self.message = message
|
|
57
|
-
self.sender = sender
|
|
58
|
-
self.subject_template = None
|
|
59
|
-
self.message_template = None
|
|
60
|
-
self.dependencies_for_template = dependencies_for_template if dependencies_for_template else []
|
|
61
|
-
|
|
62
|
-
if not to and not cc:
|
|
63
|
-
raise ValueError("You must configure at least one 'to' address or one 'cc' address")
|
|
64
|
-
num_subjects = 0
|
|
65
|
-
num_messages = 0
|
|
66
|
-
for source in [subject, subject_template, subject_template_file]:
|
|
67
|
-
if source:
|
|
68
|
-
num_subjects += 1
|
|
69
|
-
for source in [message, message_template, message_template_file]:
|
|
70
|
-
if source:
|
|
71
|
-
num_messages += 1
|
|
72
|
-
if num_subjects > 1:
|
|
73
|
-
raise ValueError(
|
|
74
|
-
"More than one of 'subject', 'subject_template', or 'subject_template_file' was set, but only one of these may be set."
|
|
75
|
-
)
|
|
76
|
-
if num_messages > 1:
|
|
77
|
-
raise ValueError(
|
|
78
|
-
"More than one of 'message', 'message_template', or 'message_template_file' was set, but only one of these may be set."
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
if subject_template_file:
|
|
82
|
-
import jinja2
|
|
83
|
-
with open(subject_template_file, "r", encoding="utf-8") as template:
|
|
84
|
-
self.subject_template = jinja2.Template(template.read())
|
|
85
|
-
elif subject_template:
|
|
86
|
-
import jinja2
|
|
87
|
-
self.subject_template = jinja2.Template(subject_template)
|
|
88
|
-
|
|
89
|
-
if message_template_file:
|
|
90
|
-
import jinja2
|
|
91
|
-
with open(message_template_file, "r", encoding="utf-8") as template:
|
|
92
|
-
self.message_template = jinja2.Template(template.read())
|
|
93
|
-
elif message_template:
|
|
94
|
-
import jinja2
|
|
95
|
-
self.message_template = jinja2.Template(message_template)
|
|
96
|
-
|
|
97
|
-
def _execute_action(self, client: ModuleType, model: Models) -> None:
|
|
98
|
-
"""Send a notification as configured."""
|
|
99
|
-
utcnow = self.di.build('utcnow')
|
|
100
|
-
|
|
101
|
-
tos = self._resolve_destination("to", model)
|
|
102
|
-
if not tos:
|
|
103
|
-
return
|
|
104
|
-
response = client.send_email(
|
|
105
|
-
Destination={
|
|
106
|
-
"ToAddresses": tos,
|
|
107
|
-
"CcAddresses": self._resolve_destination("cc", model),
|
|
108
|
-
"BccAddresses": self._resolve_destination("bcc", model),
|
|
109
|
-
},
|
|
110
|
-
Message={
|
|
111
|
-
"Body": {
|
|
112
|
-
"Html": {
|
|
113
|
-
"Charset": "utf-8",
|
|
114
|
-
"Data": self._resolve_message_as_html(model, utcnow),
|
|
115
|
-
},
|
|
116
|
-
"Text": {
|
|
117
|
-
"Charset": "utf-8",
|
|
118
|
-
"Data": self._resolve_message_as_text(model, utcnow),
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
"Subject": {
|
|
122
|
-
"Charset": "utf-8",
|
|
123
|
-
"Data": self._resolve_subject(model, utcnow)
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
Source=self.sender,
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
def _resolve_destination(self, name: str, model: clearskies.Model) -> List[str]:
|
|
130
|
-
"""
|
|
131
|
-
Return a list of to/cc/bcc addresses.
|
|
132
|
-
|
|
133
|
-
Each entry can be:
|
|
134
|
-
|
|
135
|
-
1. An email address
|
|
136
|
-
2. The name of a column in the model that contains an email address
|
|
137
|
-
"""
|
|
138
|
-
resolved = []
|
|
139
|
-
destinations = self.destinations[name]
|
|
140
|
-
for destination in destinations:
|
|
141
|
-
if callable(destination):
|
|
142
|
-
more = self.di.call_function(destination, model=model)
|
|
143
|
-
if not isinstance(more, list):
|
|
144
|
-
more = [more]
|
|
145
|
-
for entry in more:
|
|
146
|
-
if not isinstance(entry, str):
|
|
147
|
-
raise ValueError(f"I invoked a callable to fetch the '{name}' addresses for model '{model.__class__.__name__}' but it returned something other than a string. Callables must return a valid email address or a list of email addresses.")
|
|
148
|
-
if "@" not in entry:
|
|
149
|
-
raise ValueError(f"I invoked a callable to fetch the '{name}' addresses for model '{model.__class__.__name__}' but it returned a non-email address. Callables must return a valid email address or a list of email addresses.")
|
|
150
|
-
resolved.extend(more)
|
|
151
|
-
continue
|
|
152
|
-
if "@" in destination:
|
|
153
|
-
resolved.append(destination)
|
|
154
|
-
continue
|
|
155
|
-
resolved.append(model.get(destination))
|
|
156
|
-
return resolved
|
|
157
|
-
|
|
158
|
-
def _resolve_message_as_html(self, model: clearskies.Model, now: datetime.datetime) -> str:
|
|
159
|
-
"""Build the HTML for a message."""
|
|
160
|
-
if self.message:
|
|
161
|
-
return self.message
|
|
162
|
-
|
|
163
|
-
if self.message_template:
|
|
164
|
-
return str(
|
|
165
|
-
self.message_template.render(model=model, now=now, **self.more_template_variables(), text_in_html=True)
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
return ""
|
|
169
|
-
|
|
170
|
-
def _resolve_message_as_text(self, model: clearskies.Model, now: datetime.datetime) -> str:
|
|
171
|
-
"""Build the text for a message."""
|
|
172
|
-
if self.message:
|
|
173
|
-
return self.message
|
|
174
|
-
|
|
175
|
-
if self.message_template:
|
|
176
|
-
return str(self.message_template.render(model=model, now=now, **self.more_template_variables()))
|
|
177
|
-
|
|
178
|
-
return ""
|
|
179
|
-
|
|
180
|
-
def _resolve_subject(self, model: clearskies.Model, now: datetime.datetime) -> str:
|
|
181
|
-
"""Build the subject for a message."""
|
|
182
|
-
if self.subject:
|
|
183
|
-
return self.subject
|
|
184
|
-
|
|
185
|
-
if self.subject_template:
|
|
186
|
-
return str(self.subject_template.render(model=model, now=now, **self.more_template_variables()))
|
|
187
|
-
|
|
188
|
-
return ""
|
|
189
|
-
|
|
190
|
-
def more_template_variables(self) -> dict[str, Any]:
|
|
191
|
-
more_variables = {}
|
|
192
|
-
for dependency_name in self.dependencies_for_template:
|
|
193
|
-
more_variables[dependency_name] = self.di.build(dependency_name, cache=True)
|
|
194
|
-
return more_variables
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
from unittest.mock import MagicMock, call
|
|
3
|
-
from .ses import SES
|
|
4
|
-
import clearskies
|
|
5
|
-
from ..di import StandardDependencies
|
|
6
|
-
class SESTest(unittest.TestCase):
|
|
7
|
-
def setUp(self):
|
|
8
|
-
self.di = StandardDependencies()
|
|
9
|
-
self.di.bind('environment', {'AWS_REGION': 'us-east-2'})
|
|
10
|
-
self.ses = MagicMock()
|
|
11
|
-
self.ses.send_email = MagicMock()
|
|
12
|
-
self.boto3 = MagicMock()
|
|
13
|
-
self.boto3.client = MagicMock(return_value=self.ses)
|
|
14
|
-
self.environment = MagicMock()
|
|
15
|
-
self.environment.get = MagicMock(return_value='us-east-1')
|
|
16
|
-
|
|
17
|
-
def test_send(self):
|
|
18
|
-
ses = SES(self.environment, self.boto3, self.di)
|
|
19
|
-
ses.configure(
|
|
20
|
-
'test@example.com', to='jane@example.com', subject='welcome!', message_template='hi {{ model.id }}!'
|
|
21
|
-
)
|
|
22
|
-
model = MagicMock()
|
|
23
|
-
model.id = 'asdf'
|
|
24
|
-
ses(model)
|
|
25
|
-
self.ses.send_email.assert_has_calls([
|
|
26
|
-
call(
|
|
27
|
-
Destination={
|
|
28
|
-
'ToAddresses': ['jane@example.com'],
|
|
29
|
-
'CcAddresses': [],
|
|
30
|
-
'BccAddresses': []
|
|
31
|
-
},
|
|
32
|
-
Message={
|
|
33
|
-
'Body': {
|
|
34
|
-
'Html': {
|
|
35
|
-
'Charset': 'utf-8',
|
|
36
|
-
'Data': 'hi asdf!'
|
|
37
|
-
},
|
|
38
|
-
'Text': {
|
|
39
|
-
'Charset': 'utf-8',
|
|
40
|
-
'Data': 'hi asdf!'
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
'Subject': {
|
|
44
|
-
'Charset': 'utf-8',
|
|
45
|
-
'Data': 'welcome!'
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
Source='test@example.com'
|
|
49
|
-
),
|
|
50
|
-
])
|
|
51
|
-
|
|
52
|
-
def test_send_callable(self):
|
|
53
|
-
ses = SES(self.environment, self.boto3, self.di)
|
|
54
|
-
ses.configure(
|
|
55
|
-
'test@example.com',
|
|
56
|
-
to=lambda model: 'jane@example.com',
|
|
57
|
-
bcc=lambda model: ['bob@example.com', 'greg@example.com'],
|
|
58
|
-
subject='welcome!',
|
|
59
|
-
message_template='hi {{ model.id }}!'
|
|
60
|
-
)
|
|
61
|
-
model = MagicMock()
|
|
62
|
-
model.id = 'asdf'
|
|
63
|
-
ses(model)
|
|
64
|
-
self.ses.send_email.assert_has_calls([
|
|
65
|
-
call(
|
|
66
|
-
Destination={
|
|
67
|
-
'ToAddresses': ['jane@example.com'],
|
|
68
|
-
'CcAddresses': [],
|
|
69
|
-
'BccAddresses': ['bob@example.com', 'greg@example.com']
|
|
70
|
-
},
|
|
71
|
-
Message={
|
|
72
|
-
'Body': {
|
|
73
|
-
'Html': {
|
|
74
|
-
'Charset': 'utf-8',
|
|
75
|
-
'Data': 'hi asdf!'
|
|
76
|
-
},
|
|
77
|
-
'Text': {
|
|
78
|
-
'Charset': 'utf-8',
|
|
79
|
-
'Data': 'hi asdf!'
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
'Subject': {
|
|
83
|
-
'Charset': 'utf-8',
|
|
84
|
-
'Data': 'welcome!'
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
Source='test@example.com'
|
|
88
|
-
),
|
|
89
|
-
])
|
clearskies_aws/actions/sns.py
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import boto3
|
|
2
|
-
import clearskies
|
|
3
|
-
import datetime
|
|
4
|
-
import json
|
|
5
|
-
|
|
6
|
-
from botocore.exceptions import ClientError
|
|
7
|
-
from collections.abc import Sequence
|
|
8
|
-
from clearskies.environment import Environment
|
|
9
|
-
from clearskies.models import Models
|
|
10
|
-
from types import ModuleType
|
|
11
|
-
from typing import List, Optional, Callable, cast
|
|
12
|
-
|
|
13
|
-
from ..di import StandardDependencies
|
|
14
|
-
from .assume_role import AssumeRole
|
|
15
|
-
from .action_aws import ActionAws
|
|
16
|
-
class SNS(ActionAws):
|
|
17
|
-
_name = "sns"
|
|
18
|
-
|
|
19
|
-
def __init__(self, environment: Environment, boto3: boto3, di: StandardDependencies) -> None:
|
|
20
|
-
super().__init__(environment, boto3, di)
|
|
21
|
-
|
|
22
|
-
def configure(
|
|
23
|
-
self,
|
|
24
|
-
topic=None,
|
|
25
|
-
topic_environment_key=None,
|
|
26
|
-
topic_callable: Optional[Callable] = None,
|
|
27
|
-
message_callable: Optional[Callable] = None,
|
|
28
|
-
when: Optional[Callable] = None,
|
|
29
|
-
assume_role: Optional[AssumeRole] = None,
|
|
30
|
-
) -> None:
|
|
31
|
-
"""Configures the action for SNS."""
|
|
32
|
-
super().configure(message_callable=message_callable, when=when, assume_role=assume_role)
|
|
33
|
-
|
|
34
|
-
self.topic = topic
|
|
35
|
-
self.topic_environment_key = topic_environment_key
|
|
36
|
-
self.topic_callable = topic_callable
|
|
37
|
-
|
|
38
|
-
topics = 0
|
|
39
|
-
for value in [topic, topic_environment_key, topic_callable]:
|
|
40
|
-
if value:
|
|
41
|
-
topics += 1
|
|
42
|
-
if topics > 1:
|
|
43
|
-
raise ValueError(
|
|
44
|
-
"You can only provide one of 'topic', 'topic_environment_key', or 'topic_callable', but more than one were provided."
|
|
45
|
-
)
|
|
46
|
-
if not topics:
|
|
47
|
-
raise ValueError("You must provide at least one of 'topic', 'topic_environment_key', or 'topic_callable'.")
|
|
48
|
-
|
|
49
|
-
def _execute_action(self, client: ModuleType, model: Models) -> None:
|
|
50
|
-
"""Send a notification as configured."""
|
|
51
|
-
topic_arn = self.get_topic_arn(model)
|
|
52
|
-
if not topic_arn:
|
|
53
|
-
return
|
|
54
|
-
client.publish(
|
|
55
|
-
TopicArn=self.get_topic_arn(model),
|
|
56
|
-
Message=self.get_message_body(model),
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
def get_topic_arn(self, model: Models) -> str:
|
|
60
|
-
if self.topic:
|
|
61
|
-
return self.topic
|
|
62
|
-
if self.topic_environment_key:
|
|
63
|
-
return self.environment.get(self.topic_environment_key)
|
|
64
|
-
return self.di.call_function(self.topic_callable, model=model)
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
import boto3
|
|
3
|
-
|
|
4
|
-
from unittest.mock import MagicMock, call
|
|
5
|
-
from .sns import SNS
|
|
6
|
-
import clearskies
|
|
7
|
-
from ..di import StandardDependencies
|
|
8
|
-
import json
|
|
9
|
-
from collections import OrderedDict
|
|
10
|
-
class User(clearskies.Model):
|
|
11
|
-
def __init__(self, memory_backend, columns):
|
|
12
|
-
super().__init__(memory_backend, columns)
|
|
13
|
-
|
|
14
|
-
def columns_configuration(self):
|
|
15
|
-
return OrderedDict([
|
|
16
|
-
clearskies.column_types.string('name'),
|
|
17
|
-
clearskies.column_types.email('email'),
|
|
18
|
-
])
|
|
19
|
-
class SNSTest(unittest.TestCase):
|
|
20
|
-
def setUp(self):
|
|
21
|
-
self.di = StandardDependencies()
|
|
22
|
-
self.di.bind('environment', {'AWS_REGION': 'us-east-2'})
|
|
23
|
-
self.users = self.di.build(User)
|
|
24
|
-
self.sns = MagicMock()
|
|
25
|
-
self.sns.publish = MagicMock()
|
|
26
|
-
self.boto3 = MagicMock()
|
|
27
|
-
self.boto3.client = MagicMock(return_value=self.sns)
|
|
28
|
-
self.when = None
|
|
29
|
-
self.environment = MagicMock()
|
|
30
|
-
self.environment.get = MagicMock(return_value='us-east-1')
|
|
31
|
-
|
|
32
|
-
def always(self, model):
|
|
33
|
-
self.when = model
|
|
34
|
-
return True
|
|
35
|
-
|
|
36
|
-
def never(self, model):
|
|
37
|
-
self.when = model
|
|
38
|
-
return False
|
|
39
|
-
|
|
40
|
-
def test_send(self):
|
|
41
|
-
sns = SNS(self.environment, self.boto3, self.di)
|
|
42
|
-
sns.configure(
|
|
43
|
-
topic='arn:aws:my-topic',
|
|
44
|
-
when=self.always,
|
|
45
|
-
)
|
|
46
|
-
user = self.users.model({
|
|
47
|
-
"id": "1-2-3-4",
|
|
48
|
-
"name": "Jane",
|
|
49
|
-
"email": "jane@example.com",
|
|
50
|
-
})
|
|
51
|
-
sns(user)
|
|
52
|
-
self.sns.publish.assert_has_calls([
|
|
53
|
-
call(
|
|
54
|
-
TopicArn='arn:aws:my-topic',
|
|
55
|
-
Message=json.dumps({
|
|
56
|
-
"id": "1-2-3-4",
|
|
57
|
-
"name": "Jane",
|
|
58
|
-
"email": "jane@example.com",
|
|
59
|
-
}),
|
|
60
|
-
),
|
|
61
|
-
])
|
|
62
|
-
self.assertEqual(id(user), id(self.when))
|
|
63
|
-
|
|
64
|
-
def test_not_now(self):
|
|
65
|
-
sns = SNS(self.environment, self.boto3, self.di)
|
|
66
|
-
sns.configure(
|
|
67
|
-
topic='arn:aws:my-topic',
|
|
68
|
-
when=self.never,
|
|
69
|
-
)
|
|
70
|
-
user = self.users.model({
|
|
71
|
-
"id": "1-2-3-4",
|
|
72
|
-
"name": "Jane",
|
|
73
|
-
"email": "jane@example.com",
|
|
74
|
-
})
|
|
75
|
-
sns(user)
|
|
76
|
-
self.sns.publish.assert_not_called()
|
|
77
|
-
self.assertEqual(id(user), id(self.when))
|
clearskies_aws/actions/sqs.py
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import boto3
|
|
2
|
-
import json
|
|
3
|
-
import datetime
|
|
4
|
-
|
|
5
|
-
from botocore.exceptions import ClientError
|
|
6
|
-
from clearskies.environment import Environment
|
|
7
|
-
from clearskies.model import Model
|
|
8
|
-
from collections.abc import Sequence
|
|
9
|
-
from collections import OrderedDict
|
|
10
|
-
from types import ModuleType
|
|
11
|
-
from typing import List, Optional, Callable, Union
|
|
12
|
-
|
|
13
|
-
from ..di import StandardDependencies
|
|
14
|
-
from . import assume_role
|
|
15
|
-
from .action_aws import ActionAws
|
|
16
|
-
class SQS(ActionAws):
|
|
17
|
-
_name = "sqs"
|
|
18
|
-
|
|
19
|
-
def __init__(self, environment: Environment, boto3: boto3, di: StandardDependencies) -> None:
|
|
20
|
-
"""Setup action."""
|
|
21
|
-
super().__init__(environment, boto3, di)
|
|
22
|
-
|
|
23
|
-
def configure(
|
|
24
|
-
self,
|
|
25
|
-
queue_url: str = '',
|
|
26
|
-
queue_url_environment_key: str = '',
|
|
27
|
-
queue_url_callable: Optional[Callable] = None,
|
|
28
|
-
message_callable: Optional[Callable] = None,
|
|
29
|
-
when: Optional[Callable] = None,
|
|
30
|
-
assume_role: Optional[assume_role.AssumeRole] = None,
|
|
31
|
-
message_group_id: Optional[Union[str, Callable]] = None,
|
|
32
|
-
) -> None:
|
|
33
|
-
super().configure(message_callable=message_callable, when=when, assume_role=assume_role)
|
|
34
|
-
|
|
35
|
-
self.queue_url = queue_url
|
|
36
|
-
self.queue_url_environment_key = queue_url_environment_key
|
|
37
|
-
self.queue_url_callable = queue_url_callable
|
|
38
|
-
self.message_group_id = message_group_id
|
|
39
|
-
|
|
40
|
-
queue_urls = 0
|
|
41
|
-
for value in [queue_url, queue_url_environment_key, queue_url_callable]:
|
|
42
|
-
if value:
|
|
43
|
-
queue_urls += 1
|
|
44
|
-
if queue_urls > 1:
|
|
45
|
-
raise ValueError(
|
|
46
|
-
"You can only provide one of 'queue_url', 'queue_url_environment_key', or 'queue_url_callable', but more than one were provided."
|
|
47
|
-
)
|
|
48
|
-
if not queue_urls:
|
|
49
|
-
raise ValueError(
|
|
50
|
-
"You must provide at least one of 'queue_url', 'queue_url_environment_key', or 'queue_url_callable'."
|
|
51
|
-
)
|
|
52
|
-
if message_group_id and not callable(message_group_id) and not isinstance(message_group_id, str):
|
|
53
|
-
raise ValueError(
|
|
54
|
-
"If provided, 'message_group_id' must be a string or callable, but the provided value was neither."
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
def _execute_action(self, client: ModuleType, model: Model) -> None:
|
|
58
|
-
"""Send a notification as configured."""
|
|
59
|
-
params = {
|
|
60
|
-
"QueueUrl": self.get_queue_url(model),
|
|
61
|
-
"MessageBody": self.get_message_body(model),
|
|
62
|
-
}
|
|
63
|
-
if not params["QueueUrl"]:
|
|
64
|
-
return
|
|
65
|
-
|
|
66
|
-
if self.message_group_id:
|
|
67
|
-
if callable(self.message_group_id):
|
|
68
|
-
message_group_id = self.di.call_function(self.message_group_id, model=model)
|
|
69
|
-
if not isinstance(message_group_id, str):
|
|
70
|
-
raise ValueError(f"I called the message_group_id function for SQS for model '{model.__class__.__name__}' but the value it returned was not a string. The message group id must be a string.")
|
|
71
|
-
else:
|
|
72
|
-
message_group_id = self.message_group_id
|
|
73
|
-
params["MessageGroupId"] = message_group_id
|
|
74
|
-
|
|
75
|
-
client.send_message(**params)
|
|
76
|
-
|
|
77
|
-
def get_queue_url(self, model: Model):
|
|
78
|
-
if self.queue_url:
|
|
79
|
-
return self.queue_url
|
|
80
|
-
if self.queue_url_environment_key:
|
|
81
|
-
return self.environment.get(self.queue_url_environment_key)
|
|
82
|
-
return self.di.call_function(self.queue_url_callable, model=model)
|