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.
- {clear_skies_aws-2.0.1.dist-info → clear_skies_aws-2.0.3.dist-info}/METADATA +2 -2
- clear_skies_aws-2.0.3.dist-info/RECORD +63 -0
- {clear_skies_aws-2.0.1.dist-info → clear_skies_aws-2.0.3.dist-info}/WHEEL +1 -1
- clearskies_aws/__init__.py +27 -0
- clearskies_aws/actions/__init__.py +15 -0
- clearskies_aws/actions/action_aws.py +135 -0
- clearskies_aws/actions/assume_role.py +115 -0
- clearskies_aws/actions/ses.py +203 -0
- clearskies_aws/actions/sns.py +61 -0
- clearskies_aws/actions/sqs.py +81 -0
- clearskies_aws/actions/step_function.py +73 -0
- clearskies_aws/backends/__init__.py +19 -0
- clearskies_aws/backends/backend.py +106 -0
- clearskies_aws/backends/dynamo_db_backend.py +609 -0
- clearskies_aws/backends/dynamo_db_condition_parser.py +325 -0
- clearskies_aws/backends/dynamo_db_parti_ql_backend.py +965 -0
- clearskies_aws/backends/sqs_backend.py +61 -0
- clearskies_aws/configs/__init__.py +0 -0
- clearskies_aws/contexts/__init__.py +23 -0
- clearskies_aws/contexts/cli_web_socket_mock.py +20 -0
- clearskies_aws/contexts/lambda_alb.py +81 -0
- clearskies_aws/contexts/lambda_api_gateway.py +81 -0
- clearskies_aws/contexts/lambda_api_gateway_web_socket.py +79 -0
- clearskies_aws/contexts/lambda_invoke.py +138 -0
- clearskies_aws/contexts/lambda_sns.py +124 -0
- clearskies_aws/contexts/lambda_sqs_standard.py +139 -0
- clearskies_aws/di/__init__.py +6 -0
- clearskies_aws/di/aws_additional_config_auto_import.py +37 -0
- clearskies_aws/di/inject/__init__.py +6 -0
- clearskies_aws/di/inject/boto3.py +15 -0
- clearskies_aws/di/inject/boto3_session.py +13 -0
- clearskies_aws/di/inject/parameter_store.py +15 -0
- clearskies_aws/endpoints/__init__.py +1 -0
- clearskies_aws/endpoints/secrets_manager_rotation.py +194 -0
- clearskies_aws/endpoints/simple_body_routing.py +41 -0
- clearskies_aws/input_outputs/__init__.py +21 -0
- clearskies_aws/input_outputs/cli_web_socket_mock.py +20 -0
- clearskies_aws/input_outputs/lambda_alb.py +53 -0
- clearskies_aws/input_outputs/lambda_api_gateway.py +123 -0
- clearskies_aws/input_outputs/lambda_api_gateway_web_socket.py +73 -0
- clearskies_aws/input_outputs/lambda_input_output.py +89 -0
- clearskies_aws/input_outputs/lambda_invoke.py +88 -0
- clearskies_aws/input_outputs/lambda_sns.py +88 -0
- clearskies_aws/input_outputs/lambda_sqs_standard.py +86 -0
- clearskies_aws/mocks/__init__.py +1 -0
- clearskies_aws/mocks/actions/__init__.py +6 -0
- clearskies_aws/mocks/actions/ses.py +34 -0
- clearskies_aws/mocks/actions/sns.py +29 -0
- clearskies_aws/mocks/actions/sqs.py +29 -0
- clearskies_aws/mocks/actions/step_function.py +32 -0
- clearskies_aws/models/__init__.py +1 -0
- clearskies_aws/models/web_socket_connection_model.py +182 -0
- clearskies_aws/secrets/__init__.py +13 -0
- clearskies_aws/secrets/additional_configs/__init__.py +62 -0
- clearskies_aws/secrets/additional_configs/iam_db_auth.py +39 -0
- clearskies_aws/secrets/additional_configs/iam_db_auth_with_ssm.py +96 -0
- clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +80 -0
- clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssm_bastion.py +162 -0
- clearskies_aws/secrets/akeyless_with_ssm_cache.py +60 -0
- clearskies_aws/secrets/parameter_store.py +52 -0
- clearskies_aws/secrets/secrets.py +16 -0
- clearskies_aws/secrets/secrets_manager.py +96 -0
- clear_skies_aws-2.0.1.dist-info/RECORD +0 -4
- {clear_skies_aws-2.0.1.dist-info → clear_skies_aws-2.0.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clear-skies-aws
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.3
|
|
4
4
|
Summary: clearskies bindings for working in AWS
|
|
5
5
|
Project-URL: Repository, https://github.com/clearskies-py/clearskies-aws
|
|
6
6
|
Project-URL: Issues, https://github.com/clearskies-py/clearskies-aws/issues
|
|
@@ -16,7 +16,7 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
17
|
Requires-Python: <4.0,>=3.11
|
|
18
18
|
Requires-Dist: boto3<2.0.0,>=1.26.148
|
|
19
|
-
Requires-Dist: clear-skies<3.0.0,>=2.0.
|
|
19
|
+
Requires-Dist: clear-skies<3.0.0,>=2.0.20
|
|
20
20
|
Requires-Dist: types-boto3[dynamodb,sns,sqs]<2.0.0,>=1.38.13
|
|
21
21
|
Provides-Extra: akeyless
|
|
22
22
|
Requires-Dist: akeyless-cloud-id<0.5.0,>=0.2.3; extra == 'akeyless'
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
clearskies_aws/__init__.py,sha256=-vmIX17AJIxy3-2CO4iXofUcgIUAckfHfdrkHtkya8g,368
|
|
2
|
+
clearskies_aws/actions/__init__.py,sha256=YsIi3ZTdByu4R7I77uOAXDE5hzB31J_tRrIoTxe_bjo,371
|
|
3
|
+
clearskies_aws/actions/action_aws.py,sha256=JMogBFIrN72h5oBQLbyMy0LmfVLrqvWCYLQDml1qO4M,4674
|
|
4
|
+
clearskies_aws/actions/assume_role.py,sha256=XNxbVju460aBMS8NQhY80eoNVQgZRJ6-OydsNOx3neA,4319
|
|
5
|
+
clearskies_aws/actions/ses.py,sha256=Cu8USID8vy2IZC6sFOIQRzv8a0LCMvYG15yn0l-fl5g,8431
|
|
6
|
+
clearskies_aws/actions/sns.py,sha256=YS1TbEwtU-0lDbjG2HyTBs2J-ML5OL3ModAiGTMeK-c,2205
|
|
7
|
+
clearskies_aws/actions/sqs.py,sha256=r0z8njU87n09UgAq3l34JuNIbaBE85D_z8IE6ciIs9Q,3359
|
|
8
|
+
clearskies_aws/actions/step_function.py,sha256=Y6tGbQJIAD_IwV9ohwYfuv3vqtryTwpFTNGUFgdj_DQ,2689
|
|
9
|
+
clearskies_aws/backends/__init__.py,sha256=LgMNrf2yD9_PZBGbfs3yQGWM0NelSwk9GO73NfGwc44,587
|
|
10
|
+
clearskies_aws/backends/backend.py,sha256=WpsoT0pZOdilvtOkbiNtk6DoOEzmjyWcCDfUCWOlToc,4316
|
|
11
|
+
clearskies_aws/backends/dynamo_db_backend.py,sha256=cTlbSpMiJ-0YILeHRrMZ6u4blRl_76hmemZZO9BwNN4,29396
|
|
12
|
+
clearskies_aws/backends/dynamo_db_condition_parser.py,sha256=OipIlFpcfS2GQiGpge6ZFX4NZNfNtHF87qi9g3yZgIU,12622
|
|
13
|
+
clearskies_aws/backends/dynamo_db_parti_ql_backend.py,sha256=x1pLNTUsTjCxi1SDryr9_uhL-AQJS7ksP5bvQpnhaC8,46081
|
|
14
|
+
clearskies_aws/backends/sqs_backend.py,sha256=kHTzgBwpYzV31UcGaoSUcC_7eZEwm-GsCHvXFM1OLT0,2152
|
|
15
|
+
clearskies_aws/configs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
clearskies_aws/contexts/__init__.py,sha256=kBLUlkpIp88n5RgbE9gVi4rDnJou81vCE5g_aiv2v8Y,717
|
|
17
|
+
clearskies_aws/contexts/cli_web_socket_mock.py,sha256=XjsFcx0YlQqjb9Hh8q2NUW7T2Exu7_FHx9v_Am0Uq0g,784
|
|
18
|
+
clearskies_aws/contexts/lambda_alb.py,sha256=hNMWYePN_moC4waXdydAp7dJkGpSowxDZZzU39Sxc7I,3426
|
|
19
|
+
clearskies_aws/contexts/lambda_api_gateway.py,sha256=HWH-CAX_aYiDhQr8rBhR5TW99-wx2ODsyI_dqGJkcdw,3354
|
|
20
|
+
clearskies_aws/contexts/lambda_api_gateway_web_socket.py,sha256=gO0arcveIbvkCxrqjk8piVwcymuw_pgHwsSfdfElyWU,4583
|
|
21
|
+
clearskies_aws/contexts/lambda_invoke.py,sha256=Ouns3zIw0G1aCuB0yVS91PgNqFVyqT3Oe6NcDgaEcvY,4872
|
|
22
|
+
clearskies_aws/contexts/lambda_sns.py,sha256=_TyPqRWosfaJzreYGNabvR-XTLYYO4eVWMVINSIY-OE,4129
|
|
23
|
+
clearskies_aws/contexts/lambda_sqs_standard.py,sha256=uzENeXJaWsZoUxves8IXo8wFrcub_viQSKF9fmmPPvE,5293
|
|
24
|
+
clearskies_aws/di/__init__.py,sha256=pLHSIKxS1oELOgttRuwM0yXdJRxjZKXQ6tPxme2db0U,222
|
|
25
|
+
clearskies_aws/di/aws_additional_config_auto_import.py,sha256=qc9AUlFdF9jhAzS99esyvgY1MiW7mLz45fATK7Yp0rg,1271
|
|
26
|
+
clearskies_aws/di/inject/__init__.py,sha256=5_x5_BBQwC6J4k5YLdTm1DfIDM-95zXz1L5a1nMrlrY,186
|
|
27
|
+
clearskies_aws/di/inject/boto3.py,sha256=7qcn5N-RnUKiCd1U31JAjQHF6NDudS4KyBLT0krUD-Y,404
|
|
28
|
+
clearskies_aws/di/inject/boto3_session.py,sha256=11UYHz5kgrrx5lawoYaOFBm-QIoa45YUCMAOn4gT8Jo,383
|
|
29
|
+
clearskies_aws/di/inject/parameter_store.py,sha256=g0uAVwQEywLO9bCcYLbDKuyYnYgVyrfcYsOBJWYGads,475
|
|
30
|
+
clearskies_aws/endpoints/__init__.py,sha256=OUL_nhtuNs62BvQeVtC9xP_e9Hs_-qjANvb81vdLdrc,61
|
|
31
|
+
clearskies_aws/endpoints/secrets_manager_rotation.py,sha256=wLJjid_K09rXGEd_MIgoHP84im4_Eo2m_lwAxn36Jf8,7763
|
|
32
|
+
clearskies_aws/endpoints/simple_body_routing.py,sha256=B3fnfxUEMuNpzc26Pzly6618DFU9fpaCk8KhTGSaptE,1181
|
|
33
|
+
clearskies_aws/input_outputs/__init__.py,sha256=RTDFwhPWZ2S0tZQiIPH0Tkj2xF-9qBjZte_CA2cmGt8,743
|
|
34
|
+
clearskies_aws/input_outputs/cli_web_socket_mock.py,sha256=cp0MaJjVnsXE1rx5K44lpN9uHKo3MOAsNxVQ3AsJOi4,547
|
|
35
|
+
clearskies_aws/input_outputs/lambda_alb.py,sha256=iYooG5AlkrQQgzSt304A5_u_UhuqYh-9PeLvFdGM_Eg,2013
|
|
36
|
+
clearskies_aws/input_outputs/lambda_api_gateway.py,sha256=n5XiaxmaFKJ9wMQcNQwizpjcA-L_bdLk1aUk-QyO58w,4672
|
|
37
|
+
clearskies_aws/input_outputs/lambda_api_gateway_web_socket.py,sha256=B7LIXI_9HpquTtUuPVnb7aH1agJt9qTWiISWiNlobdo,2926
|
|
38
|
+
clearskies_aws/input_outputs/lambda_input_output.py,sha256=nJza0TFdAYOHdLvq6eFDW-TobpeF3EzuIsW9rdhrj3U,2819
|
|
39
|
+
clearskies_aws/input_outputs/lambda_invoke.py,sha256=9TvtxVn2MYxUq9luZ-o0DMboFulA5ff-G8RFo3_x-nA,2861
|
|
40
|
+
clearskies_aws/input_outputs/lambda_sns.py,sha256=nPvLxi5EUmgSeDYxFHnosG9mEzVKjt_mOFJvjsb1gQ4,3084
|
|
41
|
+
clearskies_aws/input_outputs/lambda_sqs_standard.py,sha256=rtOH75SKuT4ByOn4tfgDvvLog0IZVaZUh8zQ_E4Yygw,2945
|
|
42
|
+
clearskies_aws/mocks/__init__.py,sha256=mn764gINN667tYoJfnsM6HjAAhCsO_kZ6E-fUwdLY50,22
|
|
43
|
+
clearskies_aws/mocks/actions/__init__.py,sha256=to1r8B365Et2PRVfUWWnJGt7Hdr8vwwQuNyZvTSTP6g,152
|
|
44
|
+
clearskies_aws/mocks/actions/ses.py,sha256=KsII9ggU364BQoxgHfO2rxi6tjVsdnwJyMhtclOhWLQ,998
|
|
45
|
+
clearskies_aws/mocks/actions/sns.py,sha256=yGY3V0_htOZ3y8VTLdznoMSUQZvnjulQbrR5z_-0fXQ,706
|
|
46
|
+
clearskies_aws/mocks/actions/sqs.py,sha256=y0Vq7IMbjlfT5JMNHfbPsq9XVZhUF-G0kdXzQnPzyUA,710
|
|
47
|
+
clearskies_aws/mocks/actions/step_function.py,sha256=ENEVy8Ai3vPymbQre5aWa5z2McBjlnopfsLxdO7oEbc,937
|
|
48
|
+
clearskies_aws/models/__init__.py,sha256=tAU5cPGRSzSClNVRCBxzwlBq6eZO8fftuI3bG1jEyVQ,87
|
|
49
|
+
clearskies_aws/models/web_socket_connection_model.py,sha256=5M1qfQHKuWMYPUDkwT48QPo2ROey7koizvWLfapsfow,7492
|
|
50
|
+
clearskies_aws/secrets/__init__.py,sha256=0mqYja2ETBHJh4b3jgRhhJV1uGdZc9S7cUvcV5QByPs,445
|
|
51
|
+
clearskies_aws/secrets/akeyless_with_ssm_cache.py,sha256=32HUS1KQ5F6Fu70HDtojqDL7VZvP_YDgbWLTTmNJvPA,2073
|
|
52
|
+
clearskies_aws/secrets/parameter_store.py,sha256=nMpkbUnxHGcCoMD5T-weCS13f1RZA7ZHl24O7qXaZLE,1882
|
|
53
|
+
clearskies_aws/secrets/secrets.py,sha256=aDMPj-tuXdRhh8YKMnsJe9V_VLrD8Cru-xUKs8cyDIY,485
|
|
54
|
+
clearskies_aws/secrets/secrets_manager.py,sha256=hK10lVmEFJltTv7h2AkYR3ySCVXx1JVy56jWU1hyYso,3708
|
|
55
|
+
clearskies_aws/secrets/additional_configs/__init__.py,sha256=0NFOMVod8tte_K0Jq1Qf7_DDBvp6aEE4wF4hddQaW8w,1927
|
|
56
|
+
clearskies_aws/secrets/additional_configs/iam_db_auth.py,sha256=PwyiLaacpRfhBKzQBdvGWHUYf5Ymth1sG2ly7Z6RoR0,1238
|
|
57
|
+
clearskies_aws/secrets/additional_configs/iam_db_auth_with_ssm.py,sha256=ABY29X-YvrE6vvNo6kVdf4DqyRNq5cFR5SfK7MNkltE,3463
|
|
58
|
+
clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py,sha256=mLaplwvJLSbGh6oXgdOKL9Mv-6hLv5OUYCfEwHbHvLE,3700
|
|
59
|
+
clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssm_bastion.py,sha256=2VHOwto4I9gBwrpd2HGpL-Wr0T2S-jFjUhe2Ib8hNJ8,6596
|
|
60
|
+
clear_skies_aws-2.0.3.dist-info/METADATA,sha256=ZQj_x9m1wlgaf4SH_gu1AJiGiykQzDjdnBRLOgh9Q1Y,8973
|
|
61
|
+
clear_skies_aws-2.0.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
62
|
+
clear_skies_aws-2.0.3.dist-info/licenses/LICENSE,sha256=MkEX8JF8kZxdyBpTTcB0YTd-xZpWnHvbRlw-pQh8u58,1069
|
|
63
|
+
clear_skies_aws-2.0.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from clearskies_aws import (
|
|
4
|
+
actions,
|
|
5
|
+
backends,
|
|
6
|
+
contexts,
|
|
7
|
+
di,
|
|
8
|
+
endpoints,
|
|
9
|
+
handlers,
|
|
10
|
+
input_outputs,
|
|
11
|
+
mocks,
|
|
12
|
+
models,
|
|
13
|
+
secrets,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"actions",
|
|
18
|
+
"backends",
|
|
19
|
+
"contexts",
|
|
20
|
+
"di",
|
|
21
|
+
"endpoints",
|
|
22
|
+
"handlers",
|
|
23
|
+
"input_outputs",
|
|
24
|
+
"mocks",
|
|
25
|
+
"models",
|
|
26
|
+
"secrets",
|
|
27
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from clearskies_aws.actions.assume_role import AssumeRole
|
|
4
|
+
from clearskies_aws.actions.ses import SES
|
|
5
|
+
from clearskies_aws.actions.sns import SNS
|
|
6
|
+
from clearskies_aws.actions.sqs import SQS
|
|
7
|
+
from clearskies_aws.actions.step_function import StepFunction
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"AssumeRole",
|
|
11
|
+
"SES",
|
|
12
|
+
"SNS",
|
|
13
|
+
"SQS",
|
|
14
|
+
"StepFunction",
|
|
15
|
+
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from abc import ABC
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from types import ModuleType
|
|
8
|
+
from typing import Callable, Generic, TypeVar
|
|
9
|
+
|
|
10
|
+
from botocore.client import BaseClient
|
|
11
|
+
from botocore.exceptions import ClientError
|
|
12
|
+
from clearskies.action import Action
|
|
13
|
+
from clearskies.configs import Boolean, String
|
|
14
|
+
from clearskies.configs import Callable as CallableConfig
|
|
15
|
+
from clearskies.configurable import Configurable
|
|
16
|
+
from clearskies.decorators import parameters_to_properties
|
|
17
|
+
from clearskies.di.inject import Di, Environment
|
|
18
|
+
from clearskies.di.injectable_properties import InjectableProperties
|
|
19
|
+
from clearskies.functional import string
|
|
20
|
+
from clearskies.model import Model
|
|
21
|
+
|
|
22
|
+
from clearskies_aws.di import inject
|
|
23
|
+
|
|
24
|
+
from .assume_role import AssumeRole
|
|
25
|
+
|
|
26
|
+
ClientType = TypeVar("ClientType", bound=BaseClient)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ActionAws(Generic[ClientType], Action, Configurable, InjectableProperties):
|
|
30
|
+
logging = logging.getLogger(__name__)
|
|
31
|
+
boto3 = inject.Boto3()
|
|
32
|
+
environment = Environment()
|
|
33
|
+
di = Di()
|
|
34
|
+
|
|
35
|
+
client: ClientType
|
|
36
|
+
|
|
37
|
+
service_name = String(required=True)
|
|
38
|
+
|
|
39
|
+
message_callable = CallableConfig(required=False, default=None)
|
|
40
|
+
|
|
41
|
+
when = CallableConfig(required=False, default=None)
|
|
42
|
+
|
|
43
|
+
assume_role: AssumeRole | None = None
|
|
44
|
+
|
|
45
|
+
region = String(required=False)
|
|
46
|
+
|
|
47
|
+
can_cache = Boolean(default=True)
|
|
48
|
+
|
|
49
|
+
@parameters_to_properties
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
service_name: str,
|
|
53
|
+
message_callable: Callable | None = None,
|
|
54
|
+
when: Callable | None = None,
|
|
55
|
+
assume_role: AssumeRole | None = None,
|
|
56
|
+
region: str | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Set up the AWS action."""
|
|
59
|
+
|
|
60
|
+
# def configure(self, **kwargs):
|
|
61
|
+
# """Configure the action with additional parameters."""
|
|
62
|
+
# # Finalize configuration properties from Configurable
|
|
63
|
+
# Configurable.finalize_and_validate_configuration(self)
|
|
64
|
+
|
|
65
|
+
# # Handle any additional kwargs by setting them as attributes
|
|
66
|
+
# for key, value in kwargs.items():
|
|
67
|
+
# if hasattr(self, key):
|
|
68
|
+
# setattr(self, key, value)
|
|
69
|
+
|
|
70
|
+
def __call__(self, model: Model) -> None:
|
|
71
|
+
"""Send a notification as configured."""
|
|
72
|
+
if self.when and not self.di.call_function(self.when, model=model):
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
client = self._get_client()
|
|
77
|
+
self._execute_action(client, model)
|
|
78
|
+
except ClientError as e:
|
|
79
|
+
self.logging.exception(f"Failed to retrieve client for {self.__class__.__name__} action.")
|
|
80
|
+
raise e
|
|
81
|
+
|
|
82
|
+
def _get_client(self) -> ClientType:
|
|
83
|
+
"""Retrieve the boto3 client."""
|
|
84
|
+
if hasattr(self, "client") and self.client and self.can_cache:
|
|
85
|
+
return self.client
|
|
86
|
+
|
|
87
|
+
if self.assume_role:
|
|
88
|
+
boto3 = self.assume_role(self.boto3)
|
|
89
|
+
else:
|
|
90
|
+
boto3 = self.boto3
|
|
91
|
+
|
|
92
|
+
if not self.region:
|
|
93
|
+
self.region = self.default_region()
|
|
94
|
+
if self.region:
|
|
95
|
+
client = boto3.client(self.service_name, region_name=self.region)
|
|
96
|
+
else:
|
|
97
|
+
client = boto3.client(self.service_name)
|
|
98
|
+
|
|
99
|
+
if self.can_cache:
|
|
100
|
+
self.client = client
|
|
101
|
+
return client
|
|
102
|
+
|
|
103
|
+
def default_region(self):
|
|
104
|
+
region = self.environment.get("AWS_REGION", silent=True)
|
|
105
|
+
if region:
|
|
106
|
+
return region
|
|
107
|
+
region = self.environment.get("DEFAULT_AWS_REGION", silent=True)
|
|
108
|
+
if region:
|
|
109
|
+
return region
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def _execute_action(self, client: ClientType, model: Model) -> None:
|
|
113
|
+
"""Run the action."""
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
def get_message_body(self, model: Model) -> str:
|
|
117
|
+
"""Retrieve the message for the action."""
|
|
118
|
+
if self.message_callable:
|
|
119
|
+
result = self.di.call_function(self.message_callable, model=model)
|
|
120
|
+
if isinstance(result, dict) or isinstance(result, list):
|
|
121
|
+
return json.dumps(result, default=string.datetime_to_iso)
|
|
122
|
+
if not isinstance(result, str):
|
|
123
|
+
callable_name = getattr(self.message_callable, "__name__", str(self.message_callable))
|
|
124
|
+
raise TypeError(
|
|
125
|
+
f"The return value from the message callable for the {__name__} action must be a string, dictionary, or list. I received a "
|
|
126
|
+
+ f"{type(result)} after calling '{callable_name}'"
|
|
127
|
+
)
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
model_data = OrderedDict()
|
|
131
|
+
for column_name, column in model.get_columns().items():
|
|
132
|
+
if not column.is_readable:
|
|
133
|
+
continue
|
|
134
|
+
model_data.update(column.to_json(model))
|
|
135
|
+
return json.dumps(model_data, default=string.datetime_to_iso)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from types import ModuleType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AssumeRole:
|
|
7
|
+
"""
|
|
8
|
+
Used by the various actions if you need to assume a role before making an AWS call.
|
|
9
|
+
|
|
10
|
+
Note that, in all cases, this class and the actions assume that you already have AWS credentials
|
|
11
|
+
properly configured/findable by boto3 in the standard way. If you just have static IAM credentials
|
|
12
|
+
that you are trying to use... well, you can do that with some undocumented hackery, but that's not
|
|
13
|
+
really the goal for any of these classes.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
Here's a basic usage example with an SQS action on a model trigger::
|
|
17
|
+
|
|
18
|
+
class User(clearskies.Model):
|
|
19
|
+
def __init__(self, memory_backend, columns):
|
|
20
|
+
super().__init__(memory_backend, columns)
|
|
21
|
+
|
|
22
|
+
def columns_configuration(self):
|
|
23
|
+
return OrderedDict(
|
|
24
|
+
[
|
|
25
|
+
clearskies.column_types.string(
|
|
26
|
+
"name",
|
|
27
|
+
on_change=[
|
|
28
|
+
clearskies_aws.actions.sqs(
|
|
29
|
+
queue_url="https://queue.url.example.aws.com",
|
|
30
|
+
assume_role=clearskies_aws.actions.assume_role(
|
|
31
|
+
role_arn="arn:aws:iam:role/name",
|
|
32
|
+
external_id="12345",
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
],
|
|
36
|
+
),
|
|
37
|
+
]
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
Here's a more complicated example with a double-assumme-role to show how to combine them::
|
|
42
|
+
|
|
43
|
+
first_assume_role = clearskies_aws.actions.assume_role(
|
|
44
|
+
role_arn="arn:aws:123456789012:iam:role/name",
|
|
45
|
+
external_id="12345",
|
|
46
|
+
)
|
|
47
|
+
final_assume_role = clearskies_aws.actions.assume_role(
|
|
48
|
+
role_arn="arn:aws:210987654321:iam:role/name-2",
|
|
49
|
+
external_id="54321",
|
|
50
|
+
source=first_assume_role,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class User(clearskies.Model):
|
|
55
|
+
def __init__(self, memory_backend, columns):
|
|
56
|
+
super().__init__(memory_backend, columns)
|
|
57
|
+
|
|
58
|
+
def columns_configuration(self):
|
|
59
|
+
return OrderedDict(
|
|
60
|
+
[
|
|
61
|
+
clearskies.column_types.string(
|
|
62
|
+
"name",
|
|
63
|
+
on_change=[
|
|
64
|
+
clearskies_aws.actions.sqs(
|
|
65
|
+
queue_url="https://queue.url.example.aws.com",
|
|
66
|
+
assume_role=final_assume_role,
|
|
67
|
+
)
|
|
68
|
+
],
|
|
69
|
+
),
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
role_arn = ""
|
|
76
|
+
external_id = ""
|
|
77
|
+
role_session_name = ""
|
|
78
|
+
duration = 3600
|
|
79
|
+
source: AssumeRole | None = None
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
role_arn: str,
|
|
84
|
+
external_id: str = "",
|
|
85
|
+
role_session_name: str = "",
|
|
86
|
+
duration: int = 3600,
|
|
87
|
+
source: AssumeRole | None = None,
|
|
88
|
+
):
|
|
89
|
+
"""Assume a role."""
|
|
90
|
+
self.role_arn = role_arn
|
|
91
|
+
self.external_id = external_id
|
|
92
|
+
self.role_session_name = role_session_name
|
|
93
|
+
self.duration = duration
|
|
94
|
+
self.source = source
|
|
95
|
+
|
|
96
|
+
def __call__(self, boto3: ModuleType) -> ModuleType:
|
|
97
|
+
# chaining!
|
|
98
|
+
if self.source:
|
|
99
|
+
boto3 = self.source(boto3)
|
|
100
|
+
|
|
101
|
+
calling_params = {
|
|
102
|
+
"RoleArn": self.role_arn,
|
|
103
|
+
"RoleSessionName": self.role_session_name if self.role_session_name else "clearkies-aws",
|
|
104
|
+
"DurationSeconds": self.duration,
|
|
105
|
+
}
|
|
106
|
+
if self.external_id:
|
|
107
|
+
calling_params["ExternalId"] = self.external_id
|
|
108
|
+
credentials = boto3.client("sts").assume_role(**calling_params)["Credentials"]
|
|
109
|
+
|
|
110
|
+
# now let's make a new session using those
|
|
111
|
+
return boto3.Session(
|
|
112
|
+
aws_access_key_id=credentials["AccessKeyId"],
|
|
113
|
+
aws_secret_access_key=credentials["SecretAccessKey"],
|
|
114
|
+
aws_session_token=credentials["SessionToken"],
|
|
115
|
+
)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
5
|
+
|
|
6
|
+
import clearskies
|
|
7
|
+
import jinja2
|
|
8
|
+
from clearskies import Model
|
|
9
|
+
from clearskies.configs import Any as AnyConfig
|
|
10
|
+
from clearskies.configs import Email, EmailOrEmailListOrCallable, String
|
|
11
|
+
from clearskies.decorators import parameters_to_properties
|
|
12
|
+
from types_boto3_ses import SESClient
|
|
13
|
+
|
|
14
|
+
from clearskies_aws.actions import action_aws
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from clearskies_aws.actions import AssumeRole
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SES(action_aws.ActionAws[SESClient]):
|
|
21
|
+
sender = Email(required=True)
|
|
22
|
+
to = EmailOrEmailListOrCallable(required=False)
|
|
23
|
+
cc = EmailOrEmailListOrCallable(required=False)
|
|
24
|
+
bcc = EmailOrEmailListOrCallable(required=False)
|
|
25
|
+
subject = String(required=False)
|
|
26
|
+
message = String(required=False)
|
|
27
|
+
subject_template = AnyConfig(required=False)
|
|
28
|
+
message_template = AnyConfig(required=False)
|
|
29
|
+
subject_template_file = String(required=False)
|
|
30
|
+
message_template_file = String(required=False)
|
|
31
|
+
dependencies_for_template: list[Any] = []
|
|
32
|
+
|
|
33
|
+
destinations: dict[str, list[str | Callable]] = {
|
|
34
|
+
"to": [],
|
|
35
|
+
"cc": [],
|
|
36
|
+
"bcc": [],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@parameters_to_properties
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
sender: str,
|
|
43
|
+
to: list | str | Callable | None = None,
|
|
44
|
+
cc: list | str | Callable | None = None,
|
|
45
|
+
bcc: list | str | Callable | None = None,
|
|
46
|
+
subject: str | None = None,
|
|
47
|
+
message: str | None = None,
|
|
48
|
+
subject_template: jinja2.Template | None = None,
|
|
49
|
+
message_template: jinja2.Template | None = None,
|
|
50
|
+
subject_template_file: str | None = None,
|
|
51
|
+
message_template_file: str | None = None,
|
|
52
|
+
assume_role: AssumeRole | None = None,
|
|
53
|
+
dependencies_for_template: list[Any] = [],
|
|
54
|
+
when: Callable | None = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Configure the rules for this email notification."""
|
|
57
|
+
super().__init__(service_name="ses", assume_role=assume_role, when=when)
|
|
58
|
+
|
|
59
|
+
def configure(self):
|
|
60
|
+
self.finalize_and_validate_configuration()
|
|
61
|
+
# First finalize and validate configuration to set up defaults
|
|
62
|
+
|
|
63
|
+
# this just moves the data from the various "to" inputs (to, cc, bcc) into the self.destinations
|
|
64
|
+
# dictionary, after normalizing it so that it is always a list.
|
|
65
|
+
if not self.to and not self.cc and not self.bcc:
|
|
66
|
+
raise ValueError("You must configure at least one 'to' address or one 'cc' address or one 'bcc' address")
|
|
67
|
+
|
|
68
|
+
for key in self.destinations.keys():
|
|
69
|
+
destination_values = getattr(self, key, None)
|
|
70
|
+
if not destination_values:
|
|
71
|
+
continue
|
|
72
|
+
if type(destination_values) == str or callable(destination_values):
|
|
73
|
+
self.destinations[key] = [destination_values]
|
|
74
|
+
else:
|
|
75
|
+
self.destinations[key] = destination_values
|
|
76
|
+
num_subjects = 0
|
|
77
|
+
num_messages = 0
|
|
78
|
+
for source in [self.subject, self.subject_template, self.subject_template_file]:
|
|
79
|
+
if source:
|
|
80
|
+
num_subjects += 1
|
|
81
|
+
for source in [self.message, self.message_template, self.message_template_file]:
|
|
82
|
+
if source:
|
|
83
|
+
num_messages += 1
|
|
84
|
+
if num_subjects > 1:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
"More than one of 'subject', 'subject_template', or 'subject_template_file' was set, but only one of these may be set."
|
|
87
|
+
)
|
|
88
|
+
if num_messages > 1:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"More than one of 'message', 'message_template', or 'message_template_file' was set, but only one of these may be set."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if self.subject_template_file:
|
|
94
|
+
with open(self.subject_template_file, "r", encoding="utf-8") as template:
|
|
95
|
+
self.subject_template = jinja2.Template(template.read())
|
|
96
|
+
elif self.subject_template and not isinstance(self.subject_template, jinja2.Template):
|
|
97
|
+
self.subject_template = jinja2.Template(self.subject_template)
|
|
98
|
+
|
|
99
|
+
if self.message_template_file:
|
|
100
|
+
with open(self.message_template_file, "r", encoding="utf-8") as template:
|
|
101
|
+
self.message_template = jinja2.Template(template.read())
|
|
102
|
+
elif self.message_template and not isinstance(self.message_template, jinja2.Template):
|
|
103
|
+
self.message_template = jinja2.Template(self.message_template)
|
|
104
|
+
|
|
105
|
+
def _execute_action(self, client: SESClient, model: Model) -> None:
|
|
106
|
+
"""Send a notification as configured."""
|
|
107
|
+
utcnow = self.di.build("utcnow")
|
|
108
|
+
|
|
109
|
+
tos = self._resolve_destination("to", model)
|
|
110
|
+
if not tos:
|
|
111
|
+
return
|
|
112
|
+
response = client.send_email(
|
|
113
|
+
Destination={
|
|
114
|
+
"ToAddresses": tos,
|
|
115
|
+
"CcAddresses": self._resolve_destination("cc", model),
|
|
116
|
+
"BccAddresses": self._resolve_destination("bcc", model),
|
|
117
|
+
},
|
|
118
|
+
Message={
|
|
119
|
+
"Body": {
|
|
120
|
+
"Html": {
|
|
121
|
+
"Charset": "utf-8",
|
|
122
|
+
"Data": self._resolve_message_as_html(model, utcnow),
|
|
123
|
+
},
|
|
124
|
+
"Text": {
|
|
125
|
+
"Charset": "utf-8",
|
|
126
|
+
"Data": self._resolve_message_as_text(model, utcnow),
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
"Subject": {"Charset": "utf-8", "Data": self._resolve_subject(model, utcnow)},
|
|
130
|
+
},
|
|
131
|
+
Source=self.sender,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def _resolve_destination(self, name: str, model: clearskies.Model) -> list[str]:
|
|
135
|
+
"""
|
|
136
|
+
Return a list of to/cc/bcc addresses.
|
|
137
|
+
|
|
138
|
+
Each entry can be:
|
|
139
|
+
|
|
140
|
+
1. An email address
|
|
141
|
+
2. The name of a column in the model that contains an email address
|
|
142
|
+
"""
|
|
143
|
+
resolved = []
|
|
144
|
+
destinations = self.destinations[name]
|
|
145
|
+
for destination in destinations:
|
|
146
|
+
if callable(destination):
|
|
147
|
+
more = self.di.call_function(destination, model=model)
|
|
148
|
+
if not isinstance(more, list):
|
|
149
|
+
more = [more]
|
|
150
|
+
for entry in more:
|
|
151
|
+
if not isinstance(entry, str):
|
|
152
|
+
raise ValueError(
|
|
153
|
+
f"I invoked a callable to fetch the '{name}' addresses for model '{model.__class__.__name__}' but it returned something other than a string. Callables must return a valid email address or a list of email addresses."
|
|
154
|
+
)
|
|
155
|
+
if "@" not in entry:
|
|
156
|
+
raise ValueError(
|
|
157
|
+
f"I invoked a callable to fetch the '{name}' addresses for model '{model.__class__.__name__}' but it returned a non-email address. Callables must return a valid email address or a list of email addresses."
|
|
158
|
+
)
|
|
159
|
+
resolved.extend(more)
|
|
160
|
+
continue
|
|
161
|
+
if "@" in destination:
|
|
162
|
+
resolved.append(destination)
|
|
163
|
+
continue
|
|
164
|
+
resolved.append(getattr(model, destination))
|
|
165
|
+
return resolved
|
|
166
|
+
|
|
167
|
+
def _resolve_message_as_html(self, model: clearskies.Model, now: datetime.datetime) -> str:
|
|
168
|
+
"""Build the HTML for a message."""
|
|
169
|
+
if self.message:
|
|
170
|
+
return self.message
|
|
171
|
+
|
|
172
|
+
if self.message_template:
|
|
173
|
+
return str(
|
|
174
|
+
self.message_template.render(model=model, now=now, **self.more_template_variables(), text_in_html=True)
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return ""
|
|
178
|
+
|
|
179
|
+
def _resolve_message_as_text(self, model: clearskies.Model, now: datetime.datetime) -> str:
|
|
180
|
+
"""Build the text for a message."""
|
|
181
|
+
if self.message:
|
|
182
|
+
return self.message
|
|
183
|
+
|
|
184
|
+
if self.message_template:
|
|
185
|
+
return str(self.message_template.render(model=model, now=now, **self.more_template_variables()))
|
|
186
|
+
|
|
187
|
+
return ""
|
|
188
|
+
|
|
189
|
+
def _resolve_subject(self, model: clearskies.Model, now: datetime.datetime) -> str:
|
|
190
|
+
"""Build the subject for a message."""
|
|
191
|
+
if self.subject:
|
|
192
|
+
return self.subject
|
|
193
|
+
|
|
194
|
+
if self.subject_template:
|
|
195
|
+
return str(self.subject_template.render(model=model, now=now, **self.more_template_variables()))
|
|
196
|
+
|
|
197
|
+
return ""
|
|
198
|
+
|
|
199
|
+
def more_template_variables(self) -> dict[str, Any]:
|
|
200
|
+
more_variables = {}
|
|
201
|
+
for dependency_name in self.dependencies_for_template:
|
|
202
|
+
more_variables[dependency_name] = self.di.build(dependency_name, cache=True)
|
|
203
|
+
return more_variables
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
from clearskies import Model
|
|
6
|
+
from clearskies.configs import Callable as CallableConfig
|
|
7
|
+
from clearskies.configs import String
|
|
8
|
+
from clearskies.decorators import parameters_to_properties
|
|
9
|
+
from types_boto3_sns import SNSClient
|
|
10
|
+
|
|
11
|
+
from .action_aws import ActionAws
|
|
12
|
+
from .assume_role import AssumeRole
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SNS(ActionAws[SNSClient]):
|
|
16
|
+
topic = String(required=False)
|
|
17
|
+
topic_environment_key = String(required=False)
|
|
18
|
+
topic_callable = CallableConfig(required=False)
|
|
19
|
+
|
|
20
|
+
@parameters_to_properties
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
topic=None,
|
|
24
|
+
topic_environment_key=None,
|
|
25
|
+
topic_callable: Callable | None = None,
|
|
26
|
+
message_callable: Callable | None = None,
|
|
27
|
+
when: Callable | None = None,
|
|
28
|
+
assume_role: AssumeRole | None = None,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Configure the SNS action."""
|
|
31
|
+
super().__init__(service_name="sns", message_callable=message_callable, when=when, assume_role=assume_role)
|
|
32
|
+
|
|
33
|
+
def configure(self):
|
|
34
|
+
self.finalize_and_validate_configuration()
|
|
35
|
+
topics = 0
|
|
36
|
+
for value in [self.topic, self.topic_environment_key, self.topic_callable]:
|
|
37
|
+
if value:
|
|
38
|
+
topics += 1
|
|
39
|
+
if topics > 1:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
"You can only provide one of 'topic', 'topic_environment_key', or 'topic_callable', but more than one were provided."
|
|
42
|
+
)
|
|
43
|
+
if not topics:
|
|
44
|
+
raise ValueError("You must provide at least one of 'topic', 'topic_environment_key', or 'topic_callable'.")
|
|
45
|
+
|
|
46
|
+
def _execute_action(self, client: SNSClient, model: Model) -> None:
|
|
47
|
+
"""Send a notification as configured."""
|
|
48
|
+
topic_arn = self.get_topic_arn(model)
|
|
49
|
+
if not topic_arn:
|
|
50
|
+
return
|
|
51
|
+
client.publish(
|
|
52
|
+
TopicArn=self.get_topic_arn(model),
|
|
53
|
+
Message=self.get_message_body(model),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def get_topic_arn(self, model: Model) -> str:
|
|
57
|
+
if self.topic:
|
|
58
|
+
return self.topic
|
|
59
|
+
if self.topic_environment_key:
|
|
60
|
+
return self.environment.get(self.topic_environment_key)
|
|
61
|
+
return self.di.call_function(self.topic_callable, model=model)
|