clear-skies-aws 2.0.2__py3-none-any.whl → 2.0.4__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 (35) hide show
  1. {clear_skies_aws-2.0.2.dist-info → clear_skies_aws-2.0.4.dist-info}/METADATA +5 -3
  2. {clear_skies_aws-2.0.2.dist-info → clear_skies_aws-2.0.4.dist-info}/RECORD +33 -29
  3. {clear_skies_aws-2.0.2.dist-info → clear_skies_aws-2.0.4.dist-info}/WHEEL +1 -1
  4. clearskies_aws/__init__.py +16 -2
  5. clearskies_aws/backends/__init__.py +3 -3
  6. clearskies_aws/backends/dynamo_db_backend.py +1 -0
  7. clearskies_aws/backends/dynamo_db_condition_parser.py +1 -0
  8. clearskies_aws/backends/dynamo_db_parti_ql_backend.py +2 -0
  9. clearskies_aws/contexts/__init__.py +5 -5
  10. clearskies_aws/contexts/cli_web_socket_mock.py +3 -2
  11. clearskies_aws/contexts/lambda_alb.py +8 -3
  12. clearskies_aws/contexts/lambda_api_gateway.py +13 -9
  13. clearskies_aws/contexts/lambda_api_gateway_web_socket.py +24 -2
  14. clearskies_aws/contexts/lambda_invoke.py +138 -0
  15. clearskies_aws/contexts/lambda_sns.py +110 -4
  16. clearskies_aws/contexts/lambda_sqs_standard.py +139 -0
  17. clearskies_aws/cursors/__init__.py +3 -0
  18. clearskies_aws/cursors/iam/__init__.py +7 -0
  19. clearskies_aws/cursors/iam/rds_mysql.py +125 -0
  20. clearskies_aws/cursors/port_forwarding/__init__.py +3 -0
  21. clearskies_aws/di/aws_additional_config_auto_import.py +1 -1
  22. clearskies_aws/di/inject/boto3.py +1 -1
  23. clearskies_aws/endpoints/__init__.py +0 -1
  24. clearskies_aws/endpoints/secrets_manager_rotation.py +1 -2
  25. clearskies_aws/input_outputs/__init__.py +2 -2
  26. clearskies_aws/input_outputs/cli_web_socket_mock.py +2 -0
  27. clearskies_aws/input_outputs/lambda_api_gateway_web_socket.py +9 -7
  28. clearskies_aws/input_outputs/lambda_input_output.py +4 -2
  29. clearskies_aws/input_outputs/{lambda_invocation.py → lambda_invoke.py} +10 -7
  30. clearskies_aws/input_outputs/lambda_sns.py +27 -18
  31. clearskies_aws/input_outputs/lambda_sqs_standard.py +32 -30
  32. clearskies_aws/models/__init__.py +1 -0
  33. clearskies_aws/contexts/lambda_invocation.py +0 -19
  34. clearskies_aws/contexts/lambda_sqs_standard_partial_batch.py +0 -29
  35. {clear_skies_aws-2.0.2.dist-info → clear_skies_aws-2.0.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+
3
+ import traceback
4
+ from typing import Any
5
+
6
+ from clearskies.authentication import Public
7
+ from clearskies.contexts.context import Context
8
+
9
+ from clearskies_aws.input_outputs import LambdaSqsStandard as LambdaSqsStandardInputOutput
10
+
11
+
12
+ class LambdaSqsStandard(Context):
13
+ """
14
+ Process messages from an SQS Standard Queue with Lambda.
15
+
16
+ Use this context when your application lives in a Lambda and is attached to an SQS standard
17
+ queue. Lambda always uses batch processing in this case, and will invoke your clearskies application
18
+ with a batch of messags. This clearskies context will then in turn invoke your application once
19
+ for every batched message. As a result, `request_data` will contain the contents of an individual message
20
+ from the queue, rather than the original group of batched events from Lambda. If any exception is thrown,
21
+ every other message in the queue will still be sent to your application, and clearskies will inform
22
+ AWS that the message and question failed to process.
23
+
24
+ ### Usage
25
+
26
+ Here's a very simple example:
27
+
28
+ ```
29
+ import clearskies
30
+
31
+
32
+ def some_function(request_data):
33
+ return print(request_data)
34
+
35
+
36
+ lambda_sqs = clearskies_aws.contexts.LambdaSqsStandard(
37
+ clearskies.endpoints.Callable(
38
+ some_function,
39
+ ),
40
+ )
41
+
42
+
43
+ def lambda_handler(event, context):
44
+ return lambda_sqs(event, context)
45
+ ```
46
+
47
+ `lambda_handler` would then be attached to your lambda function, which is attached to some standard SQS.
48
+
49
+ Like the other lambda contexts which don't exist in an HTTP world, you can also attach a clearskies application
50
+ with routing and hard-code the path to invoke inside the lambda handler itself. This is handy if you have
51
+ a few related lambdas with similar configuration (since you only have to build a single application) or if
52
+ you have an application that already exists and you want to invoke some specific endpoint with an SQS:
53
+
54
+ ```
55
+ import clearskies
56
+
57
+
58
+ def some_function(request_data):
59
+ return request_data
60
+
61
+
62
+ def some_other_function(request_data):
63
+ return request_data
64
+
65
+
66
+ def something_else(request_data):
67
+ return request_data
68
+
69
+
70
+ lambda_invoke = clearskies_aws.contexts.LambdaSqsStandard(
71
+ clearskies.endpoints.EndpointGroup(
72
+ [
73
+ clearskies.endpoints.Callable(
74
+ some_function,
75
+ url="some_function",
76
+ ),
77
+ clearskies.endpoints.Callable(
78
+ some_other_function,
79
+ url="some_other_function",
80
+ ),
81
+ clearskies.endpoints.Callable(
82
+ something_else,
83
+ url="something_else",
84
+ ),
85
+ ]
86
+ )
87
+ )
88
+
89
+
90
+ def some_function_handler(event, context):
91
+ return lambda_invoke(event, context, url="some_function")
92
+
93
+
94
+ def some_other_function_handler(event, context):
95
+ return lambda_invoke(event, context, url="some_other_function")
96
+
97
+
98
+ def something_else_handler(event, context):
99
+ return lambda_invoke(event, context, url="something_else")
100
+ ```
101
+
102
+ ### Context Specifics
103
+
104
+ When using this context, the following named parameters become available to inject into any callable
105
+ invoked by clearskies:
106
+
107
+ ```
108
+ | Name | Type | Description |
109
+ |:---------------------------:|:----------------:|--------------------------------------------------------|
110
+ | `event` | `dict[str, Any]` | The lambda `event` object |
111
+ | `context` | `dict[str, Any]` | The lambda `context` object |
112
+ | `message_id` | `str` | The AWS message id |
113
+ | `receipt_handle` | `str` | The receipt handle |
114
+ | `source_arn` | `str` | The ARN of the SQS the lambda is receiving events from |
115
+ | `sent_timestamp` | `str` | The timestamp when the message was sent |
116
+ | `approximate_receive_count` | `str` | The approximate receive count |
117
+ | `message_attributes` | `dict[str, Any]` | The message attributes |
118
+ | `record` | `dict[str, Any]` | The full record of the message being processed |
119
+ ```
120
+
121
+ """
122
+
123
+ def __call__( # type: ignore[override]
124
+ self, event: dict[str, Any], context: dict[str, Any], url: str = "", request_method: str = ""
125
+ ) -> dict[str, Any]:
126
+ item_failures = []
127
+ for record in event["Records"]:
128
+ try:
129
+ self.execute_application(
130
+ LambdaSqsStandardInputOutput(record, event, context, url=url, request_method=request_method)
131
+ )
132
+ except Exception as e:
133
+ item_failures.append({"itemIdentifier": record["messageId"]})
134
+
135
+ if item_failures:
136
+ return {
137
+ "batchItemFailures": item_failures,
138
+ }
139
+ return {}
@@ -0,0 +1,3 @@
1
+ from clearskies_aws.cursors import iam, port_forwarding
2
+
3
+ __all__ = ["iam", "port_forwarding"]
@@ -0,0 +1,7 @@
1
+ import logging
2
+
3
+ from clearskies_aws.cursors.iam.rds_mysql import RdsMySql
4
+
5
+ logging.getLogger(__name__)
6
+
7
+ __all__ = ["RdsMySql", "MysqlWithSSM"]
@@ -0,0 +1,125 @@
1
+ """
2
+ RdsMySql: MySQL cursor with AWS RDS IAM authentication.
3
+
4
+ This class provides a MySQL cursor that uses AWS RDS IAM DB authentication.
5
+ It loads connection parameters from environment variables and generates a temporary
6
+ IAM authentication token for secure database access.
7
+
8
+ Configuration fields:
9
+ - boto3: Injected boto3 provider for AWS API access.
10
+ - environment: Injected environment variable provider.
11
+ - hostname_environment_key: Environment variable for DB host (default: "DATABASE_HOST").
12
+ - username_environment_key: Environment variable for DB user (default: "DATABASE_USERNAME").
13
+ - database_environment_key: Environment variable for DB name (default: "DATABASE_NAME").
14
+ - port_environment_key: Environment variable for DB port (default: "DATABASE_PORT").
15
+ - cert_path_environment_key: Environment variable for SSL CA cert (default: "DATABASE_CERT_PATH").
16
+ - autocommit_environment_key: Environment variable for autocommit (default: "DATABASE_AUTOCOMMIT").
17
+ - connect_timeout_environment_key: Environment variable for connect timeout (default: "DATABASE_CONNECT_TIMEOUT").
18
+ - database_region_key: Environment variable for AWS region (default: "DATABASE_REGION").
19
+
20
+ Example:
21
+ import clearskies_aws.cursors.iam.rds_mysql
22
+
23
+ cursor = clearskies_aws.cursors.iam.rds_mysql.RdsMySql()
24
+ cursor.execute("SELECT 1")
25
+ """
26
+
27
+ import os
28
+ from typing import Any
29
+
30
+ import clearskies
31
+ from clearskies import decorators
32
+ from clearskies.cursors import Mysql as MysqlBase
33
+
34
+ from clearskies_aws.di import inject
35
+
36
+
37
+ class RdsMySql(MysqlBase):
38
+ """MySQL cursor with AWS RDS IAM DB authentication."""
39
+
40
+ """Injected boto3 provider for AWS API access."""
41
+ boto3 = inject.Boto3()
42
+
43
+ """Injected environment variable provider."""
44
+ environment = clearskies.di.inject.Environment()
45
+
46
+ """Environment variable for DB host (default: "DATABASE_HOST")."""
47
+ hostname_environment_key = clearskies.configs.String(default="DATABASE_HOST")
48
+
49
+ """Environment variable for DB user (default: "DATABASE_USERNAME")."""
50
+ username_environment_key = clearskies.configs.String(default="DATABASE_USERNAME")
51
+
52
+ """Environment variable for DB name (default: "DATABASE_NAME")."""
53
+ database_environment_key = clearskies.configs.String(default="DATABASE_NAME")
54
+
55
+ """Environment variable for DB port (default: "DATABASE_PORT")."""
56
+ port_environment_key = clearskies.configs.String(default="DATABASE_PORT")
57
+
58
+ """Environment variable for SSL CA cert (default: "DATABASE_CERT_PATH")."""
59
+ cert_path_environment_key = clearskies.configs.String(default="DATABASE_CERT_PATH")
60
+
61
+ """Environment variable for autocommit (default: "DATABASE_AUTOCOMMIT")."""
62
+ autocommit_environment_key = clearskies.configs.String(default="DATABASE_AUTOCOMMIT")
63
+
64
+ """Environment variable for connect timeout (default: "DATABASE_CONNECT_TIMEOUT")."""
65
+ connect_timeout_environment_key = clearskies.configs.String(default="DATABASE_CONNECT_TIMEOUT")
66
+
67
+ """Environment variable for AWS region (default: "DATABASE_REGION")."""
68
+ database_region_key = clearskies.configs.String(default="DATABASE_REGION")
69
+
70
+ @decorators.parameters_to_properties
71
+ def __init__(
72
+ self,
73
+ hostname_environment_key: str | None = None,
74
+ username_environment_key: str | None = None,
75
+ database_environment_key: str | None = None,
76
+ port_environment_key: str | None = None,
77
+ cert_path_environment_key: str | None = None,
78
+ autocommit_environment_key: str | None = None,
79
+ database_region_key: str | None = None,
80
+ connect_timeout_environment_key: str | None = None,
81
+ port_forwarding: Any | None = None,
82
+ ):
83
+ self.finalize_and_validate_configuration()
84
+
85
+ def build_connection_kwargs(self) -> dict:
86
+ """
87
+ Build the connection kwargs for the MySQL client, using IAM DB authentication.
88
+
89
+ Returns
90
+ -------
91
+ dict
92
+ Connection parameters for the MySQL client.
93
+ """
94
+ connection_kwargs = {
95
+ "user": self.environment.get(self.username_environment_key),
96
+ "host": self.environment.get(self.hostname_environment_key),
97
+ "database": self.environment.get(self.database_environment_key),
98
+ "port": int(self.environment.get(self.port_environment_key, silent=True) or self.port),
99
+ "ssl_ca": self.environment.get(self.cert_path_environment_key, silent=True),
100
+ "autocommit": self.environment.get(self.autocommit_environment_key, silent=True),
101
+ "connect_timeout": int(
102
+ self.environment.get(self.connect_timeout_environment_key, silent=True) or self.connect_timeout
103
+ ),
104
+ }
105
+ region: str = self.environment.get(self.database_region_key, True) or self.environment.get("AWS_REGION", True)
106
+ if not region:
107
+ raise ValueError(
108
+ "To use RDS IAM DB auth you must set DATABASE_REGION or AWS_REGION in the .env file or an environment variable"
109
+ )
110
+ os.environ["LIBMYSQL_ENABLE_CLEARTEXT_PLUGIN"] = "1"
111
+
112
+ rds_api = self.boto3.Session().client("rds")
113
+ rds_token = rds_api.generate_db_auth_token(
114
+ DBHostname=connection_kwargs.get("host"),
115
+ Port=connection_kwargs.get("port", 3306),
116
+ DBUsername=connection_kwargs.get("user"),
117
+ Region=region,
118
+ )
119
+ connection_kwargs["password"] = rds_token
120
+
121
+ for kwarg in ["autocommit", "connect_timeout", "port", "ssl_ca"]:
122
+ if not connection_kwargs[kwarg]:
123
+ del connection_kwargs[kwarg]
124
+
125
+ return {**super().build_connection_kwargs(), **connection_kwargs}
@@ -0,0 +1,3 @@
1
+ from clearskies_aws.cursors.port_forwarding.ssm import Ssm
2
+
3
+ __all__ = ["Ssm"]
@@ -17,7 +17,7 @@ class AwsAdditionalConfigAutoImport(AdditionalConfigAutoImport):
17
17
  This DI auto injects boto3, boto3 Session and the parameter store.
18
18
  """
19
19
 
20
- def provide_boto3(self) -> ModuleType:
20
+ def provide_boto3_sdk(self) -> ModuleType:
21
21
  import boto3
22
22
 
23
23
  return boto3
@@ -12,4 +12,4 @@ class Boto3(Injectable):
12
12
  def __get__(self, instance, parent) -> ModuleType:
13
13
  if instance is None:
14
14
  return self # type: ignore
15
- return self._di.build_from_name("boto3", cache=self.cache)
15
+ return self._di.build_from_name("boto3_sdk", cache=self.cache)
@@ -1,2 +1 @@
1
1
  from .secrets_manager_rotation import SecretsManagerRotation
2
- from .simple_body_routing import SimpleBodyRouting
@@ -19,7 +19,6 @@ from clearskies_aws.di import inject
19
19
 
20
20
 
21
21
  class SecretsManagerRotation(Endpoint):
22
-
23
22
  di = Di()
24
23
  boto3 = inject.Boto3()
25
24
 
@@ -92,7 +91,7 @@ class SecretsManagerRotation(Endpoint):
92
91
  pending_secret_data = json.loads(pending_secret["SecretString"])
93
92
  except botocore.exceptions.ClientError as error:
94
93
  if error.response["Error"]["Code"] == "ResourceNotFoundException":
95
- pending_secret_data = None
94
+ pending_secret_data = {}
96
95
  else:
97
96
  raise error
98
97
 
@@ -6,7 +6,7 @@ from clearskies_aws.input_outputs.lambda_api_gateway import LambdaApiGateway
6
6
  from clearskies_aws.input_outputs.lambda_api_gateway_web_socket import (
7
7
  LambdaApiGatewayWebSocket,
8
8
  )
9
- from clearskies_aws.input_outputs.lambda_invocation import LambdaInvocation
9
+ from clearskies_aws.input_outputs.lambda_invoke import LambdaInvoke
10
10
  from clearskies_aws.input_outputs.lambda_sns import LambdaSns
11
11
  from clearskies_aws.input_outputs.lambda_sqs_standard import LambdaSqsStandard
12
12
 
@@ -15,7 +15,7 @@ __all__ = [
15
15
  "LambdaApiGateway",
16
16
  "LambdaApiGatewayWebSocket",
17
17
  "LambdaAlb",
18
- "LambdaInvocation",
18
+ "LambdaInvoke",
19
19
  "LambdaSns",
20
20
  "LambdaSqsStandard",
21
21
  ]
@@ -15,4 +15,6 @@ class CliWebSocketMock(clearskies.input_outputs.Cli):
15
15
  "event": {},
16
16
  "context": {},
17
17
  "connection_id": connection_id,
18
+ "domain": "",
19
+ "stage": "",
18
20
  }
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from typing import Any
4
5
 
5
6
  from clearskies.configs import String
@@ -24,7 +25,7 @@ class LambdaApiGatewayWebSocket(lambda_input_output.LambdaInputOutput):
24
25
  request_context = event.get("requestContext", {})
25
26
 
26
27
  # WebSocket uses route_key, but doesn't have either a route or a method
27
- self.route_key = request_context.get("routeKey", "")
28
+ self.route_key = request_context.get("routeKey", "GET")
28
29
  self.request_method = self.route_key.upper() # For compatibility
29
30
 
30
31
  # WebSocket connection ID
@@ -47,12 +48,13 @@ class LambdaApiGatewayWebSocket(lambda_input_output.LambdaInputOutput):
47
48
 
48
49
  raise ValueError("Unable to find the client ip inside the API Gateway")
49
50
 
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
53
- return {
54
- "statusCode": status_code,
55
- }
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}'")
56
58
 
57
59
  def context_specifics(self) -> dict[str, Any]:
58
60
  """Provide WebSocket specific context data."""
@@ -20,7 +20,9 @@ class LambdaInputOutput(InputOutput):
20
20
  _cached_body = None
21
21
  _body_was_cached = False
22
22
 
23
- def __init__(self, event: dict[str, Any], context: dict[str, Any]):
23
+ def __init__(
24
+ self, event: dict[str, Any], context: dict[str, Any], url: str | None = "", request_method: str | None = ""
25
+ ):
24
26
  # Store event and context
25
27
  self.event = event
26
28
  self.context = context
@@ -28,7 +30,7 @@ class LambdaInputOutput(InputOutput):
28
30
  # Initialize the base class
29
31
  super().__init__()
30
32
 
31
- def respond(self, body: Any, status_code: int = 200) -> dict[str, Any]:
33
+ def respond(self, body: Any, status_code: int = 200) -> dict[str, Any] | None:
32
34
  """Create standard Lambda HTTP response format."""
33
35
  if "content-type" not in self.response_headers:
34
36
  self.response_headers.content_type = "application/json; charset=UTF-8"
@@ -9,25 +9,28 @@ from clearskies.input_outputs import Headers
9
9
  from clearskies_aws.input_outputs import lambda_input_output
10
10
 
11
11
 
12
- class LambdaInvocation(lambda_input_output.LambdaInputOutput):
12
+ class LambdaInvoke(lambda_input_output.LambdaInputOutput):
13
13
  """Direct Lambda invocation specific input/output handler."""
14
14
 
15
15
  def __init__(
16
16
  self,
17
17
  event: dict[str, Any],
18
18
  context: dict[str, Any],
19
- method: str = "POST",
19
+ request_method: str = "",
20
20
  url: str = "",
21
21
  ):
22
22
  # Call parent constructor
23
23
  super().__init__(event, context)
24
24
 
25
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 = {}
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
31
34
 
32
35
  # Direct invocations don't have headers
33
36
  self.request_headers = Headers({})
@@ -12,16 +12,21 @@ from clearskies_aws.input_outputs import lambda_input_output
12
12
  class LambdaSns(lambda_input_output.LambdaInputOutput):
13
13
  """SNS specific Lambda input/output handler."""
14
14
 
15
- def __init__(self, event: dict, context: dict[str, Any], url: str = "", method: str = "POST"):
15
+ record: dict[str, Any]
16
+
17
+ def __init__(self, event: dict, context: dict[str, Any], url: str = "", request_method: str = ""):
16
18
  # Call parent constructor
17
19
  super().__init__(event, context)
18
20
 
19
21
  # 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 = {}
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
25
30
 
26
31
  # SNS events don't have headers
27
32
  self.request_headers = Headers({})
@@ -29,24 +34,29 @@ class LambdaSns(lambda_input_output.LambdaInputOutput):
29
34
  # Extract SNS message from event
30
35
  try:
31
36
  record = event["Records"][0]["Sns"]["Message"]
32
- self._record = json.loads(record)
37
+ self.record = json.loads(record)
33
38
  except (KeyError, IndexError, json.JSONDecodeError) as e:
34
39
  raise ClientError(
35
40
  "The message from AWS was not a valid SNS event with serialized JSON. "
36
41
  "The lambda_sns context for clearskies only accepts serialized JSON."
37
42
  )
38
43
 
39
- def respond(self, body: Any, status_code: int = 200) -> dict[str, Any]:
40
- """SNS events don't return responses."""
41
- return {}
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}'")
42
52
 
