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.
Files changed (86) hide show
  1. {clear_skies_aws-1.10.2.dist-info → clear_skies_aws-2.0.2.dist-info}/METADATA +36 -35
  2. clear_skies_aws-2.0.2.dist-info/RECORD +63 -0
  3. {clear_skies_aws-1.10.2.dist-info → clear_skies_aws-2.0.2.dist-info}/WHEEL +1 -1
  4. clear_skies_aws-2.0.2.dist-info/licenses/LICENSE +21 -0
  5. clearskies_aws/__init__.py +15 -2
  6. clearskies_aws/actions/__init__.py +13 -106
  7. clearskies_aws/actions/action_aws.py +74 -57
  8. clearskies_aws/actions/assume_role.py +43 -30
  9. clearskies_aws/actions/ses.py +82 -73
  10. clearskies_aws/actions/sns.py +27 -30
  11. clearskies_aws/actions/sqs.py +32 -33
  12. clearskies_aws/actions/step_function.py +38 -31
  13. clearskies_aws/backends/__init__.py +11 -4
  14. clearskies_aws/backends/backend.py +106 -0
  15. clearskies_aws/backends/dynamo_db_backend.py +150 -155
  16. clearskies_aws/backends/dynamo_db_condition_parser.py +40 -80
  17. clearskies_aws/backends/dynamo_db_parti_ql_backend.py +179 -337
  18. clearskies_aws/backends/sqs_backend.py +32 -51
  19. clearskies_aws/configs/__init__.py +0 -0
  20. clearskies_aws/contexts/__init__.py +23 -10
  21. clearskies_aws/contexts/cli_web_socket_mock.py +19 -0
  22. clearskies_aws/contexts/lambda_alb.py +76 -0
  23. clearskies_aws/contexts/lambda_api_gateway.py +75 -28
  24. clearskies_aws/contexts/lambda_api_gateway_web_socket.py +56 -29
  25. clearskies_aws/contexts/lambda_invocation.py +15 -44
  26. clearskies_aws/contexts/lambda_sns.py +8 -33
  27. clearskies_aws/contexts/lambda_sqs_standard_partial_batch.py +14 -36
  28. clearskies_aws/di/__init__.py +6 -1
  29. clearskies_aws/di/aws_additional_config_auto_import.py +37 -0
  30. clearskies_aws/di/inject/__init__.py +6 -0
  31. clearskies_aws/di/inject/boto3.py +15 -0
  32. clearskies_aws/di/inject/boto3_session.py +13 -0
  33. clearskies_aws/di/inject/parameter_store.py +15 -0
  34. clearskies_aws/{handlers → endpoints}/secrets_manager_rotation.py +76 -55
  35. clearskies_aws/endpoints/simple_body_routing.py +41 -0
  36. clearskies_aws/input_outputs/__init__.py +21 -8
  37. clearskies_aws/input_outputs/{cli_websocket_mock.py → cli_web_socket_mock.py} +9 -3
  38. clearskies_aws/input_outputs/lambda_alb.py +53 -0
  39. clearskies_aws/input_outputs/lambda_api_gateway.py +106 -88
  40. clearskies_aws/input_outputs/lambda_api_gateway_web_socket.py +69 -6
  41. clearskies_aws/input_outputs/lambda_input_output.py +87 -0
  42. clearskies_aws/input_outputs/lambda_invocation.py +77 -26
  43. clearskies_aws/input_outputs/lambda_sns.py +66 -39
  44. clearskies_aws/input_outputs/lambda_sqs_standard.py +70 -40
  45. clearskies_aws/mocks/actions/ses.py +25 -19
  46. clearskies_aws/mocks/actions/sns.py +18 -12
  47. clearskies_aws/mocks/actions/sqs.py +18 -12
  48. clearskies_aws/mocks/actions/step_function.py +19 -13
  49. clearskies_aws/models/__init__.py +0 -0
  50. clearskies_aws/models/web_socket_connection_model.py +182 -0
  51. clearskies_aws/secrets/__init__.py +13 -7
  52. clearskies_aws/secrets/additional_configs/__init__.py +10 -2
  53. clearskies_aws/secrets/additional_configs/iam_db_auth.py +26 -16
  54. clearskies_aws/secrets/additional_configs/iam_db_auth_with_ssm.py +43 -39
  55. clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +30 -31
  56. clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssm_bastion.py +70 -49
  57. clearskies_aws/secrets/akeyless_with_ssm_cache.py +32 -18
  58. clearskies_aws/secrets/parameter_store.py +34 -32
  59. clearskies_aws/secrets/secrets.py +16 -0
  60. clearskies_aws/secrets/secrets_manager.py +78 -57
  61. clear_skies_aws-1.10.2.dist-info/LICENSE +0 -7
  62. clear_skies_aws-1.10.2.dist-info/RECORD +0 -71
  63. clearskies_aws/actions/assume_role_test.py +0 -72
  64. clearskies_aws/actions/ses_test.py +0 -89
  65. clearskies_aws/actions/sns_test.py +0 -77
  66. clearskies_aws/actions/sqs_test.py +0 -127
  67. clearskies_aws/actions/step_function_test.py +0 -103
  68. clearskies_aws/backends/dynamo_db_backend_test.py +0 -300
  69. clearskies_aws/backends/dynamo_db_condition_parser_test.py +0 -266
  70. clearskies_aws/backends/dynamo_db_parti_ql_backend_test.py +0 -544
  71. clearskies_aws/backends/sqs_backend_test.py +0 -31
  72. clearskies_aws/contexts/cli.py +0 -19
  73. clearskies_aws/contexts/cli_websocket_mock.py +0 -33
  74. clearskies_aws/contexts/lambda_elb.py +0 -30
  75. clearskies_aws/contexts/lambda_http_gateway.py +0 -30
  76. clearskies_aws/contexts/lambda_sqs_standard_partial_batch_test.py +0 -66
  77. clearskies_aws/contexts/wsgi.py +0 -19
  78. clearskies_aws/di/standard_dependencies.py +0 -60
  79. clearskies_aws/handlers/simple_body_routing.py +0 -39
  80. clearskies_aws/input_outputs/lambda_api_gateway_test.py +0 -87
  81. clearskies_aws/input_outputs/lambda_elb.py +0 -21
  82. clearskies_aws/input_outputs/lambda_http_gateway.py +0 -12
  83. clearskies_aws/secrets/parameter_store_test.py +0 -18
  84. clearskies_aws/secrets/secrets_manager_test.py +0 -18
  85. clearskies_aws/web_socket_connection_model.py +0 -43
  86. clearskies_aws/{handlers → endpoints}/__init__.py +1 -1
