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
@@ -3,7 +3,8 @@ Custom pytest mark typings
3
3
  """
4
4
 
5
5
  import os
6
- from typing import TYPE_CHECKING, Callable, Optional
6
+ from collections.abc import Callable
7
+ from typing import TYPE_CHECKING
7
8
 
8
9
  import pytest
9
10
  from _pytest.config import PytestPluginManager
@@ -36,13 +37,13 @@ class SkipSnapshotVerifyMarker:
36
37
  def __call__(
37
38
  self,
38
39
  *,
39
- paths: "Optional[list[str]]" = None,
40
- condition: "Optional[Callable[[...], bool]]" = None,
40
+ paths: "list[str] | None" = None,
41
+ condition: "Callable[[...], bool] | None" = None,
41
42
  ): ...
42
43
 
43
44
 
44
45
  class MultiRuntimeMarker:
45
- def __call__(self, *, scenario: str, runtimes: Optional[list[str]] = None): ...
46
+ def __call__(self, *, scenario: str, runtimes: list[str] | None = None): ...
46
47
 
47
48
 
48
49
  class SnapshotMarkers:
@@ -58,13 +59,27 @@ class Markers:
58
59
 
59
60
  # test selection
60
61
  acceptance_test = pytest.mark.acceptance_test
62
+ """This test is an acceptance test"""
61
63
  skip_offline = pytest.mark.skip_offline
64
+ """Test is skipped if offline, as it requires some sort of internet connection to run"""
62
65
  only_on_amd64 = pytest.mark.only_on_amd64
66
+ """Test requires ability of the system to execute amd64 binaries"""
63
67
  only_on_arm64 = pytest.mark.only_on_arm64
68
+ """Test requires ability of the system to execute arm64 binaries"""
64
69
  resource_heavy = pytest.mark.resource_heavy
65
- only_in_docker = pytest.mark.only_in_docker
66
- # Tests to execute when updating snapshots for a new Lambda runtime
70
+ """Test is very resource heavy, and might be skipped in CI"""
71
+ requires_in_container = pytest.mark.requires_in_container
72
+ """Test requires LocalStack to run inside a container"""
73
+ requires_in_process = pytest.mark.requires_in_process
74
+ """The test and the LS instance have to be run in the same process"""
75
+ requires_docker = pytest.mark.requires_docker
76
+ """The test requires docker or a compatible container engine - will not work on kubernetes"""
67
77
  lambda_runtime_update = pytest.mark.lambda_runtime_update
78
+ """Tests to execute when updating snapshots for a new Lambda runtime"""
79
+ k8s_always_run = pytest.mark.k8s_always_run
80
+ """This tests will always run against k8s environment"""
81
+ skip_k8s = pytest.mark.skip_k8s
82
+ """This test will be skipped in k8s environment"""
68
83
 
69
84
 
70
85
  # pytest plugin
@@ -131,8 +146,8 @@ def filter_by_markers(config: "Config", items: list[pytest.Item]):
131
146
  "Add network connectivity and remove the --offline option when running "
132
147
  "the test."
133
148
  )
134
- only_in_docker = pytest.mark.skip(
135
- reason="Test requires execution inside Docker (e.g., to install system packages)"
149
+ requires_in_container = pytest.mark.skip(
150
+ reason="Test requires execution inside a container (e.g., to install system packages)"
136
151
  )
137
152
  only_on_amd64 = pytest.mark.skip(
138
153
  reason="Test uses features that are currently only supported for AMD64. Skipping in CI."
@@ -144,8 +159,8 @@ def filter_by_markers(config: "Config", items: list[pytest.Item]):
144
159
  for item in items:
145
160
  if is_offline and "skip_offline" in item.keywords:
146
161
  item.add_marker(skip_offline)
147
- if not is_in_docker and "only_in_docker" in item.keywords:
148
- item.add_marker(only_in_docker)
162
+ if not is_in_docker and "requires_in_container" in item.keywords:
163
+ item.add_marker(requires_in_container)
149
164
  if is_in_ci and not is_amd64 and "only_on_amd64" in item.keywords:
150
165
  item.add_marker(only_on_amd64)
151
166
  if is_in_ci and not is_arm64 and "only_on_arm64" in item.keywords:
@@ -177,7 +192,7 @@ def pytest_configure(config):
177
192
  )
178
193
  config.addinivalue_line(
179
194
  "markers",
180
- "only_in_docker: mark the test as running only in Docker (e.g., requires installation of system packages)",
195
+ "requires_in_container: mark the test as running only in a container (e.g., requires installation of system packages)",
181
196
  )
182
197
  config.addinivalue_line(
183
198
  "markers",
@@ -208,3 +223,15 @@ def pytest_configure(config):
208
223
  "markers",
209
224
  "multiruntime: parametrize test against multiple Lambda runtimes",
210
225
  )
226
+ config.addinivalue_line(
227
+ "markers",
228
+ "requires_docker: mark the test as requiring docker (or a compatible container engine) - will not work on kubernetes.",
229
+ )
230
+ config.addinivalue_line(
231
+ "markers",
232
+ "requires_in_process: mark the test as requiring the test to run inside the same process as LocalStack - will not work if tests are run against a running LS container.",
233
+ )
234
+ config.addinivalue_line(
235
+ "markers",
236
+ "k8s_always_run: mark the test to always run in k8s environment. This allows us to run tests that would otherwise be skipped, such as localstack_only tests.",
237
+ )
@@ -1,6 +1,7 @@
1
1
  import json
2
2
  import logging
3
- from typing import Callable, Final, Optional
3
+ from collections.abc import Callable
4
+ from typing import Final
4
5
 
5
6
  from botocore.exceptions import ClientError
6
7
  from jsonpath_ng.ext import parse
@@ -402,8 +403,8 @@ def create_state_machine_with_iam_role(
402
403
  create_state_machine,
403
404
  snapshot,
404
405
  definition: Definition,
405
- logging_configuration: Optional[LoggingConfiguration] = None,
406
- state_machine_name: Optional[str] = None,
406
+ logging_configuration: LoggingConfiguration | None = None,
407
+ state_machine_name: str | None = None,
407
408
  state_machine_type: StateMachineType = StateMachineType.STANDARD,
408
409
  ):
409
410
  snf_role_arn = create_state_machine_iam_role(target_aws_client=target_aws_client)
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import pwd
3
+ from collections.abc import Callable
3
4
  from multiprocessing import Process, ProcessError
4
- from typing import Callable
5
5
 
6
6
 
7
7
  def run_as_os_user(target: Callable, uid: str | int, gid: str | int = None):
@@ -9,7 +9,6 @@ import datetime
9
9
  import json
10
10
  import os
11
11
  from pathlib import Path
12
- from typing import Optional
13
12
 
14
13
  import pytest
15
14
  from pluggy import Result
@@ -28,7 +27,7 @@ Stores information from call execution phase about whether the test failed.
28
27
  """
