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
@@ -59,11 +59,7 @@ def _send_to_dead_letter_queue(source_arn: str, dlq_arn: str, event: dict, error
59
59
  except Exception as e:
60
60
  error = e
61
61
  if error or not result_code or result_code >= 400:
62
- msg = "Unable to send message to dead letter queue %s (code %s): %s" % (
63
- queue_url,
64
- result_code,
65
- error,
66
- )
62
+ msg = f"Unable to send message to dead letter queue {queue_url} (code {result_code}): {error}"
67
63
  if "InvalidMessageContents" in str(error):
68
64
  msg += f" - messages: {messages}"
69
65
  LOG.info(msg)
@@ -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]
@@ -86,7 +86,7 @@ def create_api_gateway(
86
86
  resources = resources or []
87
87
  stage_name = stage_name or "testing"
88
88
  usage_plan_name = usage_plan_name or "Basic Usage"
89
- description = description or 'Test description for API "%s"' % name
89
+ description = description or f'Test description for API "{name}"'
90
90
 
91
91
  LOG.info('Creating API resources under API Gateway "%s".', name)
92
92
  api = client.create_rest_api(name=name, description=description)
@@ -66,7 +66,7 @@ class VtlTemplate:
66
66
  empty_placeholder = " __pLaCe-HoLdEr__ "
67
67
  template = re.sub(
68
68
  r"([^\s]+)#\$({)?(.*)",
69
- r"\1#%s$\2\3" % empty_placeholder,
69
+ rf"\1#{empty_placeholder}$\2\3",
70
70
  template,
71
71
  count=re.MULTILINE,
72
72
  )
@@ -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
 
@@ -9,9 +9,9 @@ import shlex
9
9
  import signal
10
10
  import threading
11
11
  import time
12
- from collections.abc import Iterable
12
+ from collections.abc import Callable, Iterable
13
13
  from functools import wraps
14
- from typing import Any, Callable, Optional, Union
14
+ from typing import Any
15
15
 
16
16
  from localstack import config, constants
17
17
  from localstack.config import (
@@ -175,7 +175,7 @@ def get_docker_image_details(image_name: str = None) -> dict[str, str]:
175
175
  return result
176
176
 
177
177
 
178
- def get_image_environment_variable(env_name: str) -> Optional[str]:
178
+ def get_image_environment_variable(env_name: str) -> str | None:
179
179
  image_name = get_docker_image_to_start()
180
180
  image_info = DOCKER_CLIENT.inspect_image(image_name)
181
181
  image_envs = image_info["Config"]["Env"]
@@ -427,7 +427,7 @@ def validate_localstack_config(name: str):
427
427
 
428
428
  def port_exposed(port):
429
429
  for exposed in docker_ports:
430
- if re.match(r"^([0-9]+-)?%s(-[0-9]+)?$" % port, exposed):
430
+ if re.match(rf"^([0-9]+-)?{port}(-[0-9]+)?$", exposed):
431
431
  return True
432
432
 
433
433
  if not port_exposed(edge_port):
@@ -455,7 +455,7 @@ def get_docker_image_to_start():
455
455
 
456
456
  def extract_port_flags(user_flags, port_mappings: PortMappings):
457
457
  regex = r"-p\s+([0-9]+)(\-([0-9]+))?:([0-9]+)(\-([0-9]+))?"
458
- matches = re.match(".*%s" % regex, user_flags)
458
+ matches = re.match(f".*{regex}", user_flags)
459
459
  if matches:
460
460
  for match in re.findall(regex, user_flags):
461
461
  start = int(match[0])
@@ -544,7 +544,7 @@ class ContainerConfigurators:
544
544
 
545
545
  @staticmethod
546
546
  def gateway_listen(
547
- port: Union[int, Iterable[int], HostAndPort, Iterable[HostAndPort]],
547
+ port: int | Iterable[int] | HostAndPort | Iterable[HostAndPort],
548
548
  ):
549
549
  """
550
550
  Uses the given ports to configure GATEWAY_LISTEN. For instance, ``gateway_listen([4566, 443])`` would
@@ -1000,7 +1000,7 @@ class RunningContainer:
1000
1000
  return
1001
1001
  raise
1002
1002
 
1003
- def inspect(self) -> dict[str, Union[dict, str]]:
1003
+ def inspect(self) -> dict[str, dict | str]:
1004
1004
  return self.container_client.inspect_container(container_name_or_id=self.id)
1005
1005
 
1006
1006
  def attach(self):
@@ -1028,7 +1028,7 @@ class ContainerLogPrinter:
1028
1028
  self.callback = callback
1029
1029
 
1030
1030
  self._closed = threading.Event()
1031
- self._stream: Optional[CancellableStream] = None
1031
+ self._stream: CancellableStream | None = None
1032
1032
 
1033
1033
  def _can_start_streaming(self):
1034
1034
  if self._closed.is_set():
@@ -1114,8 +1114,8 @@ class LocalstackContainerServer(Server):
1114
1114
 
1115
1115
  def do_run(self):
1116
1116
  if self.is_container_running():
1117
- raise ContainerExists(
1118
- 'LocalStack container named "%s" is already running' % self.container.name
1117
+ raise ContainerRunning(
1118
+ f'LocalStack container named "{self.container.name}" is already running'
1119
1119
  )
1120
1120
 
1121
1121
  config.dirs.mkdirs()
@@ -1151,12 +1151,19 @@ class ContainerExists(Exception):
1151
1151
  pass
1152
1152
 
1153
1153
 
1154
+ class ContainerRunning(Exception):
1155
+ pass
1156
+
1157
+
1154
1158
  def prepare_docker_start():
1155
1159
  # prepare environment for docker start
1156
1160
  container_name = config.MAIN_CONTAINER_NAME
1157
1161
 
1158
1162
  if DOCKER_CLIENT.is_container_running(container_name):
1159
- raise ContainerExists('LocalStack container named "%s" is already running' % container_name)
1163
+ raise ContainerRunning(f'LocalStack container named "{container_name}" is already running')
1164
+
1165
+ if container_name in DOCKER_CLIENT.get_all_container_names():
1166
+ raise ContainerExists(f'LocalStack container named "{container_name}" already exists')
1160
1167
 
1161
1168
  config.dirs.mkdirs()
1162
1169
 
@@ -1308,7 +1315,8 @@ def start_infra_in_docker_detached(console, cli_params: dict[str, Any] = None):
1308
1315
  console.log("preparing environment")
1309
1316
  try:
1310
1317
  prepare_docker_start()
1311
- except ContainerExists as e:
1318
+ except ContainerRunning as e:
1319
+ # starting in detached mode is idempotent, return if container is already running
1312
1320
  console.print(str(e))
1313
1321
  return
1314
1322
 
@@ -1330,7 +1338,7 @@ def start_infra_in_docker_detached(console, cli_params: dict[str, Any] = None):
1330
1338
  console.log("detaching")
1331
1339
 
1332
1340
 
1333
- def wait_container_is_ready(timeout: Optional[float] = None):
1341
+ def wait_container_is_ready(timeout: float | None = None):
1334
1342
  """Blocks until the localstack main container is running and the ready marker has been printed."""
1335
1343
  container_name = config.MAIN_CONTAINER_NAME
1336
1344
  started = time.time()
@@ -0,0 +1,139 @@
1
+ import logging
2
+ from abc import abstractmethod
3
+
4
+ from plux import Plugin
5
+
6
+ from localstack.services.cloudformation.resource_provider import (
7
+ plugin_manager as cfn_plugin_manager,
8
+ )
9
+ from localstack.utils.catalog.catalog_loader import RemoteCatalogLoader
10
+ from localstack.utils.catalog.common import (
11
+ AwsServiceOperationsSupportInLatest,
12
+ AwsServicesSupportInLatest,
13
+ AwsServiceSupportAtRuntime,
14
+ CloudFormationResourcesSupportAtRuntime,
15
+ CloudFormationResourcesSupportInLatest,
16
+ LocalstackEmulatorType,
17
+ )
18
+
19
+ ServiceName = str
20
+ ServiceOperations = set[str]
21
+ ProviderName = str
22
+ CfnResourceName = str
23
+ CfnResourceMethodName = str
24
+ AwsServicesSupportStatus = (
25
+ AwsServiceSupportAtRuntime | AwsServicesSupportInLatest | AwsServiceOperationsSupportInLatest
26
+ )
27
+ CfnResourceSupportStatus = (
28
+ CloudFormationResourcesSupportInLatest | CloudFormationResourcesSupportAtRuntime
29
+ )
30
+ CfnResourceCatalog = dict[LocalstackEmulatorType, dict[CfnResourceName, set[CfnResourceMethodName]]]
31
+
32
+ LOG = logging.getLogger(__name__)
33
+
34
+
35
+ class CatalogPlugin(Plugin):
36
+ namespace = "localstack.utils.catalog"
37
+
38
+ @staticmethod
39
+ def _get_cfn_resources_catalog(cloudformation_resources: dict) -> CfnResourceCatalog:
40
+ cfn_resources_catalog = {}
41
+ for emulator_type, resources in cloudformation_resources.items():
42
+ cfn_resources_catalog[emulator_type] = {}
43
+ for resource_name, resource in resources.items():
44
+ cfn_resources_catalog[emulator_type][resource_name] = set(resource.methods)
45
+ return cfn_resources_catalog
46
+
47
+ @staticmethod
48
+ def _get_services_at_runtime() -> set[ServiceName]:
49
+ from localstack.services.plugins import SERVICE_PLUGINS
50
+
51
+ return set(SERVICE_PLUGINS.list_available())
52
+
53
+ @staticmethod
54
+ def _get_cfn_resources_available_at_runtime() -> set[CfnResourceName]:
55
+ return set(cfn_plugin_manager.list_names())
56
+
57
+ @abstractmethod
58
+ def get_aws_service_status(
59
+ self, service_name: str, operation_name: str | None = None
60
+ ) -> AwsServicesSupportStatus | None:
61
+ pass
62
+
63
+ @abstractmethod
64
+ def get_cloudformation_resource_status(
65
+ self, resource_name: str, service_name: str, is_pro_resource: bool = False
66
+ ) -> CfnResourceSupportStatus | AwsServicesSupportInLatest | None:
67
+ pass
68
+
69
+
70
+ class AwsCatalogRuntimePlugin(CatalogPlugin):
71
+ name = "aws-catalog-runtime-only"
72
+
73
+ def get_aws_service_status(
74
+ self, service_name: str, operation_name: str | None = None
75
+ ) -> AwsServicesSupportStatus | None:
76
+ return None
77
+
78
+ def get_cloudformation_resource_status(
79
+ self, resource_name: str, service_name: str, is_pro_resource: bool = False
80
+ ) -> CfnResourceSupportStatus | AwsServicesSupportInLatest | None:
81
+ return None
82
+
83
+
84
+ class AwsCatalogRemoteStatePlugin(CatalogPlugin):
85
+ name = "aws-catalog-remote-state"
86
+ current_emulator_type: LocalstackEmulatorType = LocalstackEmulatorType.COMMUNITY
87
+ services_in_latest: dict[ServiceName, dict[LocalstackEmulatorType, ServiceOperations]] = {}
88
+ services_at_runtime: set[ServiceName] = set()
89
+ cfn_resources_in_latest: CfnResourceCatalog = {}
90
+ cfn_resources_at_runtime: set[CfnResourceName] = set()
91
+
92
+ def __init__(self, remote_catalog_loader: RemoteCatalogLoader | None = None) -> None:
93
+ catalog_loader = remote_catalog_loader or RemoteCatalogLoader()
94
+ remote_catalog = catalog_loader.get_remote_catalog()
95
+ for service_name, emulators in remote_catalog.services.items():
96
+ for emulator_type, service_provider in emulators.items():
97
+ self.services_in_latest.setdefault(service_name, {})[emulator_type] = set(
98
+ service_provider.operations
99
+ )
100
+
101
+ self.cfn_resources_in_latest = self._get_cfn_resources_catalog(
102
+ remote_catalog.cloudformation_resources
103
+ )
104
+ self.cfn_resources_at_runtime = self._get_cfn_resources_available_at_runtime()
105
+ self.services_at_runtime = self._get_services_at_runtime()
106
+
107
+ def get_aws_service_status(
108
+ self, service_name: str, operation_name: str | None = None
109
+ ) -> AwsServicesSupportStatus | None:
110
+ if not self.services_in_latest:
111
+ return None
112
+ if service_name not in self.services_in_latest:
113
+ return AwsServicesSupportInLatest.NOT_SUPPORTED
114
+ if self.current_emulator_type not in self.services_in_latest[service_name]:
115
+ return AwsServicesSupportInLatest.SUPPORTED_WITH_LICENSE_UPGRADE
116
+ if not operation_name:
117
+ return AwsServicesSupportInLatest.SUPPORTED
118
+ if operation_name in self.services_in_latest[service_name][self.current_emulator_type]:
119
+ return AwsServiceOperationsSupportInLatest.SUPPORTED
120
+ for emulator_type in self.services_in_latest[service_name]:
121
+ if emulator_type is self.current_emulator_type:
122
+ continue
123
+ if operation_name in self.services_in_latest[service_name][emulator_type]:
124
+ return AwsServiceOperationsSupportInLatest.SUPPORTED_WITH_LICENSE_UPGRADE
125
+ return AwsServiceOperationsSupportInLatest.NOT_SUPPORTED
126
+
127
+ def get_cloudformation_resource_status(
128
+ self, resource_name: str, service_name: str, is_pro_resource: bool = False
129
+ ) -> CfnResourceSupportStatus | AwsServicesSupportInLatest | None:
130
+ if resource_name in self.cfn_resources_at_runtime:
131
+ return CloudFormationResourcesSupportAtRuntime.AVAILABLE
132
+ if service_name in self.services_at_runtime:
133
+ if resource_name in self.cfn_resources_in_latest[self.current_emulator_type]:
134
+ return CloudFormationResourcesSupportInLatest.SUPPORTED
135
+ else:
136
+ return CloudFormationResourcesSupportInLatest.NOT_SUPPORTED
137
+ if service_name in self.services_in_latest:
138
+ return self.get_aws_service_status(service_name, operation_name=None)
139
+ return AwsServicesSupportInLatest.NOT_SUPPORTED
@@ -0,0 +1,119 @@
1
+ import json
2
+ import logging
3
+ from json import JSONDecodeError
4
+ from pathlib import Path
5
+
6
+ import requests
7
+ from pydantic import BaseModel
8
+
9
+ from localstack import config, constants
10
+ from localstack.utils.catalog.common import AwsRemoteCatalog
11
+ from localstack.utils.http import get_proxies
12
+ from localstack.utils.json import FileMappedDocument
13
+
14
+ LOG = logging.getLogger(__name__)
15
+
16
+ AWS_CATALOG_FILE_NAME = "aws_catalog.json"
17
+
18
+
19
+ class RemoteCatalogVersionResponse(BaseModel):
20
+ emulator_type: str
21
+ version: str
22
+
23
+
24
+ class AwsCatalogLoaderException(Exception):
25
+ def __init__(self, msg: str, *args):
26
+ super().__init__(msg, *args)
27
+
28
+
29
+ class RemoteCatalogLoader:
30
+ supported_schema_version = "v1"
31
+ api_endpoint_catalog = f"{constants.API_ENDPOINT}/license/catalog"
32
+ catalog_file_path = Path(config.dirs.cache) / AWS_CATALOG_FILE_NAME
33
+
34
+ def get_remote_catalog(self) -> AwsRemoteCatalog:
35
+ catalog_doc = FileMappedDocument(self.catalog_file_path)
36
+ cached_catalog = AwsRemoteCatalog(**catalog_doc) if catalog_doc else None
37
+ if cached_catalog:
38
+ cached_catalog_version = cached_catalog.localstack.version
39
+ if not self._should_update_cached_catalog(cached_catalog_version):
40
+ return cached_catalog
41
+ catalog = self._get_catalog_from_platform()
42
+ self._save_catalog_to_cache(catalog_doc, catalog)
43
+ return catalog
44
+
45
+ def _get_latest_localstack_version(self) -> str:
46
+ try:
47
+ proxies = get_proxies()
48
+ response = requests.get(
49
+ f"{self.api_endpoint_catalog}/aws/version",
50
+ verify=not config.is_env_true("SSL_NO_VERIFY"),
51
+ proxies=proxies,
52
+ )
53
+ if response.ok:
54
+ return RemoteCatalogVersionResponse.model_validate(response.content).version
55
+ self._raise_server_error(response)
56
+ except requests.exceptions.RequestException as e:
57
+ raise AwsCatalogLoaderException(
58
+ f"An unexpected network error occurred when trying to fetch latest localstack version: {e}"
59
+ ) from e
60
+
61
+ def _should_update_cached_catalog(self, current_catalog_version: str) -> bool:
62
+ try:
63
+ latest_version = self._get_latest_localstack_version()
64
+ return latest_version != current_catalog_version
65
+ except Exception as e:
66
+ LOG.warning(
67
+ "Failed to retrieve the latest catalog version, cached catalog update skipped: %s",
68
+ e,
69
+ )
70
+ return False
71
+
72
+ def _save_catalog_to_cache(self, catalog_doc: FileMappedDocument, catalog: AwsRemoteCatalog):
73
+ catalog_doc.clear()
74
+ catalog_doc.update(catalog.model_dump())
75
+ catalog_doc.save()
76
+
77
+ def _get_catalog_from_platform(self) -> AwsRemoteCatalog:
78
+ try:
79
+ proxies = get_proxies()
80
+ response = requests.post(
81
+ self.api_endpoint_catalog,
82
+ verify=not config.is_env_true("SSL_NO_VERIFY"),
83
+ proxies=proxies,
84
+ )
85
+
86
+ if response.ok:
87
+ return self._parse_catalog(response.content)
88
+ self._raise_server_error(response)
89
+ except requests.exceptions.RequestException as e:
90
+ raise AwsCatalogLoaderException(
91
+ f"An unexpected network error occurred when trying to fetch remote catalog: {e}"
92
+ ) from e
93
+
94
+ def _parse_catalog(self, document: bytes) -> AwsRemoteCatalog | None:
95
+ try:
96
+ catalog_json = json.loads(document)
97
+ except JSONDecodeError as e:
98
+ raise AwsCatalogLoaderException(f"Could not de-serialize json catalog: {e}") from e
99
+ remote_catalog = AwsRemoteCatalog.model_validate(catalog_json)
100
+ if remote_catalog.schema_version != self.supported_schema_version:
101
+ raise AwsCatalogLoaderException(
102
+ f"Unsupported schema version: '{remote_catalog.schema_version}'. Only '{self.supported_schema_version}' is supported"
103
+ )
104
+ return remote_catalog
105
+
106
+ def _raise_server_error(self, response: requests.Response):
107
+ try:
108
+ server_error = response.json()
109
+ if error_message := server_error.get("message"):
110
+ raise AwsCatalogLoaderException(
111
+ f"Unexpected AWS catalog server error: {response.text}"
112
+ )
113
+ raise AwsCatalogLoaderException(
114
+ f"A server error occurred while calling remote catalog API (HTTP {response.status_code}): {error_message}"
115
+ )
116
+ except Exception:
117
+ raise AwsCatalogLoaderException(
118
+ f"An unexpected server error occurred while calling remote catalog API (HTTP {response.status_code}): {response.text}"
119
+ )
@@ -0,0 +1,58 @@
1
+ from enum import StrEnum
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class CloudFormationResource(BaseModel):
7
+ methods: list[str]
8
+
9
+
10
+ class AwsServiceCatalog(BaseModel):
11
+ provider: str
12
+ operations: list[str]
13
+ plans: list[str]
14
+
15
+
16
+ class LocalStackMetadata(BaseModel):
17
+ version: str
18
+
19
+
20
+ class AwsRemoteCatalog(BaseModel):
21
+ schema_version: str
22
+ localstack: LocalStackMetadata
23
+ services: dict[str, dict[str, AwsServiceCatalog]]
24
+ cloudformation_resources: dict[str, dict[str, CloudFormationResource]]
25
+
26
+
27
+ class LocalstackEmulatorType(StrEnum):
28
+ COMMUNITY = "community"
29
+ PRO = "pro"
30
+
31
+
32
+ class AwsServiceSupportAtRuntime(StrEnum):
33
+ AVAILABLE = "AVAILABLE"
34
+ AVAILABLE_WITH_LICENSE_UPGRADE = "AVAILABLE_WITH_LICENSE_UPGRADE"
35
+ NOT_IMPLEMENTED = "NOT_IMPLEMENTED"
36
+
37
+
38
+ class AwsServicesSupportInLatest(StrEnum):
39
+ SUPPORTED = "SUPPORTED"
40
+ SUPPORTED_WITH_LICENSE_UPGRADE = "SUPPORTED_WITH_LICENSE_UPGRADE"
41
+ NOT_SUPPORTED = "NOT_SUPPORTED"
42
+ NON_DEFAULT_PROVIDER = "NON_DEFAULT_PROVIDER"
43
+
44
+
45
+ class AwsServiceOperationsSupportInLatest(StrEnum):
46
+ SUPPORTED = "SUPPORTED"
47
+ SUPPORTED_WITH_LICENSE_UPGRADE = "SUPPORTED_WITH_LICENSE_UPGRADE"
48
+ NOT_SUPPORTED = "NOT_SUPPORTED"
49
+
50
+
51
+ class CloudFormationResourcesSupportInLatest(StrEnum):
52
+ SUPPORTED = "SUPPORTED"
53
+ NOT_SUPPORTED = "NOT_SUPPORTED"
54
+
55
+
56
+ class CloudFormationResourcesSupportAtRuntime(StrEnum):
57
+ AVAILABLE = "AVAILABLE"
58
+ NOT_IMPLEMENTED = "NOT_IMPLEMENTED"
@@ -0,0 +1,28 @@
1
+ import logging
2
+
3
+ from plux import PluginManager
4
+
5
+ from localstack.utils.catalog.catalog import CatalogPlugin
6
+ from localstack.utils.objects import singleton_factory
7
+
8
+ LOG = logging.getLogger(__name__)
9
+
10
+
11
+ @singleton_factory
12
+ def get_aws_catalog() -> CatalogPlugin:
13
+ plugin_manager = PluginManager(CatalogPlugin.namespace)
14
+ try:
15
+ plugin_name = "aws-catalog-remote-state-with-license"
16
+ if not plugin_manager.exists(plugin_name):
17
+ plugin_name = "aws-catalog-remote-state"
18
+ return plugin_manager.load(plugin_name)
19
+ except Exception as e:
20
+ LOG.debug(
21
+ "Failed to load catalog plugin with the latest LocalStack services support data, falling back to catalog without remote state: %s",
22
+ e,
23
+ )
24
+ # Try to load runtime catalog from pro version first
25
+ fallback_plugin_name = "aws-catalog-runtime-only-with-license"
26
+ if not plugin_manager.exists(fallback_plugin_name):
27
+ fallback_plugin_name = "aws-catalog-runtime-only"
28
+ return plugin_manager.load(fallback_plugin_name)
@@ -2,7 +2,7 @@ import logging
2
2
  import time
3
3
  from datetime import datetime, timezone
4
4
  from itertools import islice
5
- from typing import Optional, TypedDict
5
+ from typing import TypedDict
6
6
 
7
7
  from werkzeug import Response as WerkzeugResponse
8
8
 
@@ -20,8 +20,8 @@ LOG = logging.getLogger(__name__)
20
20
  class SqsMetricBatchData(TypedDict, total=False):
21
21
  MetricName: str
22
22
  QueueName: str
23
- Value: Optional[int]
24
- Unit: Optional[str]
23
+ Value: int | None
24
+ Unit: str | None
25
25
 
26
26
 
27
27
  def dimension_lambda(kwargs):
@@ -30,7 +30,7 @@ def dimension_lambda(kwargs):
30
30
 
31
31
 
32
32
  def publish_lambda_metric(
33
- metric, value, kwargs, account_id: Optional[str] = None, region_name: Optional[str] = None
33
+ metric, value, kwargs, account_id: str | None = None, region_name: str | None = None
34
34
  ):
35
35
  # publish metric only if CloudWatch service is available
36
36
  if not is_api_enabled("cloudwatch"):
@@ -155,7 +155,7 @@ def store_cloudwatch_logs(
155
155
  log_stream_name,
156
156
  log_output,
157
157
  start_time=None,
158
- auto_create_group: Optional[bool] = True,
158
+ auto_create_group: bool | None = True,
159
159
  ):
160
160
  if not is_api_enabled("logs"):
161
161
  return
@@ -5,10 +5,9 @@ and manipulate python collection (dicts, list, sets).
5
5
 
6
6
  import logging
7
7
  import re
8
- from collections.abc import Iterable, Iterator, Mapping, Sized
8
+ from collections.abc import Callable, Iterable, Iterator, Mapping, Sized
9
9
  from typing import (
10
10
  Any,
11
- Callable,
12
11
  Optional,
13
12
  TypedDict,
14
13
  TypeVar,
@@ -116,7 +115,7 @@ class PaginatedList(list[_ListType]):
116
115
  next_token: str = None,
117
116
  page_size: int = None,
118
117
  filter_function: Callable[[_ListType], bool] = None,
119
- ) -> tuple[list[_ListType], Optional[str]]:
118
+ ) -> tuple[list[_ListType], str | None]:
120
119
  if filter_function is not None:
121
120
  result_list = list(filter(filter_function, self))
122
121
  else:
@@ -148,7 +147,7 @@ class PaginatedList(list[_ListType]):
148
147
  class CustomExpiryTTLCache(cachetools.TTLCache):
149
148
  """TTLCache that allows to set custom expiry times for individual keys."""
150
149
 
151
- def set_expiry(self, key: Any, ttl: Union[float, int]) -> float:
150
+ def set_expiry(self, key: Any, ttl: float | int) -> float:
152
151
  """Set the expiry of the given key in a TTLCache to (<current_time> + <ttl>)"""
153
152
  with self.timer as time:
154
153
  # note: need to access the internal dunder API here
@@ -315,7 +314,7 @@ def is_list_or_tuple(obj) -> bool:
315
314
  return isinstance(obj, (list, tuple))
316
315
 
317
316
 
318
- def ensure_list(obj: Any, wrap_none=False) -> Optional[list]:
317
+ def ensure_list(obj: Any, wrap_none=False) -> list | None:
319
318
  """Wrap the given object in a list, or return the object itself if it already is a list."""
320
319
  if obj is None and not wrap_none:
321
320
  return obj
@@ -414,7 +413,7 @@ def items_equivalent(list1, list2, comparator):
414
413
  return True
415
414
 
416
415
 
417
- def is_none_or_empty(obj: Union[Optional[str], Optional[list]]) -> bool:
416
+ def is_none_or_empty(obj: str | None | list | None) -> bool:
418
417
  return (
419
418
  obj is None
420
419
  or (isinstance(obj, str) and obj.strip() == "")
@@ -475,7 +474,7 @@ def convert_to_typed_dict(typed_dict: type[T], obj: dict, strict: bool = False)
475
474
  return result
476
475
 
477
476
 
478
- def dict_multi_values(elements: Union[list, dict]) -> dict[str, list[Any]]:
477
+ def dict_multi_values(elements: list | dict) -> dict[str, list[Any]]:
479
478
  """
480
479
  Return a dictionary with the original keys from the list of dictionary and the
481
480
  values are the list of values of the original dictionary.
@@ -516,7 +515,7 @@ def split_list_by(
516
515
  return truthy, falsy
517
516
 
518
517
 
519
- def is_comma_delimited_list(string: str, item_regex: Optional[str] = None) -> bool:
518
+ def is_comma_delimited_list(string: str, item_regex: str | None = None) -> bool:
520
519
  """
521
520
  Checks if the given string is a comma-delimited list of items.
522
521
  The optional `item_regex` parameter specifies the regex pattern for each item in the list.