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
@@ -0,0 +1,138 @@
1
+ import base64
2
+ import json
3
+ from uuid import uuid4
4
+
5
+ from botocore.utils import InvalidArnException
6
+
7
+ from localstack.aws.api.sns import InvalidParameterException
8
+ from localstack.services.sns.constants import E164_REGEX, VALID_SUBSCRIPTION_ATTR_NAME
9
+ from localstack.utils.aws.arns import ArnData, parse_arn
10
+ from localstack.utils.strings import short_uid, to_bytes, to_str
11
+
12
+
13
+ def parse_and_validate_topic_arn(topic_arn: str | None) -> ArnData:
14
+ return _parse_and_validate_arn(topic_arn, "Topic")
15
+
16
+
17
+ def parse_and_validate_platform_application_arn(platform_application_arn: str | None) -> ArnData:
18
+ return _parse_and_validate_arn(platform_application_arn, "PlatformApplication")
19
+
20
+
21
+ def _parse_and_validate_arn(arn: str | None, resource_type: str) -> ArnData:
22
+ arn = arn or ""
23
+ try:
24
+ return parse_arn(arn)
25
+ except InvalidArnException:
26
+ count = len(arn.split(":"))
27
+ raise InvalidParameterException(
28
+ f"Invalid parameter: {resource_type}Arn Reason: An ARN must have at least 6 elements, not {count}"
29
+ )
30
+
31
+
32
+ def is_valid_e164_number(number: str) -> bool:
33
+ return E164_REGEX.match(number) is not None
34
+
35
+
36
+ def validate_subscription_attribute(
37
+ attribute_name: str,
38
+ attribute_value: str,
39
+ topic_arn: str,
40
+ endpoint: str,
41
+ is_subscribe_call: bool = False,
42
+ ) -> None:
43
+ """
44
+ Validate the subscription attribute to be set. See:
45
+ https://docs.aws.amazon.com/sns/latest/api/API_SetSubscriptionAttributes.html
46
+ :param attribute_name: the subscription attribute name, must be in VALID_SUBSCRIPTION_ATTR_NAME
47
+ :param attribute_value: the subscription attribute value
48
+ :param topic_arn: the topic_arn of the subscription, needed to know if it is FIFO
49
+ :param endpoint: the subscription endpoint (like an SQS queue ARN)
50
+ :param is_subscribe_call: the error message is different if called from Subscribe or SetSubscriptionAttributes
51
+ :raises InvalidParameterException
52
+ :return:
53
+ """
54
+ error_prefix = (
55
+ "Invalid parameter: Attributes Reason: " if is_subscribe_call else "Invalid parameter: "
56
+ )
57
+ if attribute_name not in VALID_SUBSCRIPTION_ATTR_NAME:
58
+ raise InvalidParameterException(f"{error_prefix}AttributeName")
59
+
60
+ if attribute_name == "FilterPolicy":
61
+ try:
62
+ json.loads(attribute_value or "{}")
63
+ except json.JSONDecodeError:
64
+ raise InvalidParameterException(f"{error_prefix}FilterPolicy: failed to parse JSON.")
65
+ elif attribute_name == "FilterPolicyScope":
66
+ if attribute_value not in ("MessageAttributes", "MessageBody"):
67
+ raise InvalidParameterException(
68
+ f"{error_prefix}FilterPolicyScope: Invalid value [{attribute_value}]. "
69
+ f"Please use either MessageBody or MessageAttributes"
70
+ )
71
+ elif attribute_name == "RawMessageDelivery":
72
+ # TODO: only for SQS and https(s) subs, + firehose
73
+ if attribute_value.lower() not in ("true", "false"):
74
+ raise InvalidParameterException(
75
+ f"{error_prefix}RawMessageDelivery: Invalid value [{attribute_value}]. "
76
+ f"Must be true or false."
77
+ )
78
+
79
+ elif attribute_name == "RedrivePolicy":
80
+ try:
81
+ dlq_target_arn = json.loads(attribute_value).get("deadLetterTargetArn", "")
82
+ except json.JSONDecodeError:
83
+ raise InvalidParameterException(f"{error_prefix}RedrivePolicy: failed to parse JSON.")
84
+ try:
85
+ parsed_arn = parse_arn(dlq_target_arn)
86
+ except InvalidArnException:
87
+ raise InvalidParameterException(
88
+ f"{error_prefix}RedrivePolicy: deadLetterTargetArn is an invalid arn"
89
+ )
90
+
91
+ if topic_arn.endswith(".fifo"):
92
+ if endpoint.endswith(".fifo") and (
93
+ not parsed_arn["resource"].endswith(".fifo") or "sqs" not in parsed_arn["service"]
94
+ ):
95
+ raise InvalidParameterException(
96
+ f"{error_prefix}RedrivePolicy: must use a FIFO queue as DLQ for a FIFO Subscription to a FIFO Topic."
97
+ )
98
+
99
+
100
+ def create_subscription_arn(topic_arn: str) -> str:
101
+ # This is the format of a Subscription ARN
102
+ # arn:aws:sns:us-west-2:123456789012:my-topic:8a21d249-4329-4871-acc6-7be709c6ea7f
103
+ return f"{topic_arn}:{uuid4()}"
104
+
105
+
106
+ def create_platform_endpoint_arn(
107
+ platform_application_arn: str,
108
+ ) -> str:
109
+ # This is the format of an Endpoint Arn
110
+ # arn:aws:sns:us-west-2:1234567890:endpoint/GCM/MyApplication/12345678-abcd-9012-efgh-345678901234
111
+ return f"{platform_application_arn.replace('app', 'endpoint', 1)}/{uuid4()}"
112
+
113
+
114
+ def encode_subscription_token_with_region(region: str) -> str:
115
+ """
116
+ Create a 64 characters Subscription Token with the region encoded
117
+ :param region:
118
+ :return: a subscription token with the region encoded
119
+ """
120
+ return ((region.encode() + b"/").hex() + short_uid() * 8)[:64]
121
+
122
+
123
+ def get_next_page_token_from_arn(resource_arn: str) -> str:
124
+ return to_str(base64.b64encode(to_bytes(resource_arn)))
125
+
126
+
127
+ def get_region_from_subscription_token(token: str) -> str:
128
+ """
129
+ Try to decode and return the region from a subscription token
130
+ :param token:
131
+ :return: the region if able to decode it
132
+ :raises: InvalidParameterException if the token is invalid
133
+ """
134
+ try:
135
+ region = token.split("2f", maxsplit=1)[0]
136
+ return bytes.fromhex(region).decode("utf-8")
137
+ except (IndexError, ValueError, TypeError, UnicodeDecodeError):
138
+ raise InvalidParameterException("Invalid parameter: Token")
@@ -0,0 +1,205 @@
1
+ import logging
2
+ from typing import Literal
3
+
4
+ from werkzeug import Request as WerkzeugRequest
5
+
6
+ from localstack.aws.api import CommonServiceException, ServiceException
7
+ from localstack.aws.api.sqs import (
8
+ Message,
9
+ QueueAttributeName,
10
+ QueueDoesNotExist,
11
+ ReceiveMessageResult,
12
+ )
13
+ from localstack.aws.protocol.parser import create_parser
14
+ from localstack.aws.protocol.serializer import aws_response_serializer
15
+ from localstack.aws.spec import load_service
16
+ from localstack.http import Request, route
17
+ from localstack.services.sqs.models import (
18
+ FifoQueue,
19
+ SqsMessage,
20
+ SqsQueue,
21
+ StandardQueue,
22
+ sqs_stores,
23
+ to_sqs_api_message,
24
+ )
25
+ from localstack.services.sqs.utils import (
26
+ parse_queue_url,
27
+ )
28
+ from localstack.utils.aws.request_context import extract_region_from_headers
29
+
30
+ LOG = logging.getLogger(__name__)
31
+
32
+
33
+ class InvalidAddress(ServiceException):
34
+ code = "InvalidAddress"
35
+ message = "The address https://queue.amazonaws.com/ is not valid for this endpoint."
36
+ sender_fault = True
37
+ status_code = 404
38
+
39
+
40
+ def get_sqs_protocol(request: Request) -> Literal["query", "json"]:
41
+ content_type = request.headers.get("Content-Type")
42
+ return "json" if content_type == "application/x-amz-json-1.0" else "query"
43
+
44
+
45
+ def sqs_auto_protocol_aws_response_serializer(service_name: str, operation: str):
46
+ def _decorate(fn):
47
+ def _proxy(*args, **kwargs):
48
+ # extract request from function invocation (decorator can be used for methods as well as for functions).
49
+ if len(args) > 0 and isinstance(args[0], WerkzeugRequest):
50
+ # function
51
+ request = args[0]
52
+ elif len(args) > 1 and isinstance(args[1], WerkzeugRequest):
53
+ # method (arg[0] == self)
54
+ request = args[1]
55
+ elif "request" in kwargs:
56
+ request = kwargs["request"]
57
+ else:
58
+ raise ValueError(f"could not find Request in signature of function {fn}")
59
+
60
+ protocol = get_sqs_protocol(request)
61
+ return aws_response_serializer(service_name, operation, protocol)(fn)(*args, **kwargs)
62
+
63
+ return _proxy
64
+
65
+ return _decorate
66
+
67
+
68
+ class SqsDeveloperApi:
69
+ """
70
+ A set of SQS developer tool endpoints:
71
+
72
+ - ``/_aws/sqs/messages``: list SQS messages without side effects, compatible with ``ReceiveMessage``.
73
+ """
74
+
75
+ def __init__(self, stores=None):
76
+ self.stores = stores or sqs_stores
77
+
78
+ @route("/_aws/sqs/messages", methods=["GET", "POST"])
79
+ @sqs_auto_protocol_aws_response_serializer("sqs", "ReceiveMessage")
80
+ def list_messages(self, request: Request) -> ReceiveMessageResult:
81
+ """
82
+ This endpoint expects a ``QueueUrl`` request parameter (either as query arg or form parameter), similar to
83
+ the ``ReceiveMessage`` operation. It will parse the Queue URL generated by one of the SQS endpoint strategies.
84
+ """
85
+
86
+ if "x-amz-" in request.mimetype or "x-www-form-urlencoded" in request.mimetype:
87
+ # only parse the request using a parser if it comes from an AWS client
88
+ protocol = get_sqs_protocol(request)
89
+ operation, service_request = create_parser(
90
+ load_service("sqs", protocol=protocol)
91
+ ).parse(request)
92
+ if operation.name != "ReceiveMessage":
93
+ raise CommonServiceException(
94
+ "InvalidRequest", "This endpoint only accepts ReceiveMessage calls"
95
+ )
96
+ else:
97
+ service_request = dict(request.values)
98
+
99
+ if not service_request.get("QueueUrl"):
100
+ raise QueueDoesNotExist()
101
+
102
+ try:
103
+ account_id, region, queue_name = parse_queue_url(service_request.get("QueueUrl"))
104
+ except ValueError:
105
+ LOG.error(
106
+ "Error while parsing Queue URL from request values: %s",
107
+ service_request.get,
108
+ exc_info=LOG.isEnabledFor(logging.DEBUG),
109
+ )
110
+ raise InvalidAddress()
111
+
112
+ if not region:
113
+ region = extract_region_from_headers(request.headers)
114
+
115
+ return self._get_and_serialize_messages(request, region, account_id, queue_name)
116
+
117
+ @route("/_aws/sqs/messages/<region>/<account_id>/<queue_name>")
118
+ @sqs_auto_protocol_aws_response_serializer("sqs", "ReceiveMessage")
119
+ def list_messages_for_queue_url(
120
+ self, request: Request, region: str, account_id: str, queue_name: str
121
+ ) -> ReceiveMessageResult:
122
+ """
123
+ This endpoint extracts the region, account_id, and queue_name directly from the URL rather than requiring the
124
+ QueueUrl as parameter.
125
+ """
126
+ return self._get_and_serialize_messages(request, region, account_id, queue_name)
127
+
128
+ def _get_and_serialize_messages(
129
+ self,
130
+ request: Request,
131
+ region: str,
132
+ account_id: str,
133
+ queue_name: str,
134
+ ) -> ReceiveMessageResult:
135
+ show_invisible = request.values.get("ShowInvisible", "").lower() in ["true", "1"]
136
+ show_delayed = request.values.get("ShowDelayed", "").lower() in ["true", "1"]
137
+
138
+ try:
139
+ store = self.stores[account_id][region]
140
+ queue = store.queues[queue_name]
141
+ except KeyError:
142
+ LOG.info(
143
+ "no queue named %s in region %s and account %s", queue_name, region, account_id
144
+ )
145
+ raise QueueDoesNotExist()
146
+
147
+ messages = self._collect_messages(
148
+ queue, show_invisible=show_invisible, show_delayed=show_delayed
149
+ )
150
+
151
+ return ReceiveMessageResult(Messages=messages)
152
+
153
+ def _collect_messages(
154
+ self, queue: SqsQueue, show_invisible: bool = False, show_delayed: bool = False
155
+ ) -> list[Message]:
156
+ """
157
+ Retrieves from a given SqsQueue all visible messages without causing any side effects (not setting any
158
+ receive timestamps, receive counts, or visibility state).
159
+
160
+ :param queue: the queue
161
+ :param show_invisible: show invisible messages as well
162
+ :param show_delayed: show delayed messages as well
163
+ :return: a list of messages
164
+ """
165
+ receipt_handle = "SQS/BACKDOOR/ACCESS" # dummy receipt handle
166
+
167
+ sqs_messages: list[SqsMessage] = []
168
+
169
+ if show_invisible:
170
+ sqs_messages.extend(queue.inflight)
171
+
172
+ if isinstance(queue, StandardQueue):
173
+ sqs_messages.extend(queue.visible.queue)
174
+ elif isinstance(queue, FifoQueue):
175
+ if show_invisible:
176
+ for inflight_group in queue.inflight_groups:
177
+ # messages that have been received are held in ``queue.inflight``, even for FIFO queues. however,
178
+ # for fifo queues, messages that are in the same message group as messages that have been
179
+ # received, are also considered invisible, and are held here in ``inflight_group.messages``.
180
+ for sqs_message in inflight_group.messages:
181
+ sqs_messages.append(sqs_message)
182
+
183
+ for message_group in queue.message_group_queue.queue:
184
+ # these are all messages of message groups that are visible
185
+ for sqs_message in message_group.messages:
186
+ sqs_messages.append(sqs_message)
187
+ else:
188
+ raise ValueError(f"unknown queue type {type(queue)}")
189
+
190
+ if show_delayed:
191
+ sqs_messages.extend(queue.delayed)
192
+
193
+ messages = []
194
+
195
+ for sqs_message in sqs_messages:
196
+ message: Message = to_sqs_api_message(sqs_message, [QueueAttributeName.All], ["All"])
197
+ # these are all non-standard fields so we squelch the linter
198
+ if show_invisible:
199
+ message["Attributes"]["IsVisible"] = str(sqs_message.is_visible).lower() # noqa
200
+ if show_delayed:
201
+ message["Attributes"]["IsDelayed"] = str(sqs_message.is_delayed).lower() # noqa
202
+ messages.append(message)
203
+ message["ReceiptHandle"] = receipt_handle
204
+
205
+ return messages
@@ -1,3 +1,4 @@
1
+ import copy
1
2
  import hashlib