@@ -1,8 +1,71 @@
1
- from .lambda_api_gateway import LambdaAPIGateway
2
- class LambdaAPIGatewayWebSocket(LambdaAPIGateway):
3
- def context_specifics(self):
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
- "event": self._event,
6
- "context": self._context,
7
- "connection_id": self._event["requestContext"]["connectionId"],
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 .lambda_api_gateway import LambdaAPIGateway
1
+ from __future__ import annotations
2
+
2
3
  import json
3
- from clearskies.handlers.exceptions import ClientError
4
- class LambdaInvocation(LambdaAPIGateway):
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=None,
10
- url=None,
17
+ event: dict[str, Any],
18
+ context: dict[str, Any],
19
+ method: str = "POST",
20
+ url: str = "",
11
21
  ):
12
- self._event = event
13
- self._context = context
14
- self._path = url if url else ''
15
- self._request_method = method.upper() if method else 'GET'
16
- self._query_parameters = {}
17
- self._path_parameters = []
18
- self._request_headers = {}
19
-
20
- def has_body(self):
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
- return self._event
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 json_body(self, required=True, allow_non_json_bodies=False):
27
- # we ignore the allow_non_json_bodies flag here because with the way invoking lambdas works,
28
- # the event already is an object, so it's a moot point.
29
- if required and not self._event:
30
- raise ClientError("Request body was not valid JSON")
31
- return self._event
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 respond(self, body, status_code=200):
34
- return body.decode('utf-8') if type(body) == bytes else body
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 .lambda_api_gateway import LambdaAPIGateway
2
- from clearskies.handlers.exceptions import ClientError
1
+ from __future__ import annotations
2
+
3
3
  import json
4
- class LambdaSns(LambdaAPIGateway):
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
- def respond(self, body, status_code=200):
17
- pass
6
+ from clearskies.exceptions import ClientError
7
+ from clearskies.input_outputs import Headers
18
8
 
19
- def get_body(self):
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
- def json_body(self, required=True, allow_non_json_bodies=False):
26
- if not self._record:
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
- return self._record
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
- def get_query_string(self):
34
- raise NotImplementedError("The query string doesn't exist in an SNS context")
26
+ # SNS events don't have headers
27
+ self.request_headers = Headers({})
35
28
 
36
- def get_content_type(self):
37
- raise NotImplementedError("Content type doesn't exist in an SNS context")
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 get_protocol(self):
40
- raise NotImplementedError("A request protocol is not defined in an SNS context")
47
+ def has_body(self) -> bool:
48
+ """Check if SNS message exists."""
49
+ return bool(self._record)
41
50
 
42
- def has_request_header(self, header_name):
43
- raise NotImplementedError("SNS contexts don't have request headers")
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 get_request_header(self, header_name, silent=True):
46
- raise NotImplementedError("SNS contexts don't have request headers")
55
+ def get_protocol(self) -> str:
56
+ """SNS events don't have a protocol."""
57
+ return "sns"
47
58
 
48
- def get_query_parameter(self, key):
49
- raise NotImplementedError("SNS contexts don't have query parameters")
59
+ def get_full_path(self) -> str:
60
+ """Return the configured path."""
61
+ return self.path
50
62
 