29
28
 
30
29
 
31
- def find_validation_data_for_item(item: pytest.Item) -> Optional[dict]:
30
+ def find_validation_data_for_item(item: pytest.Item) -> dict | None:
32
31
  base_path = os.path.join(item.fspath.dirname, item.fspath.purebasename)
33
32
  snapshot_path = f"{base_path}.validation.json"
34
33
 
@@ -45,6 +45,10 @@ PATTERN_KEY_ARN = re.compile(
45
45
  r"arn:(aws[a-zA-Z-]*)?:([a-zA-Z0-9-_.]+)?:([^:]+)?:(\d{12})?:key/[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"
46
46
  )
47
47
 
48
+ PATTERN_MRK_KEY_ARN = re.compile(
49
+ r"arn:(aws[a-zA-Z-]*)?:([a-zA-Z0-9-_.]+)?:([^:]+)?:(\d{12})?:key/mrk-[a-fA-F0-9]{32}"
50
+ )
51
+
48
52
 
49
53
  # TODO: split into generic/aws and put into lib
50
54
  class TransformerUtility:
@@ -500,7 +504,7 @@ class TransformerUtility:
500
504
  replacement="<stream-name>",
501
505
  ),
502
506
  TransformerUtility.key_value(
503
- "ContinuationSequenceNumber", "<continuation_sequence_number>"
507
+ "ContinuationSequenceNumber", "continuation_sequence_number"
504
508
  ),
