clear-skies-aws 2.0.1__py3-none-any.whl → 2.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {clear_skies_aws-2.0.1.dist-info → clear_skies_aws-2.0.3.dist-info}/METADATA +2 -2
  2. clear_skies_aws-2.0.3.dist-info/RECORD +63 -0
  3. {clear_skies_aws-2.0.1.dist-info → clear_skies_aws-2.0.3.dist-info}/WHEEL +1 -1
  4. clearskies_aws/__init__.py +27 -0
  5. clearskies_aws/actions/__init__.py +15 -0
  6. clearskies_aws/actions/action_aws.py +135 -0
  7. clearskies_aws/actions/assume_role.py +115 -0
  8. clearskies_aws/actions/ses.py +203 -0
  9. clearskies_aws/actions/sns.py +61 -0
  10. clearskies_aws/actions/sqs.py +81 -0
  11. clearskies_aws/actions/step_function.py +73 -0
  12. clearskies_aws/backends/__init__.py +19 -0
  13. clearskies_aws/backends/backend.py +106 -0
  14. clearskies_aws/backends/dynamo_db_backend.py +609 -0
  15. clearskies_aws/backends/dynamo_db_condition_parser.py +325 -0
  16. clearskies_aws/backends/dynamo_db_parti_ql_backend.py +965 -0
  17. clearskies_aws/backends/sqs_backend.py +61 -0
  18. clearskies_aws/configs/__init__.py +0 -0
  19. clearskies_aws/contexts/__init__.py +23 -0
  20. clearskies_aws/contexts/cli_web_socket_mock.py +20 -0
  21. clearskies_aws/contexts/lambda_alb.py +81 -0
  22. clearskies_aws/contexts/lambda_api_gateway.py +81 -0
  23. clearskies_aws/contexts/lambda_api_gateway_web_socket.py +79 -0
  24. clearskies_aws/contexts/lambda_invoke.py +138 -0
  25. clearskies_aws/contexts/lambda_sns.py +124 -0
  26. clearskies_aws/contexts/lambda_sqs_standard.py +139 -0
  27. clearskies_aws/di/__init__.py +6 -0
  28. clearskies_aws/di/aws_additional_config_auto_import.py +37 -0
  29. clearskies_aws/di/inject/__init__.py +6 -0
  30. clearskies_aws/di/inject/boto3.py +15 -0
  31. clearskies_aws/di/inject/boto3_session.py +13 -0
  32. clearskies_aws/di/inject/parameter_store.py +15 -0
  33. clearskies_aws/endpoints/__init__.py +1 -0
  34. clearskies_aws/endpoints/secrets_manager_rotation.py +194 -0
  35. clearskies_aws/endpoints/simple_body_routing.py +41 -0
  36. clearskies_aws/input_outputs/__init__.py +21 -0
  37. clearskies_aws/input_outputs/cli_web_socket_mock.py +20 -0
  38. clearskies_aws/input_outputs/lambda_alb.py +53 -0
  39. clearskies_aws/input_outputs/lambda_api_gateway.py +123 -0
  40. clearskies_aws/input_outputs/lambda_api_gateway_web_socket.py +73 -0
  41. clearskies_aws/input_outputs/lambda_input_output.py +89 -0
  42. clearskies_aws/input_outputs/lambda_invoke.py +88 -0
  43. clearskies_aws/input_outputs/lambda_sns.py +88 -0
  44. clearskies_aws/input_outputs/lambda_sqs_standard.py +86 -0
  45. clearskies_aws/mocks/__init__.py +1 -0
  46. clearskies_aws/mocks/actions/__init__.py +6 -0
  47. clearskies_aws/mocks/actions/ses.py +34 -0
  48. clearskies_aws/mocks/actions/sns.py +29 -0
  49. clearskies_aws/mocks/actions/sqs.py +29 -0
  50. clearskies_aws/mocks/actions/step_function.py +32 -0
  51. clearskies_aws/models/__init__.py +1 -0
  52. clearskies_aws/models/web_socket_connection_model.py +182 -0
  53. clearskies_aws/secrets/__init__.py +13 -0
  54. clearskies_aws/secrets/additional_configs/__init__.py +62 -0
  55. clearskies_aws/secrets/additional_configs/iam_db_auth.py +39 -0
  56. clearskies_aws/secrets/additional_configs/iam_db_auth_with_ssm.py +96 -0
  57. clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssh_cert_bastion.py +80 -0
  58. clearskies_aws/secrets/additional_configs/mysql_connection_dynamic_producer_via_ssm_bastion.py +162 -0
  59. clearskies_aws/secrets/akeyless_with_ssm_cache.py +60 -0
  60. clearskies_aws/secrets/parameter_store.py +52 -0
  61. clearskies_aws/secrets/secrets.py +16 -0
  62. clearskies_aws/secrets/secrets_manager.py +96 -0
  63. clear_skies_aws-2.0.1.dist-info/RECORD +0 -4
  64. {clear_skies_aws-2.0.1.dist-info → clear_skies_aws-2.0.3.dist-info}/licenses/LICENSE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clear-skies-aws
3
- Version: 2.0.1
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.2
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -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)