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
@@ -6,9 +6,11 @@ from werkzeug.exceptions import RequestEntityTooLarge
6
6
  from werkzeug.http import parse_dict_header
7
7
 
8
8
  from localstack.aws.spec import (
9
+ ProtocolName,
9
10
  ServiceCatalog,
10
11
  ServiceModelIdentifier,
11
12
  get_service_catalog,
13
+ is_protocol_in_service_model_identifier,
12
14
  )
13
15
  from localstack.http import Request
14
16
  from localstack.services.s3.utils import uses_host_addressing
@@ -17,6 +19,23 @@ from localstack.utils.strings import to_bytes
17
19
 
18
20
  LOG = logging.getLogger(__name__)
19
21
 
22
+ _PROTOCOL_DETECTION_PRIORITY: list[ProtocolName] = [
23
+ "smithy-rpc-v2-cbor",
24
+ "json",
25
+ "query",
26
+ "ec2",
27
+ "rest-json",
28
+ "rest-xml",
29
+ ]
30
+
31
+
32
+ class ProtocolError(Exception):
33
+ """
34
+ Error which is thrown if we cannot detect the protocol for the request.
35
+ """
36
+
37
+ pass
38
+
20
39
 
21
40
  class _ServiceIndicators(NamedTuple):
22
41
  """
@@ -43,6 +62,7 @@ def _extract_service_indicators(request: Request) -> _ServiceIndicators:
43
62
  """Extracts all different fields that might indicate which service a request is targeting."""
44
63
  x_amz_target = request.headers.get("x-amz-target")
45
64
  authorization = request.headers.get("authorization")
65
+ is_rpc_v2 = "rpc-v2-cbor" in request.headers.get("Smithy-Protocol", "")
46
66
 
47
67
  signing_name = None
48
68
  if authorization:
@@ -55,7 +75,15 @@ def _extract_service_indicators(request: Request) -> _ServiceIndicators:
55
75
  except (ValueError, KeyError):
56
76
  LOG.debug("auth header could not be parsed for service routing: %s", authorization)
57
77
  pass
58
- if x_amz_target:
78
+ if is_rpc_v2:
79
+ # https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html#requests
80
+ rpc_v2_params = request.path.lstrip("/").split("/")
81
+ if len(rpc_v2_params) >= 4:
82
+ *_, service_shape_name, __, operation = rpc_v2_params
83
+ target_prefix = service_shape_name.split("#")[-1]
84
+ else:
85
+ target_prefix, operation = None, None
86
+ elif x_amz_target:
59
87
  if "." in x_amz_target:
60
88
  target_prefix, operation = x_amz_target.split(".", 1)
61
89
  else:
@@ -67,6 +95,48 @@ def _extract_service_indicators(request: Request) -> _ServiceIndicators:
67
95
  return _ServiceIndicators(signing_name, target_prefix, operation, request.host, request.path)
68
96
 
69
97
 
98
+ def _matches_protocol(request: Request, protocol: ProtocolName) -> bool:
99
+ headers = request.headers
100
+ mimetype = request.mimetype.lower()
101
+ match protocol:
102
+ case "smithy-rpc-v2-cbor":
103
+ # Every request for the rpcv2Cbor protocol MUST contain a `Smithy-Protocol` header with the value
104
+ # of `rpc-v2-cbor`.
105
+ # https://smithy.io/2.0/additional-specs/protocols/smithy-rpc-v2.html
106
+ return headers.get("Smithy-Protocol", "") == "rpc-v2-cbor"
107
+ case "json":
108
+ return mimetype.startswith("application/x-amz-json")
109
+ case "query" | "ec2":
110
+ # https://smithy.io/2.0/aws/protocols/aws-query-protocol.html#request-serialization
111
+ return (
112
+ mimetype.startswith("application/x-www-form-urlencoded") or "Action" in request.args
113
+ )
114
+ case "rest-xml" | "rest-json":
115
+ # `rest-json` and `rest-xml` can accept any kind of Content-Type, and it can be configured on the operation
116
+ # level.
117
+ # https://smithy.io/2.0/aws/protocols/aws-restjson1-protocol.html
118
+ return True
119
+ case _:
120
+ return False
121
+
122
+
123
+ def match_available_protocols(
124
+ request: Request, available_protocols: list[ProtocolName]
125
+ ) -> ProtocolName | None:
126
+ """
127
+ Tries to match the current request and determine the protocol used amongst the available protocols given.
128
+ We use a priority order to try to determine the protocol, as some protocols are more permissive that others.
129
+ :param request: the incoming request
130
+ :param available_protocols: the available protocols of the Service the request is directed to
131
+ :return: the protocol matched, if any
132
+ """
133
+ for protocol in _PROTOCOL_DETECTION_PRIORITY:
134
+ if protocol in available_protocols and _matches_protocol(request, protocol):
135
+ return protocol
136
+
137
+ return None
138
+
139
+
70
140
  signing_name_path_prefix_rules = {
71
141
  # custom rules based on URI path prefixes that are not easily generalizable
72
142
  "apigateway": {
@@ -248,10 +318,10 @@ def resolve_conflicts(
248
318
  # The `application/x-amz-json-1.0` header is mandatory for requests targeting SQS with the `json` protocol. We
249
319
  # can safely route them to the `sqs` JSON parser/serializer. If not present, route the request to the
250
320
  # sqs-query protocol.
251
- content_type = request.headers.get("Content-Type")
321
+ protocol = match_available_protocols(request, available_protocols=["json", "query"])
252
322
  return (
253
323
  ServiceModelIdentifier("sqs")
254
- if content_type == "application/x-amz-json-1.0"
324
+ if protocol == "json"
255
325
  else ServiceModelIdentifier("sqs", "query")
256
326
  )
257
327
 
@@ -266,7 +336,25 @@ def determine_aws_service_model_for_data_plane(
266
336
  custom_host_match = custom_host_addressing_rules(request.host)
267
337
  if custom_host_match:
268
338
  services = services or get_service_catalog()
269
- return services.get(*custom_host_match)
339
+ return services.get(custom_host_match.name, custom_host_match.protocol)
340
+
341
+
342
+ def determine_aws_protocol(request: Request, service_model: ServiceModel) -> ProtocolName:
343
+ if not (protocols := service_model.metadata.get("protocols")):
344
+ # if the service does not define multiple protocols, return the `protocol` defined for the service
345
+ return service_model.protocol
346
+
347
+ if len(protocols) == 1:
348
+ return protocols[0]
349
+
350
+ if protocol := match_available_protocols(request, available_protocols=protocols):
351
+ return protocol
352
+
353
+ raise ProtocolError(
354
+ f"Could not determine the protocol for the request: "
355
+ f"{request.method} {request.path} for the service '{service_model.service_name}' "
356
+ f"(available protocols: {protocols})"
357
+ )
270
358
 
271
359
 
272
360
  def determine_aws_service_model(
@@ -287,12 +375,13 @@ def determine_aws_service_model(
287
375
  signing_name_candidates = services.by_signing_name(signing_name)
288
376
  if len(signing_name_candidates) == 1:
289
377
  # a unique signing-name -> service name mapping is the case for ~75% of service operations
290
- return services.get(*signing_name_candidates[0])
378
+ candidate = signing_name_candidates[0]
379
+ return services.get(candidate.name, candidate.protocol)
291
380
 
292
381
  # try to find a match with the custom signing name rules
293
382
  custom_match = custom_signing_name_rules(signing_name, path)
294
383
  if custom_match:
295
- return services.get(*custom_match)
384
+ return services.get(custom_match.name, custom_match.protocol)
296
385
 
297
386
  # still ambiguous - add the services to the list of candidates
298
387
  candidates.update(signing_name_candidates)
@@ -302,33 +391,34 @@ def determine_aws_service_model(
302
391
  target_candidates = services.by_target_prefix(target_prefix)
303
392
  if len(target_candidates) == 1:
304
393
  # a unique target prefix
305
- return services.get(*target_candidates[0])
394
+ candidate = target_candidates[0]
395
+ return services.get(candidate.name, candidate.protocol)
306
396
 
307
397
  # still ambiguous - add the services to the list of candidates
308
398
  candidates.update(target_candidates)
309
399
 
310
400
  # exclude services where the operation is not contained in the service spec
311
401
  for service_identifier in list(candidates):
312
- service = services.get(*service_identifier)
402
+ service = services.get(service_identifier.name, service_identifier.protocol)
313
403
  if operation not in service.operation_names:
314
404
  candidates.remove(service_identifier)
315
405
  else:
316
406
  # exclude services which have a target prefix (the current request does not have one)
317
407
  for service_identifier in list(candidates):
318
- service = services.get(*service_identifier)
408
+ service = services.get(service_identifier.name, service_identifier.protocol)
319
409
  if service.metadata.get("targetPrefix") is not None:
320
410
  candidates.remove(service_identifier)
321
411
 
322
412
  if len(candidates) == 1:
323
413
  service_identifier = candidates.pop()
324
- return services.get(*service_identifier)
414
+ return services.get(service_identifier.name, service_identifier.protocol)
325
415
 
326
416
  # 3. check the path if it is set and not a trivial root path
327
417
  if path and path != "/":
328
418
  # try to find a match with the custom path rules
329
419
  custom_path_match = custom_path_addressing_rules(path)
330
420
  if custom_path_match:
331
- return services.get(*custom_path_match)
421
+ return services.get(custom_path_match.name, custom_path_match.protocol)
332
422
 
333
423
  # 4. check the host (custom host addressing rules)
334
424
  if host:
@@ -337,12 +427,14 @@ def determine_aws_service_model(
337
427
  # this prevents a virtual host addressed bucket to be wrongly recognized
338
428
  if host.startswith(f"{prefix}.") and ".s3." not in host:
339
429
  if len(services_per_prefix) == 1:
340
- return services.get(*services_per_prefix[0])
430
+ candidate = services_per_prefix[0]
431
+ return services.get(candidate.name, candidate.protocol)
341
432
  candidates.update(services_per_prefix)
342
433
 
343
434
  custom_host_match = custom_host_addressing_rules(host)
344
435
  if custom_host_match:
345
- return services.get(*custom_host_match)
436
+ candidate = custom_host_match[0]
437
+ return services.get(candidate.name, candidate.protocol)
346
438
 
347
439
  if request.shallow:
348
440
  # from here on we would need access to the request body, which doesn't exist for shallow requests like
@@ -357,21 +449,28 @@ def determine_aws_service_model(
357
449
  query_candidates = [
358
450
  service
359
451
  for service in services.by_operation(values["Action"])
360
- if service.protocol in ("ec2", "query")
452
+ if any(
453
+ is_protocol_in_service_model_identifier(protocol, service)
454
+ for protocol in ("ec2", "query")
455
+ )
361
456
  ]
362
457
 
363
458
  if len(query_candidates) == 1:
364
- return services.get(*query_candidates[0])
459
+ candidate = query_candidates[0]
460
+ return services.get(candidate.name, candidate.protocol)
365
461
 
366
462
  if "Version" in values:
367
463
  for service_identifier in list(query_candidates):
368
- service_model = services.get(*service_identifier)
464
+ service_model = services.get(
465
+ service_identifier.name, service_identifier.protocol
466
+ )
369
467
  if values["Version"] != service_model.api_version:
370
468
  # the combination of Version and Action is not unique, add matches to the candidates
371
469
  query_candidates.remove(service_identifier)
372
470
 
373
471
  if len(query_candidates) == 1:
374
- return services.get(*query_candidates[0])
472
+ candidate = query_candidates[0]
473
+ return services.get(candidate.name, candidate.protocol)
375
474
 
376
475
  candidates.update(query_candidates)
377
476
 
@@ -387,15 +486,16 @@ def determine_aws_service_model(
387
486
  # 6. resolve service spec conflicts
388
487
  resolved_conflict = resolve_conflicts(candidates, request)
389
488
  if resolved_conflict:
390
- return services.get(*resolved_conflict)
489
+ return services.get(resolved_conflict.name, resolved_conflict.protocol)
391
490
 
392
491
  # 7. check the legacy S3 rules in the end
393
492
  legacy_match = legacy_s3_rules(request)
394
493
  if legacy_match:
395
- return services.get(*legacy_match)
494
+ return services.get(legacy_match.name, legacy_match.protocol)
396
495
 
397
496
  if signing_name:
398
497
  return services.get(name=signing_name)
399
498
  if candidates:
400
- return services.get(*candidates.pop())
499
+ candidate = candidates.pop()
500
+ return services.get(candidate.name, candidate.protocol)
401
501
  return None
@@ -159,7 +159,7 @@ class ParamValidator(BotocoreParamValidator):
159
159
  if required_member in params and params[required_member] is None:
160
160
  params.pop(required_member)
161
161
 
162
- super(ParamValidator, self)._validate_structure(params, shape, errors, name)
162
+ super()._validate_structure(params, shape, errors, name)
163
163
 
164
164
 
165
165
  def validate_request(operation: OperationModel, request: ServiceRequest) -> ValidationErrors:
@@ -221,7 +221,7 @@ class ShapeNode:
221
221
 
222
222
  def _print_as_typed_dict(self, output, doc=True, quote_types=False):
223
223
  name = to_valid_python_name(self.shape.name)
224
- output.write('%s = TypedDict("%s", {\n' % (name, name))
224
+ output.write(f'{name} = TypedDict("{name}", {{\n')
225
225
  for k, v in self.shape.members.items():
226
226
  member_name = to_valid_python_name(v.name)
227
227
  # check if the member name is the same as the type name (recursive types need to use forward references)
@@ -130,14 +130,16 @@ class Skeleton:
130
130
  self.dispatch_table = create_dispatch_table(implementation)
131
131
 
132
132
  def invoke(self, context: RequestContext) -> Response:
133
- serializer = create_serializer(context.service)
133
+ serializer = create_serializer(context.service, context.protocol)
134
134
 
135
135
  if context.operation and context.service_request:
136
136
  # if the parsed request is already set in the context, re-use them
137
137
  operation, instance = context.operation, context.service_request
138
138
  else:
139
139
  # otherwise, parse the incoming HTTPRequest
140
- operation, instance = create_parser(context.service).parse(context.request)
140
+ operation, instance = create_parser(context.service, context.protocol).parse(
141
+ context.request
142
+ )
141
143
  context.operation = operation
142
144
 
143
145
  try:
@@ -1329,6 +1329,26 @@
1329
1329
  "documentation": "<p>The Content-MD5 you specified did not match what we received.</p>",
1330
1330
  "exception": true
1331
1331
  }
1332
+ },
1333
+ {
1334
+ "op": "add",
1335
+ "path": "/shapes/AuthorizationHeaderMalformed",
1336
+ "value": {
1337
+ "type": "structure",
1338
+ "members": {
1339
+ "Region": {
1340
+ "shape": "BucketRegion"
1341
+ },
1342
+ "HostId": {
1343
+ "shape": "HostId"
1344
+ }
1345
+ },
1346
+ "error": {
1347
+ "httpStatusCode": 400
1348
+ },
1349
+ "documentation": "<p>The authorization header is malformed.</p>",
1350
+ "exception": true
1351
+ }
1332
1352
  }
1333
1353
  ],
1334
1354
  "apigatewayv2/2018-11-29/service-2": [
@@ -1352,5 +1372,43 @@
1352
1372
  "path": "/operations/CreateApiMapping/http/responseCode",
1353
1373
  "value": 200
1354
1374
  }
1375
+ ],
1376
+ "cloudwatch/2010-08-01/service-2": [
1377
+ {
1378
+ "op": "add",
1379
+ "path": "/metadata/awsQueryCompatible",
1380
+ "value": {}
1381
+ },
1382
+ {
1383
+ "op": "add",
1384
+ "path": "/metadata/jsonVersion",
1385
+ "value": "1.0"
1386
+ },
1387
+ {
1388
+ "op": "add",
1389
+ "path": "/metadata/targetPrefix",
1390
+ "value": "GraniteServiceVersion20100801"
1391
+ },
1392
+ {
1393
+ "op": "replace",
1394
+ "path": "/metadata/protocol",
1395
+ "value": "smithy-rpc-v2-cbor"
1396
+ },
1397
+ {
1398
+ "op": "replace",
1399
+ "path": "/metadata/protocols",
1400
+ "value": [
1401
+ "smithy-rpc-v2-cbor",
1402
+ "json",
1403
+ "query"
1404
+ ]
1405
+ },
1406
+ {
1407
+ "op": "add",
1408
+ "path": "/shapes/ConflictException/error",
1409
+ "value": {
1410
+ "httpStatusCode": 409
1411
+ }
1412
+ }
1355
1413
  ]
1356
1414
  }
localstack/aws/spec.py CHANGED
@@ -21,7 +21,7 @@ from localstack.utils.objects import singleton_factory
21
21
  LOG = logging.getLogger(__name__)
22
22
 
23
23
  ServiceName = str
24
- ProtocolName = Literal["query", "json", "rest-json", "rest-xml", "ec2"]
24
+ ProtocolName = Literal["query", "json", "rest-json", "rest-xml", "ec2", "smithy-rpc-v2-cbor"]
25
25
 
26
26
 
27
27
  class ServiceModelIdentifier(NamedTuple):
@@ -33,6 +33,7 @@ class ServiceModelIdentifier(NamedTuple):
33
33
 
34
34
  name: ServiceName
35
35
  protocol: ProtocolName | None = None
36
+ protocols: tuple[ProtocolName] | None = None
36
37
 
37
38
 
38
39
  spec_patches_json = os.path.join(os.path.dirname(__file__), "spec-patches.json")
@@ -69,7 +70,7 @@ class PatchingLoader(Loader):
69
70
 
70
71
  @instance_cache
71
72
  def load_data(self, name: str):
72
- result = super(PatchingLoader, self).load_data(name)
73
+ result = super().load_data(name)
73
74
 
74
75
  if patches := self.patches.get(name):
75
76
  return jsonpatch.apply_patch(result, patches)
@@ -114,9 +115,13 @@ def load_service(
114
115
  :raises: UnknownServiceProtocolError if the specific protocol of the service cannot be found
115
116
  """
