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.
- localstack/aws/api/acm/__init__.py +122 -122
- localstack/aws/api/apigateway/__init__.py +560 -559
- localstack/aws/api/cloudcontrol/__init__.py +63 -63
- localstack/aws/api/cloudformation/__init__.py +1041 -969
- localstack/aws/api/cloudwatch/__init__.py +408 -368
- localstack/aws/api/config/__init__.py +788 -786
- localstack/aws/api/core.py +4 -0
- localstack/aws/api/dynamodb/__init__.py +753 -759
- localstack/aws/api/dynamodbstreams/__init__.py +74 -74
- localstack/aws/api/ec2/__init__.py +9713 -8573
- localstack/aws/api/es/__init__.py +453 -453
- localstack/aws/api/events/__init__.py +552 -552
- localstack/aws/api/firehose/__init__.py +541 -543
- localstack/aws/api/iam/__init__.py +646 -572
- localstack/aws/api/kinesis/__init__.py +251 -144
- localstack/aws/api/kms/__init__.py +343 -333
- localstack/aws/api/lambda_/__init__.py +585 -571
- localstack/aws/api/logs/__init__.py +682 -666
- localstack/aws/api/opensearch/__init__.py +814 -785
- localstack/aws/api/pipes/__init__.py +336 -336
- localstack/aws/api/redshift/__init__.py +1192 -1164
- localstack/aws/api/resource_groups/__init__.py +175 -175
- localstack/aws/api/resourcegroupstaggingapi/__init__.py +67 -67
- localstack/aws/api/route53/__init__.py +256 -254
- localstack/aws/api/route53resolver/__init__.py +396 -396
- localstack/aws/api/s3/__init__.py +1358 -1345
- localstack/aws/api/s3control/__init__.py +616 -584
- localstack/aws/api/scheduler/__init__.py +118 -118
- localstack/aws/api/secretsmanager/__init__.py +193 -193
- localstack/aws/api/ses/__init__.py +227 -227
- localstack/aws/api/sns/__init__.py +115 -115
- localstack/aws/api/sqs/__init__.py +100 -100
- localstack/aws/api/ssm/__init__.py +1978 -1970
- localstack/aws/api/stepfunctions/__init__.py +323 -323
- localstack/aws/api/sts/__init__.py +90 -66
- localstack/aws/api/support/__init__.py +112 -112
- localstack/aws/api/swf/__init__.py +378 -386
- localstack/aws/api/transcribe/__init__.py +425 -425
- localstack/aws/client.py +7 -2
- localstack/aws/forwarder.py +52 -5
- localstack/aws/handlers/analytics.py +1 -1
- localstack/aws/handlers/logging.py +12 -2
- localstack/aws/handlers/metric_handler.py +41 -1
- localstack/aws/handlers/service.py +43 -10
- localstack/aws/protocol/parser.py +440 -21
- localstack/aws/protocol/serializer.py +684 -64
- localstack/aws/protocol/service_router.py +120 -20
- localstack/aws/scaffold.py +15 -17
- localstack/aws/skeleton.py +4 -2
- localstack/aws/spec-patches.json +58 -0
- localstack/aws/spec.py +33 -13
- localstack/cli/exceptions.py +1 -1
- localstack/cli/localstack.py +10 -5
- localstack/cli/lpm.py +3 -4
- localstack/cli/profiles.py +1 -2
- localstack/config.py +18 -12
- localstack/constants.py +4 -29
- localstack/dev/kubernetes/__main__.py +39 -4
- localstack/dev/run/paths.py +1 -1
- localstack/dns/plugins.py +5 -1
- localstack/dns/server.py +12 -3
- localstack/packages/api.py +9 -8
- localstack/packages/core.py +2 -2
- localstack/packages/plugins.py +0 -8
- localstack/runtime/init.py +1 -1
- localstack/services/apigateway/helpers.py +5 -9
- localstack/services/apigateway/legacy/provider.py +85 -12
- localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
- localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
- localstack/services/apigateway/next_gen/execute_api/test_invoke.py +50 -6
- localstack/services/apigateway/next_gen/provider.py +5 -0
- localstack/services/apigateway/patches.py +0 -9
- localstack/services/cloudformation/engine/entities.py +12 -1
- localstack/services/cloudformation/engine/v2/change_set_model.py +0 -3
- localstack/services/cloudformation/engine/v2/change_set_model_describer.py +14 -0
- localstack/services/cloudformation/engine/v2/change_set_model_executor.py +13 -15
- localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +118 -24
- localstack/services/cloudformation/engine/v2/change_set_model_transform.py +4 -1
- localstack/services/cloudformation/engine/v2/change_set_model_validator.py +5 -14
- localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
- localstack/services/cloudformation/engine/v2/resolving.py +6 -4
- localstack/services/cloudformation/engine/yaml_parser.py +9 -2
- localstack/services/cloudformation/provider.py +2 -2
- localstack/services/cloudformation/resource_provider.py +5 -1
- localstack/services/cloudformation/resources.py +24149 -0
- localstack/services/cloudformation/v2/entities.py +6 -3
- localstack/services/cloudformation/v2/provider.py +178 -33
- localstack/services/cloudformation/v2/types.py +8 -4
- localstack/services/cloudwatch/provider_v2.py +25 -28
- localstack/services/dynamodb/packages.py +2 -1
- localstack/services/dynamodb/provider.py +42 -0
- localstack/services/dynamodb/v2/provider.py +42 -0
- localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
- localstack/services/es/provider.py +2 -2
- localstack/services/events/event_rule_engine.py +31 -13
- localstack/services/events/models.py +4 -5
- localstack/services/events/target.py +17 -9
- localstack/services/iam/provider.py +11 -116
- localstack/services/iam/resources/policy_simulator.py +133 -0
- localstack/services/kinesis/models.py +15 -2
- localstack/services/kinesis/packages.py +1 -1
- localstack/services/kinesis/provider.py +77 -0
- localstack/services/kms/models.py +34 -4
- localstack/services/kms/provider.py +107 -21
- localstack/services/lambda_/api_utils.py +3 -1
- localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
- localstack/services/lambda_/packages.py +1 -1
- localstack/services/lambda_/provider.py +1 -1
- localstack/services/lambda_/runtimes.py +8 -3
- localstack/services/logs/provider.py +36 -19
- localstack/services/moto.py +2 -1
- localstack/services/opensearch/cluster.py +15 -7
- localstack/services/opensearch/packages.py +26 -7
- localstack/services/opensearch/provider.py +6 -1
- localstack/services/opensearch/versions.py +56 -7
- localstack/services/s3/constants.py +5 -2
- localstack/services/s3/cors.py +4 -4
- localstack/services/s3/notifications.py +1 -1
- localstack/services/s3/presigned_url.py +27 -43
- localstack/services/s3/provider.py +68 -12
- localstack/services/s3/utils.py +42 -11
- localstack/services/ses/provider.py +16 -7
- localstack/services/sns/constants.py +7 -1
- localstack/services/sns/v2/models.py +190 -0
- localstack/services/sns/v2/provider.py +992 -2
- localstack/services/sns/v2/utils.py +138 -0
- localstack/services/sqs/developer_api.py +205 -0
- localstack/services/sqs/models.py +79 -13
- localstack/services/sqs/provider.py +8 -309
- localstack/services/sqs/query_api.py +1 -1
- localstack/services/sqs/utils.py +121 -2
- localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
- localstack/testing/aws/cloudformation_utils.py +1 -1
- localstack/testing/pytest/cloudformation/fixtures.py +3 -3
- localstack/testing/pytest/container.py +4 -5
- localstack/testing/pytest/fixtures.py +20 -19
- localstack/testing/pytest/in_memory_localstack.py +0 -4
- localstack/testing/pytest/marking.py +13 -4
- localstack/testing/pytest/stepfunctions/utils.py +4 -3
- localstack/testing/pytest/util.py +1 -1
- localstack/testing/pytest/validation_tracking.py +1 -2
- localstack/testing/snapshots/transformer_utility.py +7 -0
- localstack/testing/testselection/matching.py +0 -1
- localstack/utils/analytics/events.py +2 -2
- localstack/utils/analytics/metadata.py +1 -2
- localstack/utils/analytics/metrics/counter.py +6 -8
- localstack/utils/analytics/publisher.py +1 -2
- localstack/utils/analytics/service_request_aggregator.py +2 -2
- localstack/utils/archives.py +11 -11
- localstack/utils/aws/arns.py +17 -9
- localstack/utils/aws/aws_responses.py +7 -7
- localstack/utils/aws/aws_stack.py +2 -3
- localstack/utils/aws/client_types.py +0 -8
- localstack/utils/aws/message_forwarding.py +1 -2
- localstack/utils/aws/request_context.py +4 -5
- localstack/utils/batch_policy.py +3 -3
- localstack/utils/bootstrap.py +7 -7
- localstack/utils/catalog/catalog.py +139 -0
- localstack/utils/catalog/catalog_loader.py +119 -0
- localstack/utils/catalog/common.py +58 -0
- localstack/utils/catalog/plugins.py +28 -0
- localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
- localstack/utils/collections.py +7 -8
- localstack/utils/config_listener.py +1 -1
- localstack/utils/container_networking.py +2 -3
- localstack/utils/container_utils/container_client.py +115 -131
- localstack/utils/container_utils/docker_cmd_client.py +42 -42
- localstack/utils/container_utils/docker_sdk_client.py +63 -62
- localstack/utils/crypto.py +109 -0
- localstack/utils/diagnose.py +2 -3
- localstack/utils/docker_utils.py +3 -4
- localstack/utils/files.py +31 -7
- localstack/utils/functions.py +3 -2
- localstack/utils/http.py +4 -5
- localstack/utils/json.py +19 -5
- localstack/utils/kinesis/kinesis_connector.py +2 -1
- localstack/utils/net.py +6 -6
- localstack/utils/no_exit_argument_parser.py +2 -2
- localstack/utils/numbers.py +9 -2
- localstack/utils/objects.py +6 -5
- localstack/utils/patch.py +2 -1
- localstack/utils/run.py +10 -9
- localstack/utils/scheduler.py +11 -11
- localstack/utils/server/tcp_proxy.py +2 -2
- localstack/utils/serving.py +2 -3
- localstack/utils/strings.py +10 -11
- localstack/utils/sync.py +126 -1
- localstack/utils/tagging.py +1 -4
- localstack/utils/testutil.py +5 -4
- localstack/utils/threads.py +2 -2
- localstack/utils/time.py +11 -3
- localstack/utils/urls.py +1 -3
- localstack/version.py +2 -2
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/METADATA +19 -13
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/RECORD +203 -199
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/entry_points.txt +4 -2
- localstack_core-4.10.1.dev42.dist-info/plux.json +1 -0
- localstack/packages/terraform.py +0 -46
- localstack/services/cloudformation/deploy.html +0 -144
- localstack/services/cloudformation/deploy_ui.py +0 -47
- localstack/services/cloudformation/plugins.py +0 -12
- localstack_core-4.7.1.dev139.dist-info/plux.json +0 -1
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack +0 -0
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack-supervisor +0 -0
- {localstack_core-4.7.1.dev139.data → localstack_core-4.10.1.dev42.data}/scripts/localstack.bat +0 -0
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/WHEEL +0 -0
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/licenses/LICENSE.txt +0 -0
- {localstack_core-4.7.1.dev139.dist-info → localstack_core-4.10.1.dev42.dist-info}/top_level.txt +0 -0
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import copy
|
|
2
|
-
import hashlib
|
|
3
1
|
import json
|
|
4
2
|
import logging
|
|
5
3
|
import re
|
|
@@ -8,15 +6,12 @@ import time
|
|
|
8
6
|
from collections.abc import Iterable
|
|
9
7
|
from concurrent.futures.thread import ThreadPoolExecutor
|
|
10
8
|
from itertools import islice
|
|
11
|
-
from typing import Literal
|
|
12
9
|
|
|
13
10
|
from botocore.utils import InvalidArnException
|
|
14
|
-
from moto.sqs.models import BINARY_TYPE_FIELD_INDEX, STRING_TYPE_FIELD_INDEX
|
|
15
|
-
from moto.sqs.models import Message as MotoMessage
|
|
16
11
|
from werkzeug import Request as WerkzeugRequest
|
|
17
12
|
|
|
18
13
|
from localstack import config
|
|
19
|
-
from localstack.aws.api import
|
|
14
|
+
from localstack.aws.api import RequestContext, ServiceException
|
|
20
15
|
from localstack.aws.api.sqs import (
|
|
21
16
|
ActionNameList,
|
|
22
17
|
AttributeNameList,
|
|
@@ -70,11 +65,8 @@ from localstack.aws.api.sqs import (
|
|
|
70
65
|
Token,
|
|
71
66
|
TooManyEntriesInBatchRequest,
|
|
72
67
|
)
|
|
73
|
-
from localstack.aws.protocol.parser import create_parser
|
|
74
|
-
from localstack.aws.protocol.serializer import aws_response_serializer
|
|
75
68
|
from localstack.aws.spec import load_service
|
|
76
69
|
from localstack.config import SQS_DISABLE_MAX_NUMBER_OF_MESSAGE_LIMIT
|
|
77
|
-
from localstack.http import Request, route
|
|
78
70
|
from localstack.services.edge import ROUTER
|
|
79
71
|
from localstack.services.plugins import ServiceLifecycleHook
|
|
80
72
|
from localstack.services.sqs import constants as sqs_constants
|
|
@@ -84,6 +76,7 @@ from localstack.services.sqs.constants import (
|
|
|
84
76
|
HEADER_LOCALSTACK_SQS_OVERRIDE_WAIT_TIME_SECONDS,
|
|
85
77
|
MAX_RESULT_LIMIT,
|
|
86
78
|
)
|
|
79
|
+
from localstack.services.sqs.developer_api import SqsDeveloperApi
|
|
87
80
|
from localstack.services.sqs.exceptions import (
|
|
88
81
|
InvalidParameterValueException,
|
|
89
82
|
MissingRequiredParameterException,
|
|
@@ -97,8 +90,10 @@ from localstack.services.sqs.models import (
|
|
|
97
90
|
SqsStore,
|
|
98
91
|
StandardQueue,
|
|
99
92
|
sqs_stores,
|
|
93
|
+
to_sqs_api_message,
|
|
100
94
|
)
|
|
101
95
|
from localstack.services.sqs.utils import (
|
|
96
|
+
create_message_attribute_hash,
|
|
102
97
|
decode_move_task_handle,
|
|
103
98
|
generate_message_id,
|
|
104
99
|
is_fifo_queue,
|
|
@@ -107,7 +102,6 @@ from localstack.services.sqs.utils import (
|
|
|
107
102
|
)
|
|
108
103
|
from localstack.services.stores import AccountRegionBundle
|
|
109
104
|
from localstack.utils.aws.arns import parse_arn
|
|
110
|
-
from localstack.utils.aws.request_context import extract_region_from_headers
|
|
111
105
|
from localstack.utils.bootstrap import is_api_enabled
|
|
112
106
|
from localstack.utils.cloudwatch.cloudwatch_util import (
|
|
113
107
|
SqsMetricBatchData,
|
|
@@ -127,13 +121,6 @@ MAX_NUMBER_OF_MESSAGES = 10
|
|
|
127
121
|
_STORE_LOCK = threading.RLock()
|
|
128
122
|
|
|
129
123
|
|
|
130
|
-
class InvalidAddress(ServiceException):
|
|
131
|
-
code = "InvalidAddress"
|
|
132
|
-
message = "The address https://queue.amazonaws.com/ is not valid for this endpoint."
|
|
133
|
-
sender_fault = True
|
|
134
|
-
status_code = 404
|
|
135
|
-
|
|
136
|
-
|
|
137
124
|
def assert_queue_name(queue_name: str, fifo: bool = False):
|
|
138
125
|
if queue_name.endswith(".fifo"):
|
|
139
126
|
if not fifo:
|
|
@@ -640,165 +627,6 @@ def check_fifo_id(fifo_id: str | None, parameter: str):
|
|
|
640
627
|
)
|
|
641
628
|
|
|
642
629
|
|
|
643
|
-
def get_sqs_protocol(request: Request) -> Literal["query", "json"]:
|
|
644
|
-
content_type = request.headers.get("Content-Type")
|
|
645
|
-
return "json" if content_type == "application/x-amz-json-1.0" else "query"
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
def sqs_auto_protocol_aws_response_serializer(service_name: str, operation: str):
|
|
649
|
-
def _decorate(fn):
|
|
650
|
-
def _proxy(*args, **kwargs):
|
|
651
|
-
# extract request from function invocation (decorator can be used for methods as well as for functions).
|
|
652
|
-
if len(args) > 0 and isinstance(args[0], WerkzeugRequest):
|
|
653
|
-
# function
|
|
654
|
-
request = args[0]
|
|
655
|
-
elif len(args) > 1 and isinstance(args[1], WerkzeugRequest):
|
|
656
|
-
# method (arg[0] == self)
|
|
657
|
-
request = args[1]
|
|
658
|
-
elif "request" in kwargs:
|
|
659
|
-
request = kwargs["request"]
|
|
660
|
-
else:
|
|
661
|
-
raise ValueError(f"could not find Request in signature of function {fn}")
|
|
662
|
-
|
|
663
|
-
protocol = get_sqs_protocol(request)
|
|
664
|
-
return aws_response_serializer(service_name, operation, protocol)(fn)(*args, **kwargs)
|
|
665
|
-
|
|
666
|
-
return _proxy
|
|
667
|
-
|
|
668
|
-
return _decorate
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
class SqsDeveloperEndpoints:
|
|
672
|
-
"""
|
|
673
|
-
A set of SQS developer tool endpoints:
|
|
674
|
-
|
|
675
|
-
- ``/_aws/sqs/messages``: list SQS messages without side effects, compatible with ``ReceiveMessage``.
|
|
676
|
-
"""
|
|
677
|
-
|
|
678
|
-
def __init__(self, stores=None):
|
|
679
|
-
self.stores = stores or sqs_stores
|
|
680
|
-
|
|
681
|
-
@route("/_aws/sqs/messages", methods=["GET", "POST"])
|
|
682
|
-
@sqs_auto_protocol_aws_response_serializer("sqs", "ReceiveMessage")
|
|
683
|
-
def list_messages(self, request: Request) -> ReceiveMessageResult:
|
|
684
|
-
"""
|
|
685
|
-
This endpoint expects a ``QueueUrl`` request parameter (either as query arg or form parameter), similar to
|
|
686
|
-
the ``ReceiveMessage`` operation. It will parse the Queue URL generated by one of the SQS endpoint strategies.
|
|
687
|
-
"""
|
|
688
|
-
|
|
689
|
-
if "x-amz-" in request.mimetype or "x-www-form-urlencoded" in request.mimetype:
|
|
690
|
-
# only parse the request using a parser if it comes from an AWS client
|
|
691
|
-
protocol = get_sqs_protocol(request)
|
|
692
|
-
operation, service_request = create_parser(
|
|
693
|
-
load_service("sqs", protocol=protocol)
|
|
694
|
-
).parse(request)
|
|
695
|
-
if operation.name != "ReceiveMessage":
|
|
696
|
-
raise CommonServiceException(
|
|
697
|
-
"InvalidRequest", "This endpoint only accepts ReceiveMessage calls"
|
|
698
|
-
)
|
|
699
|
-
else:
|
|
700
|
-
service_request = dict(request.values)
|
|
701
|
-
|
|
702
|
-
if not service_request.get("QueueUrl"):
|
|
703
|
-
raise QueueDoesNotExist()
|
|
704
|
-
|
|
705
|
-
try:
|
|
706
|
-
account_id, region, queue_name = parse_queue_url(service_request.get("QueueUrl"))
|
|
707
|
-
except ValueError:
|
|
708
|
-
LOG.error(
|
|
709
|
-
"Error while parsing Queue URL from request values: %s",
|
|
710
|
-
service_request.get,
|
|
711
|
-
exc_info=LOG.isEnabledFor(logging.DEBUG),
|
|
712
|
-
)
|
|
713
|
-
raise InvalidAddress()
|
|
714
|
-
|
|
715
|
-
if not region:
|
|
716
|
-
region = extract_region_from_headers(request.headers)
|
|
717
|
-
|
|
718
|
-
return self._get_and_serialize_messages(request, region, account_id, queue_name)
|
|
719
|
-
|
|
720
|
-
@route("/_aws/sqs/messages/<region>/<account_id>/<queue_name>")
|
|
721
|
-
@sqs_auto_protocol_aws_response_serializer("sqs", "ReceiveMessage")
|
|
722
|
-
def list_messages_for_queue_url(
|
|
723
|
-
self, request: Request, region: str, account_id: str, queue_name: str
|
|
724
|
-
) -> ReceiveMessageResult:
|
|
725
|
-
"""
|
|
726
|
-
This endpoint extracts the region, account_id, and queue_name directly from the URL rather than requiring the
|
|
727
|
-
QueueUrl as parameter.
|
|
728
|
-
"""
|
|
729
|
-
return self._get_and_serialize_messages(request, region, account_id, queue_name)
|
|
730
|
-
|
|
731
|
-
def _get_and_serialize_messages(
|
|
732
|
-
self,
|
|
733
|
-
request: Request,
|
|
734
|
-
region: str,
|
|
735
|
-
account_id: str,
|
|
736
|
-
queue_name: str,
|
|
737
|
-
) -> ReceiveMessageResult:
|
|
738
|
-
show_invisible = request.values.get("ShowInvisible", "").lower() in ["true", "1"]
|
|
739
|
-
show_delayed = request.values.get("ShowDelayed", "").lower() in ["true", "1"]
|
|
740
|
-
|
|
741
|
-
try:
|
|
742
|
-
store = SqsProvider.get_store(account_id, region)
|
|
743
|
-
queue = store.queues[queue_name]
|
|
744
|
-
except KeyError:
|
|
745
|
-
LOG.info(
|
|
746
|
-
"no queue named %s in region %s and account %s", queue_name, region, account_id
|
|
747
|
-
)
|
|
748
|
-
raise QueueDoesNotExist()
|
|
749
|
-
|
|
750
|
-
messages = self._collect_messages(
|
|
751
|
-
queue, show_invisible=show_invisible, show_delayed=show_delayed
|
|
752
|
-
)
|
|
753
|
-
|
|
754
|
-
return ReceiveMessageResult(Messages=messages)
|
|
755
|
-
|
|
756
|
-
def _collect_messages(
|
|
757
|
-
self, queue: SqsQueue, show_invisible: bool = False, show_delayed: bool = False
|
|
758
|
-
) -> list[Message]:
|
|
759
|
-
"""
|
|
760
|
-
Retrieves from a given SqsQueue all visible messages without causing any side effects (not setting any
|
|
761
|
-
receive timestamps, receive counts, or visibility state).
|
|
762
|
-
|
|
763
|
-
:param queue: the queue
|
|
764
|
-
:param show_invisible: show invisible messages as well
|
|
765
|
-
:param show_delayed: show delayed messages as well
|
|
766
|
-
:return: a list of messages
|
|
767
|
-
"""
|
|
768
|
-
receipt_handle = "SQS/BACKDOOR/ACCESS" # dummy receipt handle
|
|
769
|
-
|
|
770
|
-
sqs_messages: list[SqsMessage] = []
|
|
771
|
-
|
|
772
|
-
if show_invisible:
|
|
773
|
-
sqs_messages.extend(queue.inflight)
|
|
774
|
-
|
|
775
|
-
if isinstance(queue, StandardQueue):
|
|
776
|
-
sqs_messages.extend(queue.visible.queue)
|
|
777
|
-
elif isinstance(queue, FifoQueue):
|
|
778
|
-
for message_group in queue.message_groups.values():
|
|
779
|
-
for sqs_message in message_group.messages:
|
|
780
|
-
sqs_messages.append(sqs_message)
|
|
781
|
-
else:
|
|
782
|
-
raise ValueError(f"unknown queue type {type(queue)}")
|
|
783
|
-
|
|
784
|
-
if show_delayed:
|
|
785
|
-
sqs_messages.extend(queue.delayed)
|
|
786
|
-
|
|
787
|
-
messages = []
|
|
788
|
-
|
|
789
|
-
for sqs_message in sqs_messages:
|
|
790
|
-
message: Message = to_sqs_api_message(sqs_message, [QueueAttributeName.All], ["All"])
|
|
791
|
-
# these are all non-standard fields so we squelch the linter
|
|
792
|
-
if show_invisible:
|
|
793
|
-
message["Attributes"]["IsVisible"] = str(sqs_message.is_visible).lower() # noqa
|
|
794
|
-
if show_delayed:
|
|
795
|
-
message["Attributes"]["IsDelayed"] = str(sqs_message.is_delayed).lower() # noqa
|
|
796
|
-
messages.append(message)
|
|
797
|
-
message["ReceiptHandle"] = receipt_handle
|
|
798
|
-
|
|
799
|
-
return messages
|
|
800
|
-
|
|
801
|
-
|
|
802
630
|
class SqsProvider(SqsApi, ServiceLifecycleHook):
|
|
803
631
|
"""
|
|
804
632
|
LocalStack SQS Provider.
|
|
@@ -840,7 +668,6 @@ class SqsProvider(SqsApi, ServiceLifecycleHook):
|
|
|
840
668
|
# default are limited to 500kb payload size by Werkzeug. we make sure we only *increase* the limit if it's
|
|
841
669
|
# already set, and if it's already set to unlimited we leave it.
|
|
842
670
|
from rolo import Request as RoloRequest
|
|
843
|
-
from werkzeug import Request as WerkzeugRequest
|
|
844
671
|
|
|
845
672
|
# needed for the webserver integration (webservers create Werkzeug request objects)
|
|
846
673
|
if WerkzeugRequest.max_form_memory_size is not None:
|
|
@@ -855,7 +682,7 @@ class SqsProvider(SqsApi, ServiceLifecycleHook):
|
|
|
855
682
|
|
|
856
683
|
def on_before_start(self):
|
|
857
684
|
query_api.register(ROUTER)
|
|
858
|
-
self._router_rules = ROUTER.add(
|
|
685
|
+
self._router_rules = ROUTER.add(SqsDeveloperApi())
|
|
859
686
|
self._queue_update_worker.start()
|
|
860
687
|
self._start_cloudwatch_metrics_reporting()
|
|
861
688
|
|
|
@@ -1150,7 +977,7 @@ class SqsProvider(SqsApi, ServiceLifecycleHook):
|
|
|
1150
977
|
MD5OfMessageBody=message["MD5OfBody"],
|
|
1151
978
|
MD5OfMessageAttributes=message.get("MD5OfMessageAttributes"),
|
|
1152
979
|
SequenceNumber=queue_item.sequence_number,
|
|
1153
|
-
MD5OfMessageSystemAttributes=
|
|
980
|
+
MD5OfMessageSystemAttributes=create_message_attribute_hash(message_system_attributes),
|
|
1154
981
|
)
|
|
1155
982
|
|
|
1156
983
|
def send_message_batch(
|
|
@@ -1196,7 +1023,7 @@ class SqsProvider(SqsApi, ServiceLifecycleHook):
|
|
|
1196
1023
|
MessageId=message.get("MessageId"),
|
|
1197
1024
|
MD5OfMessageBody=message.get("MD5OfBody"),
|
|
1198
1025
|
MD5OfMessageAttributes=message.get("MD5OfMessageAttributes"),
|
|
1199
|
-
MD5OfMessageSystemAttributes=
|
|
1026
|
+
MD5OfMessageSystemAttributes=create_message_attribute_hash(
|
|
1200
1027
|
message.get("message_system_attributes")
|
|
1201
1028
|
),
|
|
1202
1029
|
SequenceNumber=queue_item.sequence_number,
|
|
@@ -1250,7 +1077,7 @@ class SqsProvider(SqsApi, ServiceLifecycleHook):
|
|
|
1250
1077
|
MD5OfBody=md5(message_body),
|
|
1251
1078
|
Body=message_body,
|
|
1252
1079
|
Attributes=self._create_message_attributes(context, message_system_attributes),
|
|
1253
|
-
MD5OfMessageAttributes=
|
|
1080
|
+
MD5OfMessageAttributes=create_message_attribute_hash(message_attributes),
|
|
1254
1081
|
MessageAttributes=message_attributes,
|
|
1255
1082
|
)
|
|
1256
1083
|
if self._cloudwatch_dispatcher:
|
|
@@ -1810,34 +1637,6 @@ class SqsProvider(SqsApi, ServiceLifecycleHook):
|
|
|
1810
1637
|
self._cloudwatch_dispatcher.shutdown()
|
|
1811
1638
|
|
|
1812
1639
|
|
|
1813
|
-
# Method from moto's attribute_md5 of moto/sqs/models.py, separated from the Message Object
|
|
1814
|
-
def _create_message_attribute_hash(message_attributes) -> str | None:
|
|
1815
|
-
# To avoid the need to check for dict conformity everytime we invoke this function
|
|
1816
|
-
if not isinstance(message_attributes, dict):
|
|
1817
|
-
return
|
|
1818
|
-
hash = hashlib.md5()
|
|
1819
|
-
|
|
1820
|
-
for attrName in sorted(message_attributes.keys()):
|
|
1821
|
-
attr_value = message_attributes[attrName]
|
|
1822
|
-
# Encode name
|
|
1823
|
-
MotoMessage.update_binary_length_and_value(hash, MotoMessage.utf8(attrName))
|
|
1824
|
-
# Encode data type
|
|
1825
|
-
MotoMessage.update_binary_length_and_value(hash, MotoMessage.utf8(attr_value["DataType"]))
|
|
1826
|
-
# Encode transport type and value
|
|
1827
|
-
if attr_value.get("StringValue"):
|
|
1828
|
-
hash.update(bytearray([STRING_TYPE_FIELD_INDEX]))
|
|
1829
|
-
MotoMessage.update_binary_length_and_value(
|
|
1830
|
-
hash, MotoMessage.utf8(attr_value.get("StringValue"))
|
|
1831
|
-
)
|
|
1832
|
-
elif attr_value.get("BinaryValue"):
|
|
1833
|
-
hash.update(bytearray([BINARY_TYPE_FIELD_INDEX]))
|
|
1834
|
-
decoded_binary_value = attr_value.get("BinaryValue")
|
|
1835
|
-
MotoMessage.update_binary_length_and_value(hash, decoded_binary_value)
|
|
1836
|
-
# string_list_value, binary_list_value type is not implemented, reserved for the future use.
|
|
1837
|
-
# See https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_MessageAttributeValue.html
|
|
1838
|
-
return hash.hexdigest()
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
1640
|
def resolve_queue_location(
|
|
1842
1641
|
context: RequestContext, queue_name: str | None = None, queue_url: str | None = None
|
|
1843
1642
|
) -> tuple[str, str | None, str]:
|
|
@@ -1862,106 +1661,6 @@ def resolve_queue_location(
|
|
|
1862
1661
|
return context.account_id, context.region, queue_name
|
|
1863
1662
|
|
|
1864
1663
|
|
|
1865
|
-
def to_sqs_api_message(
|
|
1866
|
-
standard_message: SqsMessage,
|
|
1867
|
-
attribute_names: AttributeNameList = None,
|
|
1868
|
-
message_attribute_names: MessageAttributeNameList = None,
|
|
1869
|
-
) -> Message:
|
|
1870
|
-
"""
|
|
1871
|
-
Utility function to convert an SQS message from LocalStack's internal representation to the AWS API
|
|
1872
|
-
concept 'Message', which is the format returned by the ``ReceiveMessage`` operation.
|
|
1873
|
-
|
|
1874
|
-
:param standard_message: A LocalStack SQS message
|
|
1875
|
-
:param attribute_names: the attribute name list to filter
|
|
1876
|
-
:param message_attribute_names: the message attribute names to filter
|
|
1877
|
-
:return: a copy of the original Message with updated message attributes and MD5 attribute hash sums
|
|
1878
|
-
"""
|
|
1879
|
-
# prepare message for receiver
|
|
1880
|
-
message = copy.deepcopy(standard_message.message)
|
|
1881
|
-
|
|
1882
|
-
# update system attributes of the message copy
|
|
1883
|
-
message["Attributes"][MessageSystemAttributeName.ApproximateFirstReceiveTimestamp] = str(
|
|
1884
|
-
int((standard_message.first_received or 0) * 1000)
|
|
1885
|
-
)
|
|
1886
|
-
|
|
1887
|
-
# filter attributes for receiver
|
|
1888
|
-
message_filter_attributes(message, attribute_names)
|
|
1889
|
-
message_filter_message_attributes(message, message_attribute_names)
|
|
1890
|
-
if message.get("MessageAttributes"):
|
|
1891
|
-
message["MD5OfMessageAttributes"] = _create_message_attribute_hash(
|
|
1892
|
-
message["MessageAttributes"]
|
|
1893
|
-
)
|
|
1894
|
-
else:
|
|
1895
|
-
# delete the value that was computed when creating the message
|
|
1896
|
-
message.pop("MD5OfMessageAttributes", None)
|
|
1897
|
-
return message
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
def message_filter_attributes(message: Message, names: AttributeNameList | None):
|
|
1901
|
-
"""
|
|
1902
|
-
Utility function filter from the given message (in-place) the system attributes from the given list. It will
|
|
1903
|
-
apply all rules according to:
|
|
1904
|
-
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sqs.html#SQS.Client.receive_message.
|
|
1905
|
-
|
|
1906
|
-
:param message: The message to filter (it will be modified)
|
|
1907
|
-
:param names: the attributes names/filters
|
|
1908
|
-
"""
|
|
1909
|
-
if "Attributes" not in message:
|
|
1910
|
-
return
|
|
1911
|
-
|
|
1912
|
-
if not names:
|
|
1913
|
-
del message["Attributes"]
|
|
1914
|
-
return
|
|
1915
|
-
|
|
1916
|
-
if QueueAttributeName.All in names:
|
|
1917
|
-
return
|
|
1918
|
-
|
|
1919
|
-
for k in list(message["Attributes"].keys()):
|
|
1920
|
-
if k not in names:
|
|
1921
|
-
del message["Attributes"][k]
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
def message_filter_message_attributes(message: Message, names: MessageAttributeNameList | None):
|
|
1925
|
-
"""
|
|
1926
|
-
Utility function filter from the given message (in-place) the message attributes from the given list. It will
|
|
1927
|
-
apply all rules according to:
|
|
1928
|
-
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sqs.html#SQS.Client.receive_message.
|
|
1929
|
-
|
|
1930
|
-
:param message: The message to filter (it will be modified)
|
|
1931
|
-
:param names: the attributes names/filters (can be 'All', '.*', '*' or prefix filters like 'Foo.*')
|
|
1932
|
-
"""
|
|
1933
|
-
if not message.get("MessageAttributes"):
|
|
1934
|
-
return
|
|
1935
|
-
|
|
1936
|
-
if not names:
|
|
1937
|
-
del message["MessageAttributes"]
|
|
1938
|
-
return
|
|
1939
|
-
|
|
1940
|
-
if "All" in names or ".*" in names or "*" in names:
|
|
1941
|
-
return
|
|
1942
|
-
|
|
1943
|
-
attributes = message["MessageAttributes"]
|
|
1944
|
-
matched = []
|
|
1945
|
-
|
|
1946
|
-
keys = [name for name in names if ".*" not in name]
|
|
1947
|
-
prefixes = [name.split(".*")[0] for name in names if ".*" in name]
|
|
1948
|
-
|
|
1949
|
-
# match prefix filters
|
|
1950
|
-
for k in attributes:
|
|
1951
|
-
if k in keys:
|
|
1952
|
-
matched.append(k)
|
|
1953
|
-
continue
|
|
1954
|
-
|
|
1955
|
-
for prefix in prefixes:
|
|
1956
|
-
if k.startswith(prefix):
|
|
1957
|
-
matched.append(k)
|
|
1958
|
-
break
|
|
1959
|
-
if matched:
|
|
1960
|
-
message["MessageAttributes"] = {k: attributes[k] for k in matched}
|
|
1961
|
-
else:
|
|
1962
|
-
message.pop("MessageAttributes")
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
1664
|
def extract_message_count_from_headers(context: RequestContext) -> int | None:
|
|
1966
1665
|
if override := context.request.headers.get(
|
|
1967
1666
|
HEADER_LOCALSTACK_SQS_OVERRIDE_MESSAGE_COUNT, default=None, type=int
|
localstack/services/sqs/utils.py
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import base64
|
|
2
|
+
import hashlib
|
|
2
3
|
import itertools
|
|
3
4
|
import json
|
|
4
5
|
import re
|
|
6
|
+
import struct
|
|
5
7
|
import time
|
|
6
|
-
from typing import Literal, NamedTuple
|
|
8
|
+
from typing import Any, Literal, NamedTuple
|
|
7
9
|
from urllib.parse import urlparse
|
|
8
10
|
|
|
9
|
-
from localstack.aws.api.sqs import
|
|
11
|
+
from localstack.aws.api.sqs import (
|
|
12
|
+
AttributeNameList,
|
|
13
|
+
Message,
|
|
14
|
+
MessageAttributeNameList,
|
|
15
|
+
QueueAttributeName,
|
|
16
|
+
ReceiptHandleIsInvalid,
|
|
17
|
+
)
|
|
10
18
|
from localstack.services.sqs.constants import (
|
|
11
19
|
DOMAIN_STRATEGY_URL_REGEX,
|
|
12
20
|
LEGACY_STRATEGY_URL_REGEX,
|
|
@@ -22,6 +30,11 @@ DOMAIN_ENDPOINT = re.compile(DOMAIN_STRATEGY_URL_REGEX)
|
|
|
22
30
|
PATH_ENDPOINT = re.compile(PATH_STRATEGY_URL_REGEX)
|
|
23
31
|
LEGACY_ENDPOINT = re.compile(LEGACY_STRATEGY_URL_REGEX)
|
|
24
32
|
|
|
33
|
+
STRING_TYPE_FIELD_INDEX = 1
|
|
34
|
+
BINARY_TYPE_FIELD_INDEX = 2
|
|
35
|
+
STRING_LIST_TYPE_FIELD_INDEX = 3
|
|
36
|
+
BINARY_LIST_TYPE_FIELD_INDEX = 4
|
|
37
|
+
|
|
25
38
|
|
|
26
39
|
def is_sqs_queue_url(url: str) -> bool:
|
|
27
40
|
return any(
|
|
@@ -184,3 +197,109 @@ def global_message_sequence():
|
|
|
184
197
|
|
|
185
198
|
def generate_message_id():
|
|
186
199
|
return long_uid()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def message_filter_attributes(message: Message, names: AttributeNameList | None):
|
|
203
|
+
"""
|
|
204
|
+
Utility function filter from the given message (in-place) the system attributes from the given list. It will
|
|
205
|
+
apply all rules according to:
|
|
206
|
+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sqs.html#SQS.Client.receive_message.
|
|
207
|
+
|
|
208
|
+
:param message: The message to filter (it will be modified)
|
|
209
|
+
:param names: the attributes names/filters
|
|
210
|
+
"""
|
|
211
|
+
if "Attributes" not in message:
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
if not names:
|
|
215
|
+
del message["Attributes"]
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
if QueueAttributeName.All in names:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
for k in list(message["Attributes"].keys()):
|
|
222
|
+
if k not in names:
|
|
223
|
+
del message["Attributes"][k]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def message_filter_message_attributes(message: Message, names: MessageAttributeNameList | None):
|
|
227
|
+
"""
|
|
228
|
+
Utility function filter from the given message (in-place) the message attributes from the given list. It will
|
|
229
|
+
apply all rules according to:
|
|
230
|
+
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sqs.html#SQS.Client.receive_message.
|
|
231
|
+
|
|
232
|
+
:param message: The message to filter (it will be modified)
|
|
233
|
+
:param names: the attributes names/filters (can be 'All', '.*', '*' or prefix filters like 'Foo.*')
|
|
234
|
+
"""
|
|
235
|
+
if not message.get("MessageAttributes"):
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
if not names:
|
|
239
|
+
del message["MessageAttributes"]
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
if "All" in names or ".*" in names or "*" in names:
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
attributes = message["MessageAttributes"]
|
|
246
|
+
matched = []
|
|
247
|
+
|
|
248
|
+
keys = [name for name in names if ".*" not in name]
|
|
249
|
+
prefixes = [name.split(".*")[0] for name in names if ".*" in name]
|
|
250
|
+
|
|
251
|
+
# match prefix filters
|
|
252
|
+
for k in attributes:
|
|
253
|
+
if k in keys:
|
|
254
|
+
matched.append(k)
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
for prefix in prefixes:
|
|
258
|
+
if k.startswith(prefix):
|
|
259
|
+
matched.append(k)
|
|
260
|
+
break
|
|
261
|
+
if matched:
|
|
262
|
+
message["MessageAttributes"] = {k: attributes[k] for k in matched}
|
|
263
|
+
else:
|
|
264
|
+
message.pop("MessageAttributes")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _utf8(value: Any) -> bytes: # type: ignore[misc]
|
|
268
|
+
if isinstance(value, str):
|
|
269
|
+
return value.encode("utf-8")
|
|
270
|
+
return value
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _update_binary_length_and_value(md5: Any, value: bytes) -> None: # type: ignore[misc]
|
|
274
|
+
length_bytes = struct.pack("!I".encode("ascii"), len(value))
|
|
275
|
+
md5.update(length_bytes)
|
|
276
|
+
md5.update(value)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def create_message_attribute_hash(message_attributes) -> str | None:
|
|
280
|
+
"""
|
|
281
|
+
Method from moto's attribute_md5 of moto/sqs/models.py, separated from the Message Object.
|
|
282
|
+
"""
|
|
283
|
+
# To avoid the need to check for dict conformity everytime we invoke this function
|
|
284
|
+
if not isinstance(message_attributes, dict):
|
|
285
|
+
return
|
|
286
|
+
|
|
287
|
+
hash = hashlib.md5()
|
|
288
|
+
|
|
289
|
+
for attrName in sorted(message_attributes.keys()):
|
|
290
|
+
attr_value = message_attributes[attrName]
|
|
291
|
+
# Encode name
|
|
292
|
+
_update_binary_length_and_value(hash, _utf8(attrName))
|
|
293
|
+
# Encode data type
|
|
294
|
+
_update_binary_length_and_value(hash, _utf8(attr_value["DataType"]))
|
|
295
|
+
# Encode transport type and value
|
|
296
|
+
if attr_value.get("StringValue"):
|
|
297
|
+
hash.update(bytearray([STRING_TYPE_FIELD_INDEX]))
|
|
298
|
+
_update_binary_length_and_value(hash, _utf8(attr_value.get("StringValue")))
|
|
299
|
+
elif attr_value.get("BinaryValue"):
|
|
300
|
+
hash.update(bytearray([BINARY_TYPE_FIELD_INDEX]))
|
|
301
|
+
decoded_binary_value = attr_value.get("BinaryValue")
|
|
302
|
+
_update_binary_length_and_value(hash, decoded_binary_value)
|
|
303
|
+
# string_list_value, binary_list_value type is not implemented, reserved for the future use.
|
|
304
|
+
# See https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_MessageAttributeValue.html
|
|
305
|
+
return hash.hexdigest()
|
|
@@ -7,7 +7,7 @@ from pathlib import Path
|
|
|
7
7
|
from typing import Any, Final
|
|
8
8
|
|
|
9
9
|
import jpype
|
|
10
|
-
import jpype.imports
|
|
10
|
+
import jpype.imports # noqa # Required for JVM Java class imports
|
|
11
11
|
|
|
12
12
|
from localstack.services.stepfunctions.asl.utils.encoding import to_json_str
|
|
13
13
|
from localstack.services.stepfunctions.packages import jpype_jsonata_package
|
|
@@ -29,7 +29,7 @@ def load_template_file(file_path: str | os.PathLike, *, path_ctx: str | os.PathL
|
|
|
29
29
|
elif not file_path_obj.is_absolute():
|
|
30
30
|
raise ValueError("Provided path must be absolute if no path_ctx is provided")
|
|
31
31
|
|
|
32
|
-
return load_file(file_path_obj.absolute())
|
|
32
|
+
return load_file(file_path_obj.absolute(), strict=True)
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
# TODO: TBH this utility really doesn't add anything, probably better to just remove it
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from collections import defaultdict
|
|
3
|
-
from collections.abc import Generator
|
|
4
|
-
from typing import
|
|
3
|
+
from collections.abc import Callable, Generator
|
|
4
|
+
from typing import TypedDict
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
from botocore.exceptions import WaiterError
|
|
@@ -13,7 +13,7 @@ from localstack.utils.strings import short_uid
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class NormalizedEvent(TypedDict):
|
|
16
|
-
PhysicalResourceId:
|
|
16
|
+
PhysicalResourceId: str | None
|
|
17
17
|
LogicalResourceId: str
|
|
18
18
|
ResourceType: str
|
|
19
19
|
ResourceStatus: str
|
|
@@ -2,8 +2,7 @@ import logging
|
|
|
2
2
|
import os
|
|
3
3
|
import shlex
|
|
4
4
|
import threading
|
|
5
|
-
from collections.abc import Generator
|
|
6
|
-
from typing import Callable, Optional
|
|
5
|
+
from collections.abc import Callable, Generator
|
|
7
6
|
|
|
8
7
|
import pytest
|
|
9
8
|
|
|
@@ -38,8 +37,8 @@ class ContainerFactory:
|
|
|
38
37
|
self,
|
|
39
38
|
# convenience properties
|
|
40
39
|
pro: bool = False,
|
|
41
|
-
publish:
|
|
42
|
-
configurators:
|
|
40
|
+
publish: list[int] | None = None,
|
|
41
|
+
configurators: list[ContainerConfigurator] | None = None,
|
|
43
42
|
# ContainerConfig properties
|
|
44
43
|
**kwargs,
|
|
45
44
|
) -> Container:
|
|
@@ -172,7 +171,7 @@ def container_factory() -> Generator[ContainerFactory, None, None]:
|
|
|
172
171
|
|
|
173
172
|
@pytest.fixture(scope="session")
|
|
174
173
|
def wait_for_localstack_ready():
|
|
175
|
-
def _wait_for(container: RunningContainer, timeout:
|
|
174
|
+
def _wait_for(container: RunningContainer, timeout: float | None = None):
|
|
176
175
|
container.wait_until_ready(timeout)
|
|
177
176
|
|
|
178
177
|
poll_condition(
|