505
509
  ]
506
510
 
@@ -573,6 +577,7 @@ class TransformerUtility:
573
577
  TransformerUtility.key_value("CiphertextBlob", reference_replacement=False),
574
578
  TransformerUtility.key_value("Plaintext", reference_replacement=False),
575
579
  RegexTransformer(PATTERN_KEY_ARN, replacement="<key-arn>"),
580
+ RegexTransformer(PATTERN_MRK_KEY_ARN, replacement="<mrk-key-arn>"),
576
581
  ]
577
582
 
578
583
  @staticmethod
@@ -1,8 +1,8 @@
1
1
  import abc
2
2
  import dataclasses
3
- from typing import Any, Union
3
+ from typing import Any
4
4
 
5
- EventPayload = Union[dict[str, Any], Any] # FIXME: better typing
5
+ EventPayload = dict[str, Any] | Any # FIXME: better typing
6
6
 
7
7
 
8
8
  @dataclasses.dataclass
@@ -2,7 +2,6 @@ import dataclasses
2
2
  import logging
3
3
  import os
4
4
  import platform
5
- from typing import Optional
6
5
 
7
6
  from localstack import config
8
7
  from localstack.constants import VERSION
@@ -41,7 +40,7 @@ class ClientMetadata:
41
40
  k = "*" * len(k)
42
41
  d["api_key"] = k
43
42
 
44
- return "ClientMetadata(%s)" % d
43
+ return f"ClientMetadata({d})"
45
44
 
46
45
 
47
46
  def get_version_string() -> str:
@@ -148,7 +147,10 @@ def is_license_activated() -> bool:
148
147
 
149
148
  return licensingv2.get_licensed_environment().activated
150
149
  except Exception:
151
- LOG.exception("Could not determine license activation status")
150
+ LOG.error(
151
+ "Could not determine license activation status",
152
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
153
+ )
152
154
  return False
153
155
 
154
156
 
@@ -198,7 +200,7 @@ def _generate_machine_id() -> str:
198
200
  return f"gen_{long_uid()[:12]}"
199
201
 
200
202
 
201
- def get_api_key_or_auth_token() -> Optional[str]:
203
+ def get_api_key_or_auth_token() -> str | None:
202
204
  # TODO: this is duplicated code from ext, but should probably migrate that to localstack
203
205
  auth_token = os.environ.get("LOCALSTACK_AUTH_TOKEN", "").strip("'\" ")
204
206
  if auth_token:
@@ -1,7 +1,7 @@
1
1
  import threading
2
2
  from collections import defaultdict
3
3
  from dataclasses import dataclass
4
- from typing import Any, Optional, Union
4
+ from typing import Any
5
5
 
6
6
  from localstack import config
7
7
 
@@ -38,7 +38,7 @@ class LabeledCounterPayload:
38
38
  value: int
39
39
  type: str
40
40
  schema_version: int
41
- labels: dict[str, Union[str, float]]
41
+ labels: dict[str, str | float]
42
42
 
43
43
  def as_dict(self) -> dict[str, Any]:
44
44
  payload_dict = {
@@ -66,7 +66,7 @@ class ThreadSafeCounter:
66
66
  _count: int
67
67
 
68
68
  def __init__(self):
69
- super(ThreadSafeCounter, self).__init__()
69
+ super().__init__()
70
70
  self._mutex = threading.Lock()
71
71
  self._count = 0
72
72
 
@@ -140,15 +140,11 @@ class LabeledCounter(Metric):
140
140
 
141
141
  _type: str
142
142
  _labels: list[str]
143
- _label_values: tuple[Optional[Union[str, float]], ...]
144
- _counters_by_label_values: defaultdict[
145
- tuple[Optional[Union[str, float]], ...], ThreadSafeCounter
146
- ]
143
+ _label_values: tuple[str | float | None, ...]
144
+ _counters_by_label_values: defaultdict[tuple[str | float | None, ...], ThreadSafeCounter]
147
145
 
148
146
  def __init__(self, namespace: str, name: str, labels: list[str], schema_version: int = 1):
149
- super(LabeledCounter, self).__init__(
150
- namespace=namespace, name=name, schema_version=schema_version
151
- )
147
+ super().__init__(namespace=namespace, name=name, schema_version=schema_version)
152
148
 
153
149
  if not labels:
154
150
  raise ValueError("At least one label is required; the labels list cannot be empty.")
@@ -164,7 +160,7 @@ class LabeledCounter(Metric):
164
160
  self._counters_by_label_values = defaultdict(ThreadSafeCounter)
165
161
  MetricRegistry().register(self)
166
162
 
167
- def labels(self, **kwargs: Union[str, float, None]) -> ThreadSafeCounter:
163
+ def labels(self, **kwargs: str | float | None) -> ThreadSafeCounter:
168
164
  """
169
165
  Create a scoped counter instance with specific label values.
170
166
 
@@ -200,10 +196,7 @@ class LabeledCounter(Metric):
200
196
  )
201
197
 
202
198
  # Create labels dictionary
203
- labels_dict = {
204
- label_name: label_value
205
- for label_name, label_value in zip(self._labels, label_values)
206
- }
199
+ labels_dict = dict(zip(self._labels, label_values, strict=False))
207
200
 
208
201
  payload.append(
209
202
  LabeledCounterPayload(
@@ -4,7 +4,6 @@ import logging
4
4
  import threading
5
5
  import time
6
6
  from queue import Full, Queue
7
- from typing import Optional
8
7
 
9
8
  from localstack import config
10
9
  from localstack.utils.threads import start_thread, start_worker_thread
@@ -93,7 +92,7 @@ class PublisherBuffer(EventHandler):
93
92
  self._stopping.set()
94
93
  self._command_queue.put(self._cmd_stop)
95
94
 
96
- def close_sync(self, timeout: Optional[float] = None):
95
+ def close_sync(self, timeout: float | None = None):
97
96
  self.close()
98
97
  return self._stopped.wait(timeout)
99
98
 
@@ -0,0 +1,19 @@
1
+ from localstack.runtime import hooks
2
+
3
+
4
+ @hooks.on_runtime_ready()
5
+ def publish_provider_assignment():
6
+ """
7
+ Publishes the service provider assignment to the analytics service.
8
+ """
9
+
10
+ from localstack.config import SERVICE_PROVIDER_CONFIG
11
+ from localstack.services.plugins import SERVICE_PLUGINS
12
+ from localstack.utils.analytics import log
13
+
14
+ provider_assignment = {
15
+ service: f"localstack.aws.provider/{service}:{SERVICE_PROVIDER_CONFIG[service]}"
16
+ for service in SERVICE_PLUGINS.list_available()
17
+ }
18
+
19
+ log.event("ls_service_provider_assignment", provider_assignment)
@@ -2,7 +2,7 @@ import datetime
2
2
  import logging
3
3
  import threading
4
4
  from collections import Counter
5
- from typing import NamedTuple, Optional
5
+ from typing import NamedTuple
6
6
 
7
7
  from localstack import config
8
8
  from localstack.runtime.shutdown import SHUTDOWN_HANDLERS
@@ -20,7 +20,7 @@ class ServiceRequestInfo(NamedTuple):
20
20
  service: str
21
21
  operation: str
22
22
  status_code: int
23
- err_type: Optional[str] = None
23
+ err_type: str | None = None
24
24
 
25
25
 
26
26
  class ServiceRequestAggregator:
@@ -8,7 +8,7 @@ import tempfile
8
8
  import time
9
9
  import zipfile
10
10
  from subprocess import Popen
11
- from typing import IO, Literal, Optional, Union
11
+ from typing import IO, Literal
12
12
 
13
13
  from localstack.constants import MAVEN_REPO_URL
14
14
  from localstack.utils.files import load_file, mkdir, new_tmp_file, rm_rf, save_file
@@ -22,7 +22,7 @@ from .strings import truncate
22
22
  LOG = logging.getLogger(__name__)
23
23
 
24
24
 
25
- StrPath = Union[str, os.PathLike]
25
+ StrPath = str | os.PathLike
26
26
 
27
27
 
28
28
  def is_zip_file(content):
@@ -30,13 +30,13 @@ def is_zip_file(content):
30
30
  return zipfile.is_zipfile(stream)
31
31
 
32
32
 
33
- def get_unzipped_size(zip_file: Union[str, IO[bytes]]):
33
+ def get_unzipped_size(zip_file: str | IO[bytes]):
34
34
  """Returns the size of the unzipped file."""
35
35
  with zipfile.ZipFile(zip_file, "r") as zip_ref:
36
36
  return sum(f.file_size for f in zip_ref.infolist())
37
37
 
38
38
 
39
- def unzip(path: str, target_dir: str, overwrite: bool = True) -> Optional[Union[str, Popen]]:
39
+ def unzip(path: str, target_dir: str, overwrite: bool = True) -> str | Popen | None:
40
40
  from localstack.utils.platform import is_debian
41
41
 
42
42
  use_native_cmd = is_debian() or is_command_available("unzip")
@@ -99,7 +99,7 @@ def create_zip_file_python(
99
99
  base_dir: StrPath,
100
100
  zip_file: StrPath,
101
101
  mode: Literal["r", "w", "x", "a"] = "w",
102
- content_root: Optional[str] = None,
102
+ content_root: str | None = None,
103
103
  ):
104
104
  with zipfile.ZipFile(zip_file, mode) as zip_file:
105
105
  for root, dirs, files in os.walk(base_dir):
@@ -122,7 +122,7 @@ def add_file_to_jar(class_file, class_url, target_jar, base_dir=None):
122
122
 
123
123
 
124
124
  def update_jar_manifest(
125
- jar_file_name: str, parent_dir: str, search: Union[str, re.Pattern], replace: str
125
+ jar_file_name: str, parent_dir: str, search: str | re.Pattern, replace: str
126
126
  ):
127
127
  manifest_file_path = "META-INF/MANIFEST.MF"
128
128
  jar_path = os.path.join(parent_dir, jar_file_name)
@@ -174,10 +174,10 @@ def upgrade_jar_file(base_dir: str, file_glob: str, maven_asset: str):
174
174
  def download_and_extract(
175
175
  archive_url: str,
176
176
  target_dir: str,
177
- retries: Optional[int] = 0,
178
- sleep: Optional[int] = 3,
179
- tmp_archive: Optional[str] = None,
180
- checksum_url: Optional[str] = None,
177
+ retries: int | None = 0,
178
+ sleep: int | None = 3,
179
+ tmp_archive: str | None = None,
180
+ checksum_url: str | None = None,
181
181
  ) -> None:
182
182
  """
183
183
  Download and extract an archive to a target directory with optional checksum verification.
@@ -250,7 +250,7 @@ def download_and_extract_with_retry(
250
250
  archive_url,
251
251
  tmp_archive,
252
252
  target_dir,
253
- checksum_url: Optional[str] = None,
253
+ checksum_url: str | None = None,
254
254
  ):
255
255
  try:
256
256
  download_and_extract(
@@ -40,12 +40,12 @@ class AdaptiveThreadPool(DaemonAwareThreadPool):
40
40
 
41
41
  def __init__(self, core_size=None):
42
42
  self.core_size = core_size or self.DEFAULT_CORE_POOL_SIZE
43
- super(AdaptiveThreadPool, self).__init__(max_workers=self.core_size)
43
+ super().__init__(max_workers=self.core_size)
44
44
 
45
45
  def submit(self, fn, *args, **kwargs):
46
46
  # if idle threads are available, don't spin new threads
47
47
  if self.has_idle_threads():
48
- return super(AdaptiveThreadPool, self).submit(fn, *args, **kwargs)
48
+ return super().submit(fn, *args, **kwargs)
49
49
 
50
50
  def _run(*tmpargs):
51
51
  return fn(*args, **kwargs)
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  import re
3
3
  from functools import cache
4
- from typing import Optional, TypedDict
4
+ from typing import TypedDict
5
5
 
6
6
  from botocore.utils import ArnParser, InvalidArnException
7
7
 
@@ -27,7 +27,7 @@ PARTITION_NAMES = list(REGION_PREFIX_TO_PARTITION.values()) + [DEFAULT_PARTITION
27
27
  ARN_PARTITION_REGEX = r"^arn:(" + "|".join(sorted(PARTITION_NAMES)) + ")"
28
28
 
29
29
 
30
- def get_partition(region: Optional[str]) -> str:
30
+ def get_partition(region: str | None) -> str:
31
31
  if not region:
32
32
  return DEFAULT_PARTITION
33
33
  if region in PARTITION_NAMES:
@@ -65,28 +65,28 @@ def parse_arn(arn: str) -> ArnData:
65
65
  return _arn_parser.parse_arn(arn)
66
66
 
67
67
 
68
- def extract_account_id_from_arn(arn: str) -> Optional[str]:
68
+ def extract_account_id_from_arn(arn: str) -> str | None:
69
69
  try:
70
70
  return parse_arn(arn).get("account")
71
71
  except InvalidArnException:
72
72
  return None
73
73
 
74
74
 
75
- def extract_region_from_arn(arn: str) -> Optional[str]:
75
+ def extract_region_from_arn(arn: str) -> str | None:
76
76
  try:
77
77
  return parse_arn(arn).get("region")
78
78
  except InvalidArnException:
79
79
  return None
80
80
 
81
81
 
82
- def extract_service_from_arn(arn: str) -> Optional[str]:
82
+ def extract_service_from_arn(arn: str) -> str | None:
83
83
  try:
84
84
  return parse_arn(arn).get("service")
85
85
  except InvalidArnException:
86
86
  return None
87
87
 
88
88
 
89
- def extract_resource_from_arn(arn: str) -> Optional[str]:
89
+ def extract_resource_from_arn(arn: str) -> str | None:
90
90
  try:
91
91
  return parse_arn(arn).get("resource")
92
92
  except InvalidArnException:
@@ -98,8 +98,10 @@ def extract_resource_from_arn(arn: str) -> Optional[str]:
98
98
  #
99
99
 
100
100
 
101
- def _resource_arn(name: str, pattern: str, account_id: str, region_name: str) -> str:
102
- if ":" in name:
101
+ def _resource_arn(
102
+ name: str, pattern: str, account_id: str, region_name: str, allow_colons=False
103
+ ) -> str:
104
+ if ":" in name and not allow_colons:
103
105
  return name
104
106
  if len(pattern.split("%s")) == 4:
105
107
  return pattern % (get_partition(region_name), account_id, name)
@@ -120,7 +122,7 @@ def iam_role_arn(role_name: str, account_id: str, region_name: str) -> str:
120
122
  return role_name
121
123
  if re.match(f"{ARN_PARTITION_REGEX}:iam::", role_name):
122
124
  return role_name
123
- return "arn:%s:iam::%s:role/%s" % (get_partition(region_name), account_id, role_name)
125
+ return f"arn:{get_partition(region_name)}:iam::{account_id}:role/{role_name}"
124
126
 
125
127
 
126
128
  def iam_resource_arn(resource: str, account_id: str, role: str = None) -> str:
@@ -180,13 +182,7 @@ def dynamodb_table_arn(table_name: str, account_id: str, region_name: str) -> st
180
182
  def dynamodb_stream_arn(
181
183
  table_name: str, latest_stream_label: str, account_id: str, region_name: str
182
184
  ) -> str:
183
- return "arn:%s:dynamodb:%s:%s:table/%s/stream/%s" % (
184
- get_partition(region_name),
185
- region_name,
186
- account_id,
187
- table_name,
188
- latest_stream_label,
189
- )
185
+ return f"arn:{get_partition(region_name)}:dynamodb:{region_name}:{account_id}:table/{table_name}/stream/{latest_stream_label}"
190
186
 
191
187
 
192
188
  #
@@ -291,7 +287,7 @@ def lambda_event_source_mapping_arn(uuid: str, account_id: str, region_name: str
291
287
  def lambda_function_or_layer_arn(
292
288
  type: str,
293
289
  entity_name: str,
294
- version: Optional[str],
290
+ version: str | None,
295
291
  account_id: str,
296
292
  region_name: str,
297
293
  ) -> str:
@@ -453,7 +449,7 @@ def s3_bucket_arn(bucket_name_or_arn: str, region="us-east-1") -> str:
453
449
 
454
450
  def sqs_queue_arn(queue_name: str, account_id: str, region_name: str) -> str:
455
451
  queue_name = queue_name.split("/")[-1]
456
- return "arn:%s:sqs:%s:%s:%s" % (get_partition(region_name), region_name, account_id, queue_name)
452
+ return f"arn:{get_partition(region_name)}:sqs:{region_name}:{account_id}:{queue_name}"
457
453
 
458
454
 
459
455
  #
@@ -462,20 +458,13 @@ def sqs_queue_arn(queue_name: str, account_id: str, region_name: str) -> str:
462
458
 
463
459
 
464
460
  def apigateway_restapi_arn(api_id: str, account_id: str, region_name: str) -> str:
465
- return "arn:%s:apigateway:%s:%s:/restapis/%s" % (
466
- get_partition(region_name),
467
- region_name,
468
- account_id,
469
- api_id,
461
+ return (
462
+ f"arn:{get_partition(region_name)}:apigateway:{region_name}:{account_id}:/restapis/{api_id}"
470
463
  )
471
464
 
472
465
 
473
466
  def apigateway_invocations_arn(lambda_uri: str, region_name: str) -> str:
474
- return "arn:%s:apigateway:%s:lambda:path/2015-03-31/functions/%s/invocations" % (
475
- get_partition(region_name),
476
- region_name,
477
- lambda_uri,
478
- )
467
+ return f"arn:{get_partition(region_name)}:apigateway:{region_name}:lambda:path/2015-03-31/functions/{lambda_uri}/invocations"
479
468
 
480
469
 
481
470
  #
@@ -487,6 +476,12 @@ def sns_topic_arn(topic_name: str, account_id: str, region_name: str) -> str:
487
476
  return f"arn:{get_partition(region_name)}:sns:{region_name}:{account_id}:{topic_name}"
488
477
 
489
478
 
479
+ def sns_platform_application_arn(
480
+ platform_application_name: str, platform: str, account_id: str, region_name: str
481
+ ) -> str:
482
+ return f"arn:{get_partition(region_name)}:sns:{region_name}:{account_id}:app/{platform}/{platform_application_name}"
483
+
484
+
490
485
  #
491
486
  # ECR
492
487
  #
@@ -555,7 +550,7 @@ def lambda_function_name(name_or_arn: str) -> str:
555
550
  if ":" in name_or_arn:
556
551
  arn = parse_arn(name_or_arn)
557
552
  if arn["service"] != "lambda":
558
- raise ValueError("arn is not a lambda arn %s" % name_or_arn)
553
+ raise ValueError(f"arn is not a lambda arn {name_or_arn}")
559
554
 
560
555
  return parse_arn(name_or_arn)["resource"].split(":")[1]
561
556
  else:
@@ -3,7 +3,7 @@ import datetime
3
3
  import json
4
4
  import re
5
5
  from binascii import crc32
6
- from typing import Any, Optional, Union
6
+ from typing import Any
7
7
  from urllib.parse import parse_qs
8
8
 
9
9
  import xmltodict
@@ -36,13 +36,13 @@ def requests_error_response_json(message, code=500, error_type="InternalFailure"
36
36
 
37
37
  def requests_error_response_xml(
38
38
  message: str,
39
- code: Optional[int] = 400,
40
- code_string: Optional[str] = "InvalidParameter",
41
- service: Optional[str] = None,
42
- xmlns: Optional[str] = None,
39
+ code: int | None = 400,
40
+ code_string: str | None = "InvalidParameter",
41
+ service: str | None = None,
42
+ xmlns: str | None = None,
43
43
  ):
44
44
  response = RequestsResponse()
45
- xmlns = xmlns or "http://%s.amazonaws.com/doc/2010-03-31/" % service
45
+ xmlns = xmlns or f"http://{service}.amazonaws.com/doc/2010-03-31/"
46
46
  response._content = f"""<ErrorResponse xmlns="{xmlns}"><Error>
47
47
  <Type>Sender</Type>
48
48
  <Code>{code_string}</Code>
@@ -100,7 +100,7 @@ def requests_error_response_xml_signature_calculation(
100
100
 
101
101
  def requests_error_response(
102
102
  req_headers: dict,
103
- message: Union[str, bytes],
103
+ message: str | bytes,
104
104
  code: int = 500,
105
105
  error_type: str = "InternalFailure",
106
106
  service: str = None,
@@ -201,7 +201,7 @@ def parse_query_string(url_or_qs: str, multi_values=False) -> dict[str, str]:
201
201
  return result
202
202
 
203
203
 
204
- def calculate_crc32(content: Union[str, bytes]) -> int:
204
+ def calculate_crc32(content: str | bytes) -> int:
205
205
  return crc32(to_bytes(content)) & 0xFFFFFFFF
206
206
 
207
207
 
@@ -2,7 +2,6 @@ import logging
2
2
  import re
3
3
  import socket
4
4
  from functools import lru_cache
5
- from typing import Union
6
5
 
7
6
  import boto3
8
7
 
@@ -46,7 +45,7 @@ def get_boto3_region() -> str:
46
45
  return boto3.session.Session().region_name
47
46
 
48
47
 
49
- def get_local_service_url(service_name_or_port: Union[str, int]) -> str:
48
+ def get_local_service_url(service_name_or_port: str | int) -> str:
50
49
  """Return the local service URL for the given service name or port."""
51
50
  # TODO(srw): we don't need to differentiate on service name any more, so remove the argument
52
51
  if isinstance(service_name_or_port, int):
@@ -68,7 +67,7 @@ def get_s3_hostname():
68
67
 
69
68
 
70
69
  def fix_account_id_in_arns(
71
- response, replacement: str, colon_delimiter: str = ":", existing: Union[str, list[str]] = None
70
+ response, replacement: str, colon_delimiter: str = ":", existing: str | list[str] = None
72
71
  ):
73
72
  """Fix the account ID in the ARNs returned in the given Flask response or string"""
74
73
  from moto.core import DEFAULT_ACCOUNT_ID