2
3
  import heapq
3
4
  import inspect
@@ -15,6 +16,7 @@ from localstack.aws.api.sqs import (
15
16
  AttributeNameList,
16
17
  InvalidAttributeName,
17
18
  Message,
19
+ MessageAttributeNameList,
18
20
  MessageSystemAttributeName,
19
21
  QueueAttributeMap,
20
22
  QueueAttributeName,
@@ -29,12 +31,15 @@ from localstack.services.sqs.exceptions import (
29
31
  )
30
32
  from localstack.services.sqs.queue import InterruptiblePriorityQueue, InterruptibleQueue
31
33
  from localstack.services.sqs.utils import (
34
+ create_message_attribute_hash,
32
35
  encode_move_task_handle,
33
36
  encode_receipt_handle,
34
37
  extract_receipt_handle_info,
35
38
  global_message_sequence,
36
39
  guess_endpoint_strategy_and_host,
37
40
  is_message_deduplication_id_required,
41
+ message_filter_attributes,
42
+ message_filter_message_attributes,
38
43
  )
39
44
  from localstack.services.stores import AccountRegionBundle, BaseStore, LocalAttribute
40
45
  from localstack.utils.aws.arns import get_partition
@@ -153,7 +158,7 @@ class SqsMessage:
153
158
  """
154
159
  Returns false if the message has a visibility deadline that is in the future.
155
160
 
156
- :return: whether the message is visibile or not.
161
+ :return: whether the message is visible or not.
157
162
  """
158
163
  if self.visibility_deadline is None:
159
164
  return True
@@ -190,6 +195,41 @@ class SqsMessage:
190
195
  return f"SqsMessage(id={self.message_id},group={self.message_group_id})"
191
196
 
192
197
 
198
+ def to_sqs_api_message(
199
+ standard_message: SqsMessage,
200
+ attribute_names: AttributeNameList = None,
201
+ message_attribute_names: MessageAttributeNameList = None,
202
+ ) -> Message:
203
+ """
204
+ Utility function to convert an SQS message from LocalStack's internal representation to the AWS API
205
+ concept 'Message', which is the format returned by the ``ReceiveMessage`` operation.
206
+
207
+ :param standard_message: A LocalStack SQS message
208
+ :param attribute_names: the attribute name list to filter
209
+ :param message_attribute_names: the message attribute names to filter
210
+ :return: a copy of the original Message with updated message attributes and MD5 attribute hash sums
211
+ """
212
+ # prepare message for receiver
213
+ message = copy.deepcopy(standard_message.message)
214
+
215
+ # update system attributes of the message copy
216
+ message["Attributes"][MessageSystemAttributeName.ApproximateFirstReceiveTimestamp] = str(
217
+ int((standard_message.first_received or 0) * 1000)
218
+ )
219
+
220
+ # filter attributes for receiver
221
+ message_filter_attributes(message, attribute_names)
222
+ message_filter_message_attributes(message, message_attribute_names)
223
+ if message.get("MessageAttributes"):
224
+ message["MD5OfMessageAttributes"] = create_message_attribute_hash(
225
+ message["MessageAttributes"]
226
+ )
227
+ else:
228
+ # delete the value that was computed when creating the message
229
+ message.pop("MD5OfMessageAttributes", None)
230
+ return message
231
+
232
+
193
233
  class ReceiveMessageResult:
194
234
  """
195
235
  Object to communicate the result of a "receive messages" operation between the SqsProvider and
@@ -274,7 +314,8 @@ class SqsQueue:
274
314
  purge_timestamp: float | None
275
315
 
276
316
  delayed: set[SqsMessage]
277
- inflight: set[SqsMessage]
317
+ # Simulating an ordered set in python. Only the keys are used and of interest.
318
+ inflight: dict[SqsMessage, None]
278
319
  receipts: dict[str, SqsMessage]
279
320
 
280
321
  def __init__(self, name: str, region: str, account_id: str, attributes=None, tags=None) -> None:
@@ -286,7 +327,7 @@ class SqsQueue:
286
327
  self.tags = tags or {}
287
328
 
288
329
  self.delayed = set()
289
- self.inflight = set()
330
+ self.inflight = {}
290
331
  self.receipts = {}
291
332
 
292
333
  self.attributes = self.default_attributes()
@@ -473,7 +514,7 @@ class SqsQueue:
473
514
  )
474
515
  # Terminating the visibility timeout for a message
475
516
  # https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html#terminating-message-visibility-timeout
476
- self.inflight.remove(standard_message)
517
+ del self.inflight[standard_message]
477
518
  self._put_message(standard_message)
478
519
 
479
520
  def remove(self, receipt_handle: str):
@@ -566,9 +607,17 @@ class SqsQueue:
566
607
  standard_message,
567
608
  self.arn,
568
609
  )
569
- self.inflight.remove(standard_message)
610
+ del self.inflight[standard_message]
570
611
  self._put_message(standard_message)
571
612
 
613
+ def add_inflight_message(self, message: SqsMessage):
614
+ """
615
+ We are simulating an ordered set with a dict. When a value is added, it is added as key to the dict, which
616
+ is all we need. Hence all "values" in this ordered set are None
617
+ :param message: The message to put in flight
618
+ """
619
+ self.inflight[message] = None
620
+
572
621
  def enqueue_delayed_messages(self):
573
622
  if not self.delayed:
574
623
  return
@@ -739,7 +788,6 @@ class SqsQueue:
739
788
 
740
789
  class StandardQueue(SqsQueue):
741
790
  visible: InterruptiblePriorityQueue[SqsMessage]
742
- inflight: set[SqsMessage]
743
791
 
744
792
  def __init__(self, name: str, region: str, account_id: str, attributes=None, tags=None) -> None:
745
793
  super().__init__(name, region, account_id, attributes, tags)
@@ -883,13 +931,13 @@ class StandardQueue(SqsQueue):
883
931
  if message.visibility_timeout == 0:
884
932
  self.visible.put_nowait(message)
885
933
  else:
886
- self.inflight.add(message)
934
+ self.add_inflight_message(message)
887
935
 
888
936
  return result
889
937
 
890
938
  def _on_remove_message(self, message: SqsMessage):
891
939
  try:
892
- self.inflight.remove(message)
940
+ del self.inflight[message]
893
941
  except KeyError:
894
942
  # this likely means the message was removed with an expired receipt handle unfortunately this
895
943
  # means we need to scan the queue for the element and remove it from there, and then re-heapify
@@ -1013,8 +1061,7 @@ class FifoQueue(SqsQueue):
1013
1061
  message.delay_seconds = value
1014
1062
 
1015
1063
  def _pre_delete_checks(self, message: SqsMessage, receipt_handle: str) -> None:
1016
- _, _, _, last_received = extract_receipt_handle_info(receipt_handle)
1017
- if time.time() - float(last_received) > message.visibility_timeout:
1064
+ if message.is_visible:
1018
1065
  raise InvalidParameterValueException(
1019
1066
  f"Value {receipt_handle} for parameter ReceiptHandle is invalid. Reason: The receipt handle has expired."
1020
1067
  )
@@ -1110,6 +1157,26 @@ class FifoQueue(SqsQueue):
1110
1157
  elif previously_empty:
1111
1158
  self.message_group_queue.put_nowait(message_group)
1112
1159
 
1160
+ def requeue_inflight_messages(self):
1161
+ if not self.inflight:
1162
+ return
1163
+
1164
+ with self.mutex:
1165
+ messages = list(self.inflight)
1166
+ for standard_message in messages:
1167
+ # in fifo, an invisible message blocks potentially visible messages afterwards
1168
+ # this can happen for example if multiple message of the same group are received at once, then one
1169
+ # message of this batch has its visibility timeout extended
1170
+ if not standard_message.is_visible:
1171
+ return
1172
+ LOG.debug(
1173
+ "re-queueing inflight messages %s into queue %s",
1174
+ standard_message,
1175
+ self.arn,
1176
+ )
1177
+ del self.inflight[standard_message]
1178
+ self._put_message(standard_message)
1179
+
1113
1180
  def remove_expired_messages(self):
1114
1181
  with self.mutex:
1115
1182
  retention_period = self.message_retention_period
@@ -1239,8 +1306,7 @@ class FifoQueue(SqsQueue):
1239
1306
  if message.visibility_timeout == 0:
1240
1307
  self._put_message(message)
1241
1308
  else:
1242
- self.inflight.add(message)
1243
-
1309
+ self.add_inflight_message(message)
1244
1310
  return result
1245
1311
 
1246
1312
  def _on_remove_message(self, message: SqsMessage):
@@ -1249,7 +1315,7 @@ class FifoQueue(SqsQueue):
1249
1315
 
1250
1316
  with self.mutex:
1251
1317
  try:
1252
- self.inflight.remove(message)
1318
+ del self.inflight[message]
1253
1319
  except KeyError:
1254
1320
  # in FIFO queues, this should not happen, as expired receipt handles cannot be used to
1255
1321
  # delete a message.