localstack-core 4.7.1.dev139__py3-none-any.whl → 4.10.1.dev7__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 (173) hide show
  1. localstack/aws/api/cloudformation/__init__.py +1 -0
  2. localstack/aws/api/cloudwatch/__init__.py +41 -1
  3. localstack/aws/api/config/__init__.py +4 -0
  4. localstack/aws/api/core.py +4 -0
  5. localstack/aws/api/ec2/__init__.py +1113 -56
  6. localstack/aws/api/iam/__init__.py +7 -0
  7. localstack/aws/api/kinesis/__init__.py +19 -0
  8. localstack/aws/api/kms/__init__.py +6 -0
  9. localstack/aws/api/lambda_/__init__.py +13 -0
  10. localstack/aws/api/logs/__init__.py +15 -0
  11. localstack/aws/api/redshift/__init__.py +9 -3
  12. localstack/aws/api/route53/__init__.py +2 -0
  13. localstack/aws/api/s3/__init__.py +12 -0
  14. localstack/aws/api/s3control/__init__.py +32 -0
  15. localstack/aws/api/ssm/__init__.py +2 -0
  16. localstack/aws/client.py +7 -2
  17. localstack/aws/forwarder.py +52 -5
  18. localstack/aws/handlers/analytics.py +1 -1
  19. localstack/aws/handlers/logging.py +12 -2
  20. localstack/aws/handlers/metric_handler.py +41 -1
  21. localstack/aws/handlers/service.py +32 -9
  22. localstack/aws/protocol/parser.py +440 -21
  23. localstack/aws/protocol/serializer.py +684 -64
  24. localstack/aws/protocol/service_router.py +120 -20
  25. localstack/aws/skeleton.py +4 -2
  26. localstack/aws/spec-patches.json +58 -0
  27. localstack/aws/spec.py +33 -13
  28. localstack/cli/exceptions.py +1 -1
  29. localstack/cli/localstack.py +4 -4
  30. localstack/cli/lpm.py +3 -4
  31. localstack/cli/profiles.py +1 -2
  32. localstack/config.py +18 -12
  33. localstack/constants.py +4 -29
  34. localstack/dev/kubernetes/__main__.py +1 -1
  35. localstack/dev/run/paths.py +1 -1
  36. localstack/dns/plugins.py +5 -1
  37. localstack/dns/server.py +12 -3
  38. localstack/packages/api.py +9 -8
  39. localstack/packages/core.py +2 -2
  40. localstack/packages/plugins.py +0 -8
  41. localstack/runtime/init.py +1 -1
  42. localstack/services/apigateway/legacy/provider.py +53 -3
  43. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  44. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  45. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +50 -6
  46. localstack/services/apigateway/next_gen/provider.py +5 -0
  47. localstack/services/cloudformation/engine/entities.py +12 -1
  48. localstack/services/cloudformation/engine/v2/change_set_model.py +0 -3
  49. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +14 -0
  50. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +13 -15
  51. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +118 -24
  52. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +4 -1
  53. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +5 -14
  54. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  55. localstack/services/cloudformation/engine/v2/resolving.py +6 -4
  56. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  57. localstack/services/cloudformation/resource_provider.py +5 -1
  58. localstack/services/cloudformation/resources.py +24149 -0
  59. localstack/services/cloudformation/v2/entities.py +6 -3
  60. localstack/services/cloudformation/v2/provider.py +172 -27
  61. localstack/services/cloudformation/v2/types.py +8 -4
  62. localstack/services/cloudwatch/provider_v2.py +25 -28
  63. localstack/services/dynamodb/packages.py +2 -1
  64. localstack/services/dynamodb/provider.py +42 -0
  65. localstack/services/dynamodb/v2/provider.py +42 -0
  66. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  67. localstack/services/es/provider.py +2 -2
  68. localstack/services/events/event_rule_engine.py +31 -13
  69. localstack/services/events/models.py +4 -5
  70. localstack/services/events/target.py +17 -9
  71. localstack/services/iam/provider.py +11 -116
  72. localstack/services/iam/resources/policy_simulator.py +133 -0
  73. localstack/services/kinesis/models.py +15 -2
  74. localstack/services/kinesis/provider.py +77 -0
  75. localstack/services/kms/provider.py +14 -5
  76. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  77. localstack/services/lambda_/packages.py +1 -1
  78. localstack/services/logs/provider.py +1 -1
  79. localstack/services/moto.py +2 -1
  80. localstack/services/opensearch/cluster.py +15 -7
  81. localstack/services/opensearch/packages.py +26 -7
  82. localstack/services/opensearch/provider.py +6 -1
  83. localstack/services/opensearch/versions.py +56 -7
  84. localstack/services/s3/constants.py +5 -2
  85. localstack/services/s3/cors.py +4 -4
  86. localstack/services/s3/notifications.py +1 -1
  87. localstack/services/s3/presigned_url.py +27 -43
  88. localstack/services/s3/provider.py +67 -11
  89. localstack/services/s3/utils.py +42 -11
  90. localstack/services/ses/provider.py +16 -7
  91. localstack/services/sns/constants.py +7 -1
  92. localstack/services/sns/v2/models.py +167 -0
  93. localstack/services/sns/v2/provider.py +860 -2
  94. localstack/services/sns/v2/utils.py +130 -0
  95. localstack/services/sqs/developer_api.py +205 -0
  96. localstack/services/sqs/models.py +42 -3
  97. localstack/services/sqs/provider.py +8 -309
  98. localstack/services/sqs/query_api.py +1 -1
  99. localstack/services/sqs/utils.py +121 -2
  100. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  101. localstack/testing/aws/cloudformation_utils.py +1 -1
  102. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  103. localstack/testing/pytest/container.py +4 -5
  104. localstack/testing/pytest/fixtures.py +20 -19
  105. localstack/testing/pytest/in_memory_localstack.py +0 -4
  106. localstack/testing/pytest/marking.py +13 -4
  107. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  108. localstack/testing/pytest/util.py +1 -1
  109. localstack/testing/pytest/validation_tracking.py +1 -2
  110. localstack/testing/snapshots/transformer_utility.py +5 -0
  111. localstack/utils/analytics/events.py +2 -2
  112. localstack/utils/analytics/metadata.py +1 -2
  113. localstack/utils/analytics/metrics/counter.py +6 -8
  114. localstack/utils/analytics/publisher.py +1 -2
  115. localstack/utils/analytics/service_request_aggregator.py +2 -2
  116. localstack/utils/archives.py +11 -11
  117. localstack/utils/aws/arns.py +17 -9
  118. localstack/utils/aws/aws_responses.py +7 -7
  119. localstack/utils/aws/aws_stack.py +2 -3
  120. localstack/utils/aws/message_forwarding.py +1 -2
  121. localstack/utils/aws/request_context.py +4 -5
  122. localstack/utils/batch_policy.py +3 -3
  123. localstack/utils/bootstrap.py +7 -7
  124. localstack/utils/catalog/catalog.py +139 -0
  125. localstack/utils/catalog/catalog_loader.py +11 -0
  126. localstack/utils/catalog/common.py +58 -0
  127. localstack/utils/catalog/plugins.py +28 -0
  128. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  129. localstack/utils/collections.py +7 -8
  130. localstack/utils/config_listener.py +1 -1
  131. localstack/utils/container_networking.py +2 -3
  132. localstack/utils/container_utils/container_client.py +115 -131
  133. localstack/utils/container_utils/docker_cmd_client.py +42 -42
  134. localstack/utils/container_utils/docker_sdk_client.py +63 -62
  135. localstack/utils/diagnose.py +2 -3
  136. localstack/utils/docker_utils.py +3 -4
  137. localstack/utils/files.py +31 -7
  138. localstack/utils/functions.py +3 -2
  139. localstack/utils/http.py +4 -5
  140. localstack/utils/json.py +19 -5
  141. localstack/utils/kinesis/kinesis_connector.py +2 -1
  142. localstack/utils/net.py +6 -6
  143. localstack/utils/no_exit_argument_parser.py +2 -2
  144. localstack/utils/numbers.py +9 -2
  145. localstack/utils/objects.py +6 -5
  146. localstack/utils/patch.py +2 -1
  147. localstack/utils/run.py +10 -9
  148. localstack/utils/scheduler.py +11 -11
  149. localstack/utils/server/tcp_proxy.py +2 -2
  150. localstack/utils/serving.py +2 -3
  151. localstack/utils/strings.py +10 -11
  152. localstack/utils/sync.py +126 -1
  153. localstack/utils/tagging.py +1 -4
  154. localstack/utils/testutil.py +5 -4
  155. localstack/utils/threads.py +2 -2
  156. localstack/utils/time.py +11 -3
  157. localstack/utils/urls.py +1 -3
  158. localstack/version.py +2 -2
  159. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/METADATA +17 -12
  160. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/RECORD +168 -164
  161. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/entry_points.txt +4 -2
  162. localstack_core-4.10.1.dev7.dist-info/plux.json +1 -0
  163. localstack/packages/terraform.py +0 -46
  164. localstack/services/cloudformation/deploy.html +0 -144
  165. localstack/services/cloudformation/deploy_ui.py +0 -47
  166. localstack/services/cloudformation/plugins.py +0 -12
  167. localstack_core-4.7.1.dev139.dist-info/plux.json +0 -1
  168. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack +0 -0
  169. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack-supervisor +0 -0
  170. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev7.data}/scripts/localstack.bat +0 -0
  171. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/WHEEL +0 -0
  172. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/licenses/LICENSE.txt +0 -0
  173. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev7.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,8 @@ import os