43
53
  def get_body(self) -> str:
44
54
  """Get the SNS message as a JSON string."""
45
- return json.dumps(self._record) if self._record else ""
55
+ return json.dumps(self.record) if self.record else ""
46
56
 
47
57
  def has_body(self) -> bool:
48
58
  """Check if SNS message exists."""
49
- return bool(self._record)
59
+ return bool(self.record)
50
60
 
51
61
  def get_client_ip(self) -> str:
52
62
  """SNS events don't have client IP information."""
@@ -66,14 +76,13 @@ class LambdaSns(lambda_input_output.LambdaInputOutput):
66
76
 
67
77
  return {
68
78
  **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,
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"),
74
83
  }
75
84
 
76
85
  @property
77
86
  def request_data(self) -> dict[str, Any] | list[Any] | None:
78
87
  """Return the SNS message data directly."""
79
- return self._record
88
+ return self.record
@@ -17,31 +17,44 @@ class LambdaSqsStandard(lambda_input_output.LambdaInputOutput):
17
17
  path = String(default="/")
18
18
 
19
19
  def __init__(
20
- self, record: str, event: dict[str, Any], context: dict[str, Any], url: str = "", method: str = "POST"
20
+ self,
21
+ record: dict[str, Any],
22
+ event: dict[str, Any],
23
+ context: dict[str, Any],
24
+ url: str = "",
25
+ request_method: str = "",
21
26
  ):
