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.
- {clear_skies_aws-2.0.2.dist-info → clear_skies_aws-2.0.4.dist-info}/METADATA +5 -3
- {clear_skies_aws-2.0.2.dist-info → clear_skies_aws-2.0.4.dist-info}/RECORD +33 -29
- {clear_skies_aws-2.0.2.dist-info → clear_skies_aws-2.0.4.dist-info}/WHEEL +1 -1
- clearskies_aws/__init__.py +16 -2
- clearskies_aws/backends/__init__.py +3 -3
- clearskies_aws/backends/dynamo_db_backend.py +1 -0
- clearskies_aws/backends/dynamo_db_condition_parser.py +1 -0
- clearskies_aws/backends/dynamo_db_parti_ql_backend.py +2 -0
- clearskies_aws/contexts/__init__.py +5 -5
- clearskies_aws/contexts/cli_web_socket_mock.py +3 -2
- clearskies_aws/contexts/lambda_alb.py +8 -3
- clearskies_aws/contexts/lambda_api_gateway.py +13 -9
- clearskies_aws/contexts/lambda_api_gateway_web_socket.py +24 -2
- clearskies_aws/contexts/lambda_invoke.py +138 -0
- clearskies_aws/contexts/lambda_sns.py +110 -4
- clearskies_aws/contexts/lambda_sqs_standard.py +139 -0
- clearskies_aws/cursors/__init__.py +3 -0
- clearskies_aws/cursors/iam/__init__.py +7 -0
- clearskies_aws/cursors/iam/rds_mysql.py +125 -0
- clearskies_aws/cursors/port_forwarding/__init__.py +3 -0
- clearskies_aws/di/aws_additional_config_auto_import.py +1 -1
- clearskies_aws/di/inject/boto3.py +1 -1
- clearskies_aws/endpoints/__init__.py +0 -1
- clearskies_aws/endpoints/secrets_manager_rotation.py +1 -2
- clearskies_aws/input_outputs/__init__.py +2 -2
- clearskies_aws/input_outputs/cli_web_socket_mock.py +2 -0
- clearskies_aws/input_outputs/lambda_api_gateway_web_socket.py +9 -7
- clearskies_aws/input_outputs/lambda_input_output.py +4 -2
- clearskies_aws/input_outputs/{lambda_invocation.py → lambda_invoke.py} +10 -7
- clearskies_aws/input_outputs/lambda_sns.py +27 -18
- clearskies_aws/input_outputs/lambda_sqs_standard.py +32 -30
- clearskies_aws/models/__init__.py +1 -0
- clearskies_aws/contexts/lambda_invocation.py +0 -19
- clearskies_aws/contexts/lambda_sqs_standard_partial_batch.py +0 -29
- {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,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}
|
|
@@ -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
|
|
20
|
+
def provide_boto3_sdk(self) -> ModuleType:
|
|
21
21
|
import boto3
|
|
22
22
|
|
|
23
23
|
return boto3
|
|
@@ -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 =
|
|
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.
|
|
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
|
-
"
|
|
18
|
+
"LambdaInvoke",
|
|
19
19
|
"LambdaSns",
|
|
20
20
|
"LambdaSqsStandard",
|
|
21
21
|
]
|
|
@@ -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) ->
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
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__(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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.
|
|
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) ->
|
|
40
|
-
"""SNS
|
|
41
|
-
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
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.
|
|
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,
|
|
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 =
|
|
27
|
-
print("SQS record:", self.record)
|
|
31
|
+
self.record = record
|
|
28
32
|
# SQS specific initialization
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
if url:
|
|
34
|
+
self.path = url
|
|
35
|
+
else:
|
|
36
|
+
self.supports_url = False
|
|
31
37
|
|
|
32
|
-
|
|
33
|
-
|
|
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) ->
|
|
39
|
-
"""SQS
|
|
40
|
-
|
|
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
|
|
44
|
-
return
|
|
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
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"
|
|
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
|
-
)
|