6
6
  import re
7
7
  import textwrap
8
8
  import time
9
- from typing import TYPE_CHECKING, Any, Callable, Optional, Unpack
9
+ from collections.abc import Callable
10
+ from typing import TYPE_CHECKING, Any, Unpack
10
11
 
11
12
  import botocore.auth
12
13
  import botocore.config
@@ -357,8 +358,8 @@ def sqs_create_queue(aws_client):
357
358
  def sqs_receive_messages_delete(aws_client):
358
359
  def factory(
359
360
  queue_url: str,
360
- expected_messages: Optional[int] = None,
361
- wait_time: Optional[int] = 5,
361
+ expected_messages: int | None = None,
362
+ wait_time: int | None = 5,
362
363
  ):
363
364
  response = aws_client.sqs.receive_message(
364
365
  QueueUrl=queue_url,
@@ -706,7 +707,7 @@ def route53_hosted_zone(aws_client):
706
707
  def transcribe_create_job(s3_bucket, aws_client):
707
708
  job_names = []
708
709
 
709
- def _create_job(audio_file: str, params: Optional[dict[str, Any]] = None) -> str:
710
+ def _create_job(audio_file: str, params: dict[str, Any] | None = None) -> str:
710
711
  s3_key = "test-clip.wav"
711
712
 
712
713
  if not params:
@@ -1085,18 +1086,18 @@ def deploy_cfn_template(
1085
1086
 
1086
1087
  def _deploy(
1087
1088
  *,
1088
- is_update: Optional[bool] = False,
1089
- stack_name: Optional[str] = None,
1090
- change_set_name: Optional[str] = None,
1091
- template: Optional[str] = None,
1092
- template_path: Optional[str | os.PathLike] = None,
1093
- template_mapping: Optional[dict[str, Any]] = None,
1094
- parameters: Optional[dict[str, str]] = None,
1095
- role_arn: Optional[str] = None,
1096
- max_wait: Optional[int] = None,
1097
- delay_between_polls: Optional[int] = 2,
1098
- custom_aws_client: Optional[ServiceLevelClientFactory] = None,
1099
- raw_parameters: Optional[list[Parameter]] = None,
1089
+ is_update: bool | None = False,
1090
+ stack_name: str | None = None,
1091
+ change_set_name: str | None = None,
1092
+ template: str | None = None,
1093
+ template_path: str | os.PathLike | None = None,
1094
+ template_mapping: dict[str, Any] | None = None,
1095
+ parameters: dict[str, str] | None = None,
1096
+ role_arn: str | None = None,
1097
+ max_wait: int | None = None,
1098
+ delay_between_polls: int | None = 2,
1099
+ custom_aws_client: ServiceLevelClientFactory | None = None,
1100
+ raw_parameters: list[Parameter] | None = None,
1100
1101
  ) -> DeployResult:
1101
1102
  if is_update:
1102
1103
  assert stack_name
@@ -1262,7 +1263,7 @@ def _has_stack_status(cfn_client, statuses: list[str]):
1262
1263
 
1263
1264
  @pytest.fixture
1264
1265
  def is_change_set_finished(aws_client):
1265
- def _is_change_set_finished(change_set_id: str, stack_name: Optional[str] = None):
1266
+ def _is_change_set_finished(change_set_id: str, stack_name: str | None = None):
1266
1267
  def _inner():
1267
1268
  kwargs = {"ChangeSetName": change_set_id}
1268
1269
  if stack_name:
@@ -1993,7 +1994,7 @@ def setup_sender_email_address(ses_verify_identity):
1993
1994
  email address and verify them.
1994
1995
  """
1995
1996
 
1996
- def inner(sender_email_address: Optional[str] = None) -> str:
1997
+ def inner(sender_email_address: str | None = None) -> str:
1997
1998
  if is_aws_cloud():
1998
1999
  if sender_email_address is None:
1999
2000
  raise ValueError(
@@ -2252,7 +2253,7 @@ def assert_host_customisation(monkeypatch):
2252
2253
  def asserter(
2253
2254
  url: str,
2254
2255
  *,
2255
- custom_host: Optional[str] = None,
2256
+ custom_host: str | None = None,
2256
2257
  ):
2257
2258
  if custom_host is not None:
2258
2259
  assert custom_host in url, f"Could not find `{custom_host}` in `{url}`"
@@ -63,8 +63,6 @@ def pytest_runtestloop(session: Session):
63
63
  return
64
64
  LOG.info("TEST_FORCE_LOCALSTACK_START is set, a Localstack instance will be created.")
65
65
 
66
- from localstack.utils.common import safe_requests
67
-
68
66
  if is_aws_cloud():
69
67
  localstack_config.DEFAULT_DELAY = 5
70
68
  localstack_config.DEFAULT_MAX_ATTEMPTS = 60
@@ -73,8 +71,6 @@ def pytest_runtestloop(session: Session):
73
71
  os.environ[ENV_INTERNAL_TEST_RUN] = "1"
74
72
  localstack_config.INCLUDE_STACK_TRACES_IN_HTTP_RESPONSE = True
75
73
 
76
- safe_requests.verify_ssl = False
77
-
78
74
  from localstack.runtime import current
79
75
 
80
76
  _started.set()
@@ -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:
@@ -75,6 +76,10 @@ class Markers:
75
76
  """The test requires docker or a compatible container engine - will not work on kubernetes"""
76
77
  lambda_runtime_update = pytest.mark.lambda_runtime_update
77
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"""
78
83
 
79
84
 
80
85
  # pytest plugin
@@ -226,3 +231,7 @@ def pytest_configure(config):
226
231
  "markers",
227
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.",
228
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:
@@ -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
@@ -201,7 +200,7 @@ def _generate_machine_id() -> str:
201
200
  return f"gen_{long_uid()[:12]}"
202
201
 
203
202
 
204
- def get_api_key_or_auth_token() -> Optional[str]:
203
+ def get_api_key_or_auth_token() -> str | None:
205
204
  # TODO: this is duplicated code from ext, but should probably migrate that to localstack
206
205
  auth_token = os.environ.get("LOCALSTACK_AUTH_TOKEN", "").strip("'\" ")
207
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 = {
@@ -140,10 +140,8 @@ 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
147
  super().__init__(namespace=namespace, name=name, schema_version=schema_version)
@@ -162,7 +160,7 @@ class LabeledCounter(Metric):
162
160
  self._counters_by_label_values = defaultdict(ThreadSafeCounter)
163
161
  MetricRegistry().register(self)
164
162
 
165
- def labels(self, **kwargs: Union[str, float, None]) -> ThreadSafeCounter:
163
+ def labels(self, **kwargs: str | float | None) -> ThreadSafeCounter:
166
164
  """
167
165
  Create a scoped counter instance with specific label values.
168
166
 
@@ -198,7 +196,7 @@ class LabeledCounter(Metric):
198
196
  )
199
197
 
200
198
  # Create labels dictionary
201
- labels_dict = dict(zip(self._labels, label_values))
199
+ labels_dict = dict(zip(self._labels, label_values, strict=False))
202
200
 
203
201
  payload.append(
204
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
 
@@ -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(
@@ -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)
@@ -285,7 +287,7 @@ def lambda_event_source_mapping_arn(uuid: str, account_id: str, region_name: str
285
287
  def lambda_function_or_layer_arn(
286
288
  type: str,
287
289
  entity_name: str,
288
- version: Optional[str],
290
+ version: str | None,
289
291
  account_id: str,
290
292
  region_name: str,
291
293
  ) -> str:
@@ -474,6 +476,12 @@ def sns_topic_arn(topic_name: str, account_id: str, region_name: str) -> str:
474
476
  return f"arn:{get_partition(region_name)}:sns:{region_name}:{account_id}:{topic_name}"
475
477
 
476
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
+
477
485
  #
478
486
  # ECR
479
487
  #
@@ -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,10 +36,10 @@ 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
45
  xmlns = xmlns or f"http://{service}.amazonaws.com/doc/2010-03-31/"
@@ -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
@@ -3,7 +3,6 @@ import json
3
3
  import logging
4
4
  import re
5
5
  import uuid
6
- from typing import Optional
7
6
 
8
7
  from moto.events.models import events_backends
9
8
 
@@ -214,7 +213,7 @@ def list_of_parameters_to_object(items):
214
213
  return {item.get("Key"): item.get("Value") for item in items}
215
214
 
216
215
 
217
- def send_event_to_api_destination(target_arn, event, http_parameters: Optional[dict] = None):
216
+ def send_event_to_api_destination(target_arn, event, http_parameters: dict | None = None):
218
217
  """Send an event to an EventBridge API destination
219
218
  See https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-api-destinations.html"""
220
219
 
@@ -4,7 +4,6 @@ This module has utilities relating to creating/parsing AWS requests.
4
4
 
5
5
  import logging
6
6
  import re
7
- from typing import Optional
8
7
 
9
8
  from rolo import Request as RoloRequest
10
9
 
@@ -30,7 +29,7 @@ def get_account_id_from_request(request: RoloRequest) -> str:
30
29
  return get_account_id_from_access_key_id(access_key_id)
31
30
 
32
31
 
33
- def extract_region_from_auth_header(headers) -> Optional[str]:
32
+ def extract_region_from_auth_header(headers) -> str | None:
34
33
  auth = headers.get("Authorization") or ""
35
34
  region = re.sub(r".*Credential=[^/]+/[^/]+/([^/]+)/.*", r"\1", auth)
36
35
  if region == auth:
@@ -38,12 +37,12 @@ def extract_region_from_auth_header(headers) -> Optional[str]:
38
37
  return region
39
38
 
40
39
 
41
- def extract_account_id_from_auth_header(headers) -> Optional[str]:
40
+ def extract_account_id_from_auth_header(headers) -> str | None:
42
41
  if access_key_id := extract_access_key_id_from_auth_header(headers):
43
42
  return get_account_id_from_access_key_id(access_key_id)
44
43
 
45
44
 
46
- def extract_access_key_id_from_auth_header(headers: dict[str, str]) -> Optional[str]:
45
+ def extract_access_key_id_from_auth_header(headers: dict[str, str]) -> str | None:
47
46
  auth = headers.get("Authorization") or ""
48
47
 
49
48
  if auth.startswith("AWS4-"):
@@ -67,7 +66,7 @@ def extract_region_from_headers(headers) -> str:
67
66
  return extract_region_from_auth_header(headers) or AWS_REGION_US_EAST_1
68
67
 
69
68
 
70
- def extract_service_name_from_auth_header(headers: dict) -> Optional[str]:
69
+ def extract_service_name_from_auth_header(headers: dict) -> str | None:
71
70
  try:
72
71
  auth_header = headers.get("authorization", "")
73
72
  credential_scope = auth_header.split(",")[0].split()[1]
@@ -1,6 +1,6 @@
1
1
  import copy
2
2
  import time
3
- from typing import Generic, Optional, TypeVar, overload
3
+ from typing import Generic, TypeVar, overload
4
4
 
5
5
  from pydantic import Field
6
6
  from pydantic.dataclasses import dataclass
@@ -47,8 +47,8 @@ class Batcher(Generic[T]):
47
47
  assert batcher.flush() == ["item1", "item2", "item3", "item4"]
48
48
  """
49
49
 
50
- max_count: Optional[int] = Field(default=None, description="Maximum number of items", ge=0)
51
- max_window: Optional[float] = Field(
50
+ max_count: int | None = Field(default=None, description="Maximum number of items", ge=0)
51
+ max_window: float | None = Field(
52
52
  default=None, description="Maximum time window in seconds", ge=0
53
53
  )
54
54