clear-skies-aws 1.10.2__py3-none-any.whl → 2.0.2__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.2.dist-info → clear_skies_aws-2.0.2.dist-info}/METADATA +36 -35
- clear_skies_aws-2.0.2.dist-info/RECORD +63 -0
- {clear_skies_aws-1.10.2.dist-info → clear_skies_aws-2.0.2.dist-info}/WHEEL +1 -1
- clear_skies_aws-2.0.2.dist-info/licenses/LICENSE +21 -0
- clearskies_aws/__init__.py +15 -2
- clearskies_aws/actions/__init__.py +13 -106
- clearskies_aws/actions/action_aws.py +74 -57
- clearskies_aws/actions/assume_role.py +43 -30
- clearskies_aws/actions/ses.py +82 -73
- clearskies_aws/actions/sns.py +27 -30
- clearskies_aws/actions/sqs.py +32 -33
- clearskies_aws/actions/step_function.py +38 -31
- clearskies_aws/backends/__init__.py +11 -4
- clearskies_aws/backends/backend.py +106 -0
- clearskies_aws/backends/dynamo_db_backend.py +150 -155
- clearskies_aws/backends/dynamo_db_condition_parser.py +40 -80
- clearskies_aws/backends/dynamo_db_parti_ql_backend.py +179 -337
- clearskies_aws/backends/sqs_backend.py +32 -51
- clearskies_aws/configs/__init__.py +0 -0
- clearskies_aws/contexts/__init__.py +23 -10
- clearskies_aws/contexts/cli_web_socket_mock.py +19 -0
- clearskies_aws/contexts/lambda_alb.py +76 -0
- clearskies_aws/contexts/lambda_api_gateway.py +75 -28
- clearskies_aws/contexts/lambda_api_gateway_web_socket.py +56 -29
- clearskies_aws/contexts/lambda_invocation.py +15 -44
- clearskies_aws/contexts/lambda_sns.py +8 -33
- clearskies_aws/contexts/lambda_sqs_standard_partial_batch.py +14 -36
- clearskies_aws/di/__init__.py +6 -1
- clearskies_aws/di/aws_additional_config_auto_import.py +37 -0
- clearskies_aws/di/inject/__init__.py +6 -0
- clearskies_aws/di/inject/boto3.py +15 -0
- clearskies_aws/di/inject/boto3_session.py +13 -0
- clearskies_aws/di/inject/parameter_store.py +15 -0
- clearskies_aws/{handlers → endpoints}/secrets_manager_rotation.py +76 -55
- clearskies_aws/endpoints/simple_body_routing.py +41 -0
- clearskies_aws/input_outputs/__init__.py +21 -8
- clearskies_aws/input_outputs/{cli_websocket_mock.py → cli_web_socket_mock.py} +9 -3
- clearskies_aws/input_outputs/lambda_alb.py +53 -0
- clearskies_aws/input_outputs/lambda_api_gateway.py +106 -88
- clearskies_aws/input_outputs/lambda_api_gateway_web_socket.py +69 -6
- clearskies_aws/input_outputs/lambda_input_output.py +87 -0
- clearskies_aws/input_outputs/lambda_invocation.py +77 -26
- clearskies_aws/input_outputs/lambda_sns.py +66 -39
- clearskies_aws/input_outputs/lambda_sqs_standard.py +70 -40
- clearskies_aws/mocks/actions/ses.py +25 -19
- clearskies_aws/mocks/actions/sns.py +18 -12
- clearskies_aws/mocks/actions/sqs.py +18 -12
- clearskies_aws/mocks/actions/step_function.py +19 -13
- clearskies_aws/models/__init__.py +0 -0
- clearskies_aws/models/web_socket_connection_model.py +182 -0
- clearskies_aws/secrets/__init__.py +13 -7
- clearskies_aws/secrets/additional_configs/__init__.py +10 -2
- clearskies_aws/secrets/additional_configs/iam_db_auth.py +26 -16
- clearskies_aws/secrets/additional_configs/iam_db_auth_with_ssm.py +43 -39
- clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +30 -31
- clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssm_bastion.py +70 -49
- clearskies_aws/secrets/akeyless_with_ssm_cache.py +32 -18
- clearskies_aws/secrets/parameter_store.py +34 -32
- clearskies_aws/secrets/secrets.py +16 -0
- clearskies_aws/secrets/secrets_manager.py +78 -57
- clear_skies_aws-1.10.2.dist-info/LICENSE +0 -7
- clear_skies_aws-1.10.2.dist-info/RECORD +0 -71
- clearskies_aws/actions/assume_role_test.py +0 -72
- clearskies_aws/actions/ses_test.py +0 -89
- clearskies_aws/actions/sns_test.py +0 -77
- clearskies_aws/actions/sqs_test.py +0 -127
- clearskies_aws/actions/step_function_test.py +0 -103
- clearskies_aws/backends/dynamo_db_backend_test.py +0 -300
- clearskies_aws/backends/dynamo_db_condition_parser_test.py +0 -266
- clearskies_aws/backends/dynamo_db_parti_ql_backend_test.py +0 -544
- clearskies_aws/backends/sqs_backend_test.py +0 -31
- clearskies_aws/contexts/cli.py +0 -19
- clearskies_aws/contexts/cli_websocket_mock.py +0 -33
- clearskies_aws/contexts/lambda_elb.py +0 -30
- clearskies_aws/contexts/lambda_http_gateway.py +0 -30
- clearskies_aws/contexts/lambda_sqs_standard_partial_batch_test.py +0 -66
- clearskies_aws/contexts/wsgi.py +0 -19
- clearskies_aws/di/standard_dependencies.py +0 -60
- clearskies_aws/handlers/simple_body_routing.py +0 -39
- clearskies_aws/input_outputs/lambda_api_gateway_test.py +0 -87
- clearskies_aws/input_outputs/lambda_elb.py +0 -21
- clearskies_aws/input_outputs/lambda_http_gateway.py +0 -12
- clearskies_aws/secrets/parameter_store_test.py +0 -18
- clearskies_aws/secrets/secrets_manager_test.py +0 -18
- clearskies_aws/web_socket_connection_model.py +0 -43
- clearskies_aws/{handlers → endpoints}/__init__.py +1 -1
|
@@ -1,8 +1,71 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from clearskies.configs import String
|
|
6
|
+
from clearskies.input_outputs import Headers
|
|
7
|
+
|
|
8
|
+
from clearskies_aws.input_outputs import lambda_input_output
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LambdaApiGatewayWebSocket(lambda_input_output.LambdaInputOutput):
|
|
12
|
+
"""Api Gateway WebSocket specific Lambda input/output handler."""
|
|
13
|
+
|
|
14
|
+
route_key = String(default="")
|
|
15
|
+
connection_id = String(default="")
|
|
16
|
+
|
|
17
|
+
def __init__(self, event: dict[str, Any], context: dict[str, Any], url: str = ""):
|
|
18
|
+
# Call parent constructor
|
|
19
|
+
super().__init__(event, context)
|
|
20
|
+
|
|
21
|
+
self.path = url
|
|
22
|
+
|
|
23
|
+
# WebSocket specific initialization
|
|
24
|
+
request_context = event.get("requestContext", {})
|
|
25
|
+
|
|
26
|
+
# WebSocket uses route_key, but doesn't have either a route or a method
|
|
27
|
+
self.route_key = request_context.get("routeKey", "")
|
|
28
|
+
self.request_method = self.route_key.upper() # For compatibility
|
|
29
|
+
|
|
30
|
+
# WebSocket connection ID
|
|
31
|
+
self.connection_id = request_context.get("connectionId", "")
|
|
32
|
+
|
|
33
|
+
# These will only be available, at monst, during the on-connect step
|
|
34
|
+
self.query_parameters = event.get("queryStringParameters") or {}
|
|
35
|
+
headers_dict = {}
|
|
36
|
+
for key, value in event.get("headers", {}).items():
|
|
37
|
+
headers_dict[key.lower()] = str(value)
|
|
38
|
+
self.request_headers = Headers(headers_dict)
|
|
39
|
+
|
|
40
|
+
def get_client_ip(self) -> str:
|
|
41
|
+
"""Get the client IP address from WebSocket request context."""
|
|
42
|
+
request_context = self.event.get("requestContext", {})
|
|
43
|
+
identity = request_context.get("identity", {})
|
|
44
|
+
|
|
45
|
+
if "sourceIp" in identity:
|
|
46
|
+
return identity["sourceIp"]
|
|
47
|
+
|
|
48
|
+
raise ValueError("Unable to find the client ip inside the API Gateway")
|
|
49
|
+
|
|
50
|
+
def respond(self, body: Any, status_code: int = 200) -> dict[str, Any]:
|
|
51
|
+
"""Create WebSocket specific response format."""
|
|
52
|
+
# WebSocket responses are simpler than HTTP responses
|
|
4
53
|
return {
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
54
|
+
"statusCode": status_code,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
def context_specifics(self) -> dict[str, Any]:
|
|
58
|
+
"""Provide WebSocket specific context data."""
|
|
59
|
+
request_context = self.event.get("requestContext", {})
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
**super().context_specifics(),
|
|
63
|
+
"connection_id": self.connection_id,
|
|
64
|
+
"route_key": self.route_key,
|
|
65
|
+
"stage": request_context.get("stage"),
|
|
66
|
+
"request_id": request_context.get("requestId"),
|
|
67
|
+
"api_id": request_context.get("apiId"),
|
|
68
|
+
"domain_name": request_context.get("domainName"),
|
|
69
|
+
"event_type": request_context.get("eventType"),
|
|
70
|
+
"connected_at": request_context.get("connectedAt"),
|
|
8
71
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
from abc import abstractmethod
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
from urllib.parse import urlencode
|
|
8
|
+
|
|
9
|
+
from clearskies.configs import AnyDict, String
|
|
10
|
+
from clearskies.input_outputs import InputOutput
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LambdaInputOutput(InputOutput):
|
|
14
|
+
"""Base class for Lambda input/output handlers that provides common Lambda functionality."""
|
|
15
|
+
|
|
16
|
+
event = AnyDict(default={})
|
|
17
|
+
context = AnyDict(default={})
|
|
18
|
+
path = String(default="/")
|
|
19
|
+
|
|
20
|
+
_cached_body = None
|
|
21
|
+
_body_was_cached = False
|
|
22
|
+
|
|
23
|
+
def __init__(self, event: dict[str, Any], context: dict[str, Any]):
|
|
24
|
+
# Store event and context
|
|
25
|
+
self.event = event
|
|
26
|
+
self.context = context
|
|
27
|
+
|
|
28
|
+
# Initialize the base class
|
|
29
|
+
super().__init__()
|
|
30
|
+
|
|
31
|
+
def respond(self, body: Any, status_code: int = 200) -> dict[str, Any]:
|
|
32
|
+
"""Create standard Lambda HTTP response format."""
|
|
33
|
+
if "content-type" not in self.response_headers:
|
|
34
|
+
self.response_headers.content_type = "application/json; charset=UTF-8"
|
|
35
|
+
|
|
36
|
+
is_base64 = False
|
|
37
|
+
|
|
38
|
+
if isinstance(body, bytes):
|
|
39
|
+
is_base64 = True
|
|
40
|
+
final_body = base64.encodebytes(body).decode("utf8")
|
|
41
|
+
elif isinstance(body, str):
|
|
42
|
+
final_body = body
|
|
43
|
+
else:
|
|
44
|
+
final_body = json.dumps(body)
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
"isBase64Encoded": is_base64,
|
|
48
|
+
"statusCode": status_code,
|
|
49
|
+
"headers": dict(self.response_headers),
|
|
50
|
+
"body": final_body,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def has_body(self) -> bool:
|
|
54
|
+
return bool(self.get_body())
|
|
55
|
+
|
|
56
|
+
def get_body(self) -> str:
|
|
57
|
+
"""Get request body with base64 decoding if needed."""
|
|
58
|
+
if not self._body_was_cached:
|
|
59
|
+
self._body_was_cached = True
|
|
60
|
+
self._cached_body = self.event.get("body", "")
|
|
61
|
+
if (
|
|
62
|
+
self._cached_body is not None
|
|
63
|
+
and self.event.get("isBase64Encoded", False)
|
|
64
|
+
and isinstance(self._cached_body, str)
|
|
65
|
+
):
|
|
66
|
+
self._cached_body = base64.decodebytes(self._cached_body.encode("utf-8")).decode("utf-8")
|
|
67
|
+
return self._cached_body or ""
|
|
68
|
+
|
|
69
|
+
def get_client_ip(self) -> str:
|
|
70
|
+
"""Get client IP - can be overridden by subclasses for event-specific logic."""
|
|
71
|
+
return "127.0.0.1"
|
|
72
|
+
|
|
73
|
+
def get_protocol(self) -> str:
|
|
74
|
+
"""Get protocol."""
|
|
75
|
+
# Default to HTTPS for most Lambda HTTP events
|
|
76
|
+
return "https"
|
|
77
|
+
|
|
78
|
+
def get_full_path(self) -> str:
|
|
79
|
+
"""Get full path."""
|
|
80
|
+
return self.path
|
|
81
|
+
|
|
82
|
+
def context_specifics(self) -> dict[str, Any]:
|
|
83
|
+
"""Provide Lambda-specific context."""
|
|
84
|
+
return {
|
|
85
|
+
"event": self.event,
|
|
86
|
+
"context": self.context,
|
|
87
|
+
}
|
|
@@ -1,34 +1,85 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import json
|
|
3
|
-
from
|
|
4
|
-
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from clearskies.exceptions import ClientError
|
|
7
|
+
from clearskies.input_outputs import Headers
|
|
8
|
+
|
|
9
|
+
from clearskies_aws.input_outputs import lambda_input_output
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LambdaInvocation(lambda_input_output.LambdaInputOutput):
|
|
13
|
+
"""Direct Lambda invocation specific input/output handler."""
|
|
14
|
+
|
|
5
15
|
def __init__(
|
|
6
16
|
self,
|
|
7
|
-
event,
|
|
8
|
-
context,
|
|
9
|
-
method=
|
|
10
|
-
url=
|
|
17
|
+
event: dict[str, Any],
|
|
18
|
+
context: dict[str, Any],
|
|
19
|
+
method: str = "POST",
|
|
20
|
+
url: str = "",
|
|
11
21
|
):
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
self.
|
|
17
|
-
self.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
# Call parent constructor
|
|
23
|
+
super().__init__(event, context)
|
|
24
|
+
|
|
25
|
+
# Direct invocation specific initialization
|
|
26
|
+
self.path = url
|
|
27
|
+
self.request_method = method.upper()
|
|
28
|
+
|
|
29
|
+
# Direct invocations don't have query parameters or path parameters
|
|
30
|
+
self.query_parameters = {}
|
|
31
|
+
|
|
32
|
+
# Direct invocations don't have headers
|
|
33
|
+
self.request_headers = Headers({})
|
|
34
|
+
|
|
35
|
+
def has_body(self) -> bool:
|
|
36
|
+
"""Direct invocations always have a body (the event itself)."""
|
|
21
37
|
return True
|
|
22
38
|
|
|
23
|
-
def get_body(self):
|
|
24
|
-
|
|
39
|
+
def get_body(self) -> str:
|
|
40
|
+
"""Get the entire event as the body."""
|
|
41
|
+
if isinstance(self.event, (dict, list)):
|
|
42
|
+
return json.dumps(self.event)
|
|
43
|
+
return str(self.event)
|
|
25
44
|
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
def respond(self, body: Any, status_code: int = 200) -> Any:
|
|
46
|
+
"""Return the response directly for direct invocations."""
|
|
47
|
+
if isinstance(body, bytes):
|
|
48
|
+
return body.decode("utf-8")
|
|
49
|
+
return body
|
|
50
|
+
|
|
51
|
+
def get_client_ip(self) -> str:
|
|
52
|
+
"""Direct invocations don't have client IP information."""
|
|
53
|
+
return "127.0.0.1"
|
|
54
|
+
|
|
55
|
+
def get_protocol(self) -> str:
|
|
56
|
+
"""Direct invocations don't have a protocol."""
|
|
57
|
+
return "lambda"
|
|
32
58
|
|
|
33
|
-
def
|
|
34
|
-
|
|
59
|
+
def get_full_path(self) -> str:
|
|
60
|
+
"""Return the configured path."""
|
|
61
|
+
return self.path
|
|
62
|
+
|
|
63
|
+
def context_specifics(self) -> dict[str, Any]:
|
|
64
|
+
"""Provide direct invocation specific context data."""
|
|
65
|
+
return {
|
|
66
|
+
**super().context_specifics(),
|
|
67
|
+
"invocation_type": "direct",
|
|
68
|
+
"function_name": self.context.get("function_name"),
|
|
69
|
+
"function_version": self.context.get("function_version"),
|
|
70
|
+
"request_id": self.context.get("aws_request_id"),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def request_data(self) -> dict[str, Any] | list[Any] | None:
|
|
75
|
+
"""Return the event directly as request data."""
|
|
76
|
+
return self.event
|
|
77
|
+
|
|
78
|
+
def json_body(
|
|
79
|
+
self, required: bool = True, allow_non_json_bodies: bool = False
|
|
80
|
+
) -> dict[str, Any] | list[Any] | None:
|
|
81
|
+
"""Get the event as JSON data."""
|
|
82
|
+
# For direct invocations, the event is already an object, not a JSON string
|
|
83
|
+
if required and not self.event:
|
|
84
|
+
raise ClientError("Request body was not valid JSON")
|
|
85
|
+
return self.event
|
|
@@ -1,52 +1,79 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
3
|
import json
|
|
4
|
-
|
|
5
|
-
def __init__(self, event, context, url=None, method=None):
|
|
6
|
-
self._event = event
|
|
7
|
-
self._context = context
|
|
8
|
-
self._path = url if url else ''
|
|
9
|
-
self._request_method = method.upper() if method else 'GET'
|
|
10
|
-
record = event['Records'][0]['Sns']['Message']
|
|
11
|
-
try:
|
|
12
|
-
self._record = json.loads(record)
|
|
13
|
-
except json.JSONDecodeError as e:
|
|
14
|
-
raise ClientError("The message from AWS was not a serialized JSON string. The lambda_sns context for clearskies only accepts serialized JSON")
|
|
4
|
+
from typing import Any
|
|
15
5
|
|
|
16
|
-
|
|
17
|
-
|
|
6
|
+
from clearskies.exceptions import ClientError
|
|
7
|
+
from clearskies.input_outputs import Headers
|
|
18
8
|
|
|
19
|
-
|
|
20
|
-
return self._record
|
|
9
|
+
from clearskies_aws.input_outputs import lambda_input_output
|
|
21
10
|
|
|
22
|
-
def request_data(self, required=True, allow_non_json_bodies=False):
|
|
23
|
-
return self.json_body(required=required, allow_non_json_bodies=allow_non_json_bodies)
|
|
24
11
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if required:
|
|
28
|
-
raise ClientError("No SNS message found")
|
|
29
|
-
return {}
|
|
12
|
+
class LambdaSns(lambda_input_output.LambdaInputOutput):
|
|
13
|
+
"""SNS specific Lambda input/output handler."""
|
|
30
14
|
|
|
31
|
-
|
|
15
|
+
def __init__(self, event: dict, context: dict[str, Any], url: str = "", method: str = "POST"):
|
|
16
|
+
# Call parent constructor
|
|
17
|
+
super().__init__(event, context)
|
|
18
|
+
|
|
19
|
+
# SNS specific initialization
|
|
20
|
+
self.path = url
|
|
21
|
+
self.request_method = method.upper()
|
|
22
|
+
|
|
23
|
+
# SNS events don't have query parameters or path parameters
|
|
24
|
+
self.query_parameters = {}
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
# SNS events don't have headers
|
|
27
|
+
self.request_headers = Headers({})
|
|
35
28
|
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
# Extract SNS message from event
|
|
30
|
+
try:
|
|
31
|
+
record = event["Records"][0]["Sns"]["Message"]
|
|
32
|
+
self._record = json.loads(record)
|
|
33
|
+
except (KeyError, IndexError, json.JSONDecodeError) as e:
|
|
34
|
+
raise ClientError(
|
|
35
|
+
"The message from AWS was not a valid SNS event with serialized JSON. "
|
|
36
|
+
"The lambda_sns context for clearskies only accepts serialized JSON."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def respond(self, body: Any, status_code: int = 200) -> dict[str, Any]:
|
|
40
|
+
"""SNS events don't return responses."""
|
|
41
|
+
return {}
|
|
42
|
+
|
|
43
|
+
def get_body(self) -> str:
|
|
44
|
+
"""Get the SNS message as a JSON string."""
|
|
45
|
+
return json.dumps(self._record) if self._record else ""
|
|
38
46
|
|
|
39
|
-
def
|
|
40
|
-
|
|
47
|
+
def has_body(self) -> bool:
|
|
48
|
+
"""Check if SNS message exists."""
|
|
49
|
+
return bool(self._record)
|
|
41
50
|
|
|
42
|
-
def
|
|
43
|
-
|
|
51
|
+
def get_client_ip(self) -> str:
|
|
52
|
+
"""SNS events don't have client IP information."""
|
|
53
|
+
return "127.0.0.1"
|
|
44
54
|
|
|
45
|
-
def
|
|
46
|
-
|
|
55
|
+
def get_protocol(self) -> str:
|
|
56
|
+
"""SNS events don't have a protocol."""
|
|
57
|
+
return "sns"
|
|
47
58
|
|
|
48
|
-
def
|
|
49
|
-
|
|
59
|
+
def get_full_path(self) -> str:
|
|
60
|
+
"""Return the configured path."""
|
|
61
|
+
return self.path
|
|
50
62
|
|
|
51
|
-
def
|
|
52
|
-
|
|
63
|
+
def context_specifics(self) -> dict[str, Any]:
|
|
64
|
+
"""Provide SNS specific context data."""
|
|
65
|
+
sns_record = self.event.get("Records", [{}])[0].get("Sns", {})
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
**super().context_specifics(),
|
|
69
|
+
"sns_message_id": sns_record.get("MessageId"),
|
|
70
|
+
"sns_topic_arn": sns_record.get("TopicArn"),
|
|
71
|
+
"sns_subject": sns_record.get("Subject"),
|
|
72
|
+
"sns_timestamp": sns_record.get("Timestamp"),
|
|
73
|
+
"sns_message": self._record,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def request_data(self) -> dict[str, Any] | list[Any] | None:
|
|
78
|
+
"""Return the SNS message data directly."""
|
|
79
|
+
return self._record
|
|
@@ -1,54 +1,84 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
3
|
import json
|
|
4
|
-
|
|
5
|
-
def __init__(self, record, event, context, url=None, method=None):
|
|
6
|
-
self._record = record
|
|
7
|
-
self._context = context
|
|
8
|
-
self._event = event
|
|
9
|
-
self._path = url if url else ''
|
|
10
|
-
self._request_method = method.upper() if method else 'GET'
|
|
4
|
+
from typing import Any
|
|
11
5
|
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
from clearskies.configs import AnyDict, String
|
|
7
|
+
from clearskies.exceptions import ClientError
|
|
8
|
+
from clearskies.input_outputs import Headers
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
return self._record
|
|
10
|
+
from clearskies_aws.input_outputs import lambda_input_output
|
|
17
11
|
|
|
18
|
-
def has_body(self):
|
|
19
|
-
return True
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
|
|
13
|
+
class LambdaSqsStandard(lambda_input_output.LambdaInputOutput):
|
|
14
|
+
"""SQS standard queue specific Lambda input/output handler."""
|
|
23
15
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if required:
|
|
27
|
-
raise ClientError("SQS message was not valid JSON")
|
|
28
|
-
return {}
|
|
16
|
+
record = AnyDict(default={})
|
|
17
|
+
path = String(default="/")
|
|
29
18
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
19
|
+
def __init__(
|
|
20
|
+
self, record: str, event: dict[str, Any], context: dict[str, Any], url: str = "", method: str = "POST"
|
|
21
|
+
):
|
|
22
|
+
# Call parent constructor with the full event
|
|
23
|
+
super().__init__(event, context)
|
|
24
|
+
|
|
25
|
+
# Store the individual SQS record
|
|
26
|
+
self.record = json.loads(record)
|
|
27
|
+
print("SQS record:", self.record)
|
|
28
|
+
# SQS specific initialization
|
|
29
|
+
self.path = url if url else "/"
|
|
30
|
+
self.request_method = method.upper() if method else "POST"
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
32
|
+
# SQS events don't have query parameters or path parameters
|
|
33
|
+
self.query_parameters = {}
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
# SQS events don't have headers
|
|
36
|
+
self.request_headers = Headers({})
|
|
40
37
|
|
|
41
|
-
def
|
|
42
|
-
|
|
38
|
+
def respond(self, body: Any, status_code: int = 200) -> dict[str, Any]:
|
|
39
|
+
"""SQS events don't return responses."""
|
|
40
|
+
return {}
|
|
43
41
|
|
|
44
|
-
def
|
|
45
|
-
|
|
42
|
+
def get_body(self) -> str:
|
|
43
|
+
"""Get the SQS message body."""
|
|
44
|
+
return json.dumps(self.record)
|
|
46
45
|
|
|
47
|
-
def
|
|
48
|
-
|
|
46
|
+
def has_body(self) -> bool:
|
|
47
|
+
"""Check if SQS message has a body."""
|
|
48
|
+
return True
|
|
49
49
|
|
|
50
|
-
def
|
|
51
|
-
|
|
50
|
+
def get_client_ip(self) -> str:
|
|
51
|
+
"""SQS events don't have client IP information."""
|
|
52
|
+
return "127.0.0.1"
|
|
52
53
|
|
|
53
|
-
def
|
|
54
|
-
|
|
54
|
+
def get_protocol(self) -> str:
|
|
55
|
+
"""SQS events don't have a protocol."""
|
|
56
|
+
return "sqs"
|
|
57
|
+
|
|
58
|
+
def get_full_path(self) -> str:
|
|
59
|
+
"""Return the configured path."""
|
|
60
|
+
return self.path
|
|
61
|
+
|
|
62
|
+
def context_specifics(self) -> dict[str, Any]:
|
|
63
|
+
"""Provide SQS specific context data."""
|
|
64
|
+
return {
|
|
65
|
+
**super().context_specifics(),
|
|
66
|
+
"sqs_message_id": self.record.get("messageId"),
|
|
67
|
+
"sqs_receipt_handle": self.record.get("receiptHandle"),
|
|
68
|
+
"sqs_source_arn": self.record.get("eventSourceARN"),
|
|
69
|
+
"sqs_sent_timestamp": self.record.get("attributes", {}).get("SentTimestamp"),
|
|
70
|
+
"sqs_approximate_receive_count": self.record.get("attributes", {}).get("ApproximateReceiveCount"),
|
|
71
|
+
"sqs_message_attributes": self.record.get("messageAttributes", {}),
|
|
72
|
+
"sqs_record": self.record,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def request_data(self) -> dict[str, Any]:
|
|
77
|
+
"""Return the SQS message body as parsed JSON."""
|
|
78
|
+
body = self.get_body()
|
|
79
|
+
if not body:
|
|
80
|
+
return {}
|
|
81
|
+
try:
|
|
82
|
+
return json.loads(body)
|
|
83
|
+
except json.JSONDecodeError:
|
|
84
|
+
raise ClientError("SQS message body was not valid JSON")
|
|
@@ -1,28 +1,34 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from clearskies import Model
|
|
6
|
+
from types_boto3_ses import SESClient
|
|
7
|
+
|
|
8
|
+
from clearskies_aws.actions.ses import SES as BaseSES
|
|
4
9
|
|
|
5
|
-
|
|
6
|
-
|
|
10
|
+
|
|
11
|
+
class SES(BaseSES):
|
|
12
|
+
calls: list[dict[str, Any]] | None = None
|
|
7
13
|
|
|
8
14
|
@classmethod
|
|
9
15
|
def mock(cls, di):
|
|
10
16
|
cls.calls = []
|
|
11
17
|
di.mock_class(BaseSES, SES)
|
|
12
18
|
|
|
13
|
-
def
|
|
19
|
+
def _execute_action(self, client: SESClient, model: Model) -> None:
|
|
14
20
|
"""Send a notification as configured."""
|
|
15
|
-
if SES.calls
|
|
21
|
+
if SES.calls is None:
|
|
16
22
|
SES.calls = []
|
|
17
|
-
utcnow = self.di.build(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
utcnow = self.di.build("utcnow")
|
|
24
|
+
|
|
25
|
+
SES.calls.append(
|
|
26
|
+
{
|
|
27
|
+
"from": self.sender,
|
|
28
|
+
"to": self._resolve_destination("to", model),
|
|
29
|
+
"cc": self._resolve_destination("cc", model),
|
|
30
|
+
"bcc": self._resolve_destination("bcc", model),
|
|
31
|
+
"subject": self._resolve_subject(model, utcnow),
|
|
32
|
+
"message": self._resolve_message_as_html(model, utcnow),
|
|
33
|
+
}
|
|
34
|
+
)
|
|
@@ -1,23 +1,29 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
2
5
|
from clearskies import Model
|
|
3
|
-
from
|
|
4
|
-
class SNS(BaseSNS):
|
|
5
|
-
calls = None
|
|
6
|
+
from types_boto3_sns import SNSClient
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
from clearskies_aws.actions.sns import SNS as BaseSNS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SNS(BaseSNS):
|
|
12
|
+
calls: list[dict[str, Any]] | None = None
|
|
9
13
|
|
|
10
14
|
@classmethod
|
|
11
15
|
def mock(cls, di):
|
|
12
16
|
cls.calls = []
|
|
13
17
|
di.mock_class(BaseSNS, SNS)
|
|
14
18
|
|
|
15
|
-
def _execute_action(self, client:
|
|
19
|
+
def _execute_action(self, client: SNSClient, model: Model) -> None:
|
|
16
20
|
"""Send a notification as configured."""
|
|
17
|
-
if SNS.calls
|
|
21
|
+
if SNS.calls is None:
|
|
18
22
|
SNS.calls = []
|
|
19
23
|
|
|
20
|
-
SNS.calls.append(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
SNS.calls.append(
|
|
25
|
+
{
|
|
26
|
+
"TopicArn": self.get_topic_arn(model),
|
|
27
|
+
"Message": self.get_message_body(model),
|
|
28
|
+
}
|
|
29
|
+
)
|
|
@@ -1,23 +1,29 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
2
5
|
from clearskies import Model
|
|
3
|
-
from
|
|
4
|
-
class SQS(BaseSQS):
|
|
5
|
-
calls = None
|
|
6
|
+
from types_boto3_sqs import SQSClient
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
from clearskies_aws.actions.sqs import SQS as BaseSQS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SQS(BaseSQS):
|
|
12
|
+
calls: list[dict[str, Any]] | None = None
|
|
9
13
|
|
|
10
14
|
@classmethod
|
|
11
15
|
def mock(cls, di):
|
|
12
16
|
cls.calls = []
|
|
13
17
|
di.mock_class(BaseSQS, SQS)
|
|
14
18
|
|
|
15
|
-
def _execute_action(self, client:
|
|
19
|
+
def _execute_action(self, client: SQSClient, model: Model) -> None:
|
|
16
20
|
"""Send a notification as configured."""
|
|
17
|
-
if SQS.calls
|
|
21
|
+
if SQS.calls is None:
|
|
18
22
|
SQS.calls = []
|
|
19
23
|
|
|
20
|
-
SQS.calls.append(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
SQS.calls.append(
|
|
25
|
+
{
|
|
26
|
+
"QueueUrl": self.get_queue_url(model),
|
|
27
|
+
"MessageBody": self.get_message_body(model),
|
|
28
|
+
}
|
|
29
|
+
)
|