116
117
  service_description = loader.load_service_model(service, "service-2", version)
118
+ service_metadata = service_description.get("metadata", {})
119
+ service_protocols = {service_metadata.get("protocol")}
120
+ if protocols := service_metadata.get("protocols"):
121
+ service_protocols.update(protocols)
117
122
 
118
123
  # check if the protocol is defined, and if so, if the loaded service defines this protocol
119
- if protocol is not None and protocol != service_description.get("metadata", {}).get("protocol"):
124
+ if protocol is not None and protocol not in service_protocols:
120
125
  # if the protocol is defined, but not the one of the currently loaded service,
121
126
  # check if we already loaded the custom spec based on the naming convention (<service>-<protocol>),
122
127
  # f.e. "sqs-query"
@@ -132,7 +137,7 @@ def load_service(
132
137
 
133
138
  # remove potential protocol names from the service name
134
139
  # FIXME add more protocols here if we have to internalize more than just sqs-query
135
- # TODO this should not contain specific internalized serivce names
140
+ # TODO this should not contain specific internalized service names
136
141
  service = {"sqs-query": "sqs"}.get(service, service)
137
142
  return ServiceModel(service_description, service)
138
143
 
@@ -149,6 +154,27 @@ def iterate_service_operations() -> Generator[tuple[ServiceModel, OperationModel
149
154
  yield service, service.operation_model(op_name)
150
155
 
151
156
 
157
+ def is_protocol_in_service_model_identifier(
158
+ protocol: ProtocolName, service_model_identifier: ServiceModelIdentifier
159
+ ) -> bool:
160
+ """
161
+ :param protocol: the protocol name to check
162
+ :param service_model_identifier:
163
+ :return: boolean to indicate if the protocol is available for that service
164
+ """
165
+ protocols = service_model_identifier.protocols or []
166
+ return protocol in protocols or protocol == service_model_identifier.protocol
167
+
168
+
169
+ def get_service_model_identifier(service_model: ServiceModel) -> ServiceModelIdentifier:
170
+ protocols = service_model.metadata.get("protocols")
171
+ return ServiceModelIdentifier(
172
+ name=service_model.service_name,
173
+ protocol=service_model.protocol,
174
+ protocols=tuple(protocols) if protocols else None,
175
+ )
176
+
177
+
152
178
  @dataclasses.dataclass
153
179
  class ServiceCatalogIndex:
154
180
  """
@@ -178,9 +204,7 @@ class LazyServiceCatalogIndex:
178
204
  for service_model in service_models:
179
205
  target_prefix = service_model.metadata.get("targetPrefix")
180
206
  if target_prefix:
181
- result[target_prefix].append(
182
- ServiceModelIdentifier(service_model.service_name, service_model.protocol)
183
- )
207
+ result[target_prefix].append(get_service_model_identifier(service_model))
184
208
  return dict(result)
185
209
 
186
210
  @cached_property
@@ -189,7 +213,7 @@ class LazyServiceCatalogIndex:
189
213
  for service_models in self._services.values():
190
214
  for service_model in service_models:
191
215
  result[service_model.signing_name].append(
192
- ServiceModelIdentifier(service_model.service_name, service_model.protocol)
216
+ get_service_model_identifier(service_model)
193
217
  )
194
218
  return dict(result)
195
219
 
@@ -201,11 +225,7 @@ class LazyServiceCatalogIndex:
201
225
  operations = service_model.operation_names
202
226
  if operations:
203
227
  for operation in operations:
204
- result[operation].append(
205
- ServiceModelIdentifier(
206
- service_model.service_name, service_model.protocol
207
- )
208
- )
228
+ result[operation].append(get_service_model_identifier(service_model))
209
229
  return dict(result)
210
230
 
211
231
  @cached_property
@@ -214,7 +234,7 @@ class LazyServiceCatalogIndex:
214
234
  for service_models in self._services.values():
215
235
  for service_model in service_models:
216
236
  result[service_model.endpoint_prefix].append(
217
- ServiceModelIdentifier(service_model.service_name, service_model.protocol)
237
+ get_service_model_identifier(service_model)
218
238
  )
219
239
  return dict(result)
220
240
 
@@ -343,8 +363,9 @@ def get_service_catalog() -> ServiceCatalog:
343
363
  index = build_service_index_cache(cache_catalog_file)
344
364
  return ServiceCatalog(index)
345
365
  except Exception:
346
- LOG.exception(
347
- "error while processing service catalog index cache, falling back to lazy-loaded index"
366
+ LOG.error(
367
+ "error while processing service catalog index cache, falling back to lazy-loaded index",
368
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
348
369
  )
349
370
  return ServiceCatalog()
350
371
 
@@ -12,7 +12,7 @@ class CLIError(ClickException):
12
12
  def format_message(self) -> str:
13
13
  return click.style(f"❌ Error: {self.message}", fg="red")
14
14
 
15
- def show(self, file: t.Optional[t.IO[t.Any]] = None) -> None:
15
+ def show(self, file: t.IO[t.Any] | None = None) -> None:
16
16
  if file is None:
17
17
  file = get_text_stderr()
18
18
 
@@ -3,7 +3,7 @@ import logging
3
3
  import os
4
4
  import sys
5
5
  import traceback
6
- from typing import Optional, TypedDict
6
+ from typing import TypedDict
7
7
 
8
8
  import click
9
9
  import requests
@@ -47,7 +47,7 @@ class LocalStackCliGroup(click.Group):
47
47
 
48
48
  def invoke(self, ctx: click.Context):
49
49
  try:
50
- return super(LocalStackCliGroup, self).invoke(ctx)
50
+ return super().invoke(ctx)
51
51
  except click.exceptions.Exit:
52
52
  # raise Exit exceptions unmodified (e.g., raised on --help)
53
53
  raise
@@ -320,8 +320,8 @@ class DockerStatus(TypedDict, total=False):
320
320
  image_tag: str
321
321
  image_id: str
322
322
  image_created: str
323
- container_name: Optional[str]
324
- container_ip: Optional[str]
323
+ container_name: str | None
324
+ container_ip: str | None
325
325
 
326
326
 
327
327
  def _print_docker_status(format_: str) -> None:
@@ -602,7 +602,7 @@ def cmd_stop() -> None:
602
602
 
603
603
  try:
604
604
  DOCKER_CLIENT.stop_container(container_name)
605
- console.print("container stopped: %s" % container_name)
605
+ console.print(f"container stopped: {container_name}")
606
606
  except NoSuchContainer:
607
607
  raise CLIError(
608
608
  f'Expected a running LocalStack container named "{container_name}", but found none'
@@ -699,7 +699,7 @@ def cmd_logs(follow: bool, tail: int) -> None:
699
699
  metavar="N",
700
700
  )
701
701
  @publish_invocation
702
- def cmd_wait(timeout: Optional[float] = None) -> None:
702
+ def cmd_wait(timeout: float | None = None) -> None:
703
703
  """
704
704
  Wait for the LocalStack runtime to be up and running.
705
705
 
localstack/cli/lpm.py CHANGED
@@ -1,7 +1,6 @@
1
1
  import itertools
2
2
  import logging
3
3
  from multiprocessing.pool import ThreadPool
4
- from typing import Optional
5
4
 
6
5
  import click
7
6
  from rich.console import Console
@@ -75,9 +74,9 @@ def _do_install_package(package: Package, version: str = None, target: InstallTa
75
74
  )
76
75
  def install(
77
76
  package: list[str],
78
- parallel: Optional[int] = 1,
79
- version: Optional[str] = None,
80
- target: Optional[str] = None,
77
+ parallel: int | None = 1,
78
+ version: str | None = None,
79
+ target: str | None = None,
81
80
  ):
82
81
  """Install one or more packages."""
83
82
  try:
localstack/cli/plugins.py CHANGED
@@ -56,7 +56,7 @@ def find(where, exclude, include, output):
56
56
  elif output == "dict":
57
57
  rprint(dict(plugins))
58
58
  else:
59
- raise CLIError("unknown output format %s" % output)
59
+ raise CLIError(f"unknown output format {output}")
60
60
 
61
61
 
62
62
  @cli.command("list")
@@ -1,7 +1,6 @@
1
1
  import argparse
2
2
  import os
3
3
  import sys
4
- from typing import Optional
5
4
 
6
5
  # important: this needs to be free of localstack imports
7
6
 
@@ -44,7 +43,7 @@ def set_and_remove_profile_from_sys_argv():
44
43
  os.environ["CONFIG_PROFILE"] = profile.strip()
45
44
 
46
45
 
47
- def parse_p_argument(args) -> Optional[str]:
46
+ def parse_p_argument(args) -> str | None:
48
47
  """
49
48
  Lightweight arg parsing to find the first occurrence of ``-p <config>``, or ``-p=<config>`` and return the value of
50
49
  ``<config>`` from the given arguments.