localstack-core 4.7.1.dev139__py3-none-any.whl → 4.10.1.dev42__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.

Potentially problematic release.


This version of localstack-core might be problematic. Click here for more details.

Files changed (208) hide show
  1. localstack/aws/api/acm/__init__.py +122 -122
  2. localstack/aws/api/apigateway/__init__.py +560 -559
  3. localstack/aws/api/cloudcontrol/__init__.py +63 -63
  4. localstack/aws/api/cloudformation/__init__.py +1041 -969
  5. localstack/aws/api/cloudwatch/__init__.py +408 -368
  6. localstack/aws/api/config/__init__.py +788 -786
  7. localstack/aws/api/core.py +4 -0
  8. localstack/aws/api/dynamodb/__init__.py +753 -759
  9. localstack/aws/api/dynamodbstreams/__init__.py +74 -74
  10. localstack/aws/api/ec2/__init__.py +9713 -8573
  11. localstack/aws/api/es/__init__.py +453 -453
  12. localstack/aws/api/events/__init__.py +552 -552
  13. localstack/aws/api/firehose/__init__.py +541 -543
  14. localstack/aws/api/iam/__init__.py +646 -572
  15. localstack/aws/api/kinesis/__init__.py +251 -144
  16. localstack/aws/api/kms/__init__.py +343 -333
  17. localstack/aws/api/lambda_/__init__.py +585 -571
  18. localstack/aws/api/logs/__init__.py +682 -666
  19. localstack/aws/api/opensearch/__init__.py +814 -785
  20. localstack/aws/api/pipes/__init__.py +336 -336
  21. localstack/aws/api/redshift/__init__.py +1192 -1164
  22. localstack/aws/api/resource_groups/__init__.py +175 -175
  23. localstack/aws/api/resourcegroupstaggingapi/__init__.py +67 -67
  24. localstack/aws/api/route53/__init__.py +256 -254
  25. localstack/aws/api/route53resolver/__init__.py +396 -396
  26. localstack/aws/api/s3/__init__.py +1358 -1345
  27. localstack/aws/api/s3control/__init__.py +616 -584
  28. localstack/aws/api/scheduler/__init__.py +118 -118
  29. localstack/aws/api/secretsmanager/__init__.py +193 -193
  30. localstack/aws/api/ses/__init__.py +227 -227
  31. localstack/aws/api/sns/__init__.py +115 -115
  32. localstack/aws/api/sqs/__init__.py +100 -100
  33. localstack/aws/api/ssm/__init__.py +1978 -1970
  34. localstack/aws/api/stepfunctions/__init__.py +323 -323
  35. localstack/aws/api/sts/__init__.py +90 -66
  36. localstack/aws/api/support/__init__.py +112 -112
  37. localstack/aws/api/swf/__init__.py +378 -386
  38. localstack/aws/api/transcribe/__init__.py +425 -425
  39. localstack/aws/client.py +7 -2
  40. localstack/aws/forwarder.py +52 -5
  41. localstack/aws/handlers/analytics.py +1 -1
  42. localstack/aws/handlers/logging.py +12 -2
  43. localstack/aws/handlers/metric_handler.py +41 -1
  44. localstack/aws/handlers/service.py +43 -10
  45. localstack/aws/protocol/parser.py +440 -21
  46. localstack/aws/protocol/serializer.py +684 -64
  47. localstack/aws/protocol/service_router.py +120 -20
  48. localstack/aws/scaffold.py +15 -17
  49. localstack/aws/skeleton.py +4 -2
  50. localstack/aws/spec-patches.json +58 -0
  51. localstack/aws/spec.py +33 -13
  52. localstack/cli/exceptions.py +1 -1
  53. localstack/cli/localstack.py +10 -5
  54. localstack/cli/lpm.py +3 -4
  55. localstack/cli/profiles.py +1 -2
  56. localstack/config.py +18 -12
  57. localstack/constants.py +4 -29
  58. localstack/dev/kubernetes/__main__.py +39 -4
  59. localstack/dev/run/paths.py +1 -1
  60. localstack/dns/plugins.py +5 -1
  61. localstack/dns/server.py +12 -3
  62. localstack/packages/api.py +9 -8
  63. localstack/packages/core.py +2 -2
  64. localstack/packages/plugins.py +0 -8
  65. localstack/runtime/init.py +1 -1
  66. localstack/services/apigateway/helpers.py +5 -9
  67. localstack/services/apigateway/legacy/provider.py +85 -12
  68. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  69. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  70. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +50 -6
  71. localstack/services/apigateway/next_gen/provider.py +5 -0
  72. localstack/services/apigateway/patches.py +0 -9
  73. localstack/services/cloudformation/engine/entities.py +12 -1
  74. localstack/services/cloudformation/engine/v2/change_set_model.py +0 -3
  75. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +14 -0
  76. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +13 -15
  77. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +118 -24
  78. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +4 -1
  79. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +5 -14
  80. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  81. localstack/services/cloudformation/engine/v2/resolving.py +6 -4
  82. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  83. localstack/services/cloudformation/provider.py +2 -2
  84. localstack/services/cloudformation/resource_provider.py +5 -1
  85. localstack/services/cloudformation/resources.py +24149 -0
  86. localstack/services/cloudformation/v2/entities.py +6 -3
  87. localstack/services/cloudformation/v2/provider.py +178 -33
  88. localstack/services/cloudformation/v2/types.py +8 -4
  89. localstack/services/cloudwatch/provider_v2.py +25 -28
  90. localstack/services/dynamodb/packages.py +2 -1
  91. localstack/services/dynamodb/provider.py +42 -0
  92. localstack/services/dynamodb/v2/provider.py +42 -0
  93. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  94. localstack/services/es/provider.py +2 -2
  95. localstack/services/events/event_rule_engine.py +31 -13
  96. localstack/services/events/models.py +4 -5
  97. localstack/services/events/target.py +17 -9
  98. localstack/services/iam/provider.py +11 -116
  99. localstack/services/iam/resources/policy_simulator.py +133 -0
  100. localstack/services/kinesis/models.py +15 -2
  101. localstack/services/kinesis/packages.py +1 -1
  102. localstack/services/kinesis/provider.py +77 -0
  103. localstack/services/kms/models.py +34 -4
  104. localstack/services/kms/provider.py +107 -21
  105. localstack/services/lambda_/api_utils.py +3 -1
  106. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  107. localstack/services/lambda_/packages.py +1 -1
  108. localstack/services/lambda_/provider.py +1 -1
  109. localstack/services/lambda_/runtimes.py +8 -3
  110. localstack/services/logs/provider.py +36 -19
  111. localstack/services/moto.py +2 -1
  112. localstack/services/opensearch/cluster.py +15 -7
  113. localstack/services/opensearch/packages.py +26 -7
  114. localstack/services/opensearch/provider.py +6 -1
  115. localstack/services/opensearch/versions.py +56 -7
  116. localstack/services/s3/constants.py +5 -2
  117. localstack/services/s3/cors.py +4 -4
  118. localstack/services/s3/notifications.py +1 -1
  119. localstack/services/s3/presigned_url.py +27 -43
  120. localstack/services/s3/provider.py +68 -12
  121. localstack/services/s3/utils.py +42 -11
  122. localstack/services/ses/provider.py +16 -7
  123. localstack/services/sns/constants.py +7 -1
  124. localstack/services/sns/v2/models.py +190 -0
  125. localstack/services/sns/v2/provider.py +992 -2
  126. localstack/services/sns/v2/utils.py +138 -0
  127. localstack/services/sqs/developer_api.py +205 -0
  128. localstack/services/sqs/models.py +79 -13
  129. localstack/services/sqs/provider.py +8 -309
  130. localstack/services/sqs/query_api.py +1 -1
  131. localstack/services/sqs/utils.py +121 -2
  132. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  133. localstack/testing/aws/cloudformation_utils.py +1 -1
  134. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  135. localstack/testing/pytest/container.py +4 -5
  136. localstack/testing/pytest/fixtures.py +20 -19
  137. localstack/testing/pytest/in_memory_localstack.py +0 -4
  138. localstack/testing/pytest/marking.py +13 -4
  139. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  140. localstack/testing/pytest/util.py +1 -1
  141. localstack/testing/pytest/validation_tracking.py +1 -2
  142. localstack/testing/snapshots/transformer_utility.py +7 -0
  143. localstack/testing/testselection/matching.py +0 -1
  144. localstack/utils/analytics/events.py +2 -2
  145. localstack/utils/analytics/metadata.py +1 -2
  146. localstack/utils/analytics/metrics/counter.py +6 -8
  147. localstack/utils/analytics/publisher.py +1 -2
  148. localstack/utils/analytics/service_request_aggregator.py +2 -2
  149. localstack/utils/archives.py +11 -11
  150. localstack/utils/aws/arns.py +17 -9
  151. localstack/utils/aws/aws_responses.py +7 -7
  152. localstack/utils/aws/aws_stack.py +2 -3
  153. localstack/utils/aws/client_types.py +0 -8
  154. localstack/utils/aws/message_forwarding.py +1 -2
  155. localstack/utils/aws/request_context.py +4 -5
  156. localstack/utils/batch_policy.py +3 -3
  157. localstack/utils/bootstrap.py +7 -7
  158. localstack/utils/catalog/catalog.py +139 -0
  159. localstack/utils/catalog/catalog_loader.py +119 -0
  160. localstack/utils/catalog/common.py +58 -0
  161. localstack/utils/catalog/plugins.py +28 -0
  162. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  163. localstack/utils/collections.py +7 -8
  164. localstack/utils/config_listener.py +1 -1
  165. localstack/utils/container_networking.py +2 -3
  166. localstack/utils/container_utils/container_client.py +115 -131
  167. localstack/utils/container_utils/docker_cmd_client.py +42 -42
  168. localstack/utils/container_utils/docker_sdk_client.py +63 -62
  169. localstack/utils/crypto.py +109 -0
  170. localstack/utils/diagnose.py +2 -3
  171. localstack/utils/docker_utils.py +3 -4
  172. localstack/utils/files.py +31 -7
  173. localstack/utils/functions.py +3 -2
  174. localstack/utils/http.py +4 -5
  175. localstack/utils/json.py +19 -5
  176. localstack/utils/kinesis/kinesis_connector.py +2 -1
  177. localstack/utils/net.py +6 -6
  178. localstack/utils/no_exit_argument_parser.py +2 -2
  179. localstack/utils/numbers.py +9 -2
  180. localstack/utils/objects.py +6 -5
  181. localstack/utils/patch.py +2 -1
  182. localstack/utils/run.py +10 -9
  183. localstack/utils/scheduler.py +11 -11
  184. localstack/utils/server/tcp_proxy.py +2 -2
  185. localstack/utils/serving.py +2 -3
  186. localstack/utils/strings.py +10 -11
  187. localstack/utils/sync.py +126 -1
  188. localstack/utils/tagging.py +1 -4
  189. localstack/utils/testutil.py +5 -4
  190. localstack/utils/threads.py +2 -2
  191. localstack/utils/time.py +11 -3
  192. localstack/utils/urls.py +1 -3
  193. localstack/version.py +2 -2
  194. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/METADATA +19 -13
  195. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/RECORD +203 -199
  196. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/entry_points.txt +4 -2
  197. localstack_core-4.10.1.dev42.dist-info/plux.json +1 -0
  198. localstack/packages/terraform.py +0 -46
  199. localstack/services/cloudformation/deploy.html +0 -144
  200. localstack/services/cloudformation/deploy_ui.py +0 -47
  201. localstack/services/cloudformation/plugins.py +0 -12
  202. localstack_core-4.7.1.dev139.dist-info/plux.json +0 -1
  203. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack +0 -0
  204. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack-supervisor +0 -0
  205. {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack.bat +0 -0
  206. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/WHEEL +0 -0
  207. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/licenses/LICENSE.txt +0 -0
  208. {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/top_level.txt +0 -0
localstack/aws/client.py CHANGED
@@ -21,6 +21,7 @@ from localstack.utils.strings import to_str
21
21
  from .api import CommonServiceException, RequestContext, ServiceException, ServiceResponse
22
22
  from .connect import get_service_endpoint
23
23
  from .gateway import Gateway
24
+ from .spec import ProtocolName
24
25
 
25
26
  LOG = logging.getLogger(__name__)
26
27
 
@@ -284,13 +285,17 @@ def _patch_botocore_endpoint_in_memory():
284
285
 
285
286
 
286
287
  def parse_response(
287
- operation: OperationModel, response: Response, include_response_metadata: bool = True
288
+ operation: OperationModel,
289
+ protocol: ProtocolName,
290
+ response: Response,
291
+ include_response_metadata: bool = True,
288
292
  ) -> ServiceResponse:
289
293
  """
290
294
  Parses an HTTP Response object into an AWS response object using botocore. It does this by adapting the
291
295
  procedure of ``botocore.endpoint.convert_to_response_dict`` to work with Werkzeug's server-side response object.
292
296
 
293
297
  :param operation: the operation of the original request
298
+ :param protocol: the protocol of the original request
294
299
  :param response: the HTTP response object containing the response of the operation
295
300
  :param include_response_metadata: True if the ResponseMetadata (typical for boto response dicts) should be included
296
301
  :return: a parsed dictionary as it is returned by botocore
@@ -322,7 +327,7 @@ def parse_response(
322
327
  timestamp_parser=_cbor_timestamp_parser, blob_parser=_cbor_blob_parser
323
328
  )
324
329
 
325
- parser = factory.create_parser(operation.service_model.protocol)
330
+ parser = factory.create_parser(protocol)
326
331
  parsed_response = parser.parse(response_dict, operation.output_shape)
327
332
 
328
333
  if response.status_code >= 301:
@@ -8,6 +8,8 @@ from typing import Any
8
8
 
9
9
  from botocore.awsrequest import AWSPreparedRequest, prepare_request_dict
10
10
  from botocore.config import Config as BotoConfig
11
+ from botocore.model import OperationModel
12
+ from botocore.serialize import create_serializer
11
13
  from werkzeug.datastructures import Headers
12
14
 
13
15
  from localstack.aws.api.core import (
@@ -19,7 +21,7 @@ from localstack.aws.api.core import (
19
21
  from localstack.aws.client import create_http_request, parse_response, raise_service_exception
20
22
  from localstack.aws.connect import connect_to
21
23
  from localstack.aws.skeleton import DispatchTable, create_dispatch_table
22
- from localstack.aws.spec import load_service
24
+ from localstack.aws.spec import ProtocolName, load_service
23
25
  from localstack.constants import AWS_REGION_US_EAST_1
24
26
  from localstack.http import Response
25
27
  from localstack.http.proxy import Proxy
@@ -79,7 +81,7 @@ class AwsRequestProxy:
79
81
  if not self.parse_response:
80
82
  return http_response
81
83
  parsed_response = parse_response(
82
- context.operation, http_response, self.include_response_metadata
84
+ context.operation, context.protocol, http_response, self.include_response_metadata
83
85
  )
84
86
  raise_service_exception(http_response, parsed_response)
85
87
  return parsed_response
@@ -90,6 +92,7 @@ class AwsRequestProxy:
90
92
  action=original.operation.name,
91
93
  parameters=service_request,
92
94
  region=original.region,
95
+ protocol=original.protocol,
93
96
  )
94
97
  # update the newly created context with non-payload specific request headers (the payload can differ from
95
98
  # the original request, f.e. it could be JSON encoded now while the initial request was CBOR encoded)
@@ -184,7 +187,9 @@ def dispatch_to_backend(
184
187
  :raises ServiceException: if the dispatcher returned an error response
185
188
  """
186
189
  http_response = http_request_dispatcher(context)
187
- parsed_response = parse_response(context.operation, http_response, include_response_metadata)
190
+ parsed_response = parse_response(
191
+ context.operation, context.protocol, http_response, include_response_metadata
192
+ )
188
193
  raise_service_exception(http_response, parsed_response)
189
194
  return parsed_response
190
195
 
@@ -196,6 +201,7 @@ _non_validating_boto_config = BotoConfig(parameter_validation=False)
196
201
  def create_aws_request_context(
197
202
  service_name: str,
198
203
  action: str,
204
+ protocol: ProtocolName = None,
199
205
  parameters: Mapping[str, Any] = None,
200
206
  region: str = None,
201
207
  endpoint_url: str | None = None,
@@ -210,6 +216,7 @@ def create_aws_request_context(
210
216
 
211
217
  :param service_name: the AWS service
212
218
  :param action: the action to invoke
219
+ :param protocol: the protocol to use
213
220
  :param parameters: the invocation parameters
214
221
  :param region: the region name (default is us-east-1)
215
222
  :param endpoint_url: the endpoint to call (defaults to localstack)
@@ -222,6 +229,8 @@ def create_aws_request_context(
222
229
 
223
230
  service = load_service(service_name)
224
231
  operation = service.operation_model(action)
232
+ # TODO: remove this once every usage upstream has been removed
233
+ protocol = protocol or service.resolved_protocol
225
234
 
226
235
  # we re-use botocore internals here to serialize the HTTP request,
227
236
  # but deactivate validation (validation errors should be handled by the backend)
@@ -243,8 +252,14 @@ def create_aws_request_context(
243
252
  endpoint_url = "http://localhost.localstack.cloud"
244
253
  # pre-process the request args (some params are modified using botocore event handlers)
245
254
  parameters = client._emit_api_params(parameters, operation, request_context)
246
- request_dict = client._convert_to_request_dict(
247
- parameters, operation, endpoint_url, context=request_context
255
+
256
+ request_dict = _convert_to_request_dict_with_protocol(
257
+ client=client,
258
+ protocol=protocol,
259
+ api_params=parameters,
260
+ operation_model=operation,
261
+ endpoint_url=endpoint_url,
262
+ context=request_context,
248
263
  )
249
264
 
250
265
  if auth_path := request_dict.get("auth_path"):
@@ -266,7 +281,39 @@ def create_aws_request_context(
266
281
  context = RequestContext(request=create_http_request(aws_request))
267
282
  context.service = service
268
283
  context.operation = operation
284
+ context.protocol = protocol
269
285
  context.region = region
270
286
  context.service_request = parameters
271
287
 
272
288
  return context
289
+
290
+
291
+ def _convert_to_request_dict_with_protocol(
292
+ client,
293
+ protocol: ProtocolName,
294
+ api_params: dict,
295
+ operation_model: OperationModel,
296
+ endpoint_url: str,
297
+ context: dict,
298
+ set_user_agent_header: bool = True,
299
+ ) -> dict:
300
+ """
301
+ This function is taken from botocore Client._convert_to_request_dict, but we are overriding the serializer
302
+ Botocore does not expose a way to create a client with a specific protocol, but we need this functionality
303
+ to support multi-protocols.
304
+ """
305
+ serializer = create_serializer(protocol, include_validation=False)
306
+ request_dict = serializer.serialize_to_request(api_params, operation_model)
307
+ if not client._client_config.inject_host_prefix:
308
+ request_dict.pop("host_prefix", None)
309
+ if set_user_agent_header:
310
+ user_agent = client._user_agent_creator.to_string()
311
+ else:
312
+ user_agent = None
313
+ prepare_request_dict(
314
+ request_dict,
315
+ endpoint_url=endpoint_url,
316
+ user_agent=user_agent,
317
+ context=context,
318
+ )
319
+ return request_dict
@@ -60,7 +60,7 @@ class ServiceRequestCounter:
60
60
  if context.service_exception:
61
61
  return context.service_exception.code
62
62
 
63
- response = parse_response(context.operation, response)
63
+ response = parse_response(context.operation, context.protocol, response)
64
64
  return response["Error"]["Code"]
65
65
  except Exception:
66
66
  if config.DEBUG_ANALYTICS:
@@ -1,6 +1,7 @@
1
1
  """Handlers for logging."""
2
2
 
3
3
  import logging
4
+ import types
4
5
  from functools import cached_property
5
6
 
6
7
  from localstack.aws.api import RequestContext, ServiceException
@@ -21,6 +22,14 @@ class ExceptionLogger(ExceptionHandler):
21
22
  def __init__(self, logger=None):
22
23
  self.logger = logger or LOG
23
24
 
25
+ try:
26
+ import moto.core.exceptions
27
+
28
+ self._moto_service_exception = moto.core.exceptions.ServiceException
29
+ except (ModuleNotFoundError, AttributeError):
30
+ # Moto may not be available in stripped-down versions of LocalStack, like LocalStack S3 image.
31
+ self._moto_service_exception = types.EllipsisType
32
+
24
33
  def __call__(
25
34
  self,
26
35
  chain: HandlerChain,
@@ -28,9 +37,10 @@ class ExceptionLogger(ExceptionHandler):
28
37
  context: RequestContext,
29
38
  response: Response,
30
39
  ):
31
- if isinstance(exception, ServiceException):
40
+ if isinstance(exception, (ServiceException, self._moto_service_exception)):
32
41
  # We do not want to log an error/stacktrace if the handler is working as expected, but chooses to throw
33
- # a service exception
42
+ # a service exception. It may also throw a Moto ServiceException, which should not be logged either
43
+ # because ServiceExceptionHandler understands it.
34
44
  return
35
45
  if self.logger.isEnabledFor(level=logging.DEBUG):
36
46
  self.logger.exception("exception during call chain", exc_info=exception)
@@ -1,9 +1,15 @@
1
+ import csv
1
2
  import logging
3
+ import os
4
+ from datetime import datetime
5
+ from pathlib import Path
2
6
 
3
7
  from localstack import config
4
8
  from localstack.aws.api import RequestContext
5
9
  from localstack.aws.chain import HandlerChain
10
+ from localstack.constants import ENV_INTERNAL_TEST_STORE_METRICS_PATH
6
11
  from localstack.http import Response
12
+ from localstack.utils.strings import short_uid
7
13
 
8
14
  LOG = logging.getLogger(__name__)
9
15
 
@@ -137,6 +143,32 @@ class MetricHandler:
137
143
 
138
144
  def __init__(self) -> None:
139
145
  self.metrics_handler_items = {}
146
+ self.local_filename = None
147
+
148
+ if self.should_store_metric_locally():
149
+ self.local_filename = self.create_local_file()
150
+
151
+ @staticmethod
152
+ def should_store_metric_locally() -> bool:
153
+ return config.is_collect_metrics_mode() and config.store_test_metrics_in_local_filesystem()
154
+
155
+ @staticmethod
156
+ def create_local_file():
157
+ folder = Path(
158
+ os.environ.get(ENV_INTERNAL_TEST_STORE_METRICS_PATH, "/tmp/localstack-metrics")
159
+ )
160
+ if not folder.exists():
161
+ folder.mkdir(parents=True, exist_ok=True)
162
+ LOG.debug("Metric reports will be stored in %s", folder)
163
+ filename = (
164
+ folder
165
+ / f"metric-report-raw-data-{datetime.utcnow().strftime('%Y-%m-%d__%H_%M_%S')}-{short_uid()}.csv"
166
+ )
167
+ with open(filename, "w") as fd:
168
+ LOG.debug("Creating new metric data file %s", filename)
169
+ writer = csv.writer(fd)
170
+ writer.writerow(Metric.RAW_DATA_HEADER)
171
+ return filename
140
172
 
141
173
  def create_metric_handler_item(
142
174
  self, chain: HandlerChain, context: RequestContext, response: Response
@@ -194,7 +226,15 @@ class MetricHandler:
194
226
  )
195
227
  # refrain from adding duplicates
196
228
  if metric not in MetricHandler.metric_data:
197
- MetricHandler.metric_data.append(metric)
229
+ self.append_metric(metric)
198
230
 
199
231
  # cleanup
200
232
  del self.metrics_handler_items[context]
233
+
234
+ def append_metric(self, metric: Metric):
235
+ if self.should_store_metric_locally():
236
+ with open(self.local_filename, "a") as fd:
237
+ writer = csv.writer(fd)
238
+ writer.writerow(metric)
239
+ else:
240
+ MetricHandler.metric_data.append(metric)
@@ -2,22 +2,24 @@
2
2
 
3
3
  import logging
4
4
  import traceback
5
+ import types
5
6
  from collections import defaultdict
6
7
  from typing import Any
7
8
 
8
9
  from botocore.model import OperationModel, ServiceModel
10
+ from plux.core.plugin import PluginDisabled
9
11
 
10
12
  from localstack import config
11
13
  from localstack.http import Response
12
- from localstack.utils.coverage_docs import get_coverage_link_for_service
13
14
 
15
+ from ...utils.coverage_docs import get_coverage_link_for_service
14
16
  from ..api import CommonServiceException, RequestContext, ServiceException
15
17
  from ..api.core import ServiceOperation
16
18
  from ..chain import CompositeResponseHandler, ExceptionHandler, Handler, HandlerChain
17
19
  from ..client import parse_response, parse_service_exception
18
20
  from ..protocol.parser import RequestParser, create_parser
19
21
  from ..protocol.serializer import create_serializer
20
- from ..protocol.service_router import determine_aws_service_model
22
+ from ..protocol.service_router import determine_aws_protocol, determine_aws_service_model
21
23
  from ..skeleton import Skeleton, create_skeleton
22
24
 
23
25
  LOG = logging.getLogger(__name__)
@@ -33,6 +35,8 @@ class ServiceNameParser(Handler):
33
35
  # example). If it is already set, we can skip the parsing of the request. It is very important for S3, because
34
36
  # parsing the request will consume the data stream and prevent streaming.
35
37
  if context.service:
38
+ if not context.protocol:
39
+ context.protocol = determine_aws_protocol(context.request, context.service)
36
40
  return
37
41
 
38
42
  service_model = determine_aws_service_model(context.request)
@@ -41,6 +45,7 @@ class ServiceNameParser(Handler):
41
45
  return
42
46
 
43
47
  context.service = service_model
48
+ context.protocol = determine_aws_protocol(context.request, service_model)
44
49
 
45
50
 
46
51
  class ServiceRequestParser(Handler):
@@ -63,7 +68,7 @@ class ServiceRequestParser(Handler):
63
68
  return self.parse_and_enrich(context)
64
69
 
65
70
  def parse_and_enrich(self, context: RequestContext):
66
- parser = create_parser(context.service)
71
+ parser = create_parser(context.service, context.protocol)
67
72
  operation, instance = parser.parse(context.request)
68
73
 
69
74
  # enrich context
@@ -137,7 +142,7 @@ class ServiceRequestRouter(Handler):
137
142
  operation_name = operation.name
138
143
  message = f"no handler for operation '{operation_name}' on service '{service_name}'"
139
144
  error = CommonServiceException("InternalFailure", message, status_code=501)
140
- serializer = create_serializer(context.service)
145
+ serializer = create_serializer(context.service, context.protocol)
141
146
  return serializer.serialize_error_to_response(
142
147
  error, operation, context.request.headers, context.request_id
143
148
  )
@@ -153,6 +158,15 @@ class ServiceExceptionSerializer(ExceptionHandler):
153
158
  def __init__(self):
154
159
  self.handle_internal_failures = True
155
160
 
161
+ try:
162
+ import moto.core.exceptions
163
+
164
+ self._moto_service_exception = moto.core.exceptions.ServiceException
165
+ except (ModuleNotFoundError, AttributeError) as exc:
166
+ # Moto may not be available in stripped-down versions of LocalStack, like LocalStack S3 image.
167
+ LOG.debug("Unable to set up Moto ServiceException translation: %s", exc)
168
+ self._moto_service_exception = types.EllipsisType
169
+
156
170
  def __call__(
157
171
  self,
158
172
  chain: HandlerChain,
@@ -176,9 +190,16 @@ class ServiceExceptionSerializer(ExceptionHandler):
176
190
  action_name = operation.name
177
191
  exception_message: str | None = exception.args[0] if exception.args else None
178
192
  message = exception_message or get_coverage_link_for_service(service_name, action_name)
179
- LOG.info(message)
180
193
  error = CommonServiceException("InternalFailure", message, status_code=501)
181
- context.service_exception = error
194
+ LOG.info(message)
195
+
196
+ elif isinstance(exception, self._moto_service_exception):
197
+ # Translate Moto ServiceException to native ServiceException if Moto is available.
198
+ # This allows handler chain to gracefully handles Moto errors when provider handlers invoke Moto methods directly.
199
+ error = CommonServiceException(
200
+ code=exception.code,
201
+ message=exception.message,
202
+ )
182
203
 
183
204
  elif not isinstance(exception, ServiceException):
184
205
  if not self.handle_internal_failures:
@@ -202,14 +223,24 @@ class ServiceExceptionSerializer(ExceptionHandler):
202
223
  operation = context.service.operation_model(context.service.operation_names[0])
203
224
  msg = f"exception while calling {service_name} with unknown operation: {message}"
204
225
 
205
- status_code = 501 if config.FAIL_FAST else 500
226
+ # Check for license restricted plugin message and set status code to 501
227
+ if (
228
+ isinstance(exception, PluginDisabled)
229
+ and "not part of the active license agreement"
230
+ in str(getattr(exception, "reason", "")).lower()
231
+ ):
232
+ status_code = 501
233
+ msg = f"exception while calling {service_name}.{operation.name}: {str(getattr(exception, 'reason', ''))}"
234
+ else:
235
+ status_code = 501 if config.FAIL_FAST else 500
206
236
 
207
237
  error = CommonServiceException(
208
238
  "InternalError", msg, status_code=status_code
209
239
  ).with_traceback(exception.__traceback__)
210
- context.service_exception = error
211
240
 
212
- serializer = create_serializer(context.service) # TODO: serializer cache
241
+ context.service_exception = error
242
+
243
+ serializer = create_serializer(context.service, context.protocol)
213
244
  return serializer.serialize_error_to_response(
214
245
  error, operation, context.request.headers, context.request_id
215
246
  )
@@ -252,7 +283,9 @@ class ServiceResponseParser(Handler):
252
283
  return
253
284
 
254
285
  # in this case we need to parse the raw response
255
- parsed = parse_response(context.operation, response, include_response_metadata=False)
286
+ parsed = parse_response(
287
+ context.operation, context.protocol, response, include_response_metadata=False
288
+ )
256
289
  if service_exception := parse_service_exception(response, parsed):
257
290
  context.service_exception = service_exception
258
291
  else: