clear-skies-aws 2.0.1__py3-none-any.whl → 2.0.3__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.
Files changed (64) hide show
  1. {clear_skies_aws-2.0.1.dist-info → clear_skies_aws-2.0.3.dist-info}/METADATA +2 -2
  2. clear_skies_aws-2.0.3.dist-info/RECORD +63 -0
  3. {clear_skies_aws-2.0.1.dist-info → clear_skies_aws-2.0.3.dist-info}/WHEEL +1 -1
  4. clearskies_aws/__init__.py +27 -0
  5. clearskies_aws/actions/__init__.py +15 -0
  6. clearskies_aws/actions/action_aws.py +135 -0
  7. clearskies_aws/actions/assume_role.py +115 -0
  8. clearskies_aws/actions/ses.py +203 -0
  9. clearskies_aws/actions/sns.py +61 -0
  10. clearskies_aws/actions/sqs.py +81 -0
  11. clearskies_aws/actions/step_function.py +73 -0
  12. clearskies_aws/backends/__init__.py +19 -0
  13. clearskies_aws/backends/backend.py +106 -0
  14. clearskies_aws/backends/dynamo_db_backend.py +609 -0
  15. clearskies_aws/backends/dynamo_db_condition_parser.py +325 -0
  16. clearskies_aws/backends/dynamo_db_parti_ql_backend.py +965 -0
  17. clearskies_aws/backends/sqs_backend.py +61 -0
  18. clearskies_aws/configs/__init__.py +0 -0
  19. clearskies_aws/contexts/__init__.py +23 -0
  20. clearskies_aws/contexts/cli_web_socket_mock.py +20 -0
  21. clearskies_aws/contexts/lambda_alb.py +81 -0
  22. clearskies_aws/contexts/lambda_api_gateway.py +81 -0
  23. clearskies_aws/contexts/lambda_api_gateway_web_socket.py +79 -0
  24. clearskies_aws/contexts/lambda_invoke.py +138 -0
  25. clearskies_aws/contexts/lambda_sns.py +124 -0
  26. clearskies_aws/contexts/lambda_sqs_standard.py +139 -0
  27. clearskies_aws/di/__init__.py +6 -0
  28. clearskies_aws/di/aws_additional_config_auto_import.py +37 -0
  29. clearskies_aws/di/inject/__init__.py +6 -0
  30. clearskies_aws/di/inject/boto3.py +15 -0
  31. clearskies_aws/di/inject/boto3_session.py +13 -0
  32. clearskies_aws/di/inject/parameter_store.py +15 -0
  33. clearskies_aws/endpoints/__init__.py +1 -0
  34. clearskies_aws/endpoints/secrets_manager_rotation.py +194 -0
  35. clearskies_aws/endpoints/simple_body_routing.py +41 -0
  36. clearskies_aws/input_outputs/__init__.py +21 -0
  37. clearskies_aws/input_outputs/cli_web_socket_mock.py +20 -0
  38. clearskies_aws/input_outputs/lambda_alb.py +53 -0
  39. clearskies_aws/input_outputs/lambda_api_gateway.py +123 -0
  40. clearskies_aws/input_outputs/lambda_api_gateway_web_socket.py +73 -0
  41. clearskies_aws/input_outputs/lambda_input_output.py +89 -0
  42. clearskies_aws/input_outputs/lambda_invoke.py +88 -0
  43. clearskies_aws/input_outputs/lambda_sns.py +88 -0
  44. clearskies_aws/input_outputs/lambda_sqs_standard.py +86 -0
  45. clearskies_aws/mocks/__init__.py +1 -0
  46. clearskies_aws/mocks/actions/__init__.py +6 -0
  47. clearskies_aws/mocks/actions/ses.py +34 -0
  48. clearskies_aws/mocks/actions/sns.py +29 -0
  49. clearskies_aws/mocks/actions/sqs.py +29 -0
  50. clearskies_aws/mocks/actions/step_function.py +32 -0
  51. clearskies_aws/models/__init__.py +1 -0
  52. clearskies_aws/models/web_socket_connection_model.py +182 -0
  53. clearskies_aws/secrets/__init__.py +13 -0
  54. clearskies_aws/secrets/additional_configs/__init__.py +62 -0
  55. clearskies_aws/secrets/additional_configs/iam_db_auth.py +39 -0
  56. clearskies_aws/secrets/additional_configs/iam_db_auth_with_ssm.py +96 -0
  57. clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +80 -0
  58. clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssm_bastion.py +162 -0
  59. clearskies_aws/secrets/akeyless_with_ssm_cache.py +60 -0
  60. clearskies_aws/secrets/parameter_store.py +52 -0
  61. clearskies_aws/secrets/secrets.py +16 -0
  62. clearskies_aws/secrets/secrets_manager.py +96 -0
  63. clear_skies_aws-2.0.1.dist-info/RECORD +0 -4
  64. {clear_skies_aws-2.0.1.dist-info → clear_skies_aws-2.0.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,123 @@
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 LambdaApiGateway(lambda_input_output.LambdaInputOutput):
12
+ """API Gateway v1 and v2 Lambda input/output handler."""
13
+
14
+ resource = String(default="")
15
+
16
+ def __init__(self, event: dict, context: dict[str, Any]):
17
+ # Call parent constructor
18
+ super().__init__(event, context)
19
+
20
+ # Determine API Gateway version and parse accordingly
21
+ version = self._detect_version(event)
22
+ if version == "1.0":
23
+ self._parse_event_v1(event)
24
+ elif version == "2.0":
25
+ self._parse_event_v2(event)
26
+ else:
27
+ raise ValueError(f"Unsupported API Gateway event version: {version}")
28
+
29
+ def _detect_version(self, event: dict) -> str:
30
+ """Detect API Gateway version from event structure."""
31
+ if "version" in event:
32
+ return event["version"]
33
+ elif "httpMethod" in event:
34
+ return "1.0" # v1 has httpMethod at root level
35
+ elif "requestContext" in event and "http" in event["requestContext"]:
36
+ return "2.0" # v2 has http in requestContext
37
+ else:
38
+ raise ValueError("Unable to determine API Gateway version from event structure")
39
+
40
+ def _parse_event_v1(self, event: dict) -> None:
41
+ """Parse API Gateway v1 event structure."""
42
+ self.request_method = event.get("httpMethod", "GET").upper()
43
+ self.path = event.get("path", "/")
44
+ self.resource = event.get("resource", "")
45
+
46
+ # Extract query parameters (v1 has both single and multi-value)
47
+ self.query_parameters = {
48
+ **(event.get("queryStringParameters") or {}),
49
+ **(event.get("multiValueQueryStringParameters") or {}),
50
+ }
51
+
52
+ # Extract headers (v1 has both single and multi-value)
53
+ headers_dict = {}
54
+ for key, value in {
55
+ **event.get("headers", {}),
56
+ **event.get("multiValueHeaders", {}),
57
+ }.items():
58
+ headers_dict[key.lower()] = str(value)
59
+
60
+ self.request_headers = Headers(headers_dict)
61
+
62
+ def _parse_event_v2(self, event: dict) -> None:
63
+ """Parse API Gateway v2 event structure."""
64
+ request_context = event.get("requestContext", {})
65
+ http_context = request_context.get("http", {})
66
+
67
+ self.request_method = http_context.get("method", "GET").upper()
68
+ self.path = http_context.get("path", "/")
69
+ # v2 doesn't have resource field
70
+ self.resource = ""
71
+
72
+ # Extract query parameters (v2 only has single values)
73
+ self.query_parameters = event.get("queryStringParameters") or {}
74
+
75
+ # Extract headers (v2 only has single value headers)
76
+ headers_dict = {}
77
+ for key, value in event.get("headers", {}).items():
78
+ headers_dict[key.lower()] = str(value)
79
+
80
+ self.request_headers = Headers(headers_dict)
81
+
82
+ def get_client_ip(self) -> str:
83
+ """Get the client IP address from API Gateway event."""
84
+ request_context = self.event.get("requestContext", {})
85
+
86
+ # Try v1 format first (identity.sourceIp)
87
+ identity = request_context.get("identity", {})
88
+ if "sourceIp" in identity:
89
+ return identity["sourceIp"]
90
+
91
+ # Try v2 format (http.sourceIp)
92
+ http_context = request_context.get("http", {})
93
+ if "sourceIp" in http_context:
94
+ return http_context["sourceIp"]
95
+
96
+ raise ValueError("Unable to find the client ip inside the API Gateway")
97
+
98
+ def get_protocol(self) -> str:
99
+ """Get the protocol from API Gateway request context."""
100
+ request_context = self.event.get("requestContext", {})
101
+
102
+ # Try v2 format first (has explicit protocol)
103
+ http_context = request_context.get("http", {})
104
+ if "protocol" in http_context:
105
+ protocol = http_context["protocol"]
106
+ return "https" if protocol.upper().startswith("HTTPS") else "http"
107
+
108
+ # v1 defaults to HTTPS
109
+ return "https"
110
+
111
+ def context_specifics(self) -> dict[str, Any]:
112
+ """Provide API Gateway specific context data."""
113
+ request_context = self.event.get("requestContext", {})
114
+ http_context = request_context.get("http", {})
115
+
116
+ return {
117
+ **super().context_specifics(),
118
+ "resource": self.resource,
119
+ "stage": request_context.get("stage"),
120
+ "request_id": request_context.get("requestId"),
121
+ "api_id": request_context.get("apiId"),
122
+ "api_version": self._detect_version(self.event),
123
+ }
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from clearskies.configs import String
7
+ from clearskies.input_outputs import Headers
8
+
9
+ from clearskies_aws.input_outputs import lambda_input_output
10
+
11
+
12
+ class LambdaApiGatewayWebSocket(lambda_input_output.LambdaInputOutput):
13
+ """Api Gateway WebSocket specific Lambda input/output handler."""
14
+
15
+ route_key = String(default="")
16
+ connection_id = String(default="")
17
+
18
+ def __init__(self, event: dict[str, Any], context: dict[str, Any], url: str = ""):
19
+ # Call parent constructor
20
+ super().__init__(event, context)
21
+
22
+ self.path = url
23
+
24
+ # WebSocket specific initialization
25
+ request_context = event.get("requestContext", {})
26
+
27
+ # WebSocket uses route_key, but doesn't have either a route or a method
28
+ self.route_key = request_context.get("routeKey", "GET")
29
+ self.request_method = self.route_key.upper() # For compatibility
30
+
31
+ # WebSocket connection ID
32
+ self.connection_id = request_context.get("connectionId", "")
33
+
34
+ # These will only be available, at monst, during the on-connect step
35
+ self.query_parameters = event.get("queryStringParameters") or {}
36
+ headers_dict = {}
37
+ for key, value in event.get("headers", {}).items():
38
+ headers_dict[key.lower()] = str(value)
39
+ self.request_headers = Headers(headers_dict)
40
+
41
+ def get_client_ip(self) -> str:
42
+ """Get the client IP address from WebSocket request context."""
43
+ request_context = self.event.get("requestContext", {})
44
+ identity = request_context.get("identity", {})
45
+
46
+ if "sourceIp" in identity:
47
+ return identity["sourceIp"]
48
+
49
+ raise ValueError("Unable to find the client ip inside the API Gateway")
50
+
51
+ def respond(self, body: Any, status_code: int = 200) -> None:
52
+ # since there is no response to the client, we want to raise an exception for any non-200 status code so
53
+ # the lambda execution itself will be marked as a failure.
54
+ if status_code > 299:
55
+ if not isinstance(body, str):
56
+ body = json.dumps(body)
57
+ raise Exception(f"Non-200 Status code returned by application: {status_code}. Response: '{body}'")
58
+
59
+ def context_specifics(self) -> dict[str, Any]:
60
+ """Provide WebSocket specific context data."""
61
+ request_context = self.event.get("requestContext", {})
62
+
63
+ return {
64
+ **super().context_specifics(),
65
+ "connection_id": self.connection_id,
66
+ "route_key": self.route_key,
67
+ "stage": request_context.get("stage"),
68
+ "request_id": request_context.get("requestId"),
69
+ "api_id": request_context.get("apiId"),
70
+ "domain_name": request_context.get("domainName"),
71
+ "event_type": request_context.get("eventType"),
72
+ "connected_at": request_context.get("connectedAt"),
73
+ }
@@ -0,0 +1,89 @@
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__(
24
+ self, event: dict[str, Any], context: dict[str, Any], url: str | None = "", request_method: str | None = ""
25
+ ):
26
+ # Store event and context
27
+ self.event = event
28
+ self.context = context
29
+
30
+ # Initialize the base class
31
+ super().__init__()
32
+
33
+ def respond(self, body: Any, status_code: int = 200) -> dict[str, Any] | None:
34
+ """Create standard Lambda HTTP response format."""
35
+ if "content-type" not in self.response_headers:
36
+ self.response_headers.content_type = "application/json; charset=UTF-8"
37
+
38
+ is_base64 = False
39
+
40
+ if isinstance(body, bytes):
41
+ is_base64 = True
42
+ final_body = base64.encodebytes(body).decode("utf8")
43
+ elif isinstance(body, str):
44
+ final_body = body
45
+ else:
46
+ final_body = json.dumps(body)
47
+
48
+ return {
49
+ "isBase64Encoded": is_base64,
50
+ "statusCode": status_code,
51
+ "headers": dict(self.response_headers),
52
+ "body": final_body,
53
+ }
54
+
55
+ def has_body(self) -> bool:
56
+ return bool(self.get_body())
57
+
58
+ def get_body(self) -> str:
59
+ """Get request body with base64 decoding if needed."""
60
+ if not self._body_was_cached:
61
+ self._body_was_cached = True
62
+ self._cached_body = self.event.get("body", "")
63
+ if (
64
+ self._cached_body is not None
65
+ and self.event.get("isBase64Encoded", False)
66
+ and isinstance(self._cached_body, str)
67
+ ):
68
+ self._cached_body = base64.decodebytes(self._cached_body.encode("utf-8")).decode("utf-8")
69
+ return self._cached_body or ""
70
+
71
+ def get_client_ip(self) -> str:
72
+ """Get client IP - can be overridden by subclasses for event-specific logic."""
73
+ return "127.0.0.1"
74
+
75
+ def get_protocol(self) -> str:
76
+ """Get protocol."""
77
+ # Default to HTTPS for most Lambda HTTP events
78
+ return "https"
79
+
80
+ def get_full_path(self) -> str:
81
+ """Get full path."""
82
+ return self.path
83
+
84
+ def context_specifics(self) -> dict[str, Any]:
85
+ """Provide Lambda-specific context."""
86
+ return {
87
+ "event": self.event,
88
+ "context": self.context,
89
+ }
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ import json
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 LambdaInvoke(lambda_input_output.LambdaInputOutput):
13
+ """Direct Lambda invocation specific input/output handler."""
14
+
15
+ def __init__(
16
+ self,
17
+ event: dict[str, Any],
18
+ context: dict[str, Any],
19
+ request_method: str = "",
20
+ url: str = "",
21
+ ):
22
+ # Call parent constructor
23
+ super().__init__(event, context)
24
+
25
+ # Direct invocation specific initialization
26
+ if url:
27
+ self.path = url
28
+ else:
29
+ self.supports_url = True
30
+ if request_method:
31
+ self.request_method = request_method.upper()
32
+ else:
33
+ self.supports_request_method = False
34
+
35
+ # Direct invocations don't have headers
36
+ self.request_headers = Headers({})
37
+
38
+ def has_body(self) -> bool:
39
+ """Direct invocations always have a body (the event itself)."""
40
+ return True
41
+
42
+ def get_body(self) -> str:
43
+ """Get the entire event as the body."""
44
+ if isinstance(self.event, (dict, list)):
45
+ return json.dumps(self.event)
46
+ return str(self.event)
47
+
48
+ def respond(self, body: Any, status_code: int = 200) -> Any:
49
+ """Return the response directly for direct invocations."""
50
+ if isinstance(body, bytes):
51
+ return body.decode("utf-8")
52
+ return body
53
+
54
+ def get_client_ip(self) -> str:
55
+ """Direct invocations don't have client IP information."""
56
+ return "127.0.0.1"
57
+
58
+ def get_protocol(self) -> str:
59
+ """Direct invocations don't have a protocol."""
60
+ return "lambda"
61
+
62
+ def get_full_path(self) -> str:
63
+ """Return the configured path."""
64
+ return self.path
65
+
66
+ def context_specifics(self) -> dict[str, Any]:
67
+ """Provide direct invocation specific context data."""
68
+ return {
69
+ **super().context_specifics(),
70
+ "invocation_type": "direct",
71
+ "function_name": self.context.get("function_name"),
72
+ "function_version": self.context.get("function_version"),
73
+ "request_id": self.context.get("aws_request_id"),
74
+ }
75
+
76
+ @property
77
+ def request_data(self) -> dict[str, Any] | list[Any] | None:
78
+ """Return the event directly as request data."""
79
+ return self.event
80
+
81
+ def json_body(
82
+ self, required: bool = True, allow_non_json_bodies: bool = False
83
+ ) -> dict[str, Any] | list[Any] | None:
84
+ """Get the event as JSON data."""
85
+ # For direct invocations, the event is already an object, not a JSON string
86
+ if required and not self.event:
87
+ raise ClientError("Request body was not valid JSON")
88
+ return self.event
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ import json
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 LambdaSns(lambda_input_output.LambdaInputOutput):
13
+ """SNS specific Lambda input/output handler."""
14
+
15
+ record: dict[str, Any]
16
+
17
+ def __init__(self, event: dict, context: dict[str, Any], url: str = "", request_method: str = ""):
18
+ # Call parent constructor
19
+ super().__init__(event, context)
20
+
21
+ # SNS specific initialization
22
+ if url:
23
+ self.path = url
24
+ else:
25
+ self.supports_url = False
26
+ if request_method:
27
+ self.request_method = request_method.upper()
28
+ else:
29
+ self.supports_request_method = False
30
+
31
+ # SNS events don't have headers
32
+ self.request_headers = Headers({})
33
+
34
+ # Extract SNS message from event
35
+ try:
36
+ record = event["Records"][0]["Sns"]["Message"]
37
+ self.record = json.loads(record)
38
+ except (KeyError, IndexError, json.JSONDecodeError) as e:
39
+ raise ClientError(
40
+ "The message from AWS was not a valid SNS event with serialized JSON. "
41
+ "The lambda_sns context for clearskies only accepts serialized JSON."
42
+ )
43
+
44
+ def respond(self, body: Any, status_code: int = 200) -> None:
45
+ """Respond to the client, but SNS has no client."""
46
+ # since there is no response to the client, we want to raise an exception for any non-200 status code so
47
+ # the lambda execution itself will be marked as a failure.
48
+ if status_code > 299:
49
+ if not isinstance(body, str):
50
+ body = json.dumps(body)
51
+ raise Exception(f"Non-200 Status code returned by application: {status_code}. Response: '{body}'")
52
+
53
+ def get_body(self) -> str:
54
+ """Get the SNS message as a JSON string."""
55
+ return json.dumps(self.record) if self.record else ""
56
+
57
+ def has_body(self) -> bool:
58
+ """Check if SNS message exists."""
59
+ return bool(self.record)
60
+
61
+ def get_client_ip(self) -> str:
62
+ """SNS events don't have client IP information."""
63
+ return "127.0.0.1"
64
+
65
+ def get_protocol(self) -> str:
66
+ """SNS events don't have a protocol."""
67
+ return "sns"
68
+
69
+ def get_full_path(self) -> str:
70
+ """Return the configured path."""
71
+ return self.path
72
+
73
+ def context_specifics(self) -> dict[str, Any]:
74
+ """Provide SNS specific context data."""
75
+ sns_record = self.event.get("Records", [{}])[0].get("Sns", {})
76
+
77
+ return {
78
+ **super().context_specifics(),
79
+ "message_id": sns_record.get("MessageId"),
80
+ "topic_arn": sns_record.get("TopicArn"),
81
+ "subject": sns_record.get("Subject"),
82
+ "timestamp": sns_record.get("Timestamp"),
83
+ }
84
+
85
+ @property
86
+ def request_data(self) -> dict[str, Any] | list[Any] | None:
87
+ """Return the SNS message data directly."""
88
+ return self.record
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from clearskies.configs import AnyDict, String
7
+ from clearskies.exceptions import ClientError
8
+ from clearskies.input_outputs import Headers
9
+
10
+ from clearskies_aws.input_outputs import lambda_input_output
11
+
12
+
13
+ class LambdaSqsStandard(lambda_input_output.LambdaInputOutput):
14
+ """SQS standard queue specific Lambda input/output handler."""
15
+
16
+ record = AnyDict(default={})
17
+ path = String(default="/")
18
+
19
+ def __init__(
20
+ self,
21
+ record: dict[str, Any],
22
+ event: dict[str, Any],
23
+ context: dict[str, Any],
24
+ url: str = "",
25
+ request_method: str = "",
26
+ ):
27
+ # Call parent constructor with the full event
28
+ super().__init__(event, context)
29
+
30
+ # Store the individual SQS record
31
+ self.record = record
32
+ # SQS specific initialization
33
+ if url:
34
+ self.path = url
35
+ else:
36
+ self.supports_url = False
37
+
38
+ if request_method:
39
+ self.request_method = request_method.upper()
40
+ else:
41
+ self.supports_request_method = False
42
+
43
+ # SQS events don't have headers
44
+ self.request_headers = Headers({})
45
+
46
+ def respond(self, body: Any, status_code: int = 200) -> None:
47
+ """Respond to the client, but SQS has no client."""
48
+ # since there is no response to the client, we want to raise an exception for any non-200 status code so
49
+ # the lambda execution itself will be marked as a failure.
50
+ if status_code > 299:
51
+ if not isinstance(body, str):
52
+ body = json.dumps(body)
53
+ raise Exception(f"Non-200 Status code returned by application: {status_code}. Response: '{body}'")
54
+
55
+ def get_body(self) -> str:
56
+ """Get request body with base64 decoding if needed."""
57
+ return self.record["body"]
58
+
59
+ def has_body(self) -> bool:
60
+ """Check if SQS message has a body."""
61
+ return True
62
+
63
+ def get_client_ip(self) -> str:
64
+ """SQS events don't have client IP information."""
65
+ return "127.0.0.1"
66
+
67
+ def get_protocol(self) -> str:
68
+ """SQS events don't have a protocol."""
69
+ return "sqs"
70
+
71
+ def get_full_path(self) -> str:
72
+ """Return the configured path."""
73
+ return self.path
74
+
75
+ def context_specifics(self) -> dict[str, Any]:
76
+ """Provide SQS specific context data."""
77
+ return {
78
+ **super().context_specifics(),
79
+ "message_id": self.record.get("messageId"),
80
+ "receipt_handle": self.record.get("receiptHandle"),
81
+ "source_arn": self.record.get("eventSourceARN"),
82
+ "sent_timestamp": self.record.get("attributes", {}).get("SentTimestamp"),
83
+ "approximate_receive_count": self.record.get("attributes", {}).get("ApproximateReceiveCount"),
84
+ "message_attributes": self.record.get("messageAttributes", {}),
85
+ "record": self.record,
86
+ }
@@ -0,0 +1 @@
1
+ from . import actions
@@ -0,0 +1,6 @@
1
+ from .ses import SES
2
+ from .sns import SNS
3
+ from .sqs import SQS
4
+ from .step_function import StepFunction
5
+
6
+ __all__ = ["SES", "SNS", "SQS", "StepFunction"]
@@ -0,0 +1,34 @@
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
9
+
10
+
11
+ class SES(BaseSES):
12
+ calls: list[dict[str, Any]] | None = None
13
+
14
+ @classmethod
15
+ def mock(cls, di):
16
+ cls.calls = []
17
+ di.mock_class(BaseSES, SES)
18
+
19
+ def _execute_action(self, client: SESClient, model: Model) -> None:
20
+ """Send a notification as configured."""
21
+ if SES.calls is None:
22
+ SES.calls = []
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
+ )
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from clearskies import Model
6
+ from types_boto3_sns import SNSClient
7
+
8
+ from clearskies_aws.actions.sns import SNS as BaseSNS
9
+
10
+
11
+ class SNS(BaseSNS):
12
+ calls: list[dict[str, Any]] | None = None
13
+
14
+ @classmethod
15
+ def mock(cls, di):
16
+ cls.calls = []
17
+ di.mock_class(BaseSNS, SNS)
18
+
19
+ def _execute_action(self, client: SNSClient, model: Model) -> None:
20
+ """Send a notification as configured."""
21
+ if SNS.calls is None:
22
+ SNS.calls = []
23
+
24
+ SNS.calls.append(
25
+ {
26
+ "TopicArn": self.get_topic_arn(model),
27
+ "Message": self.get_message_body(model),
28
+ }
29
+ )
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from clearskies import Model
6
+ from types_boto3_sqs import SQSClient
7
+
8
+ from clearskies_aws.actions.sqs import SQS as BaseSQS
9
+
10
+
11
+ class SQS(BaseSQS):
12
+ calls: list[dict[str, Any]] | None = None
13
+
14
+ @classmethod
15
+ def mock(cls, di):
16
+ cls.calls = []
17
+ di.mock_class(BaseSQS, SQS)
18
+
19
+ def _execute_action(self, client: SQSClient, model: Model) -> None:
20
+ """Send a notification as configured."""
21
+ if SQS.calls is None:
22
+ SQS.calls = []
23
+
24
+ SQS.calls.append(
25
+ {
26
+ "QueueUrl": self.get_queue_url(model),
27
+ "MessageBody": self.get_message_body(model),
28
+ }
29
+ )
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from clearskies import Model
6
+ from types_boto3_stepfunctions import SFNClient
7
+
8
+ from clearskies_aws.actions.step_function import StepFunction as BaseStepFunction
9
+
10
+
11
+ class StepFunction(BaseStepFunction):
12
+ calls: list[dict[str, Any]] | None = None
13
+
14
+ @classmethod
15
+ def mock(cls, di):
16
+ cls.calls = []
17
+ di.mock_class(BaseStepFunction, StepFunction)
18
+
19
+ def _execute_action(self, client: SFNClient, model: Model) -> None:
20
+ """Send a notification as configured."""
21
+ if StepFunction.calls is None:
22
+ StepFunction.calls = []
23
+
24
+ StepFunction.calls.append(
25
+ {
26
+ "stateMachineArn": self.get_arn(model),
27
+ "input": self.get_message_body(model),
28
+ }
29
+ )
30
+
31
+ if self.column_to_store_execution_arn:
32
+ model.save({self.column_to_store_execution_arn: "mock_execution_arn"})