localstack-core 4.7.1.dev49__py3-none-any.whl → 4.10.1.dev12__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 (253) hide show
  1. localstack/aws/api/cloudformation/__init__.py +18 -4
  2. localstack/aws/api/cloudwatch/__init__.py +41 -1
  3. localstack/aws/api/config/__init__.py +4 -0
  4. localstack/aws/api/core.py +6 -2
  5. localstack/aws/api/dynamodb/__init__.py +30 -0
  6. localstack/aws/api/ec2/__init__.py +1522 -65
  7. localstack/aws/api/iam/__init__.py +7 -0
  8. localstack/aws/api/kinesis/__init__.py +19 -0
  9. localstack/aws/api/kms/__init__.py +6 -0
  10. localstack/aws/api/lambda_/__init__.py +13 -0
  11. localstack/aws/api/logs/__init__.py +15 -0
  12. localstack/aws/api/redshift/__init__.py +9 -3
  13. localstack/aws/api/route53/__init__.py +5 -0
  14. localstack/aws/api/s3/__init__.py +12 -0
  15. localstack/aws/api/s3control/__init__.py +54 -0
  16. localstack/aws/api/ssm/__init__.py +2 -0
  17. localstack/aws/api/transcribe/__init__.py +17 -0
  18. localstack/aws/client.py +7 -2
  19. localstack/aws/forwarder.py +52 -5
  20. localstack/aws/handlers/analytics.py +1 -1
  21. localstack/aws/handlers/internal_requests.py +6 -1
  22. localstack/aws/handlers/logging.py +12 -2
  23. localstack/aws/handlers/metric_handler.py +41 -1
  24. localstack/aws/handlers/service.py +40 -20
  25. localstack/aws/mocking.py +2 -2
  26. localstack/aws/patches.py +2 -2
  27. localstack/aws/protocol/parser.py +459 -32
  28. localstack/aws/protocol/serializer.py +689 -69
  29. localstack/aws/protocol/service_router.py +120 -20
  30. localstack/aws/protocol/validate.py +1 -1
  31. localstack/aws/scaffold.py +1 -1
  32. localstack/aws/skeleton.py +4 -2
  33. localstack/aws/spec-patches.json +58 -0
  34. localstack/aws/spec.py +37 -16
  35. localstack/cli/exceptions.py +1 -1
  36. localstack/cli/localstack.py +6 -6
  37. localstack/cli/lpm.py +3 -4
  38. localstack/cli/plugins.py +1 -1
  39. localstack/cli/profiles.py +1 -2
  40. localstack/config.py +25 -18
  41. localstack/constants.py +4 -29
  42. localstack/dev/kubernetes/__main__.py +130 -7
  43. localstack/dev/run/configurators.py +1 -4
  44. localstack/dev/run/paths.py +1 -1
  45. localstack/dns/plugins.py +5 -1
  46. localstack/dns/server.py +13 -4
  47. localstack/logging/format.py +3 -3
  48. localstack/packages/api.py +9 -8
  49. localstack/packages/core.py +2 -2
  50. localstack/packages/plugins.py +0 -8
  51. localstack/runtime/analytics.py +3 -0
  52. localstack/runtime/hooks.py +1 -1
  53. localstack/runtime/init.py +2 -2
  54. localstack/runtime/main.py +5 -5
  55. localstack/runtime/patches.py +2 -2
  56. localstack/services/apigateway/helpers.py +1 -4
  57. localstack/services/apigateway/legacy/helpers.py +7 -8
  58. localstack/services/apigateway/legacy/integration.py +4 -3
  59. localstack/services/apigateway/legacy/invocations.py +6 -5
  60. localstack/services/apigateway/legacy/provider.py +148 -68
  61. localstack/services/apigateway/legacy/templates.py +1 -1
  62. localstack/services/apigateway/next_gen/execute_api/handlers/method_request.py +7 -2
  63. localstack/services/apigateway/next_gen/execute_api/handlers/resource_router.py +1 -2
  64. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  65. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  66. localstack/services/apigateway/next_gen/execute_api/template_mapping.py +2 -2
  67. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +114 -9
  68. localstack/services/apigateway/next_gen/provider.py +5 -0
  69. localstack/services/apigateway/resource_providers/aws_apigateway_resource.py +1 -1
  70. localstack/services/cloudformation/api_utils.py +4 -8
  71. localstack/services/cloudformation/cfn_utils.py +1 -1
  72. localstack/services/cloudformation/engine/entities.py +14 -4
  73. localstack/services/cloudformation/engine/template_deployer.py +6 -4
  74. localstack/services/cloudformation/engine/transformers.py +6 -4
  75. localstack/services/cloudformation/engine/v2/change_set_model.py +201 -13
  76. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +52 -3
  77. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +117 -76
  78. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +205 -52
  79. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +350 -116
  80. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +56 -14
  81. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  82. localstack/services/cloudformation/engine/v2/resolving.py +7 -5
  83. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  84. localstack/services/cloudformation/provider.py +7 -5
  85. localstack/services/cloudformation/resource_provider.py +7 -1
  86. localstack/services/cloudformation/resources.py +24149 -0
  87. localstack/services/cloudformation/service_models.py +2 -2
  88. localstack/services/cloudformation/v2/entities.py +19 -9
  89. localstack/services/cloudformation/v2/provider.py +336 -106
  90. localstack/services/cloudformation/v2/types.py +13 -7
  91. localstack/services/cloudformation/v2/utils.py +4 -1
  92. localstack/services/cloudwatch/alarm_scheduler.py +4 -1
  93. localstack/services/cloudwatch/provider.py +18 -13
  94. localstack/services/cloudwatch/provider_v2.py +25 -28
  95. localstack/services/dynamodb/packages.py +2 -1
  96. localstack/services/dynamodb/provider.py +42 -0
  97. localstack/services/dynamodb/server.py +2 -2
  98. localstack/services/dynamodb/v2/provider.py +42 -0
  99. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  100. localstack/services/edge.py +1 -1
  101. localstack/services/es/provider.py +2 -2
  102. localstack/services/events/event_rule_engine.py +31 -13
  103. localstack/services/events/models.py +4 -5
  104. localstack/services/events/provider.py +17 -14
  105. localstack/services/events/target.py +17 -9
  106. localstack/services/events/v1/provider.py +5 -5
  107. localstack/services/firehose/provider.py +14 -4
  108. localstack/services/iam/provider.py +11 -116
  109. localstack/services/iam/resources/policy_simulator.py +133 -0
  110. localstack/services/kinesis/models.py +15 -2
  111. localstack/services/kinesis/provider.py +86 -3
  112. localstack/services/kms/provider.py +14 -5
  113. localstack/services/lambda_/api_utils.py +6 -3
  114. localstack/services/lambda_/invocation/docker_runtime_executor.py +1 -1
  115. localstack/services/lambda_/invocation/event_manager.py +1 -1
  116. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  117. localstack/services/lambda_/invocation/lambda_models.py +10 -7
  118. localstack/services/lambda_/invocation/lambda_service.py +5 -1
  119. localstack/services/lambda_/packages.py +1 -1
  120. localstack/services/lambda_/provider.py +4 -3
  121. localstack/services/lambda_/provider_utils.py +1 -1
  122. localstack/services/logs/provider.py +36 -19
  123. localstack/services/moto.py +2 -1
  124. localstack/services/opensearch/cluster.py +15 -7
  125. localstack/services/opensearch/packages.py +26 -7
  126. localstack/services/opensearch/provider.py +8 -2
  127. localstack/services/opensearch/versions.py +56 -7
  128. localstack/services/plugins.py +11 -7
  129. localstack/services/providers.py +10 -2
  130. localstack/services/redshift/provider.py +0 -21
  131. localstack/services/s3/constants.py +5 -2
  132. localstack/services/s3/cors.py +4 -4
  133. localstack/services/s3/models.py +1 -1
  134. localstack/services/s3/notifications.py +55 -39
  135. localstack/services/s3/presigned_url.py +35 -54
  136. localstack/services/s3/provider.py +73 -15
  137. localstack/services/s3/utils.py +42 -22
  138. localstack/services/s3/validation.py +46 -32
  139. localstack/services/s3/website_hosting.py +4 -2
  140. localstack/services/ses/provider.py +18 -8
  141. localstack/services/sns/constants.py +7 -1
  142. localstack/services/sns/executor.py +9 -2
  143. localstack/services/sns/provider.py +8 -5
  144. localstack/services/sns/publisher.py +31 -16
  145. localstack/services/sns/v2/models.py +167 -0
  146. localstack/services/sns/v2/provider.py +867 -0
  147. localstack/services/sns/v2/utils.py +130 -0
  148. localstack/services/sqs/constants.py +1 -1
  149. localstack/services/sqs/developer_api.py +205 -0
  150. localstack/services/sqs/models.py +48 -5
  151. localstack/services/sqs/provider.py +38 -311
  152. localstack/services/sqs/query_api.py +6 -2
  153. localstack/services/sqs/utils.py +121 -2
  154. localstack/services/ssm/provider.py +1 -1
  155. localstack/services/stepfunctions/asl/component/intrinsic/member.py +1 -1
  156. localstack/services/stepfunctions/asl/component/state/state_choice/comparison/comparison.py +5 -11
  157. localstack/services/stepfunctions/asl/component/state/state_choice/state_choice.py +2 -2
  158. localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +2 -2
  159. localstack/services/stepfunctions/asl/component/state/state_execution/state_parallel/state_parallel.py +1 -1
  160. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py +2 -2
  161. localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +1 -1
  162. localstack/services/stepfunctions/asl/component/state/state_pass/state_pass.py +2 -2
  163. localstack/services/stepfunctions/asl/component/state/state_succeed/state_succeed.py +1 -1
  164. localstack/services/stepfunctions/asl/component/state/state_wait/state_wait.py +1 -1
  165. localstack/services/stepfunctions/asl/eval/environment.py +1 -1
  166. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  167. localstack/services/stepfunctions/backend/execution.py +2 -1
  168. localstack/services/stores.py +1 -1
  169. localstack/services/transcribe/provider.py +6 -1
  170. localstack/state/codecs.py +61 -0
  171. localstack/state/core.py +11 -5
  172. localstack/state/pickle.py +10 -49
  173. localstack/testing/aws/cloudformation_utils.py +1 -1
  174. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  175. localstack/testing/pytest/cloudformation/transformers.py +0 -0
  176. localstack/testing/pytest/container.py +4 -5
  177. localstack/testing/pytest/fixtures.py +33 -31
  178. localstack/testing/pytest/in_memory_localstack.py +0 -4
  179. localstack/testing/pytest/marking.py +38 -11
  180. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  181. localstack/testing/pytest/util.py +1 -1
  182. localstack/testing/pytest/validation_tracking.py +1 -2
  183. localstack/testing/snapshots/transformer_utility.py +6 -1
  184. localstack/utils/analytics/events.py +2 -2
  185. localstack/utils/analytics/metadata.py +6 -4
  186. localstack/utils/analytics/metrics/counter.py +8 -15
  187. localstack/utils/analytics/publisher.py +1 -2
  188. localstack/utils/analytics/service_providers.py +19 -0
  189. localstack/utils/analytics/service_request_aggregator.py +2 -2
  190. localstack/utils/archives.py +11 -11
  191. localstack/utils/asyncio.py +2 -2
  192. localstack/utils/aws/arns.py +24 -29
  193. localstack/utils/aws/aws_responses.py +8 -8
  194. localstack/utils/aws/aws_stack.py +2 -3
  195. localstack/utils/aws/dead_letter_queue.py +1 -5
  196. localstack/utils/aws/message_forwarding.py +1 -2
  197. localstack/utils/aws/request_context.py +4 -5
  198. localstack/utils/aws/resources.py +1 -1
  199. localstack/utils/aws/templating.py +1 -1
  200. localstack/utils/batch_policy.py +3 -3
  201. localstack/utils/bootstrap.py +21 -13
  202. localstack/utils/catalog/catalog.py +139 -0
  203. localstack/utils/catalog/catalog_loader.py +119 -0
  204. localstack/utils/catalog/common.py +58 -0
  205. localstack/utils/catalog/plugins.py +28 -0
  206. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  207. localstack/utils/collections.py +7 -8
  208. localstack/utils/config_listener.py +1 -1
  209. localstack/utils/container_networking.py +2 -3
  210. localstack/utils/container_utils/container_client.py +135 -136
  211. localstack/utils/container_utils/docker_cmd_client.py +85 -69
  212. localstack/utils/container_utils/docker_sdk_client.py +69 -66
  213. localstack/utils/crypto.py +10 -10
  214. localstack/utils/diagnose.py +3 -4
  215. localstack/utils/docker_utils.py +9 -5
  216. localstack/utils/files.py +33 -13
  217. localstack/utils/functions.py +4 -3
  218. localstack/utils/http.py +11 -11
  219. localstack/utils/json.py +20 -6
  220. localstack/utils/kinesis/kinesis_connector.py +2 -1
  221. localstack/utils/net.py +15 -9
  222. localstack/utils/no_exit_argument_parser.py +2 -2
  223. localstack/utils/numbers.py +9 -2
  224. localstack/utils/objects.py +7 -6
  225. localstack/utils/patch.py +10 -3
  226. localstack/utils/run.py +12 -11
  227. localstack/utils/scheduler.py +11 -11
  228. localstack/utils/server/tcp_proxy.py +2 -2
  229. localstack/utils/serving.py +3 -4
  230. localstack/utils/strings.py +15 -16
  231. localstack/utils/sync.py +126 -1
  232. localstack/utils/tagging.py +8 -6
  233. localstack/utils/testutil.py +8 -8
  234. localstack/utils/threads.py +2 -2
  235. localstack/utils/time.py +12 -4
  236. localstack/utils/urls.py +1 -3
  237. localstack/utils/xray/traceid.py +1 -1
  238. localstack/version.py +16 -3
  239. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/METADATA +18 -14
  240. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/RECORD +248 -239
  241. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/entry_points.txt +8 -4
  242. localstack_core-4.10.1.dev12.dist-info/plux.json +1 -0
  243. localstack/packages/terraform.py +0 -46
  244. localstack/services/cloudformation/deploy.html +0 -144
  245. localstack/services/cloudformation/deploy_ui.py +0 -47
  246. localstack/services/cloudformation/plugins.py +0 -12
  247. localstack_core-4.7.1.dev49.dist-info/plux.json +0 -1
  248. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack +0 -0
  249. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack-supervisor +0 -0
  250. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack.bat +0 -0
  251. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/WHEEL +0 -0
  252. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/licenses/LICENSE.txt +0 -0
  253. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/top_level.txt +0 -0