22
27
  # Call parent constructor with the full event
23
28
  super().__init__(event, context)
24
29
 
25
30
  # Store the individual SQS record
26
- self.record = json.loads(record)
27
- print("SQS record:", self.record)
31
+ self.record = record
28
32
  # SQS specific initialization
29
- self.path = url if url else "/"
30
- self.request_method = method.upper() if method else "POST"
33
+ if url:
34
+ self.path = url
35
+ else:
36
+ self.supports_url = False
31
37
 
32
- # SQS events don't have query parameters or path parameters
33
- self.query_parameters = {}
38
+ if request_method:
39
+ self.request_method = request_method.upper()
40
+ else:
41
+ self.supports_request_method = False
34
42
 
35
43
  # SQS events don't have headers
36
44
  self.request_headers = Headers({})
37
45
 
38
- def respond(self, body: Any, status_code: int = 200) -> dict[str, Any]:
39
- """SQS events don't return responses."""
40
- return {}
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}'")
41
54
 
42
55
  def get_body(self) -> str:
43
- """Get the SQS message body."""
44
- return json.dumps(self.record)
56
+ """Get request body with base64 decoding if needed."""
57
+ return self.record["body"]
45
58
 
46
59
  def has_body(self) -> bool:
47
60
  """Check if SQS message has a body."""
@@ -63,22 +76,11 @@ class LambdaSqsStandard(lambda_input_output.LambdaInputOutput):
63
76
  """Provide SQS specific context data."""
64
77
  return {
65
78
  **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,
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,
73
86
  }
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")
@@ -0,0 +1 @@
1
+ from clearskies_aws.models.web_socket_connection_model import WebSocketConnectionModel
@@ -1,19 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
- from clearskies.authentication import Public
6
- from clearskies.contexts.context import Context
7
-
8
- from clearskies_aws.input_outputs import LambdaInvocation as LambdaInvocationInputOutput
9
-
10
-
11
- class LambdaInvocation(Context):
12
-
13
- def __call__(self, event: dict[str, Any], context: dict[str, Any]) -> Any: # type: ignore[override]
14
- return self.execute_application(
15
- LambdaInvocationInputOutput(
16
- event,
17
- context,
18
- )
19
- )