51
- def get_query_parameters(self):
52
- raise NotImplementedError("SNS contexts don't have query parameters")
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 .lambda_api_gateway import LambdaAPIGateway
2
- from clearskies.handlers.exceptions import ClientError
1
+ from __future__ import annotations
2
+
3
3
  import json
4
- class LambdaSqsStandard(LambdaAPIGateway):
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
- def respond(self, body, status_code=200):
13
- pass
6
+ from clearskies.configs import AnyDict, String
7
+ from clearskies.exceptions import ClientError
8
+ from clearskies.input_outputs import Headers
14
9
 
15
- def get_body(self):
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
- def request_data(self, required=True, allow_non_json_bodies=False):
22
- return self.json_body(required=required, allow_non_json_bodies=allow_non_json_bodies)
13
+ class LambdaSqsStandard(lambda_input_output.LambdaInputOutput):
14
+ """SQS standard queue specific Lambda input/output handler."""
23
15
 
24
- def json_body(self, required=True, allow_non_json_bodies=False):
25
- if not self._record:
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
- try:
31
- return json.loads(self._record)
32
- except json.JSONDecodeError:
33
- raise ClientError("SQS message was not valid JSON")
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
- def get_query_string(self):
36
- raise NotImplementedError("The query string doesn't exist in an SQS context")
32
+ # SQS events don't have query parameters or path parameters
33
+ self.query_parameters = {}
37
34
 
38
- def get_content_type(self):
39
- raise NotImplementedError("Content type doesn't exist in an SQS context")
35
+ # SQS events don't have headers
36
+ self.request_headers = Headers({})
40
37
 
41
- def get_protocol(self):
42
- raise NotImplementedError("A request protocol is not defined in an SQS context")
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 has_request_header(self, header_name):
45
- raise NotImplementedError("SQS contexts don't have request headers")
42
+ def get_body(self) -> str:
43
+ """Get the SQS message body."""
44
+ return json.dumps(self.record)
46
45
 
47
- def get_request_header(self, header_name, silent=True):
48
- raise NotImplementedError("SQS contexts don't have request headers")
46
+ def has_body(self) -> bool:
47
+ """Check if SQS message has a body."""
48
+ return True
49
49
 
50
- def get_query_parameter(self, key):
51
- raise NotImplementedError("SQS contexts don't have query parameters")
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 get_query_parameters(self):
54
- raise NotImplementedError("SQS contexts don't have query parameters")
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 ...actions.ses import SES as BaseSES
2
- class SES(BaseSES):
3
- calls = None
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
- def __init__(self, environment, boto3, di):
6
- super().__init__(environment, boto3, di)
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 __call__(self, model) -> None:
19
+ def _execute_action(self, client: SESClient, model: Model) -> None:
14
20
  """Send a notification as configured."""
15
- if SES.calls == None:
21
+ if SES.calls is None:
16
22
  SES.calls = []
17
- utcnow = self.di.build('utcnow')
18
- if self.when and not self.di.call_function(self.when, model=model):
19
- return
20
-
21
- SES.calls.append({
22
- "from": self.sender,
23
- "to": self._resolve_destination("to", model),
24
- "cc": self._resolve_destination("cc", model),
25
- "bcc": self._resolve_destination("bcc", model),
26
- "subject": self._resolve_subject(model, utcnow),
27
- "message": self._resolve_message_as_html(model, utcnow),
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 types import ModuleType
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
2
5
  from clearskies import Model
3
- from ...actions.sns import SNS as BaseSNS
4
- class SNS(BaseSNS):
5
- calls = None
6
+ from types_boto3_sns import SNSClient
6
7
 
7
- def __init__(self, environment, boto3, di):
8
- super().__init__(environment, boto3, di)
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: ModuleType, model: Model) -> None:
19
+ def _execute_action(self, client: SNSClient, model: Model) -> None:
16
20
  """Send a notification as configured."""
17
- if SNS.calls == None:
21
+ if SNS.calls is None:
18
22
  SNS.calls = []
19
23
 
20
- SNS.calls.append({
21
- "TopicArn": self.get_topic_arn(model),
22
- "Message": self.get_message_body(model),
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 types import ModuleType
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
2
5
  from clearskies import Model
3
- from ...actions.sqs import SQS as BaseSQS
4
- class SQS(BaseSQS):
5
- calls = None
6
+ from types_boto3_sqs import SQSClient
6
7
 
7
- def __init__(self, environment, boto3, di):
8
- super().__init__(environment, boto3, di)
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: ModuleType, model: Model) -> None:
19
+ def _execute_action(self, client: SQSClient, model: Model) -> None:
16
20
  """Send a notification as configured."""
17
- if SQS.calls == None:
21
+ if SQS.calls is None:
18
22
  SQS.calls = []
19
23
 
20
- SQS.calls.append({
21
- "QueueUrl": self.get_queue_url(model),
22
- "MessageBody": self.get_message_body(model),
23
- })
24
+ SQS.calls.append(
25
+ {
26
+ "QueueUrl": self.get_queue_url(model),
27
+ "MessageBody": self.get_message_body(model),
28
+ }
29
+ )