@@ -38,7 +38,6 @@ from localstack.aws.api.s3 import Type as GranteeType
38
38
  from localstack.services.s3 import constants as s3_constants
39
39
  from localstack.services.s3.exceptions import InvalidRequest, MalformedACLError, MalformedXML
40
40
  from localstack.services.s3.utils import (
41
- _create_invalid_argument_exc,
42
41
  get_class_attrs_from_spec_class,
43
42
  get_permission_header_name,
44
43
  is_bucket_name_valid,
@@ -87,8 +86,11 @@ def validate_canned_acl(canned_acl: str) -> None:
87
86
  Validate the canned ACL value, or raise an Exception
88
87
  """
89
88
  if canned_acl and canned_acl not in VALID_CANNED_ACLS:
90
- ex = _create_invalid_argument_exc(None, "x-amz-acl", canned_acl)
91
- raise ex
89
+ raise InvalidArgument(
90
+ None,
91
+ ArgumentName="x-amz-acl",
92
+ ArgumentValue=canned_acl,
93
+ )
92
94
 
93
95
 
94
96
  def parse_grants_in_headers(permission: Permission, grantees: str) -> Grants:
@@ -98,16 +100,18 @@ def parse_grants_in_headers(permission: Permission, grantees: str) -> Grants:
98
100
  grantee_type, grantee_id = seralized_grantee.split("=")
99
101
  grantee_id = grantee_id.strip('"')
100
102
  if grantee_type not in ("uri", "id", "emailAddress"):
101
- ex = _create_invalid_argument_exc(
103
+ raise InvalidArgument(
102
104
  "Argument format not recognized",
103
- get_permission_header_name(permission),
104
- seralized_grantee,
105
+ ArgumentName=get_permission_header_name(permission),
106
+ ArgumentValue=seralized_grantee,
105
107
  )
106
- raise ex
107
108
  elif grantee_type == "uri":
108
109
  if grantee_id not in s3_constants.VALID_ACL_PREDEFINED_GROUPS:
109
- ex = _create_invalid_argument_exc("Invalid group uri", "uri", grantee_id)
110
- raise ex
110
+ raise InvalidArgument(
111
+ "Invalid group uri",
112
+ ArgumentName="uri",
113
+ ArgumentValue=grantee_id,
114
+ )
111
115
  grantee = Grantee(
112
116
  Type=GranteeType.Group,
113
117
  URI=grantee_id,
@@ -115,8 +119,11 @@ def parse_grants_in_headers(permission: Permission, grantees: str) -> Grants:
115
119
 
116
120
  elif grantee_type == "id":
117
121
  if not is_valid_canonical_id(grantee_id):
118
- ex = _create_invalid_argument_exc("Invalid id", "id", grantee_id)
119
- raise ex
122
+ raise InvalidArgument(
123
+ "Invalid id",
124
+ ArgumentName="id",
125
+ ArgumentValue=grantee_id,
126
+ )
120
127
  grantee = Grantee(
121
128
  Type=GranteeType.CanonicalUser,
122
129
  ID=grantee_id,
@@ -141,8 +148,11 @@ def validate_acl_acp(acp: AccessControlPolicy) -> None:
141
148
  )
142
149
 
143
150
  if not is_valid_canonical_id(owner_id := acp["Owner"].get("ID", "")):
144
- ex = _create_invalid_argument_exc("Invalid id", "CanonicalUser/ID", owner_id)
145
- raise ex
151
+ raise InvalidArgument(
152
+ "Invalid id",
153
+ ArgumentName="CanonicalUser/ID",
154
+ ArgumentValue=owner_id,
155
+ )
146
156
 
147
157
  for grant in acp["Grants"]:
148
158
  if grant.get("Permission") not in s3_constants.VALID_GRANTEE_PERMISSIONS:
@@ -165,8 +175,11 @@ def validate_acl_acp(acp: AccessControlPolicy) -> None:
165
175
  and (grant_uri := grantee.get("URI", ""))
166
176
  not in s3_constants.VALID_ACL_PREDEFINED_GROUPS
167
177
  ):
168
- ex = _create_invalid_argument_exc("Invalid group uri", "Group/URI", grant_uri)
169
- raise ex
178
+ raise InvalidArgument(
179
+ "Invalid group uri",
180
+ ArgumentName="Group/URI",
181
+ ArgumentValue=grant_uri,
182
+ )
170
183
 
171
184
  elif grant_type == GranteeType.AmazonCustomerByEmail:
172
185
  # TODO: add validation here
@@ -175,8 +188,11 @@ def validate_acl_acp(acp: AccessControlPolicy) -> None:
175
188
  elif grant_type == GranteeType.CanonicalUser and not is_valid_canonical_id(
176
189
  grantee_id := grantee.get("ID", "")
177
190
  ):
178
- ex = _create_invalid_argument_exc("Invalid id", "CanonicalUser/ID", grantee_id)
179
- raise ex
191
+ raise InvalidArgument(
192
+ "Invalid id",
193
+ ArgumentName="CanonicalUser/ID",
194
+ ArgumentValue=grantee_id,
195
+ )
180
196
 
181
197
 
182
198
  def validate_lifecycle_configuration(lifecycle_conf: BucketLifecycleConfiguration) -> None:
@@ -242,12 +258,12 @@ def validate_website_configuration(website_config: WebsiteConfiguration) -> None
242
258
  """
243
259
  if redirect_all_req := website_config.get("RedirectAllRequestsTo", {}):
244
260
  if len(website_config) > 1:
245
- ex = _create_invalid_argument_exc(
246
- message="RedirectAllRequestsTo cannot be provided in conjunction with other Routing Rules.",
247
- name="RedirectAllRequestsTo",
248
- value="not null",
261
+ raise InvalidArgument(
262
+ "RedirectAllRequestsTo cannot be provided in conjunction with other Routing Rules.",
263
+ ArgumentName="RedirectAllRequestsTo",
264
+ ArgumentValue="not null",
249
265
  )
250
- raise ex
266
+
251
267
  if "HostName" not in redirect_all_req:
252
268
  raise MalformedXML()
253
269
 
@@ -261,20 +277,18 @@ def validate_website_configuration(website_config: WebsiteConfiguration) -> None
261
277
  # required
262
278
  # https://docs.aws.amazon.com/AmazonS3/latest/API/API_IndexDocument.html
263
279
  if not (index_configuration := website_config.get("IndexDocument")):
264
- ex = _create_invalid_argument_exc(
265
- message="A value for IndexDocument Suffix must be provided if RedirectAllRequestsTo is empty",
266
- name="IndexDocument",
267
- value="null",
280
+ raise InvalidArgument(
281
+ "A value for IndexDocument Suffix must be provided if RedirectAllRequestsTo is empty",
282
+ ArgumentName="IndexDocument",
283
+ ArgumentValue="null",
268
284
  )
269
- raise ex
270
285
 
271
286
  if not (index_suffix := index_configuration.get("Suffix")) or "/" in index_suffix:
272
- ex = _create_invalid_argument_exc(
273
- message="The IndexDocument Suffix is not well formed",
274
- name="IndexDocument",
275
- value=index_suffix or None,
287
+ raise InvalidArgument(
288
+ "The IndexDocument Suffix is not well formed",
289
+ ArgumentName="IndexDocument",
290
+ ArgumentValue=index_suffix or None,
276
291
  )
277
- raise ex
278
292
 
279
293
  if "ErrorDocument" in website_config and not website_config.get("ErrorDocument", {}).get("Key"):
280
294
  raise MalformedXML()
@@ -93,8 +93,10 @@ class S3WebsiteHostingHandler:
93
93
  return Response(response_body, status=e.response["ResponseMetadata"]["HTTPStatusCode"])
94
94
 
95
95
  except Exception:
96
- LOG.exception(
97
- "Exception encountered while trying to serve s3-website at %s", request.url
96
+ LOG.error(
97
+ "Exception encountered while trying to serve s3-website at %s",
98
+ request.url,
99
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
98
100
  )
99
101
  return Response(_create_500_error_string(), status=500)
100
102
 
@@ -182,6 +182,8 @@ class SesProvider(SesApi, ServiceLifecycleHook):
182
182
  #
183
183
 
184
184
  def on_after_init(self):
185
+ self._apply_patches()
186
+
185
187
  # Allow sent emails to be retrieved from the SES emails endpoint
186
188
  register_ses_api_resource()
187
189
 
@@ -197,6 +199,12 @@ class SesProvider(SesApi, ServiceLifecycleHook):
197
199
  return entity.replace("From:", "").strip()
198
200
  return None
199
201
 
202
+ def _apply_patches(self) -> None:
203
+ # Suppress Moto's validation of receipt rule actions. These validations use Moto's implementation of S3, Lambda
204
+ # and SQS, which fail because these services have been internalised in LocalStack.
205
+ # Besides, AWS does not run the same validations as evidenced by our AWS-validated tests.
206
+ SESBackend._validate_receipt_rule_actions = lambda *_: None
207
+
200
208
  #
201
209
  # Implementations for SES operations
202
210
  #
@@ -517,8 +525,10 @@ class SesProvider(SesApi, ServiceLifecycleHook):
517
525
  backend.create_receipt_rule_set(rule_set_name)
518
526
  original_rule_set = backend.describe_receipt_rule_set(original_rule_set_name)
519
527
 
520
- for rule in original_rule_set:
521
- backend.create_receipt_rule(rule_set_name, rule)
528
+ after = None
529
+ for rule in original_rule_set.rules:
530
+ backend.create_receipt_rule(rule_set_name, rule, after)
531
+ after = rule["Name"]
522
532
 
523
533
  return CloneReceiptRuleSetResponse()
524
534
 
@@ -547,7 +557,7 @@ class SesProvider(SesApi, ServiceLifecycleHook):
547
557
  )
548
558
 
549
559
  backend = get_ses_backend(context)
550
- if identity not in backend.addresses:
560
+ if identity not in backend.email_identities:
551
561
  raise MessageRejected(f"Identity {identity} is not verified or does not exist.")
552
562
 
553
563
  # Store the setting in the backend
@@ -626,7 +636,7 @@ class SNSEmitter:
626
636
  Subject="Amazon SES Email Event Notification",
627
637
  )
628
638
  except ClientError:
629
- LOGGER.exception("sending SNS message")
639
+ LOGGER.error("sending SNS message", exc_info=LOGGER.isEnabledFor(logging.DEBUG))
630
640
 
631
641
  def emit_delivery_event(self, payload: EventDestinationPayload, sns_topic_arn: str):
632
642
  now = datetime.now(tz=UTC)
@@ -659,7 +669,7 @@ class SNSEmitter:
659
669
  Subject="Amazon SES Email Event Notification",
660
670
  )
661
671
  except ClientError:
662
- LOGGER.exception("sending SNS message")
672
+ LOGGER.error("sending SNS message", exc_info=LOGGER.isEnabledFor(logging.DEBUG))
663
673
 
664
674
  @staticmethod
665
675
  def _client_for_topic(topic_arn: str) -> "SNSClient":
@@ -677,7 +687,7 @@ class SNSEmitter:
677
687
  def notify_event_destinations(
678
688
  context: RequestContext,
679
689
  # FIXME: Moto stores the Event Destinations as a single value when it should be a list
680
- event_destinations: dict,
690
+ event_destinations: EventDestination | list[EventDestination],
681
691
  payload: EventDestinationPayload,
682
692
  email_type: EmailType,
683
693
  ):
@@ -690,11 +700,11 @@ def notify_event_destinations(
690
700
  if not event_destination["Enabled"]:
691
701
  continue
692
702
 
693
- sns_destination_arn = event_destination.get("SNSDestination")
703
+ sns_destination_arn = event_destination.get("SNSDestination", {}).get("TopicARN")
694
704
  if not sns_destination_arn:
695
705
  continue
696
706
 
697
- matching_event_types = event_destination.get("EventMatchingTypes") or []
707
+ matching_event_types = event_destination.get("MatchingEventTypes") or []
698
708
  if EventType.send in matching_event_types:
699
709
  emitter.emit_send_event(
700
710
  payload, sns_destination_arn, emit_source_arn=email_type != EmailType.TEMPLATED
@@ -1,5 +1,8 @@
1
1
  import re
2
2
  from string import ascii_letters, digits
3
+ from typing import get_args
4
+
5
+ from localstack.services.sns.v2.models import SnsApplicationPlatforms
3
6
 
4
7
  SNS_PROTOCOLS = [
5
8
  "http",
@@ -13,7 +16,7 @@ SNS_PROTOCOLS = [
13
16
  "firehose",
14
17
  ]
15
18
 
16
- VALID_SUBSCRIPTION_ATTR_NAME = [
19
+ VALID_SUBSCRIPTION_ATTR_NAME: list[str] = [
17
20
  "DeliveryPolicy",
18
21
  "FilterPolicy",
19
22
  "FilterPolicyScope",
@@ -39,3 +42,6 @@ SUBSCRIPTION_TOKENS_ENDPOINT = "/_aws/sns/subscription-tokens"
39
42
  SNS_CERT_ENDPOINT = "/_aws/sns/SimpleNotificationService-6c6f63616c737461636b69736e696365.pem"
40
43
 
41
44
  DUMMY_SUBSCRIPTION_PRINCIPAL = "arn:{partition}:iam::{account_id}:user/DummySNSPrincipal"
45
+ E164_REGEX = re.compile(r"^\+?[1-9]\d{1,14}$")
46
+
47
+ VALID_APPLICATION_PLATFORMS = list(get_args(SnsApplicationPlatforms))
@@ -18,7 +18,10 @@ def _worker(work_queue: queue.Queue):
18
18
  del work_item
19
19
 
20
20
  except Exception:
21
- LOG.exception("Exception in worker")
21
+ LOG.error(
22
+ "Exception in worker",
23
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
24
+ )
22
25
 
23
26
 
24
27
  class _WorkItem:
@@ -31,7 +34,11 @@ class _WorkItem:
31
34
  try:
32
35
  self.fn(*self.args, **self.kwargs)
33
36
  except Exception:
34
- LOG.exception("Unhandled Exception in while running %s", self.fn.__name__)
37
+ LOG.error(
38
+ "Unhandled Exception in while running %s",
39
+ self.fn.__name__,
40
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
41
+ )
35
42
 
36
43
 
37
44
  class TopicPartitionedThreadPoolExecutor:
@@ -1,4 +1,5 @@
1
1
  import base64
2
+ import contextlib
2
3
  import copy
3
4
  import functools
4
5
  import json
@@ -419,6 +420,10 @@ class SnsProvider(SnsApi, ServiceLifecycleHook):
419
420
  def unsubscribe(
420
421
  self, context: RequestContext, subscription_arn: subscriptionARN, **kwargs
421
422
  ) -> None:
423
+ if subscription_arn is None:
424
+ raise InvalidParameterException(
425
+ "Invalid parameter: SubscriptionArn Reason: no value for required parameter",
426
+ )
422
427
  count = len(subscription_arn.split(":"))
423
428
  try:
424
429
  parsed_arn = parse_arn(subscription_arn)
@@ -469,7 +474,8 @@ class SnsProvider(SnsApi, ServiceLifecycleHook):
469
474
  subscription_arn=subscription_arn,
470
475
  )
471
476
 
472
- store.topic_subscriptions[subscription["TopicArn"]].remove(subscription_arn)
477
+ with contextlib.suppress(ValueError):
478
+ store.topic_subscriptions[subscription["TopicArn"]].remove(subscription_arn)
473
479
  store.subscription_filter_policy.pop(subscription_arn, None)
474
480
  store.subscriptions.pop(subscription_arn, None)
475
481
 
@@ -583,10 +589,7 @@ class SnsProvider(SnsApi, ServiceLifecycleHook):
583
589
  raise InvalidParameterException(
584
590
  "Invalid parameter: MessageDeduplicationId Reason: The request includes MessageDeduplicationId parameter that is not valid for this topic type"
585
591
  )
586
- elif message_group_id:
587
- raise InvalidParameterException(
588
- "Invalid parameter: MessageGroupId Reason: The request includes MessageGroupId parameter that is not valid for this topic type"
589
- )
592
+
590
593
  is_endpoint_publish = target_arn and ":endpoint/" in target_arn
591
594
  if message_structure == "json":
592
595
  try:
@@ -89,9 +89,10 @@ class TopicPublisher(abc.ABC):
89
89
  try:
90
90
  self._publish(context=context, subscriber=subscriber)
91
91
  except Exception:
92
- LOG.exception(
92
+ LOG.error(
93
93
  "An internal error occurred while trying to send the SNS message %s",
94
94
  context.message,
95
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
95
96
  )
96
97
  return
97
98
 
@@ -147,9 +148,10 @@ class EndpointPublisher(abc.ABC):
147
148
  try:
148
149
  self._publish(context=context, endpoint=endpoint)
149
150
  except Exception:
150
- LOG.exception(
151
+ LOG.error(
151
152
  "An internal error occurred while trying to send the SNS message %s",
152
153
  context.message,
154
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
153
155
  )
154
156
  return
155
157
 
@@ -295,7 +297,10 @@ class SqsTopicPublisher(TopicPublisher):
295
297
  )
296
298
  kwargs = self.get_sqs_kwargs(msg_context=message_context, subscriber=subscriber)
297
299
  except Exception:
298
- LOG.exception("An internal error occurred while trying to format the message for SQS")
300
+ LOG.error(
301
+ "An internal error occurred while trying to format the message for SQS",
302
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
303
+ )
299
304
  return
300
305
  try:
301
306
  queue_url: str = sqs_queue_url_for_arn(subscriber["Endpoint"])
@@ -335,19 +340,29 @@ class SqsTopicPublisher(TopicPublisher):
335
340
 
336
341
  # SNS now allows regular non-fifo subscriptions to FIFO topics. Validate that the subscription target is fifo
337
342
  # before passing the FIFO-only parameters
338
- if subscriber["Endpoint"].endswith(".fifo"):
339
- if msg_context.message_group_id:
340
- kwargs["MessageGroupId"] = msg_context.message_group_id
341
- if msg_context.message_deduplication_id:
342
- kwargs["MessageDeduplicationId"] = msg_context.message_deduplication_id
343
- elif subscriber["TopicArn"].endswith(".fifo"):
344
- # Amazon SNS uses the message body provided to generate a unique hash value to use as the deduplication
345
- # ID for each message, so you don't need to set a deduplication ID when you send each message.
346
- # https://docs.aws.amazon.com/sns/latest/dg/fifo-message-dedup.html
347
- content = msg_context.message_content("sqs")
348
- kwargs["MessageDeduplicationId"] = hashlib.sha256(
349
- content.encode("utf-8")
350
- ).hexdigest()
343
+
344
+ # SNS will only forward the `MessageGroupId` for Fair Queues in some scenarios:
345
+ # - non-FIFO SNS topic to Fair Queue
346
+ # - FIFO topic to FIFO queue
347
+ # It will NOT forward it with FIFO topic to regular Queue (possibly used for internal grouping without relying
348
+ # on SQS capabilities)
349
+ if subscriber["TopicArn"].endswith(".fifo"):
350
+ if subscriber["Endpoint"].endswith(".fifo"):
351
+ if msg_context.message_group_id:
352
+ kwargs["MessageGroupId"] = msg_context.message_group_id
353
+ if msg_context.message_deduplication_id:
354
+ kwargs["MessageDeduplicationId"] = msg_context.message_deduplication_id
355
+ else:
356
+ # SNS uses the message body provided to generate a unique hash value to use as the deduplication ID
357
+ # for each message, so you don't need to set a deduplication ID when you send each message.
358
+ # https://docs.aws.amazon.com/sns/latest/dg/fifo-message-dedup.html
359
+ content = msg_context.message_content("sqs")
360
+ kwargs["MessageDeduplicationId"] = hashlib.sha256(
361
+ content.encode("utf-8")
362
+ ).hexdigest()
363
+
364
+ elif msg_context.message_group_id:
365
+ kwargs["MessageGroupId"] = msg_context.message_group_id
351
366
 
352
367
  # TODO: for message deduplication, we are using the underlying features of the SQS queue
353
368
  # however, SQS queue only deduplicate at the Queue level, where the SNS topic deduplicate on the topic level
@@ -0,0 +1,167 @@
1
+ import itertools
2
+ import time
3
+ from dataclasses import dataclass, field
4
+ from enum import StrEnum
5
+ from typing import Literal, TypedDict
6
+
7
+ from localstack.aws.api.sns import (
8
+ MessageAttributeMap,
9
+ PlatformApplication,
10
+ PublishBatchRequestEntry,
11
+ TopicAttributesMap,
12
+ subscriptionARN,
13
+ topicARN,
14
+ )
15
+ from localstack.services.stores import (
16
+ AccountRegionBundle,
17
+ BaseStore,
18
+ CrossRegionAttribute,
19
+ LocalAttribute,
20
+ )
21
+ from localstack.utils.objects import singleton_factory
22
+ from localstack.utils.strings import long_uid
23
+ from localstack.utils.tagging import TaggingService
24
+
25
+
26
+ class Topic(TypedDict, total=True):
27
+ arn: str
28
+ name: str
29
+ attributes: TopicAttributesMap
30
+ subscriptions: list[str]
31
+
32
+
33
+ SnsProtocols = Literal[
34
+ "http", "https", "email", "email-json", "sms", "sqs", "application", "lambda", "firehose"
35
+ ]
36
+
37
+ SnsApplicationPlatforms = Literal[
38
+ "APNS", "APNS_SANDBOX", "ADM", "FCM", "Baidu", "GCM", "MPNS", "WNS"
39
+ ]
40
+
41
+
42
+ SMS_ATTRIBUTE_NAMES = [
43
+ "DeliveryStatusIAMRole",
44
+ "DeliveryStatusSuccessSamplingRate",
45
+ "DefaultSenderID",
46
+ "DefaultSMSType",
47
+ "UsageReportS3Bucket",
48
+ ]
49
+ SMS_TYPES = ["Promotional", "Transactional"]
50
+ SMS_DEFAULT_SENDER_REGEX = r"^(?=[A-Za-z0-9]{1,11}$)(?=.*[A-Za-z])[A-Za-z0-9]+$"
51
+ SnsMessageProtocols = Literal[SnsProtocols, SnsApplicationPlatforms]
52
+
53
+
54
+ class SnsSubscription(TypedDict, total=False):
55
+ """
56
+ In SNS, Subscription can be represented with only TopicArn, Endpoint, Protocol, SubscriptionArn and Owner, for
57
+ example in ListSubscriptions. However, when getting a subscription with GetSubscriptionAttributes, it will return
58
+ the Subscription object merged with its own attributes.
59
+ This represents this merged object, for internal use and in GetSubscriptionAttributes
60
+ https://docs.aws.amazon.com/cli/latest/reference/sns/get-subscription-attributes.html
61
+ """
62
+
63
+ TopicArn: topicARN
64
+ Endpoint: str
65
+ Protocol: SnsProtocols
66
+ SubscriptionArn: subscriptionARN
67
+ PendingConfirmation: Literal["true", "false"]
68
+ Owner: str | None
69
+ SubscriptionPrincipal: str | None
70
+ FilterPolicy: str | None
71
+ FilterPolicyScope: Literal["MessageAttributes", "MessageBody"]
72
+ RawMessageDelivery: Literal["true", "false"]
73
+ ConfirmationWasAuthenticated: Literal["true", "false"]
74
+ SubscriptionRoleArn: str | None
75
+ DeliveryPolicy: str | None
76
+
77
+
78
+ @singleton_factory
79
+ def global_sns_message_sequence():
80
+ # creates a 20-digit number used as the start for the global sequence, adds 100 for it to be different from SQS's
81
+ # mostly for testing purpose, both global sequence would be initialized at the same and be identical
82
+ start = int(time.time() + 100) << 33
83
+ # itertools.count is thread safe over the GIL since its getAndIncrement operation is a single python bytecode op
84
+ return itertools.count(start)
85
+
86
+
87
+ def get_next_sequence_number():
88
+ return next(global_sns_message_sequence())
89
+
90
+
91
+ class SnsMessageType(StrEnum):
92
+ Notification = "Notification"
93
+ SubscriptionConfirmation = "SubscriptionConfirmation"
94
+ UnsubscribeConfirmation = "UnsubscribeConfirmation"
95
+
96
+
97
+ @dataclass
98
+ class SnsMessage:
99
+ type: SnsMessageType
100
+ message: (
101
+ str | dict
102
+ ) # can be Dict if after being JSON decoded for validation if structure is `json`
103
+ message_attributes: MessageAttributeMap | None = None
104
+ message_structure: str | None = None
105
+ subject: str | None = None
106
+ message_deduplication_id: str | None = None
107
+ message_group_id: str | None = None
108
+ token: str | None = None
109
+ message_id: str = field(default_factory=long_uid)
110
+ is_fifo: bool | None = False
111
+ sequencer_number: str | None = None
112
+
113
+ def __post_init__(self):
114
+ if self.message_attributes is None:
115
+ self.message_attributes = {}
116
+ if self.is_fifo:
117
+ self.sequencer_number = str(get_next_sequence_number())
118
+
119
+ def message_content(self, protocol: SnsMessageProtocols) -> str:
120
+ """
121
+ Helper function to retrieve the message content for the right protocol if the StructureMessage is `json`
122
+ See https://docs.aws.amazon.com/sns/latest/dg/sns-send-custom-platform-specific-payloads-mobile-devices.html
123
+ https://docs.aws.amazon.com/sns/latest/dg/example_sns_Publish_section.html
124
+ :param protocol:
125
+ :return: message content as string
126
+ """
127
+ if self.message_structure == "json":
128
+ return self.message.get(protocol, self.message.get("default"))
129
+
130
+ return self.message
131
+
132
+ @classmethod
133
+ def from_batch_entry(cls, entry: PublishBatchRequestEntry, is_fifo=False) -> "SnsMessage":
134
+ return cls(
135
+ type=SnsMessageType.Notification,
136
+ message=entry["Message"],
137
+ subject=entry.get("Subject"),
138
+ message_structure=entry.get("MessageStructure"),
139
+ message_attributes=entry.get("MessageAttributes"),
140
+ message_deduplication_id=entry.get("MessageDeduplicationId"),
141
+ message_group_id=entry.get("MessageGroupId"),
142
+ is_fifo=is_fifo,
143
+ )
144
+
145
+
146
+ class SnsStore(BaseStore):
147
+ topics: dict[str, Topic] = LocalAttribute(default=dict)
148
+
149
+ # maps subscription ARN to SnsSubscription
150
+ subscriptions: dict[str, SnsSubscription] = LocalAttribute(default=dict)
151
+
152
+ # filter policy are stored as JSON string in subscriptions, store the decoded result Dict
153
+ subscription_filter_policy: dict[subscriptionARN, dict] = LocalAttribute(default=dict)
154
+
155
+ # maps confirmation token to subscription ARN
156
+ subscription_tokens: dict[str, str] = LocalAttribute(default=dict)
157
+
158
+ # maps platform application arns to platform applications
159
+ platform_applications: dict[str, PlatformApplication] = LocalAttribute(default=dict)
160
+
161
+ # topic/subscription independent default values for sending sms messages
162
+ sms_attributes: dict[str, str] = LocalAttribute(default=dict)
163
+
164
+ TAGS: TaggingService = CrossRegionAttribute(default=TaggingService)
165
+
166
+
167
+ sns_stores = AccountRegionBundle("sns", SnsStore)