From b3b4e4f59048076f6b5fb1d13a4c57e529ac4cbb Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Thu, 28 Mar 2024 17:30:04 +0100 Subject: [PATCH 001/169] fix non-default-region compatibility for recent lambda invalid invoke test (#10566) --- localstack/services/lambda_/api_utils.py | 2 +- localstack/services/lambda_/provider.py | 2 +- tests/aws/services/lambda_/test_lambda_api.py | 3 ++- tests/aws/services/lambda_/test_lambda_api.snapshot.json | 4 ++-- tests/aws/services/lambda_/test_lambda_api.validation.json | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/localstack/services/lambda_/api_utils.py b/localstack/services/lambda_/api_utils.py index 742e4e943267d..2aaa130dd08a0 100644 --- a/localstack/services/lambda_/api_utils.py +++ b/localstack/services/lambda_/api_utils.py @@ -59,7 +59,7 @@ ) AWS_FUNCTION_NAME_REGEX = re.compile( - "^(arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{2}((-gov)|(-iso(b?)))?-[a-z]+-\\d{1}:)?(\\d{12}:)?(function:)?([a-zA-Z0-9-_.]+)(:(\\$LATEST|[a-zA-Z0-9-_]+))?$" + "^(arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:)?(\\d{12}:)?(function:)?([a-zA-Z0-9-_.]+)(:(\\$LATEST|[a-zA-Z0-9-_]+))?$" ) # Pattern for extracting various attributes from a full or partial ARN or just a function name. diff --git a/localstack/services/lambda_/provider.py b/localstack/services/lambda_/provider.py index 132d51af86628..1d143478ccf0c 100644 --- a/localstack/services/lambda_/provider.py +++ b/localstack/services/lambda_/provider.py @@ -1361,7 +1361,7 @@ def invoke( account_id, region = api_utils.get_account_and_region(function_name, context) if not api_utils.validate_function_name(function_name): raise ValidationException( - f"1 validation error detected: Value '{function_name}' at 'functionName' failed to satisfy constraint: Member must satisfy regular expression pattern: (arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{{2}}((-gov)|(-iso(b?)))?-[a-z]+-\\d{{1}}:)?(\\d{{12}}:)?(function:)?([a-zA-Z0-9-_\\.]+)(:(\\$LATEST|[a-zA-Z0-9-_]+))?" + f"1 validation error detected: Value '{function_name}' at 'functionName' failed to satisfy constraint: Member must satisfy regular expression pattern: (arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{{2}}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{{1}}:)?(\\d{{12}}:)?(function:)?([a-zA-Z0-9-_\\.]+)(:(\\$LATEST|[a-zA-Z0-9-_]+))?" ) function_name, qualifier = api_utils.get_name_and_qualifier( diff --git a/tests/aws/services/lambda_/test_lambda_api.py b/tests/aws/services/lambda_/test_lambda_api.py index 175ace55d78a7..e691eda1e3863 100644 --- a/tests/aws/services/lambda_/test_lambda_api.py +++ b/tests/aws/services/lambda_/test_lambda_api.py @@ -818,7 +818,8 @@ def test_vpc_config( def test_invalid_invoke(self, aws_client, snapshot): with pytest.raises(aws_client.lambda_.exceptions.ClientError) as e: aws_client.lambda_.invoke( - FunctionName="arn:aws:lambda:us-east-1:123400000000@function:myfn", Payload=b"{}" + FunctionName=f"arn:aws:lambda:{aws_client.lambda_.meta.region_name}:123400000000@function:myfn", + Payload=b"{}", ) snapshot.match("invoke_function_name_pattern_exc", e.value.response) diff --git a/tests/aws/services/lambda_/test_lambda_api.snapshot.json b/tests/aws/services/lambda_/test_lambda_api.snapshot.json index d592c6bc1897e..6a45db3b88692 100644 --- a/tests/aws/services/lambda_/test_lambda_api.snapshot.json +++ b/tests/aws/services/lambda_/test_lambda_api.snapshot.json @@ -13773,12 +13773,12 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_invalid_invoke": { - "recorded-date": "26-02-2024, 17:04:08", + "recorded-date": "28-03-2024, 08:44:26", "recorded-content": { "invoke_function_name_pattern_exc": { "Error": { "Code": "ValidationException", - "Message": "1 validation error detected: Value 'arn:aws:lambda::123400000000@function:myfn' at 'functionName' failed to satisfy constraint: Member must satisfy regular expression pattern: (arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{2}((-gov)|(-iso(b?)))?-[a-z]+-\\d{1}:)?(\\d{12}:)?(function:)?([a-zA-Z0-9-_\\.]+)(:(\\$LATEST|[a-zA-Z0-9-_]+))?" + "Message": "1 validation error detected: Value 'arn:aws:lambda::123400000000@function:myfn' at 'functionName' failed to satisfy constraint: Member must satisfy regular expression pattern: (arn:(aws[a-zA-Z-]*)?:lambda:)?([a-z]{2}((-gov)|(-iso([a-z]?)))?-[a-z]+-\\d{1}:)?(\\d{12}:)?(function:)?([a-zA-Z0-9-_\\.]+)(:(\\$LATEST|[a-zA-Z0-9-_]+))?" }, "ResponseMetadata": { "HTTPHeaders": {}, diff --git a/tests/aws/services/lambda_/test_lambda_api.validation.json b/tests/aws/services/lambda_/test_lambda_api.validation.json index 675b787fe5e00..543fc909d9f6e 100644 --- a/tests/aws/services/lambda_/test_lambda_api.validation.json +++ b/tests/aws/services/lambda_/test_lambda_api.validation.json @@ -78,7 +78,7 @@ "last_validated_date": "2023-11-20T15:46:48+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_invalid_invoke": { - "last_validated_date": "2024-02-26T17:04:08+00:00" + "last_validated_date": "2024-03-28T08:45:03+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_code_location_s3": { "last_validated_date": "2023-11-20T15:46:59+00:00" From 44904081e1d3112ad0c7c2e283cb221c2dc069d5 Mon Sep 17 00:00:00 2001 From: steffyP Date: Thu, 28 Mar 2024 18:07:08 +0100 Subject: [PATCH 002/169] raise exception if required ENV is missing (#10568) --- .../tinybird/upload_raw_test_metrics_and_coverage.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/scripts/tinybird/upload_raw_test_metrics_and_coverage.py b/scripts/tinybird/upload_raw_test_metrics_and_coverage.py index f3a78aa80cbdb..3fdaf3878206f 100644 --- a/scripts/tinybird/upload_raw_test_metrics_and_coverage.py +++ b/scripts/tinybird/upload_raw_test_metrics_and_coverage.py @@ -301,20 +301,16 @@ def main(): ) if not metric_report_dir: print(missing_info) - print("missing METRIC_REPORT_DIR_PATH") - return + raise Exception("missing METRIC_REPORT_DIR_PATH") if not impl_coverage_file: print(missing_info) - print("missing IMPLEMENTATION_COVERAGE_FILE") - return + raise Exception("missing IMPLEMENTATION_COVERAGE_FILE") if not source_type: print(missing_info) - print("missing SOURCE_TYPE") - return + raise Exception("missing SOURCE_TYPE") if not token: print(missing_info) - print("missing TINYBIRD_PARITY_ANALYTICS_TOKEN") - return + raise Exception("missing TINYBIRD_PARITY_ANALYTICS_TOKEN") # create one timestamp that will be used for all the data sent timestamp: str = datetime.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S") From 6c8e43feb80026ea02c9b48fc3fa0c7f1b5d9c95 Mon Sep 17 00:00:00 2001 From: Sannya Singal <32308435+sannya-singal@users.noreply.github.com> Date: Fri, 29 Mar 2024 10:47:28 +0530 Subject: [PATCH 003/169] fix clouformation ec2 tests for `ap-northeast-1` and validate against AWS (#10563) --- .../cloudformation/resources/test_ec2.py | 2 +- .../resources/test_ec2.snapshot.json | 4 +-- .../resources/test_ec2.validation.json | 5 ++- tests/aws/templates/ec2_vpc_default_sg.yaml | 36 ++++++++++++++++--- .../templates/transit_gateway_attachment.yml | 26 ++++++++++++-- 5 files changed, 63 insertions(+), 10 deletions(-) diff --git a/tests/aws/services/cloudformation/resources/test_ec2.py b/tests/aws/services/cloudformation/resources/test_ec2.py index 8e962236ef9ba..2272b4de72ad1 100644 --- a/tests/aws/services/cloudformation/resources/test_ec2.py +++ b/tests/aws/services/cloudformation/resources/test_ec2.py @@ -45,7 +45,7 @@ def test_simple_route_table_creation(deploy_cfn_template, aws_client): ec2.describe_route_tables(RouteTableIds=[route_table_id]) -@markers.aws.unknown +@markers.aws.validated def test_vpc_creates_default_sg(deploy_cfn_template, aws_client): result = deploy_cfn_template( template_path=os.path.join(THIS_FOLDER, "../../../templates/ec2_vpc_default_sg.yaml") diff --git a/tests/aws/services/cloudformation/resources/test_ec2.snapshot.json b/tests/aws/services/cloudformation/resources/test_ec2.snapshot.json index 386a1749f4234..e05c4598147bb 100644 --- a/tests/aws/services/cloudformation/resources/test_ec2.snapshot.json +++ b/tests/aws/services/cloudformation/resources/test_ec2.snapshot.json @@ -91,7 +91,7 @@ } }, "tests/aws/services/cloudformation/resources/test_ec2.py::test_transit_gateway_attachment": { - "recorded-date": "20-10-2023, 08:55:07", + "recorded-date": "28-03-2024, 06:48:11", "recorded-content": { "attachment": { "Association": { @@ -132,7 +132,7 @@ "Tags": [ { "Key": "Application", - "Value": "arn:aws:cloudformation::111111111111:stack/stack-d2a69315/f9376240-6f4f-11ee-87a2-0a5f03ecaf83" + "Value": "arn:aws:cloudformation::111111111111:stack/stack-31597705/521e4e40-ecce-11ee-806c-0affc1ff51e7" } ], "TransitGatewayArn": "arn:aws:ec2::111111111111:transit-gateway/", diff --git a/tests/aws/services/cloudformation/resources/test_ec2.validation.json b/tests/aws/services/cloudformation/resources/test_ec2.validation.json index 5c22707d583f8..c5ed79eb05b29 100644 --- a/tests/aws/services/cloudformation/resources/test_ec2.validation.json +++ b/tests/aws/services/cloudformation/resources/test_ec2.validation.json @@ -6,6 +6,9 @@ "last_validated_date": "2023-02-13T16:13:41+00:00" }, "tests/aws/services/cloudformation/resources/test_ec2.py::test_transit_gateway_attachment": { - "last_validated_date": "2023-10-20T06:55:07+00:00" + "last_validated_date": "2024-03-28T06:48:11+00:00" + }, + "tests/aws/services/cloudformation/resources/test_ec2.py::test_vpc_creates_default_sg": { + "last_validated_date": "2024-03-28T06:26:23+00:00" } } diff --git a/tests/aws/templates/ec2_vpc_default_sg.yaml b/tests/aws/templates/ec2_vpc_default_sg.yaml index 441a3daa8559b..029cfac92a6e2 100644 --- a/tests/aws/templates/ec2_vpc_default_sg.yaml +++ b/tests/aws/templates/ec2_vpc_default_sg.yaml @@ -1,6 +1,18 @@ +Parameters: + DeployRegion: + Type: String + Default: us-east-1 + +Conditions: + DeployInUSEast1: + Fn::Equals: + - !Ref DeployRegion + - us-east-1 + Resources: vpcA2121C38: Type: AWS::EC2::VPC + Condition: DeployInUSEast1 Properties: CidrBlock: 10.0.0.0/16 EnableDnsHostnames: true @@ -11,6 +23,7 @@ Resources: Value: RdsTestStack/vpc vpcPublicSubnet1Subnet2E65531E: Type: AWS::EC2::Subnet + Condition: DeployInUSEast1 Properties: CidrBlock: 10.0.0.0/18 VpcId: @@ -18,7 +31,7 @@ Resources: AvailabilityZone: Fn::Select: - 0 - - Fn::GetAZs: "" + - Fn::GetAZs: !Ref DeployRegion MapPublicIpOnLaunch: true Tags: - Key: aws-cdk:subnet-name @@ -29,6 +42,7 @@ Resources: Value: RdsTestStack/vpc/PublicSubnet1 vpcPublicSubnet1RouteTable48A2DF9B: Type: AWS::EC2::RouteTable + Condition: DeployInUSEast1 Properties: VpcId: Ref: vpcA2121C38 @@ -37,6 +51,7 @@ Resources: Value: RdsTestStack/vpc/PublicSubnet1 vpcPublicSubnet1RouteTableAssociation5D3F4579: Type: AWS::EC2::SubnetRouteTableAssociation + Condition: DeployInUSEast1 Properties: RouteTableId: Ref: vpcPublicSubnet1RouteTable48A2DF9B @@ -44,6 +59,7 @@ Resources: Ref: vpcPublicSubnet1Subnet2E65531E vpcPublicSubnet1DefaultRoute10708846: Type: AWS::EC2::Route + Condition: DeployInUSEast1 Properties: RouteTableId: Ref: vpcPublicSubnet1RouteTable48A2DF9B @@ -54,6 +70,7 @@ Resources: - vpcVPCGW7984C166 vpcPublicSubnet2Subnet009B674F: Type: AWS::EC2::Subnet + Condition: DeployInUSEast1 Properties: CidrBlock: 10.0.64.0/18 VpcId: @@ -61,7 +78,7 @@ Resources: AvailabilityZone: Fn::Select: - 1 - - Fn::GetAZs: "" + - Fn::GetAZs: !Ref DeployRegion MapPublicIpOnLaunch: true Tags: - Key: aws-cdk:subnet-name @@ -72,6 +89,7 @@ Resources: Value: RdsTestStack/vpc/PublicSubnet2 vpcPublicSubnet2RouteTableEB40D4CB: Type: AWS::EC2::RouteTable + Condition: DeployInUSEast1 Properties: VpcId: Ref: vpcA2121C38 @@ -80,6 +98,7 @@ Resources: Value: RdsTestStack/vpc/PublicSubnet2 vpcPublicSubnet2RouteTableAssociation21F81B59: Type: AWS::EC2::SubnetRouteTableAssociation + Condition: DeployInUSEast1 Properties: RouteTableId: Ref: vpcPublicSubnet2RouteTableEB40D4CB @@ -87,6 +106,7 @@ Resources: Ref: vpcPublicSubnet2Subnet009B674F vpcPublicSubnet2DefaultRouteA1EC0F60: Type: AWS::EC2::Route + Condition: DeployInUSEast1 Properties: RouteTableId: Ref: vpcPublicSubnet2RouteTableEB40D4CB @@ -97,6 +117,7 @@ Resources: - vpcVPCGW7984C166 vpcIsolatedSubnet1Subnet8B28CEB3: Type: AWS::EC2::Subnet + Condition: DeployInUSEast1 Properties: CidrBlock: 10.0.128.0/18 VpcId: @@ -104,7 +125,7 @@ Resources: AvailabilityZone: Fn::Select: - 0 - - Fn::GetAZs: "" + - Fn::GetAZs: !Ref DeployRegion MapPublicIpOnLaunch: false Tags: - Key: aws-cdk:subnet-name @@ -115,6 +136,7 @@ Resources: Value: RdsTestStack/vpc/IsolatedSubnet1 vpcIsolatedSubnet1RouteTable0D6B2D3D: Type: AWS::EC2::RouteTable + Condition: DeployInUSEast1 Properties: VpcId: Ref: vpcA2121C38 @@ -123,6 +145,7 @@ Resources: Value: RdsTestStack/vpc/IsolatedSubnet1 vpcIsolatedSubnet1RouteTableAssociation172210D4: Type: AWS::EC2::SubnetRouteTableAssociation + Condition: DeployInUSEast1 Properties: RouteTableId: Ref: vpcIsolatedSubnet1RouteTable0D6B2D3D @@ -130,6 +153,7 @@ Resources: Ref: vpcIsolatedSubnet1Subnet8B28CEB3 vpcIsolatedSubnet2Subnet2C6B375C: Type: AWS::EC2::Subnet + Condition: DeployInUSEast1 Properties: CidrBlock: 10.0.192.0/18 VpcId: @@ -137,7 +161,7 @@ Resources: AvailabilityZone: Fn::Select: - 1 - - Fn::GetAZs: "" + - Fn::GetAZs: !Ref DeployRegion MapPublicIpOnLaunch: false Tags: - Key: aws-cdk:subnet-name @@ -148,6 +172,7 @@ Resources: Value: RdsTestStack/vpc/IsolatedSubnet2 vpcIsolatedSubnet2RouteTable3455CBFC: Type: AWS::EC2::RouteTable + Condition: DeployInUSEast1 Properties: VpcId: Ref: vpcA2121C38 @@ -156,6 +181,7 @@ Resources: Value: RdsTestStack/vpc/IsolatedSubnet2 vpcIsolatedSubnet2RouteTableAssociation8A8FAF70: Type: AWS::EC2::SubnetRouteTableAssociation + Condition: DeployInUSEast1 Properties: RouteTableId: Ref: vpcIsolatedSubnet2RouteTable3455CBFC @@ -163,12 +189,14 @@ Resources: Ref: vpcIsolatedSubnet2Subnet2C6B375C vpcIGWE57CBDCA: Type: AWS::EC2::InternetGateway + Condition: DeployInUSEast1 Properties: Tags: - Key: Name Value: RdsTestStack/vpc vpcVPCGW7984C166: Type: AWS::EC2::VPCGatewayAttachment + Condition: DeployInUSEast1 Properties: VpcId: Ref: vpcA2121C38 diff --git a/tests/aws/templates/transit_gateway_attachment.yml b/tests/aws/templates/transit_gateway_attachment.yml index 3fed06c8676a5..7f03d1ac5bf65 100644 --- a/tests/aws/templates/transit_gateway_attachment.yml +++ b/tests/aws/templates/transit_gateway_attachment.yml @@ -1,6 +1,18 @@ +Parameters: + DeployRegion: + Type: String + Default: us-east-1 + +Conditions: + DeployInUSEast1: + Fn::Equals: + - !Ref DeployRegion + - us-east-1 + Resources: Vpc8378EB38: Type: AWS::EC2::VPC + Condition: DeployInUSEast1 Properties: CidrBlock: 10.0.0.0/20 EnableDnsHostnames: true @@ -8,6 +20,7 @@ Resources: InstanceTenancy: default myTransitGateway: Type: "AWS::EC2::TransitGateway" + Condition: DeployInUSEast1 Properties: AmazonSideAsn: 65000 Description: "TGW Route Integration Test" @@ -20,22 +33,25 @@ Resources: Value: !Ref 'AWS::StackId' VpcIsolatedSubnet1SubnetE48C5737: Type: AWS::EC2::Subnet + Condition: DeployInUSEast1 Properties: AvailabilityZone: Fn::Select: - 0 - - Fn::GetAZs: '' + - Fn::GetAZs: !Ref DeployRegion CidrBlock: 10.0.0.0/24 MapPublicIpOnLaunch: false VpcId: Ref: Vpc8378EB38 VpcIsolatedSubnet1RouteTable4771E3E5: Type: AWS::EC2::RouteTable + Condition: DeployInUSEast1 Properties: VpcId: Ref: Vpc8378EB38 VpcIsolatedSubnet1RouteTableAssociationD300FCBB: Type: AWS::EC2::SubnetRouteTableAssociation + Condition: DeployInUSEast1 Properties: RouteTableId: Ref: VpcIsolatedSubnet1RouteTable4771E3E5 @@ -43,6 +59,7 @@ Resources: Ref: VpcIsolatedSubnet1SubnetE48C5737 VpcIsolatedSubnet1TransitGatewayRouteA907B32D: Type: AWS::EC2::Route + Condition: DeployInUSEast1 Properties: DestinationCidrBlock: 0.0.0.0/0 RouteTableId: @@ -52,22 +69,25 @@ Resources: - TransitGatewayVpcAttachment VpcIsolatedSubnet2Subnet16364B91: Type: AWS::EC2::Subnet + Condition: DeployInUSEast1 Properties: AvailabilityZone: Fn::Select: - 1 - - Fn::GetAZs: '' + - Fn::GetAZs: !Ref DeployRegion CidrBlock: 10.0.1.0/24 MapPublicIpOnLaunch: false VpcId: Ref: Vpc8378EB38 VpcIsolatedSubnet2RouteTable1D30AF7D: Type: AWS::EC2::RouteTable + Condition: DeployInUSEast1 Properties: VpcId: Ref: Vpc8378EB38 VpcIsolatedSubnet2RouteTableAssociationF7B18CCA: Type: AWS::EC2::SubnetRouteTableAssociation + Condition: DeployInUSEast1 Properties: RouteTableId: Ref: VpcIsolatedSubnet2RouteTable1D30AF7D @@ -75,6 +95,7 @@ Resources: Ref: VpcIsolatedSubnet2Subnet16364B91 VpcIsolatedSubnet2TransitGatewayRoute1E0D0BF2: Type: AWS::EC2::Route + Condition: DeployInUSEast1 Properties: DestinationCidrBlock: 0.0.0.0/0 RouteTableId: @@ -84,6 +105,7 @@ Resources: - TransitGatewayVpcAttachment TransitGatewayVpcAttachment: Type: AWS::EC2::TransitGatewayAttachment + Condition: DeployInUSEast1 Properties: SubnetIds: - Ref: VpcIsolatedSubnet1SubnetE48C5737 From 0fea3e0aaea8bd1634d67f2fa1c8ca2e38e62d8f Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Fri, 29 Mar 2024 09:51:05 +0100 Subject: [PATCH 004/169] update README after 3.3.0 release (#10570) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 882ff7a290a29..b64ad949c831b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

-:zap: We are thrilled to announce the release of LocalStack 3.2 :zap: +:zap: We are thrilled to announce the release of LocalStack 3.3 :zap:

@@ -93,7 +93,7 @@ Start LocalStack inside a Docker container by running: / /___/ /_/ / /__/ /_/ / /___/ / /_/ /_/ / /__/ ,< /_____/\____/\___/\__,_/_//____/\__/\__,_/\___/_/|_| - 💻 LocalStack CLI 3.2.0 + 💻 LocalStack CLI 3.3.0 👤 Profile: default [12:47:13] starting LocalStack in Docker mode 🐳 localstack.py:494 From 712e677a60ebd640ab9500bba9fa52cde453fec1 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Fri, 29 Mar 2024 22:13:17 +0100 Subject: [PATCH 005/169] fix SNS RawMessageDelivery casing (#10575) --- localstack/services/moto.py | 1 - localstack/services/sns/provider.py | 61 ++++++++++++++----- tests/aws/services/sns/test_sns.py | 32 +++++++++- tests/aws/services/sns/test_sns.snapshot.json | 39 +++++++++++- .../aws/services/sns/test_sns.validation.json | 4 +- 5 files changed, 116 insertions(+), 21 deletions(-) diff --git a/localstack/services/moto.py b/localstack/services/moto.py index 39fb6a469d4b7..0f8aab3dc25f4 100644 --- a/localstack/services/moto.py +++ b/localstack/services/moto.py @@ -59,7 +59,6 @@ def call_moto_with_request( :param context: the original request context :param service_request: the dictionary containing the service request parameters - :param override_headers: whether to override headers that are also request parameters :return: an ASF ServiceResponse (same as a service provider would return) """ local_context = create_aws_request_context( diff --git a/localstack/services/sns/provider.py b/localstack/services/sns/provider.py index cf118965ddde6..fc8a8c00ff051 100644 --- a/localstack/services/sns/provider.py +++ b/localstack/services/sns/provider.py @@ -29,8 +29,10 @@ PublishBatchResponse, PublishBatchResultEntry, PublishResponse, + SetSubscriptionAttributesInput, SnsApi, String, + SubscribeInput, SubscribeResponse, SubscriptionAttributesMap, TagKeyList, @@ -51,7 +53,7 @@ from localstack.constants import AWS_REGION_US_EAST_1, DEFAULT_AWS_ACCOUNT_ID from localstack.http import Request, Response, Router, route from localstack.services.edge import ROUTER -from localstack.services.moto import call_moto +from localstack.services.moto import call_moto, call_moto_with_request from localstack.services.plugins import ServiceLifecycleHook from localstack.services.sns import constants as sns_constants from localstack.services.sns.certificate import SNS_SERVER_CERT @@ -264,8 +266,15 @@ def set_subscription_attributes( topic_arn=sub["TopicArn"], endpoint=sub["Endpoint"], ) + if attribute_name == "RawMessageDelivery": + attribute_value = attribute_value.lower() try: - call_moto(context) + request = SetSubscriptionAttributesInput( + SubscriptionArn=subscription_arn, + AttributeName=attribute_name, + AttributeValue=attribute_value, + ) + call_moto_with_request(context, service_request=request) except CommonServiceException as e: # Moto errors don't send the "Type": "Sender" field in their SNS exception if e.code == "InvalidParameter": @@ -613,9 +622,25 @@ def subscribe( attribute_value=attr_value, topic_arn=topic_arn, endpoint=endpoint, + is_subscribe_call=True, ) + if attributes and "RawMessageDelivery" in attributes: + # Moto does not lower case the value, so we need to override the request + attrs_copy = { + **attributes, + "RawMessageDelivery": attributes["RawMessageDelivery"].lower(), + } + request = SubscribeInput( + TopicArn=topic_arn, + Protocol=protocol, + Endpoint=endpoint, + Attributes=attrs_copy, + ReturnSubscriptionArn=return_subscription_arn, + ) + moto_response = call_moto_with_request(context, service_request=request) + else: + moto_response = call_moto(context) - moto_response = call_moto(context) subscription_arn = moto_response.get("SubscriptionArn") parsed_topic_arn = parse_and_validate_topic_arn(topic_arn) @@ -658,6 +683,8 @@ def subscribe( store.subscription_filter_policy[subscription_arn] = ( json.loads(attributes["FilterPolicy"]) if attributes["FilterPolicy"] else None ) + if raw_msg_delivery := attributes.get("RawMessageDelivery"): + subscription["RawMessageDelivery"] = raw_msg_delivery.lower() store.subscriptions[subscription_arn] = subscription @@ -770,6 +797,7 @@ def validate_subscription_attribute( attribute_value: str, topic_arn: str, endpoint: str, + is_subscribe_call: bool = False, ) -> None: """ Validate the subscription attribute to be set. See: @@ -778,40 +806,45 @@ def validate_subscription_attribute( :param attribute_value: the subscription attribute value :param topic_arn: the topic_arn of the subscription, needed to know if it is FIFO :param endpoint: the subscription endpoint (like an SQS queue ARN) + :param is_subscribe_call: the error message is different if called from Subscribe or SetSubscriptionAttributes :raises InvalidParameterException :return: """ + error_prefix = ( + "Invalid parameter: Attributes Reason: " if is_subscribe_call else "Invalid parameter: " + ) if attribute_name not in sns_constants.VALID_SUBSCRIPTION_ATTR_NAME: - raise InvalidParameterException("Invalid parameter: AttributeName") + raise InvalidParameterException(f"{error_prefix}AttributeName") if attribute_name == "FilterPolicy": try: json.loads(attribute_value or "{}") except json.JSONDecodeError: - raise InvalidParameterException( - "Invalid parameter: FilterPolicy: failed to parse JSON." - ) + raise InvalidParameterException(f"{error_prefix}FilterPolicy: failed to parse JSON.") elif attribute_name == "FilterPolicyScope": if attribute_value not in ("MessageAttributes", "MessageBody"): raise InvalidParameterException( - f"Invalid parameter: FilterPolicyScope: Invalid value [{attribute_value}]. Please use either MessageBody or MessageAttributes" + f"{error_prefix}FilterPolicyScope: Invalid value [{attribute_value}]. " + f"Please use either MessageBody or MessageAttributes" ) elif attribute_name == "RawMessageDelivery": # TODO: only for SQS and https(s) subs, + firehose - return + if attribute_value.lower() not in ("true", "false"): + raise InvalidParameterException( + f"{error_prefix}RawMessageDelivery: Invalid value [{attribute_value}]. " + f"Must be true or false." + ) elif attribute_name == "RedrivePolicy": try: dlq_target_arn = json.loads(attribute_value).get("deadLetterTargetArn", "") except json.JSONDecodeError: - raise InvalidParameterException( - "Invalid parameter: RedrivePolicy: failed to parse JSON." - ) + raise InvalidParameterException(f"{error_prefix}RedrivePolicy: failed to parse JSON.") try: parsed_arn = parse_arn(dlq_target_arn) except InvalidArnException: raise InvalidParameterException( - "Invalid parameter: RedrivePolicy: deadLetterTargetArn is an invalid arn" + f"{error_prefix}RedrivePolicy: deadLetterTargetArn is an invalid arn" ) if topic_arn.endswith(".fifo"): @@ -819,7 +852,7 @@ def validate_subscription_attribute( not parsed_arn["resource"].endswith(".fifo") or "sqs" not in parsed_arn["service"] ): raise InvalidParameterException( - "Invalid parameter: RedrivePolicy: must use a FIFO queue as DLQ for a FIFO Subscription to a FIFO Topic." + f"{error_prefix}RedrivePolicy: must use a FIFO queue as DLQ for a FIFO Subscription to a FIFO Topic." ) diff --git a/tests/aws/services/sns/test_sns.py b/tests/aws/services/sns/test_sns.py index 671f441266e37..e6c6ad599f1e7 100644 --- a/tests/aws/services/sns/test_sns.py +++ b/tests/aws/services/sns/test_sns.py @@ -580,12 +580,26 @@ def test_create_subscriptions_with_attributes( queue_url = sqs_create_queue() queue_arn = sqs_get_queue_arn(queue_url) + with pytest.raises(ClientError) as e: + sns_subscription( + TopicArn=topic_arn, + Protocol="sqs", + Endpoint=queue_arn, + Attributes={ + "RawMessageDelivery": "wrongvalue", # set an weird case value, SNS will lower it + "FilterPolicyScope": "MessageBody", + "FilterPolicy": "", + }, + ReturnSubscriptionArn=True, + ) + snapshot.match("subscribe-wrong-attr", e.value.response) + subscribe_resp = sns_subscription( TopicArn=topic_arn, Protocol="sqs", Endpoint=queue_arn, Attributes={ - "RawMessageDelivery": "true", + "RawMessageDelivery": "TrUe", # set an weird case value, SNS will lower it "FilterPolicyScope": "MessageBody", "FilterPolicy": "", }, @@ -682,6 +696,22 @@ def test_validate_set_sub_attributes( ) snapshot.match("fake-attribute", e.value.response) + with pytest.raises(ClientError) as e: + aws_client.sns.set_subscription_attributes( + SubscriptionArn=sub_arn, + AttributeName="RawMessageDelivery", + AttributeValue="test-ValUe", + ) + snapshot.match("raw-message-wrong-value", e.value.response) + + with pytest.raises(ClientError) as e: + aws_client.sns.set_subscription_attributes( + SubscriptionArn=sub_arn, + AttributeName="RawMessageDelivery", + AttributeValue="", + ) + snapshot.match("raw-message-empty-value", e.value.response) + with pytest.raises(ClientError) as e: aws_client.sns.set_subscription_attributes( SubscriptionArn=sub_arn, diff --git a/tests/aws/services/sns/test_sns.snapshot.json b/tests/aws/services/sns/test_sns.snapshot.json index e11f572b938c3..cd020730a1406 100644 --- a/tests/aws/services/sns/test_sns.snapshot.json +++ b/tests/aws/services/sns/test_sns.snapshot.json @@ -653,8 +653,19 @@ } }, "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_create_subscriptions_with_attributes": { - "recorded-date": "24-08-2023, 23:27:53", + "recorded-date": "29-03-2024, 19:44:43", "recorded-content": { + "subscribe-wrong-attr": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: RawMessageDelivery: Invalid value [wrongvalue]. Must be true or false.", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, "subscribe": { "SubscriptionArn": "arn:aws:sns::111111111111::", "ResponseMetadata": { @@ -755,7 +766,7 @@ } }, "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_validate_set_sub_attributes": { - "recorded-date": "24-08-2023, 23:27:58", + "recorded-date": "29-03-2024, 19:30:24", "recorded-content": { "fake-attribute": { "Error": { @@ -768,6 +779,28 @@ "HTTPStatusCode": 400 } }, + "raw-message-wrong-value": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: RawMessageDelivery: Invalid value [test-ValUe]. Must be true or false.", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "raw-message-empty-value": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: RawMessageDelivery: Invalid value []. Must be true or false.", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, "fake-arn-redrive-policy": { "Error": { "Code": "InvalidParameter", @@ -782,7 +815,7 @@ "invalid-json-redrive-policy": { "Error": { "Code": "InvalidParameter", - "Message": "Invalid parameter: RedrivePolicy: failed to parse JSON. Unexpected character ('i' (code 105)): was expecting double-quote to start field name\n at [Source: java.io.StringReader@5498a819; line: 1, column: 3]", + "Message": "Invalid parameter: RedrivePolicy: failed to parse JSON. Unexpected character ('i' (code 105)): was expecting double-quote to start field name\n at [Source: java.io.StringReader@469cc9aa; line: 1, column: 3]", "Type": "Sender" }, "ResponseMetadata": { diff --git a/tests/aws/services/sns/test_sns.validation.json b/tests/aws/services/sns/test_sns.validation.json index 4cae7e9259036..a581ab159b6e0 100644 --- a/tests/aws/services/sns/test_sns.validation.json +++ b/tests/aws/services/sns/test_sns.validation.json @@ -78,7 +78,7 @@ "last_validated_date": "2023-08-24T22:20:07+00:00" }, "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_create_subscriptions_with_attributes": { - "last_validated_date": "2023-08-24T21:27:53+00:00" + "last_validated_date": "2024-03-29T19:44:42+00:00" }, "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_list_subscriptions": { "last_validated_date": "2023-08-25T14:23:53+00:00" @@ -105,7 +105,7 @@ "last_validated_date": "2023-10-20T10:52:36+00:00" }, "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_validate_set_sub_attributes": { - "last_validated_date": "2023-08-24T21:27:58+00:00" + "last_validated_date": "2024-03-29T19:30:23+00:00" }, "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionHttp::test_subscribe_external_http_endpoint[False]": { "last_validated_date": "2023-10-11T22:47:29+00:00" From a5c6433f2b748bad130748343826c1deb4f3b869 Mon Sep 17 00:00:00 2001 From: cloutierMat <79954947+cloutierMat@users.noreply.github.com> Date: Fri, 29 Mar 2024 16:17:14 -0600 Subject: [PATCH 006/169] fix APIGW http integration connection to lambda url (#10561) --- localstack/services/apigateway/integration.py | 7 ++ localstack/testing/pytest/fixtures.py | 78 +++++++++++++ tests/aws/services/apigateway/conftest.py | 16 ++- .../apigateway/test_apigateway_http.py | 103 ++++++++++++++++++ .../test_apigateway_http.snapshot.json | 96 ++++++++++++++++ .../test_apigateway_http.validation.json | 11 ++ tests/aws/services/lambda_/test_lambda.py | 60 ++++++++++ .../lambda_/test_lambda.snapshot.json | 58 ++++++++++ .../lambda_/test_lambda.validation.json | 9 ++ 9 files changed, 432 insertions(+), 6 deletions(-) create mode 100644 tests/aws/services/apigateway/test_apigateway_http.py create mode 100644 tests/aws/services/apigateway/test_apigateway_http.snapshot.json create mode 100644 tests/aws/services/apigateway/test_apigateway_http.validation.json diff --git a/localstack/services/apigateway/integration.py b/localstack/services/apigateway/integration.py index d552434baa530..9aaf6965cd961 100644 --- a/localstack/services/apigateway/integration.py +++ b/localstack/services/apigateway/integration.py @@ -737,6 +737,12 @@ def invoke(self, invocation_context: ApiInvocationContext): class HTTPIntegration(BackendIntegration): + @staticmethod + def _set_http_apigw_headers(headers: Dict[str, Any], invocation_context: ApiInvocationContext): + del headers["host"] + headers["x-amzn-apigateway-api-id"] = invocation_context.api_id + return headers + def invoke(self, invocation_context: ApiInvocationContext): invocation_path = invocation_context.path_with_query_string integration = invocation_context.integration @@ -750,6 +756,7 @@ def invoke(self, invocation_context: ApiInvocationContext): # resolve integration parameters integration_parameters = self.request_params_resolver.resolve(context=invocation_context) headers.update(integration_parameters.get("headers", {})) + self._set_http_apigw_headers(headers, invocation_context) if ":servicediscovery:" in uri: # check if this is a servicediscovery integration URI diff --git a/localstack/testing/pytest/fixtures.py b/localstack/testing/pytest/fixtures.py index 1750eb5b16fbe..163a130684551 100644 --- a/localstack/testing/pytest/fixtures.py +++ b/localstack/testing/pytest/fixtures.py @@ -4,6 +4,7 @@ import logging import os import re +import textwrap import time from typing import Any, Callable, Dict, List, Optional, Tuple @@ -1298,6 +1299,83 @@ def _create_function(): LOG.debug(f"Unable to delete log group {log_group_name} in cleanup") +@pytest.fixture +def create_echo_http_server(aws_client, create_lambda_function): + from localstack.aws.api.lambda_ import Runtime + + lambda_client = aws_client.lambda_ + handler_code = textwrap.dedent(""" + import json + import os + + + def make_response(body: dict, status_code: int = 200): + return { + "statusCode": status_code, + "headers": {"Content-Type": "application/json"}, + "body": body, + } + + + def trim_headers(headers): + if not int(os.getenv("TRIM_X_HEADERS", 0)): + return headers + return { + key: value for key, value in headers.items() + if not (key.startswith("x-amzn") or key.startswith("x-forwarded-")) + } + + + def handler(event, context): + print(json.dumps(event)) + response = { + "args": event.get("queryStringParameters", {}), + "data": event.get("body", ""), + "domain": event["requestContext"].get("domainName", ""), + "headers": trim_headers(event.get("headers", {})), + "method": event["requestContext"]["http"].get("method", ""), + "origin": event["requestContext"]["http"].get("sourceIp", ""), + "path": event["requestContext"]["http"].get("path", ""), + } + return make_response(response)""") + + def _create_echo_http_server(trim_x_headers: bool = False) -> str: + """Creates a server that will echo any request. Any request will be returned with the + following format. Any unset values will have those defaults. + `trim_x_headers` can be set to True to trim some headers that are automatically added by lambda in + order to create easier Snapshot testing. Default: `False` + { + "args": {}, + "headers": {}, + "data": "", + "method": "", + "domain": "", + "origin": "", + "path": "" + }""" + zip_file = testutil.create_lambda_archive(handler_code, get_content=True) + func_name = f"echo-http-{short_uid()}" + create_lambda_function( + func_name=func_name, + zip_file=zip_file, + runtime=Runtime.python3_9, + envvars={"TRIM_X_HEADERS": "1" if trim_x_headers else "0"}, + ) + url_response = lambda_client.create_function_url_config( + FunctionName=func_name, AuthType="NONE" + ) + aws_client.lambda_.add_permission( + FunctionName=func_name, + StatementId="urlPermission", + Action="lambda:InvokeFunctionUrl", + Principal="*", + FunctionUrlAuthType="NONE", + ) + return url_response["FunctionUrl"] + + yield _create_echo_http_server + + @pytest.fixture def create_event_source_mapping(aws_client): uuids = [] diff --git a/tests/aws/services/apigateway/conftest.py b/tests/aws/services/apigateway/conftest.py index cf42c066b1a7c..3d90a076cbbd0 100644 --- a/tests/aws/services/apigateway/conftest.py +++ b/tests/aws/services/apigateway/conftest.py @@ -97,14 +97,18 @@ def _factory( policy = APIGATEWAY_DYNAMODB_POLICY elif ":kinesis:" in integration_uri: policy = APIGATEWAY_KINESIS_POLICY + elif integration_type in ("HTTP", "HTTP_PROXY"): + policy = None else: raise Exception(f"Unexpected integration URI: {integration_uri}") - assume_role_arn = create_iam_role_with_policy( - RoleName=f"role-apigw-{short_uid()}", - PolicyName=f"policy-apigw-{short_uid()}", - RoleDefinition=APIGATEWAY_ASSUME_ROLE_POLICY, - PolicyDefinition=policy, - ) + assume_role_arn = "" + if policy: + assume_role_arn = create_iam_role_with_policy( + RoleName=f"role-apigw-{short_uid()}", + PolicyName=f"policy-apigw-{short_uid()}", + RoleDefinition=APIGATEWAY_ASSUME_ROLE_POLICY, + PolicyDefinition=policy, + ) create_rest_api_integration( aws_client.apigateway, diff --git a/tests/aws/services/apigateway/test_apigateway_http.py b/tests/aws/services/apigateway/test_apigateway_http.py new file mode 100644 index 0000000000000..ca6ec4f3adc3b --- /dev/null +++ b/tests/aws/services/apigateway/test_apigateway_http.py @@ -0,0 +1,103 @@ +import json + +import pytest +import requests + +from localstack.testing.pytest import markers +from localstack.utils.sync import retry +from tests.aws.services.apigateway.apigateway_fixtures import api_invoke_url + + +@pytest.fixture +def add_http_integration_transformers(snapshot): + key_value_transform = [ + "date", + "domain", + "host", + "origin", + "rest_api_id", + "x-amz-apigw-id", + "x-amzn-tls-cipher-suite", + "x-amzn-tls-version", + "x-amzn-requestid", + "x-amzn-trace-id", + "x-forwarded-for", + "x-forwarded-port", + "x-forwarded-proto", + ] + for key in key_value_transform: + snapshot.add_transformer(snapshot.transform.key_value(key)) + + snapshot.add_transformer( + snapshot.transform.jsonpath( + "$.*.headers.content-length", + reference_replacement=True, + value_replacement="content_length", + ), + priority=1, + ) + + +@markers.aws.validated +@markers.snapshot.skip_snapshot_verify( + paths=[ + "$..content.headers.accept-encoding", # TODO: We add an extra space when adding this header + "$..content.headers.user-agent", # TODO: We have to properly set that header on non proxied requests. + # TODO: x-forwarded-for header is actually set when the request is sent to `requests.request`. + # Custom servers receive the header, but lambda execution code receives an empty string. + "$..content.headers.x-forwarded-for", + "$..content.headers.x-localstack-edge", + "$..headers.server", + "$..headers.x-amz-apigw-id", # TODO: we should add that header when forwarding the response + # TODO the remapped headers are currently not added to apigateway response + "$..headers.x-amzn-remapped-connection", + "$..headers.x-amzn-remapped-content-length", + "$..headers.x-amzn-remapped-date", + "$..headers.x-amzn-remapped-x-amzn-requestid", + "$..headers.x-amzn-requestid", + "$..headers.x-amzn-trace-id", + "$..origin", + ] +) +@pytest.mark.parametrize("integration_type", ["HTTP", "HTTP_PROXY"]) +def test_http_integration_with_lambda( + integration_type, + create_echo_http_server, + create_rest_api_with_integration, + snapshot, + add_http_integration_transformers, +): + echo_server_url = create_echo_http_server(trim_x_headers=False) + # create api gateway + stage_name = "test" + api_id = create_rest_api_with_integration( + integration_uri=echo_server_url, integration_type=integration_type, stage=stage_name + ) + snapshot.match("api_id", {"rest_api_id": api_id}) + invocation_url = api_invoke_url( + api_id=api_id, + stage=stage_name, + path="/test", + ) + + def invoke_api(url): + response = requests.post( + url, + data=json.dumps({"message": "hello world"}), + headers={ + "Content-Type": "application/json", + "accept": "application/json", + "user-Agent": "test/integration", + }, + verify=False, + ) + assert response.status_code == 200 + return { + "content": response.json(), + "headers": {k.lower(): v for k, v in dict(response.headers).items()}, + "status_code": response.status_code, + } + + # retry is necessary against AWS, probably IAM permission delay + invoke_response = retry(invoke_api, sleep=2, retries=10, url=invocation_url) + snapshot.match("http-invocation-lambda-url", invoke_response) diff --git a/tests/aws/services/apigateway/test_apigateway_http.snapshot.json b/tests/aws/services/apigateway/test_apigateway_http.snapshot.json new file mode 100644 index 0000000000000..ea899510322c0 --- /dev/null +++ b/tests/aws/services/apigateway/test_apigateway_http.snapshot.json @@ -0,0 +1,96 @@ +{ + "tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_with_lambda[HTTP]": { + "recorded-date": "29-03-2024, 15:12:45", + "recorded-content": { + "api_id": { + "rest_api_id": "" + }, + "http-invocation-lambda-url": { + "content": { + "args": {}, + "data": { + "message": "hello world" + }, + "domain": "", + "headers": { + "accept": "application/json", + "accept-encoding": "gzip,deflate", + "content-length": "26", + "content-type": "application/json", + "host": "", + "user-agent": "AmazonAPIGateway_", + "x-amzn-apigateway-api-id": "", + "x-amzn-tls-cipher-suite": "", + "x-amzn-tls-version": "", + "x-amzn-trace-id": "", + "x-forwarded-for": "", + "x-forwarded-port": "", + "x-forwarded-proto": "" + }, + "method": "POST", + "origin": "", + "path": "/" + }, + "headers": { + "connection": "keep-alive", + "content-length": "", + "content-type": "application/json", + "date": "", + "x-amz-apigw-id": "", + "x-amzn-requestid": "", + "x-amzn-trace-id": "" + }, + "status_code": 200 + } + } + }, + "tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_with_lambda[HTTP_PROXY]": { + "recorded-date": "29-03-2024, 15:13:24", + "recorded-content": { + "api_id": { + "rest_api_id": "" + }, + "http-invocation-lambda-url": { + "content": { + "args": {}, + "data": { + "message": "hello world" + }, + "domain": "", + "headers": { + "accept": "application/json", + "accept-encoding": "gzip, deflate", + "content-length": "26", + "content-type": "application/json", + "host": "", + "user-agent": "test/integration", + "x-amzn-apigateway-api-id": "", + "x-amzn-tls-cipher-suite": "", + "x-amzn-tls-version": "", + "x-amzn-trace-id": "", + "x-forwarded-for": "", + "x-forwarded-port": "", + "x-forwarded-proto": "" + }, + "method": "POST", + "origin": "", + "path": "/" + }, + "headers": { + "connection": "keep-alive", + "content-length": "", + "content-type": "application/json", + "date": "", + "x-amz-apigw-id": "", + "x-amzn-remapped-connection": "keep-alive", + "x-amzn-remapped-content-length": "", + "x-amzn-remapped-date": "", + "x-amzn-remapped-x-amzn-requestid": "", + "x-amzn-requestid": "", + "x-amzn-trace-id": "" + }, + "status_code": 200 + } + } + } +} diff --git a/tests/aws/services/apigateway/test_apigateway_http.validation.json b/tests/aws/services/apigateway/test_apigateway_http.validation.json new file mode 100644 index 0000000000000..4d8b89a4c97bf --- /dev/null +++ b/tests/aws/services/apigateway/test_apigateway_http.validation.json @@ -0,0 +1,11 @@ +{ + "tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_with_lambda": { + "last_validated_date": "2024-03-28T19:24:09+00:00" + }, + "tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_with_lambda[HTTP]": { + "last_validated_date": "2024-03-29T15:12:45+00:00" + }, + "tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_with_lambda[HTTP_PROXY]": { + "last_validated_date": "2024-03-29T15:13:24+00:00" + } +} diff --git a/tests/aws/services/lambda_/test_lambda.py b/tests/aws/services/lambda_/test_lambda.py index a6c7c635b4319..3d2c7501a050e 100644 --- a/tests/aws/services/lambda_/test_lambda.py +++ b/tests/aws/services/lambda_/test_lambda.py @@ -1125,6 +1125,66 @@ def test_lambda_url_invalid_invoke_mode(self, create_lambda_function, snapshot, ) snapshot.match("invoke_function_invalid_invoke_type", e.value.response) + @markers.snapshot.skip_snapshot_verify( + paths=[ + "$..headers.domain", # TODO: LS Lambda should populate this value for AWS parity + "$..headers.x-forwarded-for", + "$..headers.x-amzn-trace-id", + "$..origin", # TODO: LS Lambda should populate this value for AWS parity + ] + ) + @markers.aws.validated + def test_lambda_url_echo_http_fixture_default( + self, create_echo_http_server, snapshot, aws_client + ): + key_value_transform = [ + "domain", + "origin", + "x-amzn-tls-cipher-suite", + "x-amzn-tls-version", + "x-amzn-trace-id", + "x-forwarded-for", + "x-forwarded-port", + "x-forwarded-proto", + ] + for key in key_value_transform: + snapshot.add_transformer(snapshot.transform.key_value(key)) + echo_url = create_echo_http_server() + response = requests.post( + url=echo_url + "/path/1?q=query", + headers={ + "content-type": "application/json", + "ExTrA-HeadErs": "With WeiRd CapS", + "user-agent": "test/echo", + }, + json={"foo": "bar"}, + ) + snapshot.match("url_response", response.json()) + + @markers.snapshot.skip_snapshot_verify( + paths=[ + "$..content.headers.domain", # TODO: LS Lambda should populate this value for AWS parity + "$..origin", # TODO: LS Lambda should populate this value for AWS parity + ] + ) + @markers.aws.validated + def test_lambda_url_echo_http_fixture_trim_x_headers( + self, create_echo_http_server, snapshot, aws_client + ): + snapshot.add_transformer(snapshot.transform.key_value("domain")) + snapshot.add_transformer(snapshot.transform.key_value("origin")) + echo_url = create_echo_http_server(trim_x_headers=True) + response = requests.post( + url=echo_url + "/path/1?q=query", + headers={ + "content-type": "application/json", + "ExTrA-HeadErs": "With WeiRd CapS", + "user-agent": "test/echo", + }, + json={"foo": "bar"}, + ) + snapshot.match("url_response", response.json()) + @pytest.mark.skipif(not is_aws_cloud(), reason="Not yet implemented") class TestLambdaPermissions: diff --git a/tests/aws/services/lambda_/test_lambda.snapshot.json b/tests/aws/services/lambda_/test_lambda.snapshot.json index 2f724c91d2818..91f068fd40009 100644 --- a/tests/aws/services/lambda_/test_lambda.snapshot.json +++ b/tests/aws/services/lambda_/test_lambda.snapshot.json @@ -4008,5 +4008,63 @@ } } } + }, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_http_fixture_default": { + "recorded-date": "29-03-2024, 14:57:46", + "recorded-content": { + "url_response": { + "args": { + "q": "query" + }, + "data": { + "foo": "bar" + }, + "domain": "", + "headers": { + "accept": "*/*", + "accept-encoding": "gzip, deflate", + "content-length": "14", + "content-type": "application/json", + "extra-headers": "With WeiRd CapS", + "host": "", + "user-agent": "test/echo", + "x-amzn-tls-cipher-suite": "", + "x-amzn-tls-version": "", + "x-amzn-trace-id": "", + "x-forwarded-for": "", + "x-forwarded-port": "", + "x-forwarded-proto": "" + }, + "method": "POST", + "origin": "", + "path": "/path/1" + } + } + }, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_http_fixture_trim_x_headers": { + "recorded-date": "29-03-2024, 15:03:37", + "recorded-content": { + "url_response": { + "args": { + "q": "query" + }, + "data": { + "foo": "bar" + }, + "domain": "", + "headers": { + "accept": "*/*", + "accept-encoding": "gzip, deflate", + "content-length": "14", + "content-type": "application/json", + "extra-headers": "With WeiRd CapS", + "host": "", + "user-agent": "test/echo" + }, + "method": "POST", + "origin": "", + "path": "/path/1" + } + } } } diff --git a/tests/aws/services/lambda_/test_lambda.validation.json b/tests/aws/services/lambda_/test_lambda.validation.json index 3b55ec6d255e2..0c9244b8f0c46 100644 --- a/tests/aws/services/lambda_/test_lambda.validation.json +++ b/tests/aws/services/lambda_/test_lambda.validation.json @@ -149,6 +149,15 @@ "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_update_function_url_config": { "last_validated_date": "2024-02-19T15:49:42+00:00" }, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_http_fixture": { + "last_validated_date": "2024-03-28T22:20:14+00:00" + }, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_http_fixture_default": { + "last_validated_date": "2024-03-29T14:57:45+00:00" + }, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_http_fixture_trim_x_headers": { + "last_validated_date": "2024-03-29T15:03:36+00:00" + }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_invoke[BUFFERED]": { "last_validated_date": "2024-02-19T16:25:40+00:00" }, From 05c52b2eead858512858154d0bf3d8a2e68dee66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristopher=20Pinz=C3=B3n?= Date: Fri, 29 Mar 2024 18:27:23 -0500 Subject: [PATCH 007/169] add update operations for AWS::SNS::Subscription and AWS::SQS::QueuePolicy. (#10545) --- .../aws_sns_subscription.py | 34 +++++++++++++--- .../resource_providers/aws_sqs_queuepolicy.py | 11 +++++- .../cloudformation/resources/test_sns.py | 31 +++++++++++++++ .../resources/test_sns.snapshot.json | 39 +++++++++++++++++++ .../resources/test_sns.validation.json | 3 ++ .../cloudformation/resources/test_sqs.py | 39 +++++++++++++++++++ .../resources/test_sqs.snapshot.json | 35 +++++++++++++++++ .../resources/test_sqs.validation.json | 3 ++ tests/aws/templates/sns_subscription.yml | 20 ++++++++++ .../aws/templates/sns_subscription_update.yml | 20 ++++++++++ tests/aws/templates/sqs_with_queuepolicy.yaml | 2 - .../sqs_with_queuepolicy_updated.yaml | 25 ++++++++++++ 12 files changed, 253 insertions(+), 9 deletions(-) create mode 100644 tests/aws/templates/sns_subscription.yml create mode 100644 tests/aws/templates/sns_subscription_update.yml create mode 100644 tests/aws/templates/sqs_with_queuepolicy_updated.yaml diff --git a/localstack/services/sns/resource_providers/aws_sns_subscription.py b/localstack/services/sns/resource_providers/aws_sns_subscription.py index bc4496ebf7804..af59bbde4f0aa 100644 --- a/localstack/services/sns/resource_providers/aws_sns_subscription.py +++ b/localstack/services/sns/resource_providers/aws_sns_subscription.py @@ -66,9 +66,6 @@ def create( params = util.select_attributes(model=model, params=["TopicArn", "Protocol", "Endpoint"]) - def attr_val(val): - return json.dumps(val) if isinstance(val, (dict, list)) else str(val) - attrs = [ "DeliveryPolicy", "FilterPolicy", @@ -76,7 +73,7 @@ def attr_val(val): "RawMessageDelivery", "RedrivePolicy", ] - attributes = {a: attr_val(model[a]) for a in attrs if a in model} + attributes = {a: self.attr_val(model[a]) for a in attrs if a in model} if attributes: params["Attributes"] = attributes @@ -128,6 +125,31 @@ def update( """ Update a resource - """ - raise NotImplementedError + model = request.desired_state + model["Id"] = request.previous_state["Id"] + sns = request.aws_client_factory.sns + + attrs = [ + "DeliveryPolicy", + "FilterPolicy", + "FilterPolicyScope", + "RawMessageDelivery", + "RedrivePolicy", + ] + for a in attrs: + if a in model: + sns.set_subscription_attributes( + SubscriptionArn=model["Id"], + AttributeName=a, + AttributeValue=self.attr_val(model[a]), + ) + return ProgressEvent( + status=OperationStatus.SUCCESS, + resource_model=model, + custom_context=request.custom_context, + ) + + @staticmethod + def attr_val(val): + return json.dumps(val) if isinstance(val, dict) else str(val) diff --git a/localstack/services/sqs/resource_providers/aws_sqs_queuepolicy.py b/localstack/services/sqs/resource_providers/aws_sqs_queuepolicy.py index 40776ab849fb0..cc7bdecfa9254 100644 --- a/localstack/services/sqs/resource_providers/aws_sqs_queuepolicy.py +++ b/localstack/services/sqs/resource_providers/aws_sqs_queuepolicy.py @@ -98,4 +98,13 @@ def update( """ Update a resource """ - raise NotImplementedError + model = request.desired_state + sqs = request.aws_client_factory.sqs + for queue in model.get("Queues", []): + policy = json.dumps(model["PolicyDocument"]) + sqs.set_queue_attributes(QueueUrl=queue, Attributes={"Policy": policy}) + + return ProgressEvent( + status=OperationStatus.SUCCESS, + resource_model=request.desired_state, + ) diff --git a/tests/aws/services/cloudformation/resources/test_sns.py b/tests/aws/services/cloudformation/resources/test_sns.py index 892550cf0f55f..f4b5e054741b3 100644 --- a/tests/aws/services/cloudformation/resources/test_sns.py +++ b/tests/aws/services/cloudformation/resources/test_sns.py @@ -100,3 +100,34 @@ def test_deploy_stack_with_sns_topic(deploy_cfn_template, aws_client): rs = aws_client.sns.list_topics() topics = [tp for tp in rs["Topics"] if tp["TopicArn"] == topic_arn] assert not topics + + +@markers.aws.validated +def test_update_subscription(snapshot, deploy_cfn_template, aws_client, sqs_queue, sns_topic): + topic_arn = sns_topic["Attributes"]["TopicArn"] + queue_url = sqs_queue + queue_arn = aws_client.sqs.get_queue_attributes( + QueueUrl=queue_url, AttributeNames=["QueueArn"] + )["Attributes"]["QueueArn"] + + stack = deploy_cfn_template( + parameters={"TopicArn": topic_arn, "QueueArn": queue_arn}, + template_path=os.path.join( + os.path.dirname(__file__), "../../../templates/sns_subscription.yml" + ), + ) + sub_arn = stack.outputs["SubscriptionArn"] + subscription = aws_client.sns.get_subscription_attributes(SubscriptionArn=sub_arn) + snapshot.match("subscription-1", subscription) + + deploy_cfn_template( + parameters={"TopicArn": topic_arn, "QueueArn": queue_arn}, + template_path=os.path.join( + os.path.dirname(__file__), "../../../templates/sns_subscription_update.yml" + ), + stack_name=stack.stack_name, + is_update=True, + ) + subscription_updated = aws_client.sns.get_subscription_attributes(SubscriptionArn=sub_arn) + snapshot.match("subscription-2", subscription_updated) + snapshot.add_transformer(snapshot.transform.cloudformation_api()) diff --git a/tests/aws/services/cloudformation/resources/test_sns.snapshot.json b/tests/aws/services/cloudformation/resources/test_sns.snapshot.json index 763ec15698a6e..d213cb13b190e 100644 --- a/tests/aws/services/cloudformation/resources/test_sns.snapshot.json +++ b/tests/aws/services/cloudformation/resources/test_sns.snapshot.json @@ -65,5 +65,44 @@ } } } + }, + "tests/aws/services/cloudformation/resources/test_sns.py::test_update_subscription": { + "recorded-date": "29-03-2024, 21:16:26", + "recorded-content": { + "subscription-1": { + "Attributes": { + "ConfirmationWasAuthenticated": "true", + "Endpoint": "arn:aws:sqs::111111111111:", + "Owner": "111111111111", + "PendingConfirmation": "false", + "Protocol": "sqs", + "RawMessageDelivery": "true", + "SubscriptionArn": "arn:aws:sns::111111111111::", + "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/", + "TopicArn": "arn:aws:sns::111111111111:" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "subscription-2": { + "Attributes": { + "ConfirmationWasAuthenticated": "true", + "Endpoint": "arn:aws:sqs::111111111111:", + "Owner": "111111111111", + "PendingConfirmation": "false", + "Protocol": "sqs", + "RawMessageDelivery": "false", + "SubscriptionArn": "arn:aws:sns::111111111111::", + "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/", + "TopicArn": "arn:aws:sns::111111111111:" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/cloudformation/resources/test_sns.validation.json b/tests/aws/services/cloudformation/resources/test_sns.validation.json index db56aadfc3793..6ba4c5f5b2e22 100644 --- a/tests/aws/services/cloudformation/resources/test_sns.validation.json +++ b/tests/aws/services/cloudformation/resources/test_sns.validation.json @@ -1,5 +1,8 @@ { "tests/aws/services/cloudformation/resources/test_sns.py::test_sns_topic_fifo_with_deduplication": { "last_validated_date": "2023-11-27T20:27:29+00:00" + }, + "tests/aws/services/cloudformation/resources/test_sns.py::test_update_subscription": { + "last_validated_date": "2024-03-29T21:16:21+00:00" } } diff --git a/tests/aws/services/cloudformation/resources/test_sqs.py b/tests/aws/services/cloudformation/resources/test_sqs.py index 5ce54672e3ff9..ddb17ac989c69 100644 --- a/tests/aws/services/cloudformation/resources/test_sqs.py +++ b/tests/aws/services/cloudformation/resources/test_sqs.py @@ -5,6 +5,7 @@ from localstack.testing.pytest import markers from localstack.utils.strings import short_uid +from localstack.utils.sync import wait_until @markers.aws.unknown @@ -120,3 +121,41 @@ def test_update_queue_no_change(deploy_cfn_template, aws_client, snapshot): }, ) snapshot.match("outputs-2", updated_stack.outputs) + + +@markers.aws.validated +def test_update_sqs_queuepolicy(deploy_cfn_template, aws_client, snapshot): + stack = deploy_cfn_template( + template_path=os.path.join( + os.path.dirname(__file__), "../../../templates/sqs_with_queuepolicy.yaml" + ) + ) + + policy = aws_client.sqs.get_queue_attributes( + QueueUrl=stack.outputs["QueueUrlOutput"], AttributeNames=["Policy"] + ) + snapshot.match("policy1", policy["Attributes"]["Policy"]) + + updated_stack = deploy_cfn_template( + template_path=os.path.join( + os.path.dirname(__file__), "../../../templates/sqs_with_queuepolicy_updated.yaml" + ), + is_update=True, + stack_name=stack.stack_name, + ) + + def check_policy_updated(): + policy_updated = aws_client.sqs.get_queue_attributes( + QueueUrl=updated_stack.outputs["QueueUrlOutput"], AttributeNames=["Policy"] + ) + assert policy_updated["Attributes"]["Policy"] != policy["Attributes"]["Policy"] + return policy_updated + + wait_until(check_policy_updated) + + policy = aws_client.sqs.get_queue_attributes( + QueueUrl=updated_stack.outputs["QueueUrlOutput"], AttributeNames=["Policy"] + ) + + snapshot.match("policy2", policy["Attributes"]["Policy"]) + snapshot.add_transformer(snapshot.transform.cloudformation_api()) diff --git a/tests/aws/services/cloudformation/resources/test_sqs.snapshot.json b/tests/aws/services/cloudformation/resources/test_sqs.snapshot.json index 7ebd7dfce9e86..10304744f1064 100644 --- a/tests/aws/services/cloudformation/resources/test_sqs.snapshot.json +++ b/tests/aws/services/cloudformation/resources/test_sqs.snapshot.json @@ -11,5 +11,40 @@ "QueueUrl": "" } } + }, + "tests/aws/services/cloudformation/resources/test_sqs.py::test_update_sqs_queuepolicy": { + "recorded-date": "27-03-2024, 20:30:24", + "recorded-content": { + "policy1": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": [ + "sqs:SendMessage", + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl" + ], + "Resource": "arn:aws:sqs::111111111111:" + } + ] + }, + "policy2": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Deny", + "Principal": "*", + "Action": [ + "sqs:SendMessage", + "sqs:GetQueueAttributes", + "sqs:GetQueueUrl" + ], + "Resource": "arn:aws:sqs::111111111111:" + } + ] + } + } } } diff --git a/tests/aws/services/cloudformation/resources/test_sqs.validation.json b/tests/aws/services/cloudformation/resources/test_sqs.validation.json index 8278f0cc25b43..2d70db6c86b4a 100644 --- a/tests/aws/services/cloudformation/resources/test_sqs.validation.json +++ b/tests/aws/services/cloudformation/resources/test_sqs.validation.json @@ -1,5 +1,8 @@ { "tests/aws/services/cloudformation/resources/test_sqs.py::test_update_queue_no_change": { "last_validated_date": "2023-12-08T20:11:26+00:00" + }, + "tests/aws/services/cloudformation/resources/test_sqs.py::test_update_sqs_queuepolicy": { + "last_validated_date": "2024-03-27T20:30:23+00:00" } } diff --git a/tests/aws/templates/sns_subscription.yml b/tests/aws/templates/sns_subscription.yml new file mode 100644 index 0000000000000..d7c99ae0bf27f --- /dev/null +++ b/tests/aws/templates/sns_subscription.yml @@ -0,0 +1,20 @@ +Parameters: + TopicArn: + Type: String + Description: The ARN of the SNS topic to subscribe to + QueueArn: + Type: String + Description: The URL of the SQS queue to send messages to +Resources: + SnsSubscription: + Type: AWS::SNS::Subscription + Properties: + Protocol: sqs + TopicArn: !Ref TopicArn + Endpoint: !Ref QueueArn + RawMessageDelivery: true + +Outputs: + SubscriptionArn: + Value: !Ref SnsSubscription + Description: The ARN of the SNS subscription diff --git a/tests/aws/templates/sns_subscription_update.yml b/tests/aws/templates/sns_subscription_update.yml new file mode 100644 index 0000000000000..5bbec3d89c67c --- /dev/null +++ b/tests/aws/templates/sns_subscription_update.yml @@ -0,0 +1,20 @@ +Parameters: + TopicArn: + Type: String + Description: The ARN of the SNS topic to subscribe to + QueueArn: + Type: String + Description: The URL of the SQS queue to send messages to +Resources: + SnsSubscription: + Type: AWS::SNS::Subscription + Properties: + Protocol: sqs + TopicArn: !Ref TopicArn + Endpoint: !Ref QueueArn + RawMessageDelivery: false + +Outputs: + SubscriptionArn: + Value: !Ref SnsSubscription + Description: The ARN of the SNS subscription diff --git a/tests/aws/templates/sqs_with_queuepolicy.yaml b/tests/aws/templates/sqs_with_queuepolicy.yaml index 48fbca12bd726..0fabf18902847 100644 --- a/tests/aws/templates/sqs_with_queuepolicy.yaml +++ b/tests/aws/templates/sqs_with_queuepolicy.yaml @@ -1,8 +1,6 @@ Resources: Queue4A7E3555: Type: AWS::SQS::Queue - UpdateReplacePolicy: Delete - DeletionPolicy: Delete QueuePolicy25439813: Type: AWS::SQS::QueuePolicy Properties: diff --git a/tests/aws/templates/sqs_with_queuepolicy_updated.yaml b/tests/aws/templates/sqs_with_queuepolicy_updated.yaml new file mode 100644 index 0000000000000..17818bddc3fd2 --- /dev/null +++ b/tests/aws/templates/sqs_with_queuepolicy_updated.yaml @@ -0,0 +1,25 @@ +Resources: + Queue4A7E3555: + Type: AWS::SQS::Queue + QueuePolicy25439813: + Type: AWS::SQS::QueuePolicy + Properties: + PolicyDocument: + Statement: + - Action: + - sqs:SendMessage + - sqs:GetQueueAttributes + - sqs:GetQueueUrl + Effect: Deny + Principal: "*" + Resource: + Fn::GetAtt: + - Queue4A7E3555 + - Arn + Version: "2012-10-17" + Queues: + - Ref: Queue4A7E3555 +Outputs: + QueueUrlOutput: + Value: + Ref: Queue4A7E3555 From 628b96b44a4fc63d880a4c1238a4f15f5803a3f2 Mon Sep 17 00:00:00 2001 From: Sannya Singal <32308435+sannya-singal@users.noreply.github.com> Date: Sat, 30 Mar 2024 14:59:36 +0530 Subject: [PATCH 008/169] remove the conditional provider in `test_dashboard_lifecycle` (#10571) --- tests/aws/services/cloudwatch/test_cloudwatch.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/aws/services/cloudwatch/test_cloudwatch.py b/tests/aws/services/cloudwatch/test_cloudwatch.py index b4414ede3e122..e96339382779a 100644 --- a/tests/aws/services/cloudwatch/test_cloudwatch.py +++ b/tests/aws/services/cloudwatch/test_cloudwatch.py @@ -1391,8 +1391,16 @@ def contains_receive_delete_metrics() -> int: @markers.aws.validated @markers.snapshot.skip_snapshot_verify( - paths=["$..DashboardArn", "$..DashboardEntries..Size"], condition=is_old_provider - ) # ARN has a typo in moto + condition=is_old_provider, + paths=[ + "$..DashboardArn", # ARN has a typo in moto + ], + ) + @markers.snapshot.skip_snapshot_verify( + paths=[ + "$..DashboardEntries..Size", # need to be skipped because size changes if the region name length is longer + ] + ) def test_dashboard_lifecycle(self, aws_client, region_name, snapshot): dashboard_name = f"test-{short_uid()}" dashboard_body = { From 693772f275a6056153fdbd471d312945338e5c99 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Mon, 1 Apr 2024 13:09:15 +0200 Subject: [PATCH 009/169] Update ASF APIs (#10577) --- localstack/aws/api/ec2/__init__.py | 68 ++++++++++++++++++++++++++++++ pyproject.toml | 2 +- requirements-base-runtime.txt | 4 +- requirements-dev.txt | 6 +-- requirements-runtime.txt | 6 +-- requirements-test.txt | 6 +-- requirements-typehint.txt | 6 +-- 7 files changed, 83 insertions(+), 15 deletions(-) diff --git a/localstack/aws/api/ec2/__init__.py b/localstack/aws/api/ec2/__init__.py index a1a5d35e86ac4..91c906955e57b 100644 --- a/localstack/aws/api/ec2/__init__.py +++ b/localstack/aws/api/ec2/__init__.py @@ -17,6 +17,7 @@ BaselineThroughputInMBps = float Boolean = bool BoxedDouble = float +BoxedInteger = int BundleId = str BurstablePerformanceFlag = bool CancelCapacityReservationFleetErrorCode = str @@ -749,6 +750,18 @@ class DatafeedSubscriptionState(str): Inactive = "Inactive" +class DefaultInstanceMetadataEndpointState(str): + disabled = "disabled" + enabled = "enabled" + no_preference = "no-preference" + + +class DefaultInstanceMetadataTagsState(str): + disabled = "disabled" + enabled = "enabled" + no_preference = "no-preference" + + class DefaultRouteTableAssociationValue(str): enable = "enable" disable = "disable" @@ -2006,6 +2019,9 @@ class InstanceType(str): r7i_metal_48xl = "r7i.metal-48xl" r7iz_metal_16xl = "r7iz.metal-16xl" r7iz_metal_32xl = "r7iz.metal-32xl" + c7gd_metal = "c7gd.metal" + m7gd_metal = "m7gd.metal" + r7gd_metal = "r7gd.metal" class InstanceTypeHypervisor(str): @@ -2352,6 +2368,12 @@ class MembershipType(str): igmp = "igmp" +class MetadataDefaultHttpTokensState(str): + optional = "optional" + required = "required" + no_preference = "no-preference" + + class MetricType(str): aggregate_latency = "aggregate-latency" @@ -15132,6 +15154,21 @@ class GetImageBlockPublicAccessStateResult(TypedDict, total=False): ImageBlockPublicAccessState: Optional[String] +class GetInstanceMetadataDefaultsRequest(ServiceRequest): + DryRun: Optional[Boolean] + + +class InstanceMetadataDefaultsResponse(TypedDict, total=False): + HttpTokens: Optional[HttpTokensState] + HttpPutResponseHopLimit: Optional[BoxedInteger] + HttpEndpoint: Optional[InstanceMetadataEndpointState] + InstanceMetadataTags: Optional[InstanceMetadataTagsState] + + +class GetInstanceMetadataDefaultsResult(TypedDict, total=False): + AccountLevel: Optional[InstanceMetadataDefaultsResponse] + + VirtualizationTypeSet = List[VirtualizationType] @@ -16488,6 +16525,18 @@ class ModifyInstanceMaintenanceOptionsResult(TypedDict, total=False): AutoRecovery: Optional[InstanceAutoRecoveryState] +class ModifyInstanceMetadataDefaultsRequest(ServiceRequest): + HttpTokens: Optional[MetadataDefaultHttpTokensState] + HttpPutResponseHopLimit: Optional[BoxedInteger] + HttpEndpoint: Optional[DefaultInstanceMetadataEndpointState] + InstanceMetadataTags: Optional[DefaultInstanceMetadataTagsState] + DryRun: Optional[Boolean] + + +class ModifyInstanceMetadataDefaultsResult(TypedDict, total=False): + Return: Optional[Boolean] + + class ModifyInstanceMetadataOptionsRequest(ServiceRequest): InstanceId: InstanceId HttpTokens: Optional[HttpTokensState] @@ -23455,6 +23504,12 @@ def get_image_block_public_access_state( ) -> GetImageBlockPublicAccessStateResult: raise NotImplementedError + @handler("GetInstanceMetadataDefaults") + def get_instance_metadata_defaults( + self, context: RequestContext, dry_run: Boolean = None, **kwargs + ) -> GetInstanceMetadataDefaultsResult: + raise NotImplementedError + @handler("GetInstanceTypesFromInstanceRequirements") def get_instance_types_from_instance_requirements( self, @@ -24224,6 +24279,19 @@ def modify_instance_maintenance_options( ) -> ModifyInstanceMaintenanceOptionsResult: raise NotImplementedError + @handler("ModifyInstanceMetadataDefaults") + def modify_instance_metadata_defaults( + self, + context: RequestContext, + http_tokens: MetadataDefaultHttpTokensState = None, + http_put_response_hop_limit: BoxedInteger = None, + http_endpoint: DefaultInstanceMetadataEndpointState = None, + instance_metadata_tags: DefaultInstanceMetadataTagsState = None, + dry_run: Boolean = None, + **kwargs, + ) -> ModifyInstanceMetadataDefaultsResult: + raise NotImplementedError + @handler("ModifyInstanceMetadataOptions") def modify_instance_metadata_options( self, diff --git a/pyproject.toml b/pyproject.toml index 60f8b32248c63..8a19472d5302d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ Issues = "https://github.com/localstack/localstack/issues" base-runtime = [ "awscrt>=0.13.14", "boto3>=1.26.121", - "botocore==1.34.69", + "botocore==1.34.74", "cbor2>=5.2.0", "dnspython>=1.16.0", # TODO tag incompatibility introduced in 7.0.0 with https://github.com/docker/docker-py/pull/3191 diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index d4428cbfdd95e..310962d84e0ad 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -14,9 +14,9 @@ blinker==1.7.0 # via # flask # quart -boto3==1.34.69 +boto3==1.34.74 # via localstack-core (pyproject.toml) -botocore==1.34.69 +botocore==1.34.74 # via # boto3 # localstack-core (pyproject.toml) diff --git a/requirements-dev.txt b/requirements-dev.txt index 57f72e36b39e1..1fa7caf0adcba 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.86.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.69 +awscli==1.32.74 # via localstack-core awscrt==0.20.5 # via localstack-core @@ -55,12 +55,12 @@ blinker==1.7.0 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.69 +boto3==1.34.74 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.69 +botocore==1.34.74 # via # aws-xray-sdk # awscli diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 84d5c972f3b12..eea77fccc64c6 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -33,7 +33,7 @@ aws-sam-translator==1.86.0 # localstack-core (pyproject.toml) aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.69 +awscli==1.32.74 # via localstack-core (pyproject.toml) awscrt==0.20.5 # via localstack-core @@ -43,12 +43,12 @@ blinker==1.7.0 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.69 +boto3==1.34.74 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.69 +botocore==1.34.74 # via # aws-xray-sdk # awscli diff --git a/requirements-test.txt b/requirements-test.txt index 2565731605fc2..19cee38bb54c1 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.86.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.69 +awscli==1.32.74 # via localstack-core awscrt==0.20.5 # via localstack-core @@ -55,12 +55,12 @@ blinker==1.7.0 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.69 +boto3==1.34.74 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.69 +botocore==1.34.74 # via # aws-xray-sdk # awscli diff --git a/requirements-typehint.txt b/requirements-typehint.txt index e70fb4bda8ca6..00f728337d1e6 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.86.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.69 +awscli==1.32.74 # via localstack-core awscrt==0.20.5 # via localstack-core @@ -55,14 +55,14 @@ blinker==1.7.0 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.69 +boto3==1.34.74 # via # aws-sam-translator # localstack-core # moto-ext boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.69 # via localstack-core (pyproject.toml) -botocore==1.34.69 +botocore==1.34.74 # via # aws-xray-sdk # awscli From 42380b6afde709fc43cad41cdd1d46c5093de352 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 07:30:56 +0200 Subject: [PATCH 010/169] Bump the docker-base-images group with 2 updates (#10584) --- Dockerfile | 4 ++-- Dockerfile.s3 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7e26047c8433a..9482fe9bc98e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # java-builder: Stage to build a custom JRE (with jlink) -FROM eclipse-temurin:11@sha256:1cfde03be30cc838fe93164ef7e64dbb38656d926103c901cb2e0f806c26ae43 as java-builder +FROM eclipse-temurin:11@sha256:ea119fd944947a21b3d617d402c8cb13671aa40cb28e3a87da5b02df3d95e935 as java-builder # create a custom, minimized JRE via jlink RUN jlink --add-modules \ @@ -29,7 +29,7 @@ jdk.localedata --include-locales en,th \ # base: Stage which installs necessary runtime dependencies (OS packages, java,...) -FROM python:3.11.8-slim-bookworm@sha256:bc6a38f1284b39dc87a602c50d027ad268d091af6956dd5142337121ce90b3f0 as base +FROM python:3.11.8-slim-bookworm@sha256:90f8795536170fd08236d2ceb74fe7065dbf74f738d8b84bfbf263656654dc9b as base ARG TARGETARCH # Install runtime OS package dependencies diff --git a/Dockerfile.s3 b/Dockerfile.s3 index 489f2c7c4d32d..e062b46009c7f 100644 --- a/Dockerfile.s3 +++ b/Dockerfile.s3 @@ -1,5 +1,5 @@ # base: Stage which installs necessary runtime dependencies (OS packages, filesystem...) -FROM python:3.11.8-slim-bookworm@sha256:bc6a38f1284b39dc87a602c50d027ad268d091af6956dd5142337121ce90b3f0 as base +FROM python:3.11.8-slim-bookworm@sha256:90f8795536170fd08236d2ceb74fe7065dbf74f738d8b84bfbf263656654dc9b as base ARG TARGETARCH # set workdir From 779fa338e4a37e4fdd9a847fdd721c2797d74f30 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Tue, 2 Apr 2024 09:40:16 +0200 Subject: [PATCH 011/169] Upgrade pinned Python dependencies (#10585) --- requirements-base-runtime.txt | 10 +++---- requirements-basic.txt | 4 +-- requirements-dev.txt | 26 +++++++++--------- requirements-runtime.txt | 14 +++++----- requirements-test.txt | 20 +++++++------- requirements-typehint.txt | 50 +++++++++++++++++------------------ 6 files changed, 62 insertions(+), 62 deletions(-) diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index 310962d84e0ad..8cc77ac915705 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -8,7 +8,7 @@ aiofiles==23.2.1 # via quart attrs==23.2.0 # via localstack-twisted -awscrt==0.20.5 +awscrt==0.20.6 # via localstack-core (pyproject.toml) blinker==1.7.0 # via @@ -21,7 +21,7 @@ botocore==1.34.74 # boto3 # localstack-core (pyproject.toml) # s3transfer -build==1.1.1 +build==1.2.1 # via localstack-core (pyproject.toml) cachetools==5.3.3 # via localstack-core (pyproject.toml) @@ -122,7 +122,7 @@ priority==1.3.0 # localstack-twisted psutil==5.9.8 # via localstack-core (pyproject.toml) -pycparser==2.21 +pycparser==2.22 # via cffi pygments==2.17.2 # via rich @@ -138,7 +138,7 @@ python-dotenv==1.0.1 # via localstack-core (pyproject.toml) pyyaml==6.0.1 # via localstack-core (pyproject.toml) -quart==0.19.4 +quart==0.19.5 # via localstack-core (pyproject.toml) readerwriterlock==1.0.9 # via localstack-core (pyproject.toml) @@ -180,7 +180,7 @@ urllib3==2.2.1 # requests websocket-client==1.7.0 # via docker -werkzeug==3.0.1 +werkzeug==3.0.2 # via # flask # localstack-core (pyproject.toml) diff --git a/requirements-basic.txt b/requirements-basic.txt index f66a9a2eda5da..af61d47953689 100644 --- a/requirements-basic.txt +++ b/requirements-basic.txt @@ -4,7 +4,7 @@ # # pip-compile --output-file=requirements-basic.txt --strip-extras --unsafe-package=distribute --unsafe-package=localstack-core --unsafe-package=pip --unsafe-package=setuptools pyproject.toml # -build==1.1.1 +build==1.2.1 # via localstack-core (pyproject.toml) cachetools==5.3.3 # via localstack-core (pyproject.toml) @@ -38,7 +38,7 @@ plux==1.9.0 # via localstack-core (pyproject.toml) psutil==5.9.8 # via localstack-core (pyproject.toml) -pycparser==2.21 +pycparser==2.22 # via cffi pygments==2.17.2 # via rich diff --git a/requirements-dev.txt b/requirements-dev.txt index 1fa7caf0adcba..688bb4d4473a4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -35,9 +35,9 @@ aws-cdk-asset-awscli-v1==2.2.202 # via aws-cdk-lib aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib -aws-cdk-asset-node-proxy-agent-v6==2.0.1 +aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.133.0 +aws-cdk-lib==2.135.0 # via localstack-core aws-sam-translator==1.86.0 # via @@ -47,7 +47,7 @@ aws-xray-sdk==2.13.0 # via moto-ext awscli==1.32.74 # via localstack-core -awscrt==0.20.5 +awscrt==0.20.6 # via localstack-core blinker==1.7.0 # via @@ -69,7 +69,7 @@ botocore==1.34.74 # localstack-snapshot # moto-ext # s3transfer -build==1.1.1 +build==1.2.1 # via # localstack-core # localstack-core (pyproject.toml) @@ -123,7 +123,7 @@ cryptography==42.0.5 # localstack-core (pyproject.toml) # moto-ext # pyopenssl -cython==3.0.9 +cython==3.0.10 # via localstack-core (pyproject.toml) decorator==5.1.1 # via jsonpath-rw @@ -154,7 +154,7 @@ docopt==0.6.2 # via coveralls docutils==0.16 # via awscli -filelock==3.13.1 +filelock==3.13.3 # via virtualenv flask==3.0.2 # via @@ -174,7 +174,7 @@ h2==4.1.0 # localstack-twisted hpack==4.0.0 # via h2 -httpcore==1.0.4 +httpcore==1.0.5 # via httpx httpx[http2]==0.27.0 # via localstack-core @@ -346,9 +346,9 @@ publication==0.0.3 # jsii py-partiql-parser==0.5.1 # via moto-ext -pyasn1==0.5.1 +pyasn1==0.6.0 # via rsa -pycparser==2.21 +pycparser==2.22 # via cffi pydantic==2.6.4 # via aws-sam-translator @@ -356,7 +356,7 @@ pydantic-core==2.16.3 # via pydantic pygments==2.17.2 # via rich -pymongo==4.6.2 +pymongo==4.6.3 # via localstack-core pyopenssl==24.1.0 # via @@ -402,7 +402,7 @@ pyyaml==6.0.1 # moto-ext # pre-commit # responses -quart==0.19.4 +quart==0.19.5 # via localstack-core readerwriterlock==1.0.9 # via localstack-core @@ -446,7 +446,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core (pyproject.toml) -ruff==0.3.4 +ruff==0.3.5 # via localstack-core (pyproject.toml) s3transfer==0.10.1 # via @@ -512,7 +512,7 @@ websocket-client==1.7.0 # via # docker # localstack-core -werkzeug==3.0.1 +werkzeug==3.0.2 # via # flask # localstack-core diff --git a/requirements-runtime.txt b/requirements-runtime.txt index eea77fccc64c6..a456939b83de8 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -35,7 +35,7 @@ aws-xray-sdk==2.13.0 # via moto-ext awscli==1.32.74 # via localstack-core (pyproject.toml) -awscrt==0.20.5 +awscrt==0.20.6 # via localstack-core blinker==1.7.0 # via @@ -56,7 +56,7 @@ botocore==1.34.74 # localstack-core # moto-ext # s3transfer -build==1.1.1 +build==1.2.1 # via # localstack-core # localstack-core (pyproject.toml) @@ -254,9 +254,9 @@ psutil==5.9.8 # localstack-core (pyproject.toml) py-partiql-parser==0.5.1 # via moto-ext -pyasn1==0.5.1 +pyasn1==0.6.0 # via rsa -pycparser==2.21 +pycparser==2.22 # via cffi pydantic==2.6.4 # via aws-sam-translator @@ -264,7 +264,7 @@ pydantic-core==2.16.3 # via pydantic pygments==2.17.2 # via rich -pymongo==4.6.2 +pymongo==4.6.3 # via localstack-core (pyproject.toml) pyopenssl==24.1.0 # via @@ -293,7 +293,7 @@ pyyaml==6.0.1 # localstack-core (pyproject.toml) # moto-ext # responses -quart==0.19.4 +quart==0.19.5 # via localstack-core readerwriterlock==1.0.9 # via localstack-core @@ -380,7 +380,7 @@ urllib3==2.2.1 # responses websocket-client==1.7.0 # via docker -werkzeug==3.0.1 +werkzeug==3.0.2 # via # flask # localstack-core diff --git a/requirements-test.txt b/requirements-test.txt index 19cee38bb54c1..278b46ab7256f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -35,9 +35,9 @@ aws-cdk-asset-awscli-v1==2.2.202 # via aws-cdk-lib aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib -aws-cdk-asset-node-proxy-agent-v6==2.0.1 +aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.133.0 +aws-cdk-lib==2.135.0 # via localstack-core (pyproject.toml) aws-sam-translator==1.86.0 # via @@ -47,7 +47,7 @@ aws-xray-sdk==2.13.0 # via moto-ext awscli==1.32.74 # via localstack-core -awscrt==0.20.5 +awscrt==0.20.6 # via localstack-core blinker==1.7.0 # via @@ -69,7 +69,7 @@ botocore==1.34.74 # localstack-snapshot # moto-ext # s3transfer -build==1.1.1 +build==1.2.1 # via # localstack-core # localstack-core (pyproject.toml) @@ -160,7 +160,7 @@ h2==4.1.0 # localstack-twisted hpack==4.0.0 # via h2 -httpcore==1.0.4 +httpcore==1.0.5 # via httpx httpx[http2]==0.27.0 # via localstack-core (pyproject.toml) @@ -317,9 +317,9 @@ publication==0.0.3 # jsii py-partiql-parser==0.5.1 # via moto-ext -pyasn1==0.5.1 +pyasn1==0.6.0 # via rsa -pycparser==2.21 +pycparser==2.22 # via cffi pydantic==2.6.4 # via aws-sam-translator @@ -327,7 +327,7 @@ pydantic-core==2.16.3 # via pydantic pygments==2.17.2 # via rich -pymongo==4.6.2 +pymongo==4.6.3 # via localstack-core pyopenssl==24.1.0 # via @@ -370,7 +370,7 @@ pyyaml==6.0.1 # localstack-core (pyproject.toml) # moto-ext # responses -quart==0.19.4 +quart==0.19.5 # via localstack-core readerwriterlock==1.0.9 # via localstack-core @@ -473,7 +473,7 @@ websocket-client==1.7.0 # via # docker # localstack-core (pyproject.toml) -werkzeug==3.0.1 +werkzeug==3.0.2 # via # flask # localstack-core diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 00f728337d1e6..d7a8e6ac113a8 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -35,9 +35,9 @@ aws-cdk-asset-awscli-v1==2.2.202 # via aws-cdk-lib aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib -aws-cdk-asset-node-proxy-agent-v6==2.0.1 +aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.133.0 +aws-cdk-lib==2.135.0 # via localstack-core aws-sam-translator==1.86.0 # via @@ -47,7 +47,7 @@ aws-xray-sdk==2.13.0 # via moto-ext awscli==1.32.74 # via localstack-core -awscrt==0.20.5 +awscrt==0.20.6 # via localstack-core blinker==1.7.0 # via @@ -60,7 +60,7 @@ boto3==1.34.74 # aws-sam-translator # localstack-core # moto-ext -boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.69 +boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.75 # via localstack-core (pyproject.toml) botocore==1.34.74 # via @@ -73,7 +73,7 @@ botocore==1.34.74 # s3transfer botocore-stubs==1.34.69 # via boto3-stubs -build==1.1.1 +build==1.2.1 # via # localstack-core # localstack-core (pyproject.toml) @@ -127,7 +127,7 @@ cryptography==42.0.5 # localstack-core (pyproject.toml) # moto-ext # pyopenssl -cython==3.0.9 +cython==3.0.10 # via localstack-core decorator==5.1.1 # via jsonpath-rw @@ -158,7 +158,7 @@ docopt==0.6.2 # via coveralls docutils==0.16 # via awscli -filelock==3.13.1 +filelock==3.13.3 # via virtualenv flask==3.0.2 # via @@ -178,7 +178,7 @@ h2==4.1.0 # localstack-twisted hpack==4.0.0 # via h2 -httpcore==1.0.4 +httpcore==1.0.5 # via httpx httpx[http2]==0.27.0 # via localstack-core @@ -307,9 +307,9 @@ mypy-boto3-autoscaling==1.34.54 # via boto3-stubs mypy-boto3-backup==1.34.64 # via boto3-stubs -mypy-boto3-batch==1.34.59 +mypy-boto3-batch==1.34.72 # via boto3-stubs -mypy-boto3-ce==1.34.52 +mypy-boto3-ce==1.34.71 # via boto3-stubs mypy-boto3-cloudcontrol==1.34.0 # via boto3-stubs @@ -319,7 +319,7 @@ mypy-boto3-cloudfront==1.34.0 # via boto3-stubs mypy-boto3-cloudtrail==1.34.59 # via boto3-stubs -mypy-boto3-cloudwatch==1.34.40 +mypy-boto3-cloudwatch==1.34.75 # via boto3-stubs mypy-boto3-codecommit==1.34.6 # via boto3-stubs @@ -335,23 +335,23 @@ mypy-boto3-dynamodb==1.34.67 # via boto3-stubs mypy-boto3-dynamodbstreams==1.34.0 # via boto3-stubs -mypy-boto3-ec2==1.34.66 +mypy-boto3-ec2==1.34.73 # via boto3-stubs mypy-boto3-ecr==1.34.0 # via boto3-stubs -mypy-boto3-ecs==1.34.39 +mypy-boto3-ecs==1.34.71 # via boto3-stubs mypy-boto3-efs==1.34.0 # via boto3-stubs -mypy-boto3-eks==1.34.53 +mypy-boto3-eks==1.34.73 # via boto3-stubs -mypy-boto3-elasticache==1.34.60 +mypy-boto3-elasticache==1.34.72 # via boto3-stubs mypy-boto3-elasticbeanstalk==1.34.0 # via boto3-stubs mypy-boto3-elbv2==1.34.63 # via boto3-stubs -mypy-boto3-emr==1.34.44 +mypy-boto3-emr==1.34.75 # via boto3-stubs mypy-boto3-emr-serverless==1.34.0 # via boto3-stubs @@ -377,7 +377,7 @@ mypy-boto3-iot-data==1.34.0 # via boto3-stubs mypy-boto3-iotanalytics==1.34.0 # via boto3-stubs -mypy-boto3-iotwireless==1.34.0 +mypy-boto3-iotwireless==1.34.74 # via boto3-stubs mypy-boto3-kafka==1.34.61 # via boto3-stubs @@ -439,11 +439,11 @@ mypy-boto3-s3==1.34.65 # via boto3-stubs mypy-boto3-s3control==1.34.18 # via boto3-stubs -mypy-boto3-sagemaker==1.34.64 +mypy-boto3-sagemaker==1.34.74 # via boto3-stubs mypy-boto3-sagemaker-runtime==1.34.0 # via boto3-stubs -mypy-boto3-secretsmanager==1.34.63 +mypy-boto3-secretsmanager==1.34.72 # via boto3-stubs mypy-boto3-serverlessrepo==1.34.0 # via boto3-stubs @@ -542,9 +542,9 @@ publication==0.0.3 # jsii py-partiql-parser==0.5.1 # via moto-ext -pyasn1==0.5.1 +pyasn1==0.6.0 # via rsa -pycparser==2.21 +pycparser==2.22 # via cffi pydantic==2.6.4 # via aws-sam-translator @@ -552,7 +552,7 @@ pydantic-core==2.16.3 # via pydantic pygments==2.17.2 # via rich -pymongo==4.6.2 +pymongo==4.6.3 # via localstack-core pyopenssl==24.1.0 # via @@ -598,7 +598,7 @@ pyyaml==6.0.1 # moto-ext # pre-commit # responses -quart==0.19.4 +quart==0.19.5 # via localstack-core readerwriterlock==1.0.9 # via localstack-core @@ -642,7 +642,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core -ruff==0.3.4 +ruff==0.3.5 # via localstack-core s3transfer==0.10.1 # via @@ -809,7 +809,7 @@ websocket-client==1.7.0 # via # docker # localstack-core -werkzeug==3.0.1 +werkzeug==3.0.2 # via # flask # localstack-core From ef456317c50c58b40da6435fb8e741e746ad21ed Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 2 Apr 2024 09:53:43 +0200 Subject: [PATCH 012/169] Feature: Eventbridge v2: Scaffold new provider (#10552) --- localstack/services/events/provider_v2.py | 75 +++++++++++++++++++ localstack/services/providers.py | 19 ++++- tests/aws/services/events/helper_functions.py | 7 ++ .../test_events_scheduled_rules_logs.py | 2 + .../test_events_scheduled_rules_sqs.py | 4 + tests/aws/services/events/test_events.py | 30 +++++++- .../services/events/test_events.snapshot.json | 12 +++ .../events/test_events.validation.json | 3 + .../aws/services/events/test_events_inputs.py | 6 ++ .../events/test_events_integrations.py | 10 +++ .../aws/services/events/test_events_rules.py | 11 ++- 11 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 localstack/services/events/provider_v2.py create mode 100644 tests/aws/services/events/helper_functions.py diff --git a/localstack/services/events/provider_v2.py b/localstack/services/events/provider_v2.py new file mode 100644 index 0000000000000..278f612b593d2 --- /dev/null +++ b/localstack/services/events/provider_v2.py @@ -0,0 +1,75 @@ +import logging + +from localstack.aws.api import RequestContext, handler +from localstack.aws.api.events import ( + CreateEventBusResponse, + EventBusName, + EventBusNameOrArn, + EventPattern, + EventsApi, + EventSourceName, + PutRuleResponse, + RoleArn, + RuleDescription, + RuleName, + RuleState, + ScheduleExpression, + TagList, +) +from localstack.services.plugins import ServiceLifecycleHook + +LOG = logging.getLogger(__name__) + + +class EventsProvider(EventsApi, ServiceLifecycleHook): + def __init__(self): + self._rules = {} + self._event_buses = {} + + @handler("CreateEventBus") + def create_event_bus( + self, + context: RequestContext, + name: EventBusName, + event_source_name: EventSourceName = None, + tags: TagList = None, + **kwargs, + ) -> CreateEventBusResponse: + event_bus_arn = f"arn:aws:events:{context.region}:{context.account_id}:event-bus/{name}" + event_bus = {"Name": name, "Arn": event_bus_arn} + self._event_buses[name] = event_bus + + response = CreateEventBusResponse( + EventBusArn=event_bus_arn, + ) + return response + + @handler("PutRule") + def put_rule( + self, + context: RequestContext, + name: RuleName, + schedule_expression: ScheduleExpression = None, + event_pattern: EventPattern = None, + state: RuleState = None, + description: RuleDescription = None, + role_arn: RoleArn = None, + tags: TagList = None, + event_bus_name: EventBusNameOrArn = None, + **kwargs, + ) -> PutRuleResponse: + rule = { + "Name": name, + "ScheduleExpression": schedule_expression, + "EventPattern": event_pattern, + "State": state, + "Description": description, + "RoleArn": role_arn, + "EventBusName": event_bus_name, + } + self._rules[name] = rule + + response = PutRuleResponse( + RuleArn=f"arn:aws:events:{context.region}:{context.account_id}:rule/{name}", + ) + return response diff --git a/localstack/services/providers.py b/localstack/services/providers.py index 873a6da2b9863..c892cf2d5c7f1 100644 --- a/localstack/services/providers.py +++ b/localstack/services/providers.py @@ -336,7 +336,7 @@ def ssm(): return Service.for_provider(provider, dispatch_table_factory=MotoFallbackDispatcher) -@aws_provider() +@aws_provider(api="events", name="default") def events(): from localstack.services.events.provider import EventsProvider from localstack.services.moto import MotoFallbackDispatcher @@ -345,6 +345,23 @@ def events(): return Service.for_provider(provider, dispatch_table_factory=MotoFallbackDispatcher) +@aws_provider(api="events", name="v1") +def events_v1(): + from localstack.services.events.provider import EventsProvider + from localstack.services.moto import MotoFallbackDispatcher + + provider = EventsProvider() + return Service.for_provider(provider, dispatch_table_factory=MotoFallbackDispatcher) + + +@aws_provider(api="events", name="v2") +def events_v2(): + from localstack.services.events.provider_v2 import EventsProvider + + provider = EventsProvider() + return Service.for_provider(provider) + + @aws_provider() def stepfunctions(): from localstack.services.stepfunctions.provider import StepFunctionsProvider diff --git a/tests/aws/services/events/helper_functions.py b/tests/aws/services/events/helper_functions.py new file mode 100644 index 0000000000000..ecf6238bf7ed8 --- /dev/null +++ b/tests/aws/services/events/helper_functions.py @@ -0,0 +1,7 @@ +import os + +from localstack.testing.aws.util import is_aws_cloud + + +def is_v2_provider(): + return os.environ.get("PROVIDER_OVERRIDE_EVENTS") == "v2" and not is_aws_cloud() diff --git a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.py b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.py index 1618788463974..76d8989c32420 100644 --- a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.py +++ b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.py @@ -8,6 +8,7 @@ from localstack.testing.snapshots.transformer_utility import TransformerUtility from localstack.utils.strings import short_uid from localstack.utils.sync import retry +from tests.aws.services.events.helper_functions import is_v2_provider LOG = logging.getLogger(__name__) @@ -21,6 +22,7 @@ def logs_log_group(aws_client): @pytest.fixture +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def add_logs_resource_policy_for_rule(aws_client): policies = [] diff --git a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.py b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.py index 488a91c25e80d..e38f691a480da 100644 --- a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.py +++ b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.py @@ -1,6 +1,8 @@ import json import logging +import pytest + from localstack.testing.aws.eventbus_utils import ( allow_event_rule_to_sqs_queue, trigger_scheduled_rule, @@ -9,11 +11,13 @@ from localstack.testing.snapshots.transformer_utility import TransformerUtility from localstack.utils.strings import short_uid from localstack.utils.sync import retry +from tests.aws.services.events.helper_functions import is_v2_provider LOG = logging.getLogger(__name__) @markers.aws.validated +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_scheduled_rule_sqs( sqs_create_queue, events_put_rule, diff --git a/tests/aws/services/events/test_events.py b/tests/aws/services/events/test_events.py index 1273716906634..6861de3c81e65 100644 --- a/tests/aws/services/events/test_events.py +++ b/tests/aws/services/events/test_events.py @@ -24,12 +24,14 @@ from localstack.utils.strings import long_uid, short_uid, to_str from localstack.utils.sync import poll_condition, retry from tests.aws.services.events.conftest import assert_valid_event, sqs_collect_messages +from tests.aws.services.events.helper_functions import is_v2_provider THIS_FOLDER = os.path.dirname(os.path.realpath(__file__)) TEST_EVENT_BUS_NAME = "command-bus-dev" EVENT_DETAIL = {"command": "update-account", "payload": {"acc_id": "0a787ecb-4015", "sf_id": "baz"}} + TEST_EVENT_PATTERN = { "source": ["core.update-account-command"], "detail-type": ["core.update-account-command"], @@ -75,6 +77,7 @@ class TestEvents: @markers.aws.unknown + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_events_written_to_disk_are_timestamp_prefixed_for_chronological_ordering( self, aws_client ): @@ -108,6 +111,7 @@ def test_events_written_to_disk_are_timestamp_prefixed_for_chronological_orderin assert [json.loads(event["Detail"]) for event in sorted_events] == event_details_to_publish @markers.aws.validated + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_list_tags_for_resource(self, aws_client, clean_up): rule_name = "rule-{}".format(short_uid()) @@ -135,6 +139,7 @@ def test_list_tags_for_resource(self, aws_client, clean_up): clean_up(rule_name=rule_name) @markers.aws.unknown + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_values_in_array(self, put_events_with_filter_to_sqs): pattern = {"detail": {"event": {"data": {"type": ["1", "2"]}}}} entries1 = [ @@ -166,6 +171,7 @@ def test_put_events_with_values_in_array(self, put_events_with_filter_to_sqs): ) @markers.aws.validated + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_nested_event_pattern(self, put_events_with_filter_to_sqs): pattern = {"detail": {"event": {"data": {"type": ["1"]}}}} entries1 = [ @@ -197,6 +203,7 @@ def test_put_events_with_nested_event_pattern(self, put_events_with_filter_to_sq ) @markers.aws.unknown + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_scheduled_expression_events( self, sns_create_topic, @@ -330,11 +337,13 @@ def received(q_urls): # clean up target_ids = [topic_target_id, sm_target_id, queue_target_id, fifo_queue_target_id] + clean_up(rule_name=rule_name, target_ids=target_ids, queue_url=queue_url) aws_client.stepfunctions.delete_state_machine(stateMachineArn=state_machine_arn) - @pytest.mark.parametrize("auth", API_DESTINATION_AUTHS) @markers.aws.unknown + @pytest.mark.parametrize("auth", API_DESTINATION_AUTHS) + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_api_destinations(self, httpserver: HTTPServer, auth, aws_client, clean_up): token = short_uid() bearer = f"Bearer {token}" @@ -493,6 +502,7 @@ def _handler(_request: Request): assert oauth_request.args["oauthquery"] == "value3" @markers.aws.unknown + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_create_connection_validations(self, aws_client): connection_name = "This should fail with two errors 123467890123412341234123412341234" @@ -517,6 +527,7 @@ def test_create_connection_validations(self, aws_client): assert "must satisfy enum value set: [BASIC, OAUTH_CLIENT_CREDENTIALS, API_KEY]" in message @markers.aws.unknown + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_event_without_source(self, aws_client_factory): events_client = aws_client_factory(region_name="eu-west-1").events @@ -524,6 +535,7 @@ def test_put_event_without_source(self, aws_client_factory): assert response.get("Entries") @markers.aws.unknown + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_event_without_detail(self, aws_client_factory): events_client = aws_client_factory(region_name="eu-west-1").events @@ -537,6 +549,7 @@ def test_put_event_without_detail(self, aws_client_factory): assert response.get("Entries") @markers.aws.validated + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_target_id_validation( self, sqs_create_queue, sqs_get_queue_arn, events_put_rule, snapshot, aws_client ): @@ -579,6 +592,7 @@ def test_put_target_id_validation( ) @markers.aws.validated + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_event_pattern(self, aws_client, snapshot, account_id, region_name): response = aws_client.events.test_event_pattern( Event=json.dumps( @@ -622,6 +636,7 @@ def test_event_pattern(self, aws_client, snapshot, account_id, region_name): snapshot.match("eventbridge-test-event-pattern-response-no-match", response) @markers.aws.validated + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_time( self, aws_client, @@ -692,8 +707,19 @@ def _get_sqs_messages(): class TestEventsEventBus: + @markers.aws.validated + def test_create_custom_event_bus(self, aws_client, cleanups, snapshot): + events = aws_client.events + bus_name = "test-bus" + + response = events.create_event_bus(Name=bus_name) + cleanups.append(lambda: events.delete_event_bus(Name=bus_name)) + + snapshot.match("create-custom-event-bus", response) + @markers.aws.unknown @pytest.mark.parametrize("strategy", ["standard", "domain", "path"]) + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_into_event_bus( self, monkeypatch, @@ -761,6 +787,7 @@ def test_put_events_into_event_bus( aws_client.sqs.delete_queue(QueueUrl=queue_url) @markers.aws.validated + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_to_default_eventbus_for_custom_eventbus( self, events_create_event_bus, @@ -893,6 +920,7 @@ def test_put_events_to_default_eventbus_for_custom_eventbus( assert_valid_event(received_event) @markers.aws.validated + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_nonexistent_event_bus( self, aws_client, diff --git a/tests/aws/services/events/test_events.snapshot.json b/tests/aws/services/events/test_events.snapshot.json index d99a3845372e0..20a17f89a230a 100644 --- a/tests/aws/services/events/test_events.snapshot.json +++ b/tests/aws/services/events/test_events.snapshot.json @@ -208,5 +208,17 @@ } } } + }, + "tests/aws/services/events/test_events.py::TestEventsEventBus::test_create_custom_event_bus": { + "recorded-date": "27-03-2024, 09:15:34", + "recorded-content": { + "create-custom-event-bus": { + "EventBusArn": "arn:aws:events::111111111111:event-bus/test-bus", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/events/test_events.validation.json b/tests/aws/services/events/test_events.validation.json index 2d12e561943bc..941fb027501ad 100644 --- a/tests/aws/services/events/test_events.validation.json +++ b/tests/aws/services/events/test_events.validation.json @@ -26,6 +26,9 @@ "tests/aws/services/events/test_events.py::TestEvents::test_put_target_id_validation": { "last_validated_date": "2024-03-26T14:07:18+00:00" }, + "tests/aws/services/events/test_events.py::TestEventsEventBus::test_create_custom_event_bus": { + "last_validated_date": "2024-03-27T09:15:34+00:00" + }, "tests/aws/services/events/test_events.py::TestEventsEventBus::test_put_events_nonexistent_event_bus": { "last_validated_date": "2024-03-26T14:11:46+00:00" }, diff --git a/tests/aws/services/events/test_events_inputs.py b/tests/aws/services/events/test_events_inputs.py index b1d6afecd45a9..5ff7962ce7c8e 100644 --- a/tests/aws/services/events/test_events_inputs.py +++ b/tests/aws/services/events/test_events_inputs.py @@ -2,16 +2,20 @@ import json +import pytest + from localstack.constants import TEST_AWS_ACCOUNT_ID, TEST_AWS_REGION_NAME from localstack.testing.pytest import markers from localstack.utils.aws import arns from localstack.utils.strings import short_uid from tests.aws.services.events.conftest import sqs_collect_messages +from tests.aws.services.events.helper_functions import is_v2_provider from tests.aws.services.events.test_events import EVENT_DETAIL, TEST_EVENT_PATTERN class TestEventsInputPath: @markers.aws.unknown + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_input_path(self, aws_client, clean_up): queue_name = f"queue-{short_uid()}" rule_name = f"rule-{short_uid()}" @@ -65,6 +69,7 @@ def test_put_events_with_input_path(self, aws_client, clean_up): clean_up(bus_name=bus_name, rule_name=rule_name, target_ids=target_id, queue_url=queue_url) @markers.aws.unknown + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_input_path_multiple(self, aws_client, clean_up): queue_name = "queue-{}".format(short_uid()) queue_name_1 = "queue-{}".format(short_uid()) @@ -143,6 +148,7 @@ def test_put_events_with_input_path_multiple(self, aws_client, clean_up): class TestEventsInputTransformers: @markers.aws.validated + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_input_transformation_to_sqs( self, put_events_with_filter_to_sqs, snapshot ): diff --git a/tests/aws/services/events/test_events_integrations.py b/tests/aws/services/events/test_events_integrations.py index ea0bb8cac9aef..71da2d81af880 100644 --- a/tests/aws/services/events/test_events_integrations.py +++ b/tests/aws/services/events/test_events_integrations.py @@ -14,11 +14,13 @@ from localstack.utils.sync import retry from localstack.utils.testutil import check_expected_lambda_log_events_length from tests.aws.services.events.conftest import assert_valid_event, sqs_collect_messages +from tests.aws.services.events.helper_functions import is_v2_provider from tests.aws.services.events.test_events import EVENT_DETAIL, TEST_EVENT_PATTERN from tests.aws.services.lambda_.test_lambda import TEST_LAMBDA_PYTHON_ECHO @markers.aws.validated +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_target_sqs(put_events_with_filter_to_sqs): entries = [ { @@ -34,6 +36,7 @@ def test_put_events_with_target_sqs(put_events_with_filter_to_sqs): @markers.aws.unknown +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_target_sqs_new_region(aws_client_factory): events_client = aws_client_factory(region_name="eu-west-1").events queue_name = "queue-{}".format(short_uid()) @@ -75,6 +78,7 @@ def test_put_events_with_target_sqs_new_region(aws_client_factory): @markers.aws.unknown +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_target_sqs_event_detail_match(put_events_with_filter_to_sqs): entries1 = [ { @@ -103,6 +107,7 @@ def test_put_events_with_target_sqs_event_detail_match(put_events_with_filter_to @markers.aws.unknown @pytest.mark.parametrize("strategy", ["standard", "domain", "path"]) +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_target_sns( monkeypatch, sns_subscription, @@ -171,6 +176,7 @@ def test_put_events_with_target_sns( @markers.aws.unknown +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_target_lambda(create_lambda_function, cleanups, aws_client, clean_up): rule_name = f"rule-{short_uid()}" function_name = f"lambda-func-{short_uid()}" @@ -232,6 +238,7 @@ def test_put_events_with_target_lambda(create_lambda_function, cleanups, aws_cli @markers.aws.validated +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_should_ignore_schedules_for_put_event(create_lambda_function, cleanups, aws_client): """Regression test for https://github.com/localstack/localstack/issues/7847""" fn_name = f"test-event-fn-{short_uid()}" @@ -281,6 +288,7 @@ def check_invocation(): @markers.aws.unknown +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_target_firehose(aws_client, clean_up): s3_bucket = "s3-{}".format(short_uid()) s3_prefix = "testeventdata" @@ -349,6 +357,7 @@ def test_put_events_with_target_firehose(aws_client, clean_up): @markers.aws.unknown +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_target_kinesis(aws_client): rule_name = "rule-{}".format(short_uid()) target_id = "target-{}".format(short_uid()) @@ -422,6 +431,7 @@ def check_stream_status(): @markers.aws.unknown @pytest.mark.parametrize("strategy", ["standard", "domain", "path"]) +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_trigger_event_on_ssm_change(monkeypatch, aws_client, clean_up, strategy): monkeypatch.setattr(config, "SQS_ENDPOINT_STRATEGY", strategy) diff --git a/tests/aws/services/events/test_events_rules.py b/tests/aws/services/events/test_events_rules.py index 97fc9da23eae9..28e4747bf8147 100644 --- a/tests/aws/services/events/test_events_rules.py +++ b/tests/aws/services/events/test_events_rules.py @@ -13,10 +13,12 @@ from localstack.utils.strings import short_uid from localstack.utils.sync import poll_condition from tests.aws.services.events.conftest import assert_valid_event, sqs_collect_messages +from tests.aws.services.events.helper_functions import is_v2_provider from tests.aws.services.events.test_events import TEST_EVENT_BUS_NAME, TEST_EVENT_PATTERN @markers.aws.validated +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_rule(aws_client, snapshot, clean_up): rule_name = f"rule-{short_uid()}" snapshot.add_transformer(snapshot.transform.regex(rule_name, "")) @@ -36,6 +38,7 @@ def test_put_rule(aws_client, snapshot, clean_up): @markers.aws.validated +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_rule_disable(aws_client, clean_up): rule_name = f"rule-{short_uid()}" aws_client.events.put_rule(Name=rule_name, ScheduleExpression="rate(1 minute)") @@ -73,6 +76,7 @@ def test_rule_disable(aws_client, clean_up): " rate(10 minutes)", ], ) +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_rule_invalid_rate_schedule_expression(expression, aws_client): with pytest.raises(ClientError) as e: aws_client.events.put_rule(Name=f"rule-{short_uid()}", ScheduleExpression=expression) @@ -84,6 +88,7 @@ def test_put_rule_invalid_rate_schedule_expression(expression, aws_client): @markers.aws.validated +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_rule_anything_but_to_sqs(put_events_with_filter_to_sqs, snapshot): snapshot.add_transformer( [ @@ -137,6 +142,7 @@ def test_put_events_with_rule_anything_but_to_sqs(put_events_with_filter_to_sqs, @markers.aws.validated +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_rule_exists_true_to_sqs(put_events_with_filter_to_sqs, snapshot): """ Exists matching True condition: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-exists-matching @@ -184,6 +190,7 @@ def test_put_events_with_rule_exists_true_to_sqs(put_events_with_filter_to_sqs, @markers.aws.validated +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_rule_exists_false_to_sqs(put_events_with_filter_to_sqs, snapshot): """ Exists matching False condition: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-exists-matching @@ -231,6 +238,7 @@ def test_put_events_with_rule_exists_false_to_sqs(put_events_with_filter_to_sqs, @markers.aws.unknown +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_event_with_content_base_rule_in_pattern(aws_client, clean_up): queue_name = f"queue-{short_uid()}" rule_name = f"rule-{short_uid()}" @@ -327,8 +335,9 @@ def test_put_event_with_content_base_rule_in_pattern(aws_client, clean_up): ) -@pytest.mark.parametrize("schedule_expression", ["rate(1 minute)", "rate(1 day)", "rate(1 hour)"]) @markers.aws.validated +@pytest.mark.parametrize("schedule_expression", ["rate(1 minute)", "rate(1 day)", "rate(1 hour)"]) +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_create_rule_with_one_unit_in_singular_should_succeed( schedule_expression, aws_client, clean_up ): From 6fe023ac7d23501cb1e6efe32578311c4f795a61 Mon Sep 17 00:00:00 2001 From: Sannya Singal <32308435+sannya-singal@users.noreply.github.com> Date: Tue, 2 Apr 2024 14:34:28 +0530 Subject: [PATCH 013/169] fix cfn templates deployed in invalid AZs (#10586) --- .../engine/template_deployer.py | 8 +++-- .../resources/test_ec2.validation.json | 2 +- tests/aws/templates/ec2_vpc_default_sg.yaml | 36 +++---------------- .../templates/transit_gateway_attachment.yml | 26 ++------------ 4 files changed, 12 insertions(+), 60 deletions(-) diff --git a/localstack/services/cloudformation/engine/template_deployer.py b/localstack/services/cloudformation/engine/template_deployer.py index 167570181127b..ecc910468c9c2 100644 --- a/localstack/services/cloudformation/engine/template_deployer.py +++ b/localstack/services/cloudformation/engine/template_deployer.py @@ -610,9 +610,11 @@ def _resolve_refs_recursively( ) or region_name ) - azs = [] - for az in ("a", "b", "c", "d", "e", "f"): - azs.append("%s%s" % (region, az)) + + get_availability_zones = connect_to( + aws_access_key_id=account_id, region_name=region + ).ec2.describe_availability_zones()["AvailabilityZones"] + azs = [az["ZoneName"] for az in get_availability_zones] return azs diff --git a/tests/aws/services/cloudformation/resources/test_ec2.validation.json b/tests/aws/services/cloudformation/resources/test_ec2.validation.json index c5ed79eb05b29..2381663d2ac85 100644 --- a/tests/aws/services/cloudformation/resources/test_ec2.validation.json +++ b/tests/aws/services/cloudformation/resources/test_ec2.validation.json @@ -9,6 +9,6 @@ "last_validated_date": "2024-03-28T06:48:11+00:00" }, "tests/aws/services/cloudformation/resources/test_ec2.py::test_vpc_creates_default_sg": { - "last_validated_date": "2024-03-28T06:26:23+00:00" + "last_validated_date": "2024-04-01T11:21:54+00:00" } } diff --git a/tests/aws/templates/ec2_vpc_default_sg.yaml b/tests/aws/templates/ec2_vpc_default_sg.yaml index 029cfac92a6e2..441a3daa8559b 100644 --- a/tests/aws/templates/ec2_vpc_default_sg.yaml +++ b/tests/aws/templates/ec2_vpc_default_sg.yaml @@ -1,18 +1,6 @@ -Parameters: - DeployRegion: - Type: String - Default: us-east-1 - -Conditions: - DeployInUSEast1: - Fn::Equals: - - !Ref DeployRegion - - us-east-1 - Resources: vpcA2121C38: Type: AWS::EC2::VPC - Condition: DeployInUSEast1 Properties: CidrBlock: 10.0.0.0/16 EnableDnsHostnames: true @@ -23,7 +11,6 @@ Resources: Value: RdsTestStack/vpc vpcPublicSubnet1Subnet2E65531E: Type: AWS::EC2::Subnet - Condition: DeployInUSEast1 Properties: CidrBlock: 10.0.0.0/18 VpcId: @@ -31,7 +18,7 @@ Resources: AvailabilityZone: Fn::Select: - 0 - - Fn::GetAZs: !Ref DeployRegion + - Fn::GetAZs: "" MapPublicIpOnLaunch: true Tags: - Key: aws-cdk:subnet-name @@ -42,7 +29,6 @@ Resources: Value: RdsTestStack/vpc/PublicSubnet1 vpcPublicSubnet1RouteTable48A2DF9B: Type: AWS::EC2::RouteTable - Condition: DeployInUSEast1 Properties: VpcId: Ref: vpcA2121C38 @@ -51,7 +37,6 @@ Resources: Value: RdsTestStack/vpc/PublicSubnet1 vpcPublicSubnet1RouteTableAssociation5D3F4579: Type: AWS::EC2::SubnetRouteTableAssociation - Condition: DeployInUSEast1 Properties: RouteTableId: Ref: vpcPublicSubnet1RouteTable48A2DF9B @@ -59,7 +44,6 @@ Resources: Ref: vpcPublicSubnet1Subnet2E65531E vpcPublicSubnet1DefaultRoute10708846: Type: AWS::EC2::Route - Condition: DeployInUSEast1 Properties: RouteTableId: Ref: vpcPublicSubnet1RouteTable48A2DF9B @@ -70,7 +54,6 @@ Resources: - vpcVPCGW7984C166 vpcPublicSubnet2Subnet009B674F: Type: AWS::EC2::Subnet - Condition: DeployInUSEast1 Properties: CidrBlock: 10.0.64.0/18 VpcId: @@ -78,7 +61,7 @@ Resources: AvailabilityZone: Fn::Select: - 1 - - Fn::GetAZs: !Ref DeployRegion + - Fn::GetAZs: "" MapPublicIpOnLaunch: true Tags: - Key: aws-cdk:subnet-name @@ -89,7 +72,6 @@ Resources: Value: RdsTestStack/vpc/PublicSubnet2 vpcPublicSubnet2RouteTableEB40D4CB: Type: AWS::EC2::RouteTable - Condition: DeployInUSEast1 Properties: VpcId: Ref: vpcA2121C38 @@ -98,7 +80,6 @@ Resources: Value: RdsTestStack/vpc/PublicSubnet2 vpcPublicSubnet2RouteTableAssociation21F81B59: Type: AWS::EC2::SubnetRouteTableAssociation - Condition: DeployInUSEast1 Properties: RouteTableId: Ref: vpcPublicSubnet2RouteTableEB40D4CB @@ -106,7 +87,6 @@ Resources: Ref: vpcPublicSubnet2Subnet009B674F vpcPublicSubnet2DefaultRouteA1EC0F60: Type: AWS::EC2::Route - Condition: DeployInUSEast1 Properties: RouteTableId: Ref: vpcPublicSubnet2RouteTableEB40D4CB @@ -117,7 +97,6 @@ Resources: - vpcVPCGW7984C166 vpcIsolatedSubnet1Subnet8B28CEB3: Type: AWS::EC2::Subnet - Condition: DeployInUSEast1 Properties: CidrBlock: 10.0.128.0/18 VpcId: @@ -125,7 +104,7 @@ Resources: AvailabilityZone: Fn::Select: - 0 - - Fn::GetAZs: !Ref DeployRegion + - Fn::GetAZs: "" MapPublicIpOnLaunch: false Tags: - Key: aws-cdk:subnet-name @@ -136,7 +115,6 @@ Resources: Value: RdsTestStack/vpc/IsolatedSubnet1 vpcIsolatedSubnet1RouteTable0D6B2D3D: Type: AWS::EC2::RouteTable - Condition: DeployInUSEast1 Properties: VpcId: Ref: vpcA2121C38 @@ -145,7 +123,6 @@ Resources: Value: RdsTestStack/vpc/IsolatedSubnet1 vpcIsolatedSubnet1RouteTableAssociation172210D4: Type: AWS::EC2::SubnetRouteTableAssociation - Condition: DeployInUSEast1 Properties: RouteTableId: Ref: vpcIsolatedSubnet1RouteTable0D6B2D3D @@ -153,7 +130,6 @@ Resources: Ref: vpcIsolatedSubnet1Subnet8B28CEB3 vpcIsolatedSubnet2Subnet2C6B375C: Type: AWS::EC2::Subnet - Condition: DeployInUSEast1 Properties: CidrBlock: 10.0.192.0/18 VpcId: @@ -161,7 +137,7 @@ Resources: AvailabilityZone: Fn::Select: - 1 - - Fn::GetAZs: !Ref DeployRegion + - Fn::GetAZs: "" MapPublicIpOnLaunch: false Tags: - Key: aws-cdk:subnet-name @@ -172,7 +148,6 @@ Resources: Value: RdsTestStack/vpc/IsolatedSubnet2 vpcIsolatedSubnet2RouteTable3455CBFC: Type: AWS::EC2::RouteTable - Condition: DeployInUSEast1 Properties: VpcId: Ref: vpcA2121C38 @@ -181,7 +156,6 @@ Resources: Value: RdsTestStack/vpc/IsolatedSubnet2 vpcIsolatedSubnet2RouteTableAssociation8A8FAF70: Type: AWS::EC2::SubnetRouteTableAssociation - Condition: DeployInUSEast1 Properties: RouteTableId: Ref: vpcIsolatedSubnet2RouteTable3455CBFC @@ -189,14 +163,12 @@ Resources: Ref: vpcIsolatedSubnet2Subnet2C6B375C vpcIGWE57CBDCA: Type: AWS::EC2::InternetGateway - Condition: DeployInUSEast1 Properties: Tags: - Key: Name Value: RdsTestStack/vpc vpcVPCGW7984C166: Type: AWS::EC2::VPCGatewayAttachment - Condition: DeployInUSEast1 Properties: VpcId: Ref: vpcA2121C38 diff --git a/tests/aws/templates/transit_gateway_attachment.yml b/tests/aws/templates/transit_gateway_attachment.yml index 7f03d1ac5bf65..3fed06c8676a5 100644 --- a/tests/aws/templates/transit_gateway_attachment.yml +++ b/tests/aws/templates/transit_gateway_attachment.yml @@ -1,18 +1,6 @@ -Parameters: - DeployRegion: - Type: String - Default: us-east-1 - -Conditions: - DeployInUSEast1: - Fn::Equals: - - !Ref DeployRegion - - us-east-1 - Resources: Vpc8378EB38: Type: AWS::EC2::VPC - Condition: DeployInUSEast1 Properties: CidrBlock: 10.0.0.0/20 EnableDnsHostnames: true @@ -20,7 +8,6 @@ Resources: InstanceTenancy: default myTransitGateway: Type: "AWS::EC2::TransitGateway" - Condition: DeployInUSEast1 Properties: AmazonSideAsn: 65000 Description: "TGW Route Integration Test" @@ -33,25 +20,22 @@ Resources: Value: !Ref 'AWS::StackId' VpcIsolatedSubnet1SubnetE48C5737: Type: AWS::EC2::Subnet - Condition: DeployInUSEast1 Properties: AvailabilityZone: Fn::Select: - 0 - - Fn::GetAZs: !Ref DeployRegion + - Fn::GetAZs: '' CidrBlock: 10.0.0.0/24 MapPublicIpOnLaunch: false VpcId: Ref: Vpc8378EB38 VpcIsolatedSubnet1RouteTable4771E3E5: Type: AWS::EC2::RouteTable - Condition: DeployInUSEast1 Properties: VpcId: Ref: Vpc8378EB38 VpcIsolatedSubnet1RouteTableAssociationD300FCBB: Type: AWS::EC2::SubnetRouteTableAssociation - Condition: DeployInUSEast1 Properties: RouteTableId: Ref: VpcIsolatedSubnet1RouteTable4771E3E5 @@ -59,7 +43,6 @@ Resources: Ref: VpcIsolatedSubnet1SubnetE48C5737 VpcIsolatedSubnet1TransitGatewayRouteA907B32D: Type: AWS::EC2::Route - Condition: DeployInUSEast1 Properties: DestinationCidrBlock: 0.0.0.0/0 RouteTableId: @@ -69,25 +52,22 @@ Resources: - TransitGatewayVpcAttachment VpcIsolatedSubnet2Subnet16364B91: Type: AWS::EC2::Subnet - Condition: DeployInUSEast1 Properties: AvailabilityZone: Fn::Select: - 1 - - Fn::GetAZs: !Ref DeployRegion + - Fn::GetAZs: '' CidrBlock: 10.0.1.0/24 MapPublicIpOnLaunch: false VpcId: Ref: Vpc8378EB38 VpcIsolatedSubnet2RouteTable1D30AF7D: Type: AWS::EC2::RouteTable - Condition: DeployInUSEast1 Properties: VpcId: Ref: Vpc8378EB38 VpcIsolatedSubnet2RouteTableAssociationF7B18CCA: Type: AWS::EC2::SubnetRouteTableAssociation - Condition: DeployInUSEast1 Properties: RouteTableId: Ref: VpcIsolatedSubnet2RouteTable1D30AF7D @@ -95,7 +75,6 @@ Resources: Ref: VpcIsolatedSubnet2Subnet16364B91 VpcIsolatedSubnet2TransitGatewayRoute1E0D0BF2: Type: AWS::EC2::Route - Condition: DeployInUSEast1 Properties: DestinationCidrBlock: 0.0.0.0/0 RouteTableId: @@ -105,7 +84,6 @@ Resources: - TransitGatewayVpcAttachment TransitGatewayVpcAttachment: Type: AWS::EC2::TransitGatewayAttachment - Condition: DeployInUSEast1 Properties: SubnetIds: - Ref: VpcIsolatedSubnet1SubnetE48C5737 From 396497338215f936a6bb1dca4eab9f7bc22482d3 Mon Sep 17 00:00:00 2001 From: Macwan Nevil Date: Tue, 2 Apr 2024 14:35:11 +0530 Subject: [PATCH 014/169] fix lastRotatedDate updation on secret rotation (#10564) --- .../services/secretsmanager/provider.py | 2 +- .../secretsmanager/test_secretsmanager.py | 12 +++- .../test_secretsmanager.snapshot.json | 66 +++++++++++++++++-- .../test_secretsmanager.validation.json | 4 +- 4 files changed, 73 insertions(+), 11 deletions(-) diff --git a/localstack/services/secretsmanager/provider.py b/localstack/services/secretsmanager/provider.py index 871e0457ef46e..a000f85d1a3a0 100644 --- a/localstack/services/secretsmanager/provider.py +++ b/localstack/services/secretsmanager/provider.py @@ -792,7 +792,7 @@ def backend_rotate_secret( raise pending_version.pop() # Fall through if there is no previously pending version so we'll "stuck" with a new # secret version in AWSPENDING state. - + secret.last_rotation_date = int(time.time()) return secret.to_short_dict(version_id=new_version_id) diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.py b/tests/aws/services/secretsmanager/test_secretsmanager.py index b2e57c1328b4f..30765250f45ca 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.py +++ b/tests/aws/services/secretsmanager/test_secretsmanager.py @@ -9,7 +9,6 @@ import pytest import requests from botocore.auth import SigV4Auth -from localstack_snapshot.snapshots.transformer import SortingTransformer from localstack.aws.api.lambda_ import Runtime from localstack.aws.api.secretsmanager import ( @@ -367,7 +366,9 @@ def test_resource_policy(self, secret_name, aws_client, sm_snapshot, cleanups): assert rs["ResponseMetadata"]["HTTPStatusCode"] == 200 @pytest.mark.parametrize("rotate_immediately", [True, None]) - @markers.snapshot.skip_snapshot_verify(paths=["$..Versions..KmsKeyIds"]) + @markers.snapshot.skip_snapshot_verify( + paths=["$..VersionIdsToStages", "$..Versions", "$..VersionId"] + ) @markers.aws.validated def test_rotate_secret_with_lambda_success( self, @@ -388,7 +389,9 @@ def test_rotate_secret_with_lambda_success( Description="testing rotation of secrets", ) - sm_snapshot.add_transformer(SortingTransformer("Versions", lambda x: x["CreatedDate"])) + sm_snapshot.add_transformer( + sm_snapshot.transform.key_value("RotationLambdaARN", "lambda-arn") + ) sm_snapshot.add_transformers_list( sm_snapshot.transform.secretsmanager_secret_id_arn(cre_res, 0) ) @@ -423,6 +426,9 @@ def test_rotate_secret_with_lambda_success( self._wait_rotation(aws_client.secretsmanager, secret_name, rot_res["VersionId"]) + response = aws_client.secretsmanager.describe_secret(SecretId=secret_name) + sm_snapshot.match("describe_secret_rotated", response) + list_secret_versions_1 = aws_client.secretsmanager.list_secret_version_ids( SecretId=secret_name ) diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json index 8e65681bdd1ee..2061e5228ecaa 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json @@ -3687,7 +3687,7 @@ } }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[True]": { - "recorded-date": "14-03-2024, 21:02:51", + "recorded-date": "28-03-2024, 06:58:46", "recorded-content": { "rotate_secret_immediately": { "ARN": "arn:aws:secretsmanager::111111111111:secret:", @@ -3698,6 +3698,34 @@ "HTTPStatusCode": 200 } }, + "describe_secret_rotated": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "Description": "testing rotation of secrets", + "LastAccessedDate": "datetime", + "LastChangedDate": "datetime", + "LastRotatedDate": "datetime", + "Name": "", + "NextRotationDate": "datetime", + "RotationEnabled": true, + "RotationLambdaARN": "", + "RotationRules": { + "AutomaticallyAfterDays": 1 + }, + "VersionIdsToStages": { + "": [ + "AWSCURRENT", + "AWSPENDING" + ], + "": [ + "AWSPREVIOUS" + ] + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, "list_secret_versions_rotated_1": { "ARN": "arn:aws:secretsmanager::111111111111:secret:", "Name": "", @@ -3733,12 +3761,40 @@ } }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[None]": { - "recorded-date": "14-03-2024, 21:03:01", + "recorded-date": "28-03-2024, 06:58:58", "recorded-content": { "rotate_secret_immediately": { "ARN": "arn:aws:secretsmanager::111111111111:secret:", "Name": "", - "VersionId": "", + "VersionId": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe_secret_rotated": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "Description": "testing rotation of secrets", + "LastAccessedDate": "datetime", + "LastChangedDate": "datetime", + "LastRotatedDate": "datetime", + "Name": "", + "NextRotationDate": "datetime", + "RotationEnabled": true, + "RotationLambdaARN": "", + "RotationRules": { + "AutomaticallyAfterDays": 1 + }, + "VersionIdsToStages": { + "": [ + "AWSPREVIOUS" + ], + "": [ + "AWSCURRENT", + "AWSPENDING" + ] + }, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -3754,7 +3810,7 @@ "DefaultEncryptionKey" ], "LastAccessedDate": "datetime", - "VersionId": "", + "VersionId": "", "VersionStages": [ "AWSPREVIOUS" ] @@ -3764,7 +3820,7 @@ "KmsKeyIds": [ "DefaultEncryptionKey" ], - "VersionId": "", + "VersionId": "", "VersionStages": [ "AWSCURRENT", "AWSPENDING" diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json index 12510f949b546..00c51c648d5d8 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json @@ -87,10 +87,10 @@ "last_validated_date": "2024-03-15T08:12:22+00:00" }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[None]": { - "last_validated_date": "2024-03-14T21:03:56+00:00" + "last_validated_date": "2024-03-28T06:58:56+00:00" }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[True]": { - "last_validated_date": "2024-03-14T21:03:47+00:00" + "last_validated_date": "2024-03-28T06:58:44+00:00" }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_secret_exists": { "last_validated_date": "2024-03-15T08:14:33+00:00" From 92fee6775c259af99a793b68a93ee693953b86c3 Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:13:22 +0200 Subject: [PATCH 015/169] update pre-commit hook versions on dep updates (#10587) --- .pre-commit-config.yaml | 2 +- Makefile | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9ef6836fb7433..3fad8b28a1d85 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.3.3 + rev: v0.3.5 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/Makefile b/Makefile index 0b4aceb7cf8fd..bf534375573e0 100644 --- a/Makefile +++ b/Makefile @@ -33,16 +33,15 @@ venv: $(VENV_ACTIVATE) ## Create a new (empty) virtual environment freeze: ## Run pip freeze -l in the virtual environment @$(VENV_RUN); pip freeze -l -pip-tools: venv - $(VENV_RUN); $(PIP_CMD) install --upgrade pip-tools - -upgrade-pinned-dependencies: pip-tools +upgrade-pinned-dependencies: venv + $(VENV_RUN); $(PIP_CMD) install --upgrade pip-tools pre-commit $(VENV_RUN); pip-compile --upgrade --strip-extras -o requirements-basic.txt pyproject.toml $(VENV_RUN); pip-compile --upgrade --extra runtime -o requirements-runtime.txt pyproject.toml $(VENV_RUN); pip-compile --upgrade --extra test -o requirements-test.txt pyproject.toml $(VENV_RUN); pip-compile --upgrade --extra dev -o requirements-dev.txt pyproject.toml $(VENV_RUN); pip-compile --upgrade --extra typehint -o requirements-typehint.txt pyproject.toml $(VENV_RUN); pip-compile --upgrade --extra base-runtime -o requirements-base-runtime.txt pyproject.toml + $(VENV_RUN); pre-commit autoupdate install-basic: venv ## Install basic dependencies for CLI usage into venv $(VENV_RUN); $(PIP_CMD) install -r requirements-basic.txt @@ -268,4 +267,4 @@ clean-dist: ## Clean up python distribution directories rm -rf dist/ build/ rm -rf *.egg-info -.PHONY: usage freeze install-basic install-runtime install-test install-dev install entrypoints dist publish coveralls start docker-save-image docker-build docker-build-multiarch docker-push-master docker-create-push-manifests docker-run-tests docker-run docker-mount-run docker-cp-coverage test test-coverage test-docker test-docker-mount test-docker-mount-code lint lint-modified format format-modified init-precommit clean clean-dist pip-tools upgrade-pinned-dependencies +.PHONY: usage freeze install-basic install-runtime install-test install-dev install entrypoints dist publish coveralls start docker-save-image docker-build docker-build-multiarch docker-push-master docker-create-push-manifests docker-run-tests docker-run docker-mount-run docker-cp-coverage test test-coverage test-docker test-docker-mount test-docker-mount-code lint lint-modified format format-modified init-precommit clean clean-dist upgrade-pinned-dependencies From a38de240ac3e7b8be8840ac212105b939cb28f0d Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 2 Apr 2024 13:45:28 +0200 Subject: [PATCH 016/169] Feat: Events v2: add ci step for eventbridge v2 provider (#10553) --- .circleci/config.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9a753cf990ecc..684138b3b7e1f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -269,6 +269,32 @@ jobs: - store_test_results: path: target/reports/ + itest-events-v2-provider: + executor: ubuntu-machine-amd64 + working_directory: /tmp/workspace/repo + steps: + - attach_workspace: + at: /tmp/workspace + - prepare-pytest-tinybird + - prepare-account-region-randomization + - run: + name: Test EventBridge v2 provider + environment: + PROVIDER_OVERRIDE_EVENTS: "v2" + TEST_PATH: "tests/aws/services/events/" + COVERAGE_ARGS: "-p" + command: | + COVERAGE_FILE="target/coverage/.coverage.eventsV2.${CIRCLE_NODE_INDEX}" \ + PYTEST_ARGS="${TINYBIRD_PYTEST_ARGS}${TESTSELECTION_PYTEST_ARGS}--reruns 3 --junitxml=target/reports/events_v2.xml -o junit_suite_name='events_v2'" \ + make test-coverage + - persist_to_workspace: + root: + /tmp/workspace + paths: + - repo/target/coverage/ + - store_test_results: + path: target/reports/ + docker-build: parameters: platform: @@ -653,6 +679,10 @@ workflows: requires: - preflight - test-selection + - itest-events-v2-provider: + requires: + - preflight + - test-selection - unit-tests: requires: - preflight @@ -700,6 +730,7 @@ workflows: - itest-sfn-legacy-provider - itest-s3-v2-legacy-provider - itest-cloudwatch-v2-provider + - itest-events-v2-provider - acceptance-tests - docker-test-amd64 - docker-test-arm64 @@ -713,6 +744,7 @@ workflows: - itest-sfn-legacy-provider - itest-s3-v2-legacy-provider - itest-cloudwatch-v2-provider + - itest-events-v2-provider - acceptance-tests - docker-test-amd64 - docker-test-arm64 From 85486d2606e7f974700186d761789444fae8fbfa Mon Sep 17 00:00:00 2001 From: Mathieu Cloutier <79954947+cloutierMat@users.noreply.github.com> Date: Tue, 2 Apr 2024 08:58:23 -0600 Subject: [PATCH 017/169] fix apigw http proxy response passthrough (#10583) --- localstack/services/apigateway/integration.py | 5 ++- tests/aws/services/apigateway/conftest.py | 35 ++++++++++++++- .../apigateway/test_apigateway_http.py | 45 +++++++++++++++++++ .../test_apigateway_http.snapshot.json | 38 ++++++++++++++++ .../test_apigateway_http.validation.json | 9 ++-- .../functions/lambda_echo_status_code.py | 19 ++++++++ tests/aws/services/lambda_/test_lambda.py | 3 ++ 7 files changed, 148 insertions(+), 6 deletions(-) create mode 100644 tests/aws/services/lambda_/functions/lambda_echo_status_code.py diff --git a/localstack/services/apigateway/integration.py b/localstack/services/apigateway/integration.py index 9aaf6965cd961..0aa5b85d57ce9 100644 --- a/localstack/services/apigateway/integration.py +++ b/localstack/services/apigateway/integration.py @@ -802,9 +802,10 @@ def invoke(self, invocation_context: ApiInvocationContext): uri, result.status_code, ) - # apply custom response template + # apply custom response template for non-proxy integration invocation_context.response = result - self.response_templates.render(invocation_context) + if integration["type"] != "HTTP_PROXY": + self.response_templates.render(invocation_context) return invocation_context.response diff --git a/tests/aws/services/apigateway/conftest.py b/tests/aws/services/apigateway/conftest.py index 3d90a076cbbd0..e6a4caf8dc0be 100644 --- a/tests/aws/services/apigateway/conftest.py +++ b/tests/aws/services/apigateway/conftest.py @@ -13,6 +13,7 @@ delete_rest_api, import_rest_api, ) +from tests.aws.services.lambda_.test_lambda import TEST_LAMBDA_PYTHON_ECHO_STATUS_CODE # default name used for created REST API stages DEFAULT_STAGE_NAME = "dev" @@ -69,6 +70,8 @@ def create_rest_api_with_integration( ): def _factory( integration_uri, + path_part="test", + req_parameters=None, req_templates=None, res_templates=None, integration_type=None, @@ -80,9 +83,12 @@ def _factory( ) resource_id, _ = create_rest_resource( - aws_client.apigateway, restApiId=api_id, parentId=root_id, pathPart="test" + aws_client.apigateway, restApiId=api_id, parentId=root_id, pathPart=path_part ) + if req_parameters is None: + req_parameters = {} + method, _ = create_rest_resource_method( aws_client.apigateway, restApiId=api_id, @@ -90,6 +96,7 @@ def _factory( httpMethod="POST", authorizationType="NONE", apiKeyRequired=False, + requestParameters={value: True for value in req_parameters.values()}, ) # set AWS policy to give API GW access to backend resources @@ -120,6 +127,7 @@ def _factory( credentials=assume_role_arn, uri=integration_uri, requestTemplates=req_templates or {}, + requestParameters=req_parameters, ) create_rest_api_method_response( @@ -150,6 +158,31 @@ def _factory( yield _factory +@pytest.fixture +def create_status_code_echo_server(aws_client, create_lambda_function): + lambda_client = aws_client.lambda_ + + def _create_status_code_echo_server(): + function_name = f"lambda_fn_echo_status_{short_uid()}" + create_lambda_function( + func_name=function_name, + handler_file=TEST_LAMBDA_PYTHON_ECHO_STATUS_CODE, + ) + create_url_response = lambda_client.create_function_url_config( + FunctionName=function_name, AuthType="NONE", InvokeMode="BUFFERED" + ) + aws_client.lambda_.add_permission( + FunctionName=function_name, + StatementId="urlPermission", + Action="lambda:InvokeFunctionUrl", + Principal="*", + FunctionUrlAuthType="NONE", + ) + return create_url_response["FunctionUrl"] + + return _create_status_code_echo_server + + @pytest.fixture def apigw_redeploy_api(aws_client): def _factory(rest_api_id: str, stage_name: str): diff --git a/tests/aws/services/apigateway/test_apigateway_http.py b/tests/aws/services/apigateway/test_apigateway_http.py index ca6ec4f3adc3b..a2c86275c30b0 100644 --- a/tests/aws/services/apigateway/test_apigateway_http.py +++ b/tests/aws/services/apigateway/test_apigateway_http.py @@ -1,4 +1,5 @@ import json +from http import HTTPMethod import pytest import requests @@ -101,3 +102,47 @@ def invoke_api(url): # retry is necessary against AWS, probably IAM permission delay invoke_response = retry(invoke_api, sleep=2, retries=10, url=invocation_url) snapshot.match("http-invocation-lambda-url", invoke_response) + + +@markers.aws.validated +@pytest.mark.parametrize("integration_type", ["HTTP", "HTTP_PROXY"]) +def test_http_integration_invoke_status_code_passthrough( + aws_client, + create_status_code_echo_server, + create_rest_api_with_integration, + snapshot, + integration_type, +): + # Create echo serve + echo_server_url = create_status_code_echo_server() + # Create apigw + stage_name = "test" + apigw_id = create_rest_api_with_integration( + integration_uri=f"{echo_server_url}{{map}}", + integration_type=integration_type, + path_part="{map+}", + req_parameters={ + "integration.request.path.map": "method.request.path.map", + }, + stage=stage_name, + ) + + def invoke_api(url: str, method: HTTPMethod = HTTPMethod.POST): + response = requests.request(url=url, method=method) + status_code = response.status_code + assert status_code != 403 + return {"body": response.json(), "status_code": status_code} + + invocation_url = api_invoke_url( + api_id=apigw_id, + stage=stage_name, + path="/status", + ) + + # Invoke with matching response code + invoke_response = retry(invoke_api, sleep=2, retries=10, url=f"{invocation_url}/200") + snapshot.match("matching-response", invoke_response) + + # invoke non matching response code + invoke_response = retry(invoke_api, sleep=2, retries=10, url=f"{invocation_url}/400") + snapshot.match("non-matching-response", invoke_response) diff --git a/tests/aws/services/apigateway/test_apigateway_http.snapshot.json b/tests/aws/services/apigateway/test_apigateway_http.snapshot.json index ea899510322c0..a4bf5004eab39 100644 --- a/tests/aws/services/apigateway/test_apigateway_http.snapshot.json +++ b/tests/aws/services/apigateway/test_apigateway_http.snapshot.json @@ -92,5 +92,43 @@ "status_code": 200 } } + }, + "tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_invoke_status_code_passthrough[HTTP]": { + "recorded-date": "01-04-2024, 21:02:52", + "recorded-content": { + "matching-response": { + "body": { + "message": "", + "status_code": 200 + }, + "status_code": 200 + }, + "non-matching-response": { + "body": { + "message": "", + "status_code": 400 + }, + "status_code": 200 + } + } + }, + "tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_invoke_status_code_passthrough[HTTP_PROXY]": { + "recorded-date": "01-04-2024, 20:44:46", + "recorded-content": { + "matching-response": { + "body": { + "message": "", + "status_code": 200 + }, + "status_code": 200 + }, + "non-matching-response": { + "body": { + "message": "", + "status_code": 400 + }, + "status_code": 400 + } + } } } diff --git a/tests/aws/services/apigateway/test_apigateway_http.validation.json b/tests/aws/services/apigateway/test_apigateway_http.validation.json index 4d8b89a4c97bf..511d38ce02f48 100644 --- a/tests/aws/services/apigateway/test_apigateway_http.validation.json +++ b/tests/aws/services/apigateway/test_apigateway_http.validation.json @@ -1,9 +1,12 @@ { - "tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_with_lambda": { - "last_validated_date": "2024-03-28T19:24:09+00:00" + "tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_invoke_status_code_passthrough[HTTP]": { + "last_validated_date": "2024-04-01T21:45:48+00:00" + }, + "tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_invoke_status_code_passthrough[HTTP_PROXY]": { + "last_validated_date": "2024-04-01T21:46:23+00:00" }, "tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_with_lambda[HTTP]": { - "last_validated_date": "2024-03-29T15:12:45+00:00" + "last_validated_date": "2024-04-01T21:51:36+00:00" }, "tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_with_lambda[HTTP_PROXY]": { "last_validated_date": "2024-03-29T15:13:24+00:00" diff --git a/tests/aws/services/lambda_/functions/lambda_echo_status_code.py b/tests/aws/services/lambda_/functions/lambda_echo_status_code.py new file mode 100644 index 0000000000000..fd41fae575e96 --- /dev/null +++ b/tests/aws/services/lambda_/functions/lambda_echo_status_code.py @@ -0,0 +1,19 @@ +import json +from http import HTTPStatus + + +def make_response(status_code: int, message: str): + return { + "statusCode": status_code, + "headers": {"Content-Type": "application/json"}, + "body": {"status_code": status_code, "message": message}, + } + + +def handler(event, context): + print(json.dumps(event)) + path: str = event["requestContext"]["http"].get("path", "") + status_code = path.split("/")[-1] + if not status_code.isdigit() or int(status_code) not in list(HTTPStatus): + return make_response(HTTPStatus.BAD_REQUEST, f"No valid status found at end of path {path}") + return make_response(int(status_code), "") diff --git a/tests/aws/services/lambda_/test_lambda.py b/tests/aws/services/lambda_/test_lambda.py index 3d2c7501a050e..a20ab46ca7555 100644 --- a/tests/aws/services/lambda_/test_lambda.py +++ b/tests/aws/services/lambda_/test_lambda.py @@ -49,6 +49,9 @@ TEST_LAMBDA_PYTHON = os.path.join(THIS_FOLDER, "functions/lambda_integration.py") TEST_LAMBDA_PYTHON_ECHO = os.path.join(THIS_FOLDER, "functions/lambda_echo.py") TEST_LAMBDA_PYTHON_ECHO_JSON_BODY = os.path.join(THIS_FOLDER, "functions/lambda_echo_json_body.py") +TEST_LAMBDA_PYTHON_ECHO_STATUS_CODE = os.path.join( + THIS_FOLDER, "functions/lambda_echo_status_code.py" +) TEST_LAMBDA_PYTHON_REQUEST_ID = os.path.join(THIS_FOLDER, "functions/lambda_request_id.py") TEST_LAMBDA_PYTHON_ECHO_VERSION_ENV = os.path.join( THIS_FOLDER, "functions/lambda_echo_version_env.py" From 9d5104a3a379e077eab19e93616855cdcdffe498 Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Tue, 2 Apr 2024 21:35:35 +0200 Subject: [PATCH 018/169] StepFunctions, Fix Evaluation of Nested Map States (#10574) --- .../distributed_iteration_component.py | 4 +- .../iteration/inline_iteration_component.py | 4 +- .../state_map/iteration/job.py | 21 +- .../scenarios/scenarios_templates.py | 6 + .../statemachines/map_state_nested.json5 | 31 + .../statemachines/parallel_state_nested.json5 | 31 + .../v2/scenarios/test_base_scenarios.py | 64 ++ .../test_base_scenarios.snapshot.json | 598 ++++++++++++++++++ .../test_base_scenarios.validation.json | 6 + 9 files changed, 755 insertions(+), 10 deletions(-) create mode 100644 tests/aws/services/stepfunctions/templates/scenarios/statemachines/map_state_nested.json5 create mode 100644 tests/aws/services/stepfunctions/templates/scenarios/statemachines/parallel_state_nested.json5 diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/distributed_iteration_component.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/distributed_iteration_component.py index 56f2862613ef6..109eb3ae27f30 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/distributed_iteration_component.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/distributed_iteration_component.py @@ -38,7 +38,7 @@ IterationWorker, ) from localstack.services.stepfunctions.asl.component.state.state_execution.state_map.iteration.job import ( - Job, + JobClosed, JobPool, ) from localstack.services.stepfunctions.asl.component.state.state_execution.state_map.max_concurrency import ( @@ -133,7 +133,7 @@ def _map_run(self, env: Environment) -> None: if worker_exception is not None: raise worker_exception - closed_jobs: list[Job] = self._job_pool.get_closed_jobs() + closed_jobs: list[JobClosed] = self._job_pool.get_closed_jobs() outputs: list[Any] = [closed_job.job_output for closed_job in closed_jobs] env.stack.append(outputs) diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/inline_iteration_component.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/inline_iteration_component.py index 8d4d539341458..837387944ae96 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/inline_iteration_component.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/inline_iteration_component.py @@ -22,7 +22,7 @@ IterationWorker, ) from localstack.services.stepfunctions.asl.component.state.state_execution.state_map.iteration.job import ( - Job, + JobClosed, JobPool, ) from localstack.services.stepfunctions.asl.component.state.state_execution.state_map.max_concurrency import ( @@ -110,7 +110,7 @@ def _eval_body(self, env: Environment) -> None: if worker_exception is not None: raise worker_exception - closed_jobs: list[Job] = self._job_pool.get_closed_jobs() + closed_jobs: list[JobClosed] = self._job_pool.get_closed_jobs() outputs: list[Any] = [closed_job.job_output for closed_job in closed_jobs] env.stack.append(outputs) diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/job.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/job.py index 8a82a335dfd6b..ff4cb5a490925 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/job.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/job.py @@ -21,6 +21,15 @@ def __init__(self, job_index: int, job_program: Program, job_input: Optional[Any self.job_input = job_input self.job_output = None + +class JobClosed: + job_index: Final[int] + job_output: Optional[Any] + + def __init__(self, job_index: int, job_output: Optional[Any]): + self.job_index = job_index + self.job_output = job_output + def __hash__(self): return hash(self.job_index) @@ -32,7 +41,7 @@ class JobPool: _jobs_number: Final[int] _open_jobs: Final[list[Job]] - _closed_jobs: Final[set[Job]] + _closed_jobs: Final[set[JobClosed]] def __init__(self, job_program: Program, job_inputs: list[Any]): self._mutex = threading.Lock() @@ -59,14 +68,14 @@ def next_job(self) -> Optional[Any]: def _is_terminated(self) -> bool: return len(self._closed_jobs) == self._jobs_number or self._worker_exception is not None - def _notify_on_termination(self): + def _notify_on_termination(self) -> None: if self._is_terminated(): self._termination_event.set() def get_worker_exception(self) -> Optional[Exception]: return self._worker_exception - def close_job(self, job: Job): + def close_job(self, job: Job) -> None: with self._mutex: if self._is_terminated(): return @@ -79,15 +88,15 @@ def close_job(self, job: Job): if isinstance(job.job_output, Exception): self._worker_exception = job.job_output else: - self._closed_jobs.add(job) + self._closed_jobs.add(JobClosed(job_index=job.job_index, job_output=job.job_output)) self._notify_on_termination() - def get_closed_jobs(self) -> list[Job]: + def get_closed_jobs(self) -> list[JobClosed]: with self._mutex: closed_jobs = copy.deepcopy(self._closed_jobs) return sorted(closed_jobs, key=lambda closed_job: closed_job.job_index) - def await_jobs(self): + def await_jobs(self) -> None: if not self._is_terminated(): self._termination_event.wait() diff --git a/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py b/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py index 0286a1bc96bdb..0d32cde6afc05 100644 --- a/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py +++ b/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py @@ -15,6 +15,9 @@ class ScenariosTemplate(TemplateLoader): PARALLEL_STATE_FAIL: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/parallel_state_fail.json5" ) + PARALLEL_NESTED_NESTED: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/parallel_state_nested.json5" + ) PARALLEL_STATE_CATCH: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/parallel_state_catch.json5" ) @@ -58,6 +61,9 @@ class ScenariosTemplate(TemplateLoader): MAP_STATE_CONFIG_INLINE_ITEM_SELECTOR: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/map_state_config_inline_item_selector.json5" ) + MAP_STATE_NESTED: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/map_state_nested.json5" + ) MAP_STATE_NO_PROCESSOR_CONFIG: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/map_state_no_processor_config.json5" ) diff --git a/tests/aws/services/stepfunctions/templates/scenarios/statemachines/map_state_nested.json5 b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/map_state_nested.json5 new file mode 100644 index 0000000000000..49e4cac930b80 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/map_state_nested.json5 @@ -0,0 +1,31 @@ +{ + "StartAt": "MapL1", + "States": { + "MapL1": { + "Type": "Map", + "MaxConcurrency": 1, + "ItemProcessor": { + "ProcessorConfig": { "Mode": "INLINE" }, + "StartAt": "MapL2", + "States": { + "MapL2": { + "Type": "Map", + "MaxConcurrency": 1, + "ItemProcessor": { + "ProcessorConfig": { "Mode": "INLINE" }, + "StartAt": "MapL2Pass", + "States": { + "MapL2Pass": { + "Type": "Pass", + "End": true + } + } + }, + "End": true + } + } + }, + "End": true + } + } +} diff --git a/tests/aws/services/stepfunctions/templates/scenarios/statemachines/parallel_state_nested.json5 b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/parallel_state_nested.json5 new file mode 100644 index 0000000000000..910365f8b004c --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/parallel_state_nested.json5 @@ -0,0 +1,31 @@ +{ + "StartAt": "ParallelStateL1", + "States": { + "ParallelStateL1": { + "Type": "Parallel", + "End": true, + "Branches": [ + { + "StartAt": "ParallelStateL2", + "States": { + "ParallelStateL2": { + "Type": "Parallel", + "End": true, + "Branches": [ + { + "StartAt": "BranchL2", + "States": { + "BranchL2": { + "Type": "Pass", + "End": true + } + } + } + ] + } + } + } + ] + } + } +} diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py index c94746695f4fc..80abb3b50d080 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py @@ -157,6 +157,37 @@ def test_parallel_state_fail( exec_input, ) + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + paths=[ + # TODO: AWS appears to have changed json encoding to include spaces after separators, + # other v2 test suite snapshots need to be re-recorded + "$..events..stateEnteredEventDetails.input", + "$..events..stateExitedEventDetails.output", + "$..events..executionSucceededEventDetails.output", + ] + ) + def test_parallel_state_nested( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + ): + sfn_snapshot.add_transformer(SfnNoneRecursiveParallelTransformer()) + template = ST.load_sfn_template(ST.PARALLEL_NESTED_NESTED) + definition = json.dumps(template) + + exec_input = json.dumps([[1, 2, 3], [4, 5, 6]]) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + @markers.aws.validated def test_parallel_state_catch( self, @@ -220,6 +251,39 @@ def test_map_state( exec_input, ) + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + paths=[ + # TODO: AWS appears to have changed json encoding to include spaces after separators, + # other v2 test suite snapshots need to be re-recorded + "$..events..stateEnteredEventDetails.input" + ] + ) + def test_map_state_nested( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + ): + template = ST.load_sfn_template(ST.MAP_STATE_NESTED) + definition = json.dumps(template) + + exec_input = json.dumps( + [ + [1, 2, 3], + [4, 5, 6], + ] + ) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + @markers.aws.validated def test_map_state_no_processor_config( self, diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json index a5ae3b3a44d6c..4bfdf62592438 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json @@ -15053,5 +15053,603 @@ } } } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_nested": { + "recorded-date": "29-03-2024, 16:26:02", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": "[[1, 2, 3], [4, 5, 6]]", + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": "[[1, 2, 3], [4, 5, 6]]", + "inputDetails": { + "truncated": false + }, + "name": "MapL1" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "id": 3, + "mapStateStartedEventDetails": { + "length": 2 + }, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "MapStateStarted" + }, + { + "id": 4, + "mapIterationStartedEventDetails": { + "index": 0, + "name": "MapL1" + }, + "previousEventId": 3, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 5, + "previousEventId": 4, + "stateEnteredEventDetails": { + "input": "[1,2,3]", + "inputDetails": { + "truncated": false + }, + "name": "MapL2" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "id": 6, + "mapStateStartedEventDetails": { + "length": 3 + }, + "previousEventId": 5, + "timestamp": "timestamp", + "type": "MapStateStarted" + }, + { + "id": 7, + "mapIterationStartedEventDetails": { + "index": 0, + "name": "MapL2" + }, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 8, + "previousEventId": 7, + "stateEnteredEventDetails": { + "input": "1", + "inputDetails": { + "truncated": false + }, + "name": "MapL2Pass" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 9, + "previousEventId": 8, + "stateExitedEventDetails": { + "name": "MapL2Pass", + "output": "1", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 10, + "mapIterationSucceededEventDetails": { + "index": 0, + "name": "MapL2" + }, + "previousEventId": 9, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 11, + "mapIterationStartedEventDetails": { + "index": 1, + "name": "MapL2" + }, + "previousEventId": 9, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 12, + "previousEventId": 11, + "stateEnteredEventDetails": { + "input": "2", + "inputDetails": { + "truncated": false + }, + "name": "MapL2Pass" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 13, + "previousEventId": 12, + "stateExitedEventDetails": { + "name": "MapL2Pass", + "output": "2", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 14, + "mapIterationSucceededEventDetails": { + "index": 1, + "name": "MapL2" + }, + "previousEventId": 13, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 15, + "mapIterationStartedEventDetails": { + "index": 2, + "name": "MapL2" + }, + "previousEventId": 13, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 16, + "previousEventId": 15, + "stateEnteredEventDetails": { + "input": "3", + "inputDetails": { + "truncated": false + }, + "name": "MapL2Pass" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 17, + "previousEventId": 16, + "stateExitedEventDetails": { + "name": "MapL2Pass", + "output": "3", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 18, + "mapIterationSucceededEventDetails": { + "index": 2, + "name": "MapL2" + }, + "previousEventId": 17, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 19, + "previousEventId": 18, + "timestamp": "timestamp", + "type": "MapStateSucceeded" + }, + { + "id": 20, + "previousEventId": 18, + "stateExitedEventDetails": { + "name": "MapL2", + "output": "[1,2,3]", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "MapStateExited" + }, + { + "id": 21, + "mapIterationSucceededEventDetails": { + "index": 0, + "name": "MapL1" + }, + "previousEventId": 20, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 22, + "mapIterationStartedEventDetails": { + "index": 1, + "name": "MapL1" + }, + "previousEventId": 20, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 23, + "previousEventId": 22, + "stateEnteredEventDetails": { + "input": "[4,5,6]", + "inputDetails": { + "truncated": false + }, + "name": "MapL2" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "id": 24, + "mapStateStartedEventDetails": { + "length": 3 + }, + "previousEventId": 23, + "timestamp": "timestamp", + "type": "MapStateStarted" + }, + { + "id": 25, + "mapIterationStartedEventDetails": { + "index": 0, + "name": "MapL2" + }, + "previousEventId": 24, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 26, + "previousEventId": 25, + "stateEnteredEventDetails": { + "input": "4", + "inputDetails": { + "truncated": false + }, + "name": "MapL2Pass" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 27, + "previousEventId": 26, + "stateExitedEventDetails": { + "name": "MapL2Pass", + "output": "4", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 28, + "mapIterationSucceededEventDetails": { + "index": 0, + "name": "MapL2" + }, + "previousEventId": 27, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 29, + "mapIterationStartedEventDetails": { + "index": 1, + "name": "MapL2" + }, + "previousEventId": 27, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 30, + "previousEventId": 29, + "stateEnteredEventDetails": { + "input": "5", + "inputDetails": { + "truncated": false + }, + "name": "MapL2Pass" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 31, + "previousEventId": 30, + "stateExitedEventDetails": { + "name": "MapL2Pass", + "output": "5", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 32, + "mapIterationSucceededEventDetails": { + "index": 1, + "name": "MapL2" + }, + "previousEventId": 31, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 33, + "mapIterationStartedEventDetails": { + "index": 2, + "name": "MapL2" + }, + "previousEventId": 31, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 34, + "previousEventId": 33, + "stateEnteredEventDetails": { + "input": "6", + "inputDetails": { + "truncated": false + }, + "name": "MapL2Pass" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 35, + "previousEventId": 34, + "stateExitedEventDetails": { + "name": "MapL2Pass", + "output": "6", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 36, + "mapIterationSucceededEventDetails": { + "index": 2, + "name": "MapL2" + }, + "previousEventId": 35, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 37, + "previousEventId": 36, + "timestamp": "timestamp", + "type": "MapStateSucceeded" + }, + { + "id": 38, + "previousEventId": 36, + "stateExitedEventDetails": { + "name": "MapL2", + "output": "[4,5,6]", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "MapStateExited" + }, + { + "id": 39, + "mapIterationSucceededEventDetails": { + "index": 1, + "name": "MapL1" + }, + "previousEventId": 38, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 40, + "previousEventId": 39, + "timestamp": "timestamp", + "type": "MapStateSucceeded" + }, + { + "id": 41, + "previousEventId": 39, + "stateExitedEventDetails": { + "name": "MapL1", + "output": "[[1,2,3],[4,5,6]]", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "MapStateExited" + }, + { + "executionSucceededEventDetails": { + "output": "[[1,2,3],[4,5,6]]", + "outputDetails": { + "truncated": false + } + }, + "id": 42, + "previousEventId": 41, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_parallel_state_nested": { + "recorded-date": "29-03-2024, 17:05:02", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": "[[1, 2, 3], [4, 5, 6]]", + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": "[[1, 2, 3], [4, 5, 6]]", + "inputDetails": { + "truncated": false + }, + "name": "ParallelStateL1" + }, + "timestamp": "timestamp", + "type": "ParallelStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "ParallelStateStarted" + }, + { + "id": [ + 0 + ], + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": "[[1, 2, 3], [4, 5, 6]]", + "inputDetails": { + "truncated": false + }, + "name": "ParallelStateL2" + }, + "timestamp": "timestamp", + "type": "ParallelStateEntered" + }, + { + "id": 5, + "previousEventId": 4, + "timestamp": "timestamp", + "type": "ParallelStateStarted" + }, + { + "id": [ + 0 + ], + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": "[[1, 2, 3], [4, 5, 6]]", + "inputDetails": { + "truncated": false + }, + "name": "BranchL2" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": [ + 0 + ], + "previousEventId": 0, + "stateExitedEventDetails": { + "name": "BranchL2", + "output": "[[1, 2, 3], [4, 5, 6]]", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 11, + "previousEventId": 9, + "stateExitedEventDetails": { + "name": "ParallelStateL1", + "output": "[[[[1, 2, 3], [4, 5, 6]]]]", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "ParallelStateExited" + }, + { + "executionSucceededEventDetails": { + "output": "[[[[1, 2, 3], [4, 5, 6]]]]", + "outputDetails": { + "truncated": false + } + }, + "id": 12, + "previousEventId": 11, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json index 0c9a96aca38db..f06853b390894 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json @@ -152,6 +152,9 @@ "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_legacy_config_inline_parameters": { "last_validated_date": "2024-02-08T21:07:39+00:00" }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_nested": { + "last_validated_date": "2024-03-29T16:26:02+00:00" + }, "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_no_processor_config": { "last_validated_date": "2023-12-15T21:25:27+00:00" }, @@ -173,6 +176,9 @@ "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_parallel_state": { "last_validated_date": "2023-07-17T10:41:25+00:00" }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_parallel_state_nested": { + "last_validated_date": "2024-03-29T17:05:02+00:00" + }, "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_parallel_state_order": { "last_validated_date": "2023-12-15T22:15:11+00:00" }, From 269626392f8734a148cc2f89fdc4052995c2dbb5 Mon Sep 17 00:00:00 2001 From: Silvio Vasiljevic Date: Wed, 3 Apr 2024 11:56:11 +0200 Subject: [PATCH 019/169] Fix release helper for cli release (#10597) --- bin/release-helper.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/bin/release-helper.sh b/bin/release-helper.sh index d6e8d09be42b3..8668160dfca0a 100755 --- a/bin/release-helper.sh +++ b/bin/release-helper.sh @@ -159,8 +159,8 @@ function cmd-set-dep-ver() { dep=$1 ver=$2 - grep -Eh "^(\s*\")${dep}(\[[a-zA-Z0-9]+\])?(>|=|<)(.*\",)" ${DEPENDENCY_FILE} || { echo "dependency ${dep} not found in ${DEPENDENCY_FILE}"; return 1; } - sed -i -r "s/^(\s*\")(${dep})(\[[a-zA-Z0-9,]+\])?(>|=|<)(.*\",)/\1\2\3${ver}\",/g" ${DEPENDENCY_FILE} + egrep -h "^(\s*\"?)(${dep})(\[[a-zA-Z0-9,]+\])?(>=|==|<=)([^\"]*)(\")?(,)?$" ${DEPENDENCY_FILE} || { echo "dependency ${dep} not found in ${DEPENDENCY_FILE}"; return 1; } + sed -i -r "s/^(\s*\"?)(${dep})(\[[a-zA-Z0-9,]+\])?(>=|==|<=)([^\"]*)(\")?(,)?$/\1\2\3${ver}\6\7/g" ${DEPENDENCY_FILE} } function cmd-github-outputs() { @@ -192,13 +192,17 @@ function cmd-git-commit-release() { echo $1 || verify_valid_version - git add ${VERSION_FILE} ${VERSION_PY} ${DEPENDENCY_FILE} + for file in ${VERSION_FILE} ${VERSION_PY} ${DEPENDENCY_FILE}; do + [ -e "$file" ] && git add "$file" + done git commit -m "release version ${1}" git tag -a "v${1}" -m "Release version ${1}" } function cmd-git-commit-increment() { - git add ${VERSION_FILE} ${VERSION_PY} ${DEPENDENCY_FILE} + for file in ${VERSION_FILE} ${VERSION_PY} ${DEPENDENCY_FILE}; do + [ -e "$file" ] && git add "$file" + done git commit -m "prepare next development iteration" } From dc073770a5450df3777946869ebb904881cfba9b Mon Sep 17 00:00:00 2001 From: Viren Nadkarni Date: Wed, 3 Apr 2024 15:28:00 +0530 Subject: [PATCH 020/169] Bump moto-ext to 5.0.4.post1 (#10589) --- pyproject.toml | 2 +- requirements-dev.txt | 4 ++-- requirements-runtime.txt | 4 ++-- requirements-test.txt | 4 ++-- requirements-typehint.txt | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8a19472d5302d..d05517389e797 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,7 +85,7 @@ runtime = [ "json5>=0.9.11", "jsonpath-ng>=1.6.1", "jsonpath-rw>=1.4.0", - "moto-ext[all]==5.0.3.post1", + "moto-ext[all]==5.0.4.post1", "opensearch-py>=2.4.1", "pymongo>=4.2.0", "pyopenssl>=23.0.0", diff --git a/requirements-dev.txt b/requirements-dev.txt index 688bb4d4473a4..5f30b4fb3b7fd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -273,7 +273,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext[all]==5.0.3.post1 +moto-ext[all]==5.0.4.post1 # via localstack-core mpmath==1.3.0 # via sympy @@ -344,7 +344,7 @@ publication==0.0.3 # aws-cdk-lib # constructs # jsii -py-partiql-parser==0.5.1 +py-partiql-parser==0.5.2 # via moto-ext pyasn1==0.6.0 # via rsa diff --git a/requirements-runtime.txt b/requirements-runtime.txt index a456939b83de8..6d319cb3d92b2 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -210,7 +210,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext[all]==5.0.3.post1 +moto-ext[all]==5.0.4.post1 # via localstack-core (pyproject.toml) mpmath==1.3.0 # via sympy @@ -252,7 +252,7 @@ psutil==5.9.8 # via # localstack-core # localstack-core (pyproject.toml) -py-partiql-parser==0.5.1 +py-partiql-parser==0.5.2 # via moto-ext pyasn1==0.6.0 # via rsa diff --git a/requirements-test.txt b/requirements-test.txt index 278b46ab7256f..ae0f714d9cf49 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -257,7 +257,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext[all]==5.0.3.post1 +moto-ext[all]==5.0.4.post1 # via localstack-core mpmath==1.3.0 # via sympy @@ -315,7 +315,7 @@ publication==0.0.3 # aws-cdk-lib # constructs # jsii -py-partiql-parser==0.5.1 +py-partiql-parser==0.5.2 # via moto-ext pyasn1==0.6.0 # via rsa diff --git a/requirements-typehint.txt b/requirements-typehint.txt index d7a8e6ac113a8..453ed24b2c156 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -277,7 +277,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext[all]==5.0.3.post1 +moto-ext[all]==5.0.4.post1 # via localstack-core mpmath==1.3.0 # via sympy @@ -540,7 +540,7 @@ publication==0.0.3 # aws-cdk-lib # constructs # jsii -py-partiql-parser==0.5.1 +py-partiql-parser==0.5.2 # via moto-ext pyasn1==0.6.0 # via rsa From 3261414a5385362db651ab041121447a3a801439 Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:23:20 +0200 Subject: [PATCH 021/169] StepFunctions: Makefile Targets for Parsers Generation (#10193) --- .../stepfunctions/asl/antlr/.gitignore | 4 + .../services/stepfunctions/asl/antlr/Makefile | 39 ++ .../antlr/runtime/ASLIntrinsicLexer.interp | 123 ----- .../asl/antlr/runtime/ASLIntrinsicLexer.py | 2 +- .../antlr/runtime/ASLIntrinsicLexer.tokens | 59 --- .../antlr/runtime/ASLIntrinsicParser.interp | 82 ---- .../asl/antlr/runtime/ASLIntrinsicParser.py | 2 +- .../antlr/runtime/ASLIntrinsicParser.tokens | 59 --- .../runtime/ASLIntrinsicParserListener.py | 2 +- .../runtime/ASLIntrinsicParserVisitor.py | 2 +- .../asl/antlr/runtime/ASLLexer.interp | 442 ------------------ .../asl/antlr/runtime/ASLLexer.py | 2 +- .../asl/antlr/runtime/ASLLexer.tokens | 273 ----------- .../asl/antlr/runtime/ASLParser.interp | 383 --------------- .../asl/antlr/runtime/ASLParser.py | 2 +- .../asl/antlr/runtime/ASLParser.tokens | 273 ----------- .../asl/antlr/runtime/ASLParserListener.py | 2 +- .../asl/antlr/runtime/ASLParserVisitor.py | 2 +- 18 files changed, 51 insertions(+), 1702 deletions(-) create mode 100644 localstack/services/stepfunctions/asl/antlr/.gitignore create mode 100644 localstack/services/stepfunctions/asl/antlr/Makefile delete mode 100644 localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicLexer.interp delete mode 100644 localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicLexer.tokens delete mode 100644 localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParser.interp delete mode 100644 localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParser.tokens delete mode 100644 localstack/services/stepfunctions/asl/antlr/runtime/ASLLexer.interp delete mode 100644 localstack/services/stepfunctions/asl/antlr/runtime/ASLLexer.tokens delete mode 100644 localstack/services/stepfunctions/asl/antlr/runtime/ASLParser.interp delete mode 100644 localstack/services/stepfunctions/asl/antlr/runtime/ASLParser.tokens diff --git a/localstack/services/stepfunctions/asl/antlr/.gitignore b/localstack/services/stepfunctions/asl/antlr/.gitignore new file mode 100644 index 0000000000000..ade3e916efb0c --- /dev/null +++ b/localstack/services/stepfunctions/asl/antlr/.gitignore @@ -0,0 +1,4 @@ +.antlr +/.antlr* +*.tokens +*.interp diff --git a/localstack/services/stepfunctions/asl/antlr/Makefile b/localstack/services/stepfunctions/asl/antlr/Makefile new file mode 100644 index 0000000000000..4e5be502af50f --- /dev/null +++ b/localstack/services/stepfunctions/asl/antlr/Makefile @@ -0,0 +1,39 @@ +# Define default ANTLR4 tool dump directory. +ANTLR4_DIR = .antlr + +# Define the default input and output directory for ANTLR4 grammars. +ANTLR4_SRC_DIR = . +ANTLR4_TARGET_DIR = $(ANTLR4_SRC_DIR)/runtime +ANTLR4_GRAMMAR_FILES = $(wildcard $(ANTLR4_SRC_DIR)/*.g4) + +# Define the default ANTLR4 version and jar file. +ANTLR4_VERSION ?= 4.13.1 +ANTLR4_JAR ?= $(ANTLR4_DIR)/antlr-$(ANTLR4_VERSION)-complete.jar + +# Define the download path for ANTLR4 parser generator. +ANTLR4_URL = https://www.antlr.org/download/antlr-$(ANTLR4_VERSION)-complete.jar + +# Define the default ANTLR4 run command and options. +RUN_ANTLR4 = java -jar $(ANTLR4_JAR) -Dlanguage=Python3 -visitor + +install: ## Install the dependencies for compiling the ANTLR4 project. + @npm i -g --save-dev antlr-format@2.1.4 + @mkdir -p $(ANTLR4_DIR) + @curl -o $(ANTLR4_JAR) $(ANTLR4_URL) + +build: $(ANTLR4_GRAMMAR_FILES) ## Build the ANTLR4 project. + @echo "Compiling grammar files in $(ANTLR_SRC_DIR)" + @mkdir -p $(ANTLR4_TARGET_DIR) + @for grammar in $^ ; do \ + echo "Processing $$grammar..."; \ + $(RUN_ANTLR4) $$grammar -o $(ANTLR4_TARGET_DIR) -Xexact-output-dir; \ + done + +format: + @antlr-format *.g4 + +clean: ## Clean up the ANTLR4 project directory. + rm -rf $(ANTLR4_TARGET_DIR) + rm -rf $(ANTLR4_DIR) + +.PHONY: install build format clean diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicLexer.interp b/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicLexer.interp deleted file mode 100644 index 6346c020be0be..0000000000000 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicLexer.interp +++ /dev/null @@ -1,123 +0,0 @@ -token literal names: -null -null -null -'$' -'(' -')' -',' -'.' -'true' -'false' -'States' -'Format' -'StringToJson' -'JsonToString' -'Array' -'ArrayPartition' -'ArrayContains' -'ArrayRange' -'ArrayGetItem' -'ArrayLength' -'ArrayUnique' -'Base64Encode' -'Base64Decode' -'Hash' -'JsonMerge' -'MathRandom' -'MathAdd' -'StringSplit' -'UUID' -null -null -null -null -null - -token symbolic names: -null -CONTEXT_PATH_STRING -JSON_PATH_STRING -DOLLAR -LPAREN -RPAREN -COMMA -DOT -TRUE -FALSE -States -Format -StringToJson -JsonToString -Array -ArrayPartition -ArrayContains -ArrayRange -ArrayGetItem -ArrayLength -ArrayUnique -Base64Encode -Base64Decode -Hash -JsonMerge -MathRandom -MathAdd -StringSplit -UUID -STRING -INT -NUMBER -IDENTIFIER -WS - -rule names: -CONTEXT_PATH_STRING -JSON_PATH_STRING -JSON_PATH_BODY -JSON_PATH_BRACK -DOLLAR -LPAREN -RPAREN -COMMA -DOT -TRUE -FALSE -States -Format -StringToJson -JsonToString -Array -ArrayPartition -ArrayContains -ArrayRange -ArrayGetItem -ArrayLength -ArrayUnique -Base64Encode -Base64Decode -Hash -JsonMerge -MathRandom -MathAdd -StringSplit -UUID -STRING -ESC -UNICODE -HEX -SAFECODEPOINT -INT -NUMBER -EXP -IDENTIFIER -WS - -channel names: -DEFAULT_TOKEN_CHANNEL -HIDDEN - -mode names: -DEFAULT_MODE - -atn: -[4, 0, 33, 406, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 2, 3, 2, 90, 8, 2, 1, 2, 1, 2, 3, 2, 94, 8, 2, 1, 2, 3, 2, 97, 8, 2, 5, 2, 99, 8, 2, 10, 2, 12, 2, 102, 9, 2, 1, 3, 1, 3, 1, 3, 5, 3, 107, 8, 3, 10, 3, 12, 3, 110, 9, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 5, 30, 338, 8, 30, 10, 30, 12, 30, 341, 9, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 3, 31, 348, 8, 31, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 34, 1, 34, 1, 35, 3, 35, 361, 8, 35, 1, 35, 1, 35, 1, 35, 5, 35, 366, 8, 35, 10, 35, 12, 35, 369, 9, 35, 3, 35, 371, 8, 35, 1, 36, 3, 36, 374, 8, 36, 1, 36, 1, 36, 1, 36, 4, 36, 379, 8, 36, 11, 36, 12, 36, 380, 3, 36, 383, 8, 36, 1, 36, 3, 36, 386, 8, 36, 1, 37, 1, 37, 3, 37, 390, 8, 37, 1, 37, 1, 37, 1, 38, 1, 38, 4, 38, 396, 8, 38, 11, 38, 12, 38, 397, 1, 39, 4, 39, 401, 8, 39, 11, 39, 12, 39, 402, 1, 39, 1, 39, 1, 339, 0, 40, 1, 1, 3, 2, 5, 0, 7, 0, 9, 3, 11, 4, 13, 5, 15, 6, 17, 7, 19, 8, 21, 9, 23, 10, 25, 11, 27, 12, 29, 13, 31, 14, 33, 15, 35, 16, 37, 17, 39, 18, 41, 19, 43, 20, 45, 21, 47, 22, 49, 23, 51, 24, 53, 25, 55, 26, 57, 27, 59, 28, 61, 29, 63, 0, 65, 0, 67, 0, 69, 0, 71, 30, 73, 31, 75, 0, 77, 32, 79, 33, 1, 0, 9, 1, 0, 93, 93, 3, 0, 48, 57, 65, 70, 97, 102, 3, 0, 0, 31, 39, 39, 92, 92, 1, 0, 49, 57, 1, 0, 48, 57, 2, 0, 69, 69, 101, 101, 2, 0, 43, 43, 45, 45, 4, 0, 48, 57, 65, 90, 95, 95, 97, 122, 2, 0, 9, 10, 32, 32, 418, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0, 55, 1, 0, 0, 0, 0, 57, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 71, 1, 0, 0, 0, 0, 73, 1, 0, 0, 0, 0, 77, 1, 0, 0, 0, 0, 79, 1, 0, 0, 0, 1, 81, 1, 0, 0, 0, 3, 85, 1, 0, 0, 0, 5, 89, 1, 0, 0, 0, 7, 103, 1, 0, 0, 0, 9, 113, 1, 0, 0, 0, 11, 115, 1, 0, 0, 0, 13, 117, 1, 0, 0, 0, 15, 119, 1, 0, 0, 0, 17, 121, 1, 0, 0, 0, 19, 123, 1, 0, 0, 0, 21, 128, 1, 0, 0, 0, 23, 134, 1, 0, 0, 0, 25, 141, 1, 0, 0, 0, 27, 148, 1, 0, 0, 0, 29, 161, 1, 0, 0, 0, 31, 174, 1, 0, 0, 0, 33, 180, 1, 0, 0, 0, 35, 195, 1, 0, 0, 0, 37, 209, 1, 0, 0, 0, 39, 220, 1, 0, 0, 0, 41, 233, 1, 0, 0, 0, 43, 245, 1, 0, 0, 0, 45, 257, 1, 0, 0, 0, 47, 270, 1, 0, 0, 0, 49, 283, 1, 0, 0, 0, 51, 288, 1, 0, 0, 0, 53, 298, 1, 0, 0, 0, 55, 309, 1, 0, 0, 0, 57, 317, 1, 0, 0, 0, 59, 329, 1, 0, 0, 0, 61, 334, 1, 0, 0, 0, 63, 344, 1, 0, 0, 0, 65, 349, 1, 0, 0, 0, 67, 355, 1, 0, 0, 0, 69, 357, 1, 0, 0, 0, 71, 360, 1, 0, 0, 0, 73, 373, 1, 0, 0, 0, 75, 387, 1, 0, 0, 0, 77, 395, 1, 0, 0, 0, 79, 400, 1, 0, 0, 0, 81, 82, 3, 9, 4, 0, 82, 83, 3, 9, 4, 0, 83, 84, 3, 5, 2, 0, 84, 2, 1, 0, 0, 0, 85, 86, 3, 9, 4, 0, 86, 87, 3, 5, 2, 0, 87, 4, 1, 0, 0, 0, 88, 90, 3, 7, 3, 0, 89, 88, 1, 0, 0, 0, 89, 90, 1, 0, 0, 0, 90, 100, 1, 0, 0, 0, 91, 93, 3, 17, 8, 0, 92, 94, 3, 77, 38, 0, 93, 92, 1, 0, 0, 0, 93, 94, 1, 0, 0, 0, 94, 96, 1, 0, 0, 0, 95, 97, 3, 7, 3, 0, 96, 95, 1, 0, 0, 0, 96, 97, 1, 0, 0, 0, 97, 99, 1, 0, 0, 0, 98, 91, 1, 0, 0, 0, 99, 102, 1, 0, 0, 0, 100, 98, 1, 0, 0, 0, 100, 101, 1, 0, 0, 0, 101, 6, 1, 0, 0, 0, 102, 100, 1, 0, 0, 0, 103, 108, 5, 91, 0, 0, 104, 107, 3, 7, 3, 0, 105, 107, 8, 0, 0, 0, 106, 104, 1, 0, 0, 0, 106, 105, 1, 0, 0, 0, 107, 110, 1, 0, 0, 0, 108, 106, 1, 0, 0, 0, 108, 109, 1, 0, 0, 0, 109, 111, 1, 0, 0, 0, 110, 108, 1, 0, 0, 0, 111, 112, 5, 93, 0, 0, 112, 8, 1, 0, 0, 0, 113, 114, 5, 36, 0, 0, 114, 10, 1, 0, 0, 0, 115, 116, 5, 40, 0, 0, 116, 12, 1, 0, 0, 0, 117, 118, 5, 41, 0, 0, 118, 14, 1, 0, 0, 0, 119, 120, 5, 44, 0, 0, 120, 16, 1, 0, 0, 0, 121, 122, 5, 46, 0, 0, 122, 18, 1, 0, 0, 0, 123, 124, 5, 116, 0, 0, 124, 125, 5, 114, 0, 0, 125, 126, 5, 117, 0, 0, 126, 127, 5, 101, 0, 0, 127, 20, 1, 0, 0, 0, 128, 129, 5, 102, 0, 0, 129, 130, 5, 97, 0, 0, 130, 131, 5, 108, 0, 0, 131, 132, 5, 115, 0, 0, 132, 133, 5, 101, 0, 0, 133, 22, 1, 0, 0, 0, 134, 135, 5, 83, 0, 0, 135, 136, 5, 116, 0, 0, 136, 137, 5, 97, 0, 0, 137, 138, 5, 116, 0, 0, 138, 139, 5, 101, 0, 0, 139, 140, 5, 115, 0, 0, 140, 24, 1, 0, 0, 0, 141, 142, 5, 70, 0, 0, 142, 143, 5, 111, 0, 0, 143, 144, 5, 114, 0, 0, 144, 145, 5, 109, 0, 0, 145, 146, 5, 97, 0, 0, 146, 147, 5, 116, 0, 0, 147, 26, 1, 0, 0, 0, 148, 149, 5, 83, 0, 0, 149, 150, 5, 116, 0, 0, 150, 151, 5, 114, 0, 0, 151, 152, 5, 105, 0, 0, 152, 153, 5, 110, 0, 0, 153, 154, 5, 103, 0, 0, 154, 155, 5, 84, 0, 0, 155, 156, 5, 111, 0, 0, 156, 157, 5, 74, 0, 0, 157, 158, 5, 115, 0, 0, 158, 159, 5, 111, 0, 0, 159, 160, 5, 110, 0, 0, 160, 28, 1, 0, 0, 0, 161, 162, 5, 74, 0, 0, 162, 163, 5, 115, 0, 0, 163, 164, 5, 111, 0, 0, 164, 165, 5, 110, 0, 0, 165, 166, 5, 84, 0, 0, 166, 167, 5, 111, 0, 0, 167, 168, 5, 83, 0, 0, 168, 169, 5, 116, 0, 0, 169, 170, 5, 114, 0, 0, 170, 171, 5, 105, 0, 0, 171, 172, 5, 110, 0, 0, 172, 173, 5, 103, 0, 0, 173, 30, 1, 0, 0, 0, 174, 175, 5, 65, 0, 0, 175, 176, 5, 114, 0, 0, 176, 177, 5, 114, 0, 0, 177, 178, 5, 97, 0, 0, 178, 179, 5, 121, 0, 0, 179, 32, 1, 0, 0, 0, 180, 181, 5, 65, 0, 0, 181, 182, 5, 114, 0, 0, 182, 183, 5, 114, 0, 0, 183, 184, 5, 97, 0, 0, 184, 185, 5, 121, 0, 0, 185, 186, 5, 80, 0, 0, 186, 187, 5, 97, 0, 0, 187, 188, 5, 114, 0, 0, 188, 189, 5, 116, 0, 0, 189, 190, 5, 105, 0, 0, 190, 191, 5, 116, 0, 0, 191, 192, 5, 105, 0, 0, 192, 193, 5, 111, 0, 0, 193, 194, 5, 110, 0, 0, 194, 34, 1, 0, 0, 0, 195, 196, 5, 65, 0, 0, 196, 197, 5, 114, 0, 0, 197, 198, 5, 114, 0, 0, 198, 199, 5, 97, 0, 0, 199, 200, 5, 121, 0, 0, 200, 201, 5, 67, 0, 0, 201, 202, 5, 111, 0, 0, 202, 203, 5, 110, 0, 0, 203, 204, 5, 116, 0, 0, 204, 205, 5, 97, 0, 0, 205, 206, 5, 105, 0, 0, 206, 207, 5, 110, 0, 0, 207, 208, 5, 115, 0, 0, 208, 36, 1, 0, 0, 0, 209, 210, 5, 65, 0, 0, 210, 211, 5, 114, 0, 0, 211, 212, 5, 114, 0, 0, 212, 213, 5, 97, 0, 0, 213, 214, 5, 121, 0, 0, 214, 215, 5, 82, 0, 0, 215, 216, 5, 97, 0, 0, 216, 217, 5, 110, 0, 0, 217, 218, 5, 103, 0, 0, 218, 219, 5, 101, 0, 0, 219, 38, 1, 0, 0, 0, 220, 221, 5, 65, 0, 0, 221, 222, 5, 114, 0, 0, 222, 223, 5, 114, 0, 0, 223, 224, 5, 97, 0, 0, 224, 225, 5, 121, 0, 0, 225, 226, 5, 71, 0, 0, 226, 227, 5, 101, 0, 0, 227, 228, 5, 116, 0, 0, 228, 229, 5, 73, 0, 0, 229, 230, 5, 116, 0, 0, 230, 231, 5, 101, 0, 0, 231, 232, 5, 109, 0, 0, 232, 40, 1, 0, 0, 0, 233, 234, 5, 65, 0, 0, 234, 235, 5, 114, 0, 0, 235, 236, 5, 114, 0, 0, 236, 237, 5, 97, 0, 0, 237, 238, 5, 121, 0, 0, 238, 239, 5, 76, 0, 0, 239, 240, 5, 101, 0, 0, 240, 241, 5, 110, 0, 0, 241, 242, 5, 103, 0, 0, 242, 243, 5, 116, 0, 0, 243, 244, 5, 104, 0, 0, 244, 42, 1, 0, 0, 0, 245, 246, 5, 65, 0, 0, 246, 247, 5, 114, 0, 0, 247, 248, 5, 114, 0, 0, 248, 249, 5, 97, 0, 0, 249, 250, 5, 121, 0, 0, 250, 251, 5, 85, 0, 0, 251, 252, 5, 110, 0, 0, 252, 253, 5, 105, 0, 0, 253, 254, 5, 113, 0, 0, 254, 255, 5, 117, 0, 0, 255, 256, 5, 101, 0, 0, 256, 44, 1, 0, 0, 0, 257, 258, 5, 66, 0, 0, 258, 259, 5, 97, 0, 0, 259, 260, 5, 115, 0, 0, 260, 261, 5, 101, 0, 0, 261, 262, 5, 54, 0, 0, 262, 263, 5, 52, 0, 0, 263, 264, 5, 69, 0, 0, 264, 265, 5, 110, 0, 0, 265, 266, 5, 99, 0, 0, 266, 267, 5, 111, 0, 0, 267, 268, 5, 100, 0, 0, 268, 269, 5, 101, 0, 0, 269, 46, 1, 0, 0, 0, 270, 271, 5, 66, 0, 0, 271, 272, 5, 97, 0, 0, 272, 273, 5, 115, 0, 0, 273, 274, 5, 101, 0, 0, 274, 275, 5, 54, 0, 0, 275, 276, 5, 52, 0, 0, 276, 277, 5, 68, 0, 0, 277, 278, 5, 101, 0, 0, 278, 279, 5, 99, 0, 0, 279, 280, 5, 111, 0, 0, 280, 281, 5, 100, 0, 0, 281, 282, 5, 101, 0, 0, 282, 48, 1, 0, 0, 0, 283, 284, 5, 72, 0, 0, 284, 285, 5, 97, 0, 0, 285, 286, 5, 115, 0, 0, 286, 287, 5, 104, 0, 0, 287, 50, 1, 0, 0, 0, 288, 289, 5, 74, 0, 0, 289, 290, 5, 115, 0, 0, 290, 291, 5, 111, 0, 0, 291, 292, 5, 110, 0, 0, 292, 293, 5, 77, 0, 0, 293, 294, 5, 101, 0, 0, 294, 295, 5, 114, 0, 0, 295, 296, 5, 103, 0, 0, 296, 297, 5, 101, 0, 0, 297, 52, 1, 0, 0, 0, 298, 299, 5, 77, 0, 0, 299, 300, 5, 97, 0, 0, 300, 301, 5, 116, 0, 0, 301, 302, 5, 104, 0, 0, 302, 303, 5, 82, 0, 0, 303, 304, 5, 97, 0, 0, 304, 305, 5, 110, 0, 0, 305, 306, 5, 100, 0, 0, 306, 307, 5, 111, 0, 0, 307, 308, 5, 109, 0, 0, 308, 54, 1, 0, 0, 0, 309, 310, 5, 77, 0, 0, 310, 311, 5, 97, 0, 0, 311, 312, 5, 116, 0, 0, 312, 313, 5, 104, 0, 0, 313, 314, 5, 65, 0, 0, 314, 315, 5, 100, 0, 0, 315, 316, 5, 100, 0, 0, 316, 56, 1, 0, 0, 0, 317, 318, 5, 83, 0, 0, 318, 319, 5, 116, 0, 0, 319, 320, 5, 114, 0, 0, 320, 321, 5, 105, 0, 0, 321, 322, 5, 110, 0, 0, 322, 323, 5, 103, 0, 0, 323, 324, 5, 83, 0, 0, 324, 325, 5, 112, 0, 0, 325, 326, 5, 108, 0, 0, 326, 327, 5, 105, 0, 0, 327, 328, 5, 116, 0, 0, 328, 58, 1, 0, 0, 0, 329, 330, 5, 85, 0, 0, 330, 331, 5, 85, 0, 0, 331, 332, 5, 73, 0, 0, 332, 333, 5, 68, 0, 0, 333, 60, 1, 0, 0, 0, 334, 339, 5, 39, 0, 0, 335, 338, 3, 63, 31, 0, 336, 338, 3, 69, 34, 0, 337, 335, 1, 0, 0, 0, 337, 336, 1, 0, 0, 0, 338, 341, 1, 0, 0, 0, 339, 340, 1, 0, 0, 0, 339, 337, 1, 0, 0, 0, 340, 342, 1, 0, 0, 0, 341, 339, 1, 0, 0, 0, 342, 343, 5, 39, 0, 0, 343, 62, 1, 0, 0, 0, 344, 347, 5, 92, 0, 0, 345, 348, 3, 65, 32, 0, 346, 348, 9, 0, 0, 0, 347, 345, 1, 0, 0, 0, 347, 346, 1, 0, 0, 0, 348, 64, 1, 0, 0, 0, 349, 350, 5, 117, 0, 0, 350, 351, 3, 67, 33, 0, 351, 352, 3, 67, 33, 0, 352, 353, 3, 67, 33, 0, 353, 354, 3, 67, 33, 0, 354, 66, 1, 0, 0, 0, 355, 356, 7, 1, 0, 0, 356, 68, 1, 0, 0, 0, 357, 358, 8, 2, 0, 0, 358, 70, 1, 0, 0, 0, 359, 361, 5, 45, 0, 0, 360, 359, 1, 0, 0, 0, 360, 361, 1, 0, 0, 0, 361, 370, 1, 0, 0, 0, 362, 371, 5, 48, 0, 0, 363, 367, 7, 3, 0, 0, 364, 366, 7, 4, 0, 0, 365, 364, 1, 0, 0, 0, 366, 369, 1, 0, 0, 0, 367, 365, 1, 0, 0, 0, 367, 368, 1, 0, 0, 0, 368, 371, 1, 0, 0, 0, 369, 367, 1, 0, 0, 0, 370, 362, 1, 0, 0, 0, 370, 363, 1, 0, 0, 0, 371, 72, 1, 0, 0, 0, 372, 374, 5, 45, 0, 0, 373, 372, 1, 0, 0, 0, 373, 374, 1, 0, 0, 0, 374, 375, 1, 0, 0, 0, 375, 382, 3, 71, 35, 0, 376, 378, 5, 46, 0, 0, 377, 379, 7, 4, 0, 0, 378, 377, 1, 0, 0, 0, 379, 380, 1, 0, 0, 0, 380, 378, 1, 0, 0, 0, 380, 381, 1, 0, 0, 0, 381, 383, 1, 0, 0, 0, 382, 376, 1, 0, 0, 0, 382, 383, 1, 0, 0, 0, 383, 385, 1, 0, 0, 0, 384, 386, 3, 75, 37, 0, 385, 384, 1, 0, 0, 0, 385, 386, 1, 0, 0, 0, 386, 74, 1, 0, 0, 0, 387, 389, 7, 5, 0, 0, 388, 390, 7, 6, 0, 0, 389, 388, 1, 0, 0, 0, 389, 390, 1, 0, 0, 0, 390, 391, 1, 0, 0, 0, 391, 392, 3, 71, 35, 0, 392, 76, 1, 0, 0, 0, 393, 396, 7, 7, 0, 0, 394, 396, 3, 65, 32, 0, 395, 393, 1, 0, 0, 0, 395, 394, 1, 0, 0, 0, 396, 397, 1, 0, 0, 0, 397, 395, 1, 0, 0, 0, 397, 398, 1, 0, 0, 0, 398, 78, 1, 0, 0, 0, 399, 401, 7, 8, 0, 0, 400, 399, 1, 0, 0, 0, 401, 402, 1, 0, 0, 0, 402, 400, 1, 0, 0, 0, 402, 403, 1, 0, 0, 0, 403, 404, 1, 0, 0, 0, 404, 405, 6, 39, 0, 0, 405, 80, 1, 0, 0, 0, 21, 0, 89, 93, 96, 100, 106, 108, 337, 339, 347, 360, 367, 370, 373, 380, 382, 385, 389, 395, 397, 402, 1, 6, 0, 0] \ No newline at end of file diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicLexer.py b/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicLexer.py index 5366d85444056..2100b417d9913 100644 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicLexer.py +++ b/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicLexer.py @@ -1,4 +1,4 @@ -# Generated from /Users/mep/LocalStack/localstack/localstack/services/stepfunctions/asl/antlr/ASLIntrinsicLexer.g4 by ANTLR 4.13.1 +# Generated from ASLIntrinsicLexer.g4 by ANTLR 4.13.1 from antlr4 import * from io import StringIO import sys diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicLexer.tokens b/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicLexer.tokens deleted file mode 100644 index 0d9bb4b665cdf..0000000000000 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicLexer.tokens +++ /dev/null @@ -1,59 +0,0 @@ -CONTEXT_PATH_STRING=1 -JSON_PATH_STRING=2 -DOLLAR=3 -LPAREN=4 -RPAREN=5 -COMMA=6 -DOT=7 -TRUE=8 -FALSE=9 -States=10 -Format=11 -StringToJson=12 -JsonToString=13 -Array=14 -ArrayPartition=15 -ArrayContains=16 -ArrayRange=17 -ArrayGetItem=18 -ArrayLength=19 -ArrayUnique=20 -Base64Encode=21 -Base64Decode=22 -Hash=23 -JsonMerge=24 -MathRandom=25 -MathAdd=26 -StringSplit=27 -UUID=28 -STRING=29 -INT=30 -NUMBER=31 -IDENTIFIER=32 -WS=33 -'$'=3 -'('=4 -')'=5 -','=6 -'.'=7 -'true'=8 -'false'=9 -'States'=10 -'Format'=11 -'StringToJson'=12 -'JsonToString'=13 -'Array'=14 -'ArrayPartition'=15 -'ArrayContains'=16 -'ArrayRange'=17 -'ArrayGetItem'=18 -'ArrayLength'=19 -'ArrayUnique'=20 -'Base64Encode'=21 -'Base64Decode'=22 -'Hash'=23 -'JsonMerge'=24 -'MathRandom'=25 -'MathAdd'=26 -'StringSplit'=27 -'UUID'=28 diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParser.interp b/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParser.interp deleted file mode 100644 index 148ca7b7445a2..0000000000000 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParser.interp +++ /dev/null @@ -1,82 +0,0 @@ -token literal names: -null -null -null -'$' -'(' -')' -',' -'.' -'true' -'false' -'States' -'Format' -'StringToJson' -'JsonToString' -'Array' -'ArrayPartition' -'ArrayContains' -'ArrayRange' -'ArrayGetItem' -'ArrayLength' -'ArrayUnique' -'Base64Encode' -'Base64Decode' -'Hash' -'JsonMerge' -'MathRandom' -'MathAdd' -'StringSplit' -'UUID' -null -null -null -null -null - -token symbolic names: -null -CONTEXT_PATH_STRING -JSON_PATH_STRING -DOLLAR -LPAREN -RPAREN -COMMA -DOT -TRUE -FALSE -States -Format -StringToJson -JsonToString -Array -ArrayPartition -ArrayContains -ArrayRange -ArrayGetItem -ArrayLength -ArrayUnique -Base64Encode -Base64Decode -Hash -JsonMerge -MathRandom -MathAdd -StringSplit -UUID -STRING -INT -NUMBER -IDENTIFIER -WS - -rule names: -func_decl -states_func_decl -state_fun_name -func_arg_list -func_arg - - -atn: -[4, 1, 33, 45, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 3, 5, 3, 25, 8, 3, 10, 3, 12, 3, 28, 9, 3, 1, 3, 1, 3, 1, 3, 1, 3, 3, 3, 34, 8, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 1, 4, 3, 4, 43, 8, 4, 1, 4, 0, 0, 5, 0, 2, 4, 6, 8, 0, 2, 1, 0, 11, 28, 1, 0, 8, 9, 47, 0, 10, 1, 0, 0, 0, 2, 13, 1, 0, 0, 0, 4, 18, 1, 0, 0, 0, 6, 33, 1, 0, 0, 0, 8, 42, 1, 0, 0, 0, 10, 11, 3, 2, 1, 0, 11, 12, 5, 0, 0, 1, 12, 1, 1, 0, 0, 0, 13, 14, 5, 10, 0, 0, 14, 15, 5, 7, 0, 0, 15, 16, 3, 4, 2, 0, 16, 17, 3, 6, 3, 0, 17, 3, 1, 0, 0, 0, 18, 19, 7, 0, 0, 0, 19, 5, 1, 0, 0, 0, 20, 21, 5, 4, 0, 0, 21, 26, 3, 8, 4, 0, 22, 23, 5, 6, 0, 0, 23, 25, 3, 8, 4, 0, 24, 22, 1, 0, 0, 0, 25, 28, 1, 0, 0, 0, 26, 24, 1, 0, 0, 0, 26, 27, 1, 0, 0, 0, 27, 29, 1, 0, 0, 0, 28, 26, 1, 0, 0, 0, 29, 30, 5, 5, 0, 0, 30, 34, 1, 0, 0, 0, 31, 32, 5, 4, 0, 0, 32, 34, 5, 5, 0, 0, 33, 20, 1, 0, 0, 0, 33, 31, 1, 0, 0, 0, 34, 7, 1, 0, 0, 0, 35, 43, 5, 29, 0, 0, 36, 43, 5, 30, 0, 0, 37, 43, 5, 31, 0, 0, 38, 43, 7, 1, 0, 0, 39, 43, 5, 1, 0, 0, 40, 43, 5, 2, 0, 0, 41, 43, 3, 2, 1, 0, 42, 35, 1, 0, 0, 0, 42, 36, 1, 0, 0, 0, 42, 37, 1, 0, 0, 0, 42, 38, 1, 0, 0, 0, 42, 39, 1, 0, 0, 0, 42, 40, 1, 0, 0, 0, 42, 41, 1, 0, 0, 0, 43, 9, 1, 0, 0, 0, 3, 26, 33, 42] \ No newline at end of file diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParser.py b/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParser.py index a2e2b7b658efd..a6ec563c97d83 100644 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParser.py +++ b/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParser.py @@ -1,4 +1,4 @@ -# Generated from /Users/mep/LocalStack/localstack/localstack/services/stepfunctions/asl/antlr/ASLIntrinsicParser.g4 by ANTLR 4.13.1 +# Generated from ASLIntrinsicParser.g4 by ANTLR 4.13.1 # encoding: utf-8 from antlr4 import * from io import StringIO diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParser.tokens b/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParser.tokens deleted file mode 100644 index 0d9bb4b665cdf..0000000000000 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParser.tokens +++ /dev/null @@ -1,59 +0,0 @@ -CONTEXT_PATH_STRING=1 -JSON_PATH_STRING=2 -DOLLAR=3 -LPAREN=4 -RPAREN=5 -COMMA=6 -DOT=7 -TRUE=8 -FALSE=9 -States=10 -Format=11 -StringToJson=12 -JsonToString=13 -Array=14 -ArrayPartition=15 -ArrayContains=16 -ArrayRange=17 -ArrayGetItem=18 -ArrayLength=19 -ArrayUnique=20 -Base64Encode=21 -Base64Decode=22 -Hash=23 -JsonMerge=24 -MathRandom=25 -MathAdd=26 -StringSplit=27 -UUID=28 -STRING=29 -INT=30 -NUMBER=31 -IDENTIFIER=32 -WS=33 -'$'=3 -'('=4 -')'=5 -','=6 -'.'=7 -'true'=8 -'false'=9 -'States'=10 -'Format'=11 -'StringToJson'=12 -'JsonToString'=13 -'Array'=14 -'ArrayPartition'=15 -'ArrayContains'=16 -'ArrayRange'=17 -'ArrayGetItem'=18 -'ArrayLength'=19 -'ArrayUnique'=20 -'Base64Encode'=21 -'Base64Decode'=22 -'Hash'=23 -'JsonMerge'=24 -'MathRandom'=25 -'MathAdd'=26 -'StringSplit'=27 -'UUID'=28 diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParserListener.py b/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParserListener.py index ccff20dad0013..5bf7e03b34344 100644 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParserListener.py +++ b/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParserListener.py @@ -1,4 +1,4 @@ -# Generated from /Users/mep/LocalStack/localstack/localstack/services/stepfunctions/asl/antlr/ASLIntrinsicParser.g4 by ANTLR 4.13.1 +# Generated from ASLIntrinsicParser.g4 by ANTLR 4.13.1 from antlr4 import * if "." in __name__: from .ASLIntrinsicParser import ASLIntrinsicParser diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParserVisitor.py b/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParserVisitor.py index 25a7911cc56f4..ecde4f461c525 100644 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParserVisitor.py +++ b/localstack/services/stepfunctions/asl/antlr/runtime/ASLIntrinsicParserVisitor.py @@ -1,4 +1,4 @@ -# Generated from /Users/mep/LocalStack/localstack/localstack/services/stepfunctions/asl/antlr/ASLIntrinsicParser.g4 by ANTLR 4.13.1 +# Generated from ASLIntrinsicParser.g4 by ANTLR 4.13.1 from antlr4 import * if "." in __name__: from .ASLIntrinsicParser import ASLIntrinsicParser diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLLexer.interp b/localstack/services/stepfunctions/asl/antlr/runtime/ASLLexer.interp deleted file mode 100644 index 0e9c6668f13ef..0000000000000 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLLexer.interp +++ /dev/null @@ -1,442 +0,0 @@ -token literal names: -null -',' -':' -'[' -']' -'{' -'}' -'true' -'false' -'null' -'"Comment"' -'"States"' -'"StartAt"' -'"NextState"' -'"Version"' -'"Type"' -'"Task"' -'"Choice"' -'"Fail"' -'"Succeed"' -'"Pass"' -'"Wait"' -'"Parallel"' -'"Map"' -'"Choices"' -'"Variable"' -'"Default"' -'"Branches"' -'"And"' -'"BooleanEquals"' -'"BooleanEqualsPath"' -'"IsBoolean"' -'"IsNull"' -'"IsNumeric"' -'"IsPresent"' -'"IsString"' -'"IsTimestamp"' -'"Not"' -'"NumericEquals"' -'"NumericEqualsPath"' -'"NumericGreaterThan"' -'"NumericGreaterThanPath"' -'"NumericGreaterThanEquals"' -'"NumericGreaterThanEqualsPath"' -'"NumericLessThan"' -'"NumericLessThanPath"' -'"NumericLessThanEquals"' -'"NumericLessThanEqualsPath"' -'"Or"' -'"StringEquals"' -'"StringEqualsPath"' -'"StringGreaterThan"' -'"StringGreaterThanPath"' -'"StringGreaterThanEquals"' -'"StringGreaterThanEqualsPath"' -'"StringLessThan"' -'"StringLessThanPath"' -'"StringLessThanEquals"' -'"StringLessThanEqualsPath"' -'"StringMatches"' -'"TimestampEquals"' -'"TimestampEqualsPath"' -'"TimestampGreaterThan"' -'"TimestampGreaterThanPath"' -'"TimestampGreaterThanEquals"' -'"TimestampGreaterThanEqualsPath"' -'"TimestampLessThan"' -'"TimestampLessThanPath"' -'"TimestampLessThanEquals"' -'"TimestampLessThanEqualsPath"' -'"SecondsPath"' -'"Seconds"' -'"TimestampPath"' -'"Timestamp"' -'"TimeoutSeconds"' -'"TimeoutSecondsPath"' -'"HeartbeatSeconds"' -'"HeartbeatSecondsPath"' -'"ProcessorConfig"' -'"Mode"' -'"INLINE"' -'"DISTRIBUTED"' -'"ExecutionType"' -'"STANDARD"' -'"ItemProcessor"' -'"Iterator"' -'"ItemSelector"' -'"MaxConcurrency"' -'"Resource"' -'"InputPath"' -'"OutputPath"' -'"ItemsPath"' -'"ResultPath"' -'"Result"' -'"Parameters"' -'"ResultSelector"' -'"ItemReader"' -'"ReaderConfig"' -'"InputType"' -'"CSVHeaderLocation"' -'"CSVHeaders"' -'"MaxItems"' -'"MaxItemsPath"' -'"Next"' -'"End"' -'"Cause"' -'"CausePath"' -'"Error"' -'"ErrorPath"' -'"Retry"' -'"ErrorEquals"' -'"IntervalSeconds"' -'"MaxAttempts"' -'"BackoffRate"' -'"MaxDelaySeconds"' -'"JitterStrategy"' -'"FULL"' -'"NONE"' -'"Catch"' -'"States.ALL"' -'"States.DataLimitExceeded"' -'"States.HeartbeatTimeout"' -'"States.Timeout"' -'"States.TaskFailed"' -'"States.Permissions"' -'"States.ResultPathMatchFailure"' -'"States.ParameterPathFailure"' -'"States.BranchFailed"' -'"States.NoChoiceMatched"' -'"States.IntrinsicFailure"' -'"States.ExceedToleratedFailureThreshold"' -'"States.ItemReaderFailed"' -'"States.ResultWriterFailed"' -'"States.Runtime"' -null -null -null -null -null -null -null - -token symbolic names: -null -COMMA -COLON -LBRACK -RBRACK -LBRACE -RBRACE -TRUE -FALSE -NULL -COMMENT -STATES -STARTAT -NEXTSTATE -VERSION -TYPE -TASK -CHOICE -FAIL -SUCCEED -PASS -WAIT -PARALLEL -MAP -CHOICES -VARIABLE -DEFAULT -BRANCHES -AND -BOOLEANEQUALS -BOOLEANQUALSPATH -ISBOOLEAN -ISNULL -ISNUMERIC -ISPRESENT -ISSTRING -ISTIMESTAMP -NOT -NUMERICEQUALS -NUMERICEQUALSPATH -NUMERICGREATERTHAN -NUMERICGREATERTHANPATH -NUMERICGREATERTHANEQUALS -NUMERICGREATERTHANEQUALSPATH -NUMERICLESSTHAN -NUMERICLESSTHANPATH -NUMERICLESSTHANEQUALS -NUMERICLESSTHANEQUALSPATH -OR -STRINGEQUALS -STRINGEQUALSPATH -STRINGGREATERTHAN -STRINGGREATERTHANPATH -STRINGGREATERTHANEQUALS -STRINGGREATERTHANEQUALSPATH -STRINGLESSTHAN -STRINGLESSTHANPATH -STRINGLESSTHANEQUALS -STRINGLESSTHANEQUALSPATH -STRINGMATCHES -TIMESTAMPEQUALS -TIMESTAMPEQUALSPATH -TIMESTAMPGREATERTHAN -TIMESTAMPGREATERTHANPATH -TIMESTAMPGREATERTHANEQUALS -TIMESTAMPGREATERTHANEQUALSPATH -TIMESTAMPLESSTHAN -TIMESTAMPLESSTHANPATH -TIMESTAMPLESSTHANEQUALS -TIMESTAMPLESSTHANEQUALSPATH -SECONDSPATH -SECONDS -TIMESTAMPPATH -TIMESTAMP -TIMEOUTSECONDS -TIMEOUTSECONDSPATH -HEARTBEATSECONDS -HEARTBEATSECONDSPATH -PROCESSORCONFIG -MODE -INLINE -DISTRIBUTED -EXECUTIONTYPE -STANDARD -ITEMPROCESSOR -ITERATOR -ITEMSELECTOR -MAXCONCURRENCY -RESOURCE -INPUTPATH -OUTPUTPATH -ITEMSPATH -RESULTPATH -RESULT -PARAMETERS -RESULTSELECTOR -ITEMREADER -READERCONFIG -INPUTTYPE -CSVHEADERLOCATION -CSVHEADERS -MAXITEMS -MAXITEMSPATH -NEXT -END -CAUSE -CAUSEPATH -ERROR -ERRORPATH -RETRY -ERROREQUALS -INTERVALSECONDS -MAXATTEMPTS -BACKOFFRATE -MAXDELAYSECONDS -JITTERSTRATEGY -FULL -NONE -CATCH -ERRORNAMEStatesALL -ERRORNAMEStatesDataLimitExceeded -ERRORNAMEStatesHeartbeatTimeout -ERRORNAMEStatesTimeout -ERRORNAMEStatesTaskFailed -ERRORNAMEStatesPermissions -ERRORNAMEStatesResultPathMatchFailure -ERRORNAMEStatesParameterPathFailure -ERRORNAMEStatesBranchFailed -ERRORNAMEStatesNoChoiceMatched -ERRORNAMEStatesIntrinsicFailure -ERRORNAMEStatesExceedToleratedFailureThreshold -ERRORNAMEStatesItemReaderFailed -ERRORNAMEStatesResultWriterFailed -ERRORNAMEStatesRuntime -STRINGDOLLAR -STRINGPATHCONTEXTOBJ -STRINGPATH -STRING -INT -NUMBER -WS - -rule names: -COMMA -COLON -LBRACK -RBRACK -LBRACE -RBRACE -TRUE -FALSE -NULL -COMMENT -STATES -STARTAT -NEXTSTATE -VERSION -TYPE -TASK -CHOICE -FAIL -SUCCEED -PASS -WAIT -PARALLEL -MAP -CHOICES -VARIABLE -DEFAULT -BRANCHES -AND -BOOLEANEQUALS -BOOLEANQUALSPATH -ISBOOLEAN -ISNULL -ISNUMERIC -ISPRESENT -ISSTRING -ISTIMESTAMP -NOT -NUMERICEQUALS -NUMERICEQUALSPATH -NUMERICGREATERTHAN -NUMERICGREATERTHANPATH -NUMERICGREATERTHANEQUALS -NUMERICGREATERTHANEQUALSPATH -NUMERICLESSTHAN -NUMERICLESSTHANPATH -NUMERICLESSTHANEQUALS -NUMERICLESSTHANEQUALSPATH -OR -STRINGEQUALS -STRINGEQUALSPATH -STRINGGREATERTHAN -STRINGGREATERTHANPATH -STRINGGREATERTHANEQUALS -STRINGGREATERTHANEQUALSPATH -STRINGLESSTHAN -STRINGLESSTHANPATH -STRINGLESSTHANEQUALS -STRINGLESSTHANEQUALSPATH -STRINGMATCHES -TIMESTAMPEQUALS -TIMESTAMPEQUALSPATH -TIMESTAMPGREATERTHAN -TIMESTAMPGREATERTHANPATH -TIMESTAMPGREATERTHANEQUALS -TIMESTAMPGREATERTHANEQUALSPATH -TIMESTAMPLESSTHAN -TIMESTAMPLESSTHANPATH -TIMESTAMPLESSTHANEQUALS -TIMESTAMPLESSTHANEQUALSPATH -SECONDSPATH -SECONDS -TIMESTAMPPATH -TIMESTAMP -TIMEOUTSECONDS -TIMEOUTSECONDSPATH -HEARTBEATSECONDS -HEARTBEATSECONDSPATH -PROCESSORCONFIG -MODE -INLINE -DISTRIBUTED -EXECUTIONTYPE -STANDARD -ITEMPROCESSOR -ITERATOR -ITEMSELECTOR -MAXCONCURRENCY -RESOURCE -INPUTPATH -OUTPUTPATH -ITEMSPATH -RESULTPATH -RESULT -PARAMETERS -RESULTSELECTOR -ITEMREADER -READERCONFIG -INPUTTYPE -CSVHEADERLOCATION -CSVHEADERS -MAXITEMS -MAXITEMSPATH -NEXT -END -CAUSE -CAUSEPATH -ERROR -ERRORPATH -RETRY -ERROREQUALS -INTERVALSECONDS -MAXATTEMPTS -BACKOFFRATE -MAXDELAYSECONDS -JITTERSTRATEGY -FULL -NONE -CATCH -ERRORNAMEStatesALL -ERRORNAMEStatesDataLimitExceeded -ERRORNAMEStatesHeartbeatTimeout -ERRORNAMEStatesTimeout -ERRORNAMEStatesTaskFailed -ERRORNAMEStatesPermissions -ERRORNAMEStatesResultPathMatchFailure -ERRORNAMEStatesParameterPathFailure -ERRORNAMEStatesBranchFailed -ERRORNAMEStatesNoChoiceMatched -ERRORNAMEStatesIntrinsicFailure -ERRORNAMEStatesExceedToleratedFailureThreshold -ERRORNAMEStatesItemReaderFailed -ERRORNAMEStatesResultWriterFailed -ERRORNAMEStatesRuntime -STRINGDOLLAR -STRINGPATHCONTEXTOBJ -STRINGPATH -STRING -ESC -UNICODE -HEX -SAFECODEPOINT -INT -NUMBER -EXP -WS - -channel names: -DEFAULT_TOKEN_CHANNEL -HIDDEN - -mode names: -DEFAULT_MODE - -atn: -[4, 0, 140, 2442, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 2, 83, 7, 83, 2, 84, 7, 84, 2, 85, 7, 85, 2, 86, 7, 86, 2, 87, 7, 87, 2, 88, 7, 88, 2, 89, 7, 89, 2, 90, 7, 90, 2, 91, 7, 91, 2, 92, 7, 92, 2, 93, 7, 93, 2, 94, 7, 94, 2, 95, 7, 95, 2, 96, 7, 96, 2, 97, 7, 97, 2, 98, 7, 98, 2, 99, 7, 99, 2, 100, 7, 100, 2, 101, 7, 101, 2, 102, 7, 102, 2, 103, 7, 103, 2, 104, 7, 104, 2, 105, 7, 105, 2, 106, 7, 106, 2, 107, 7, 107, 2, 108, 7, 108, 2, 109, 7, 109, 2, 110, 7, 110, 2, 111, 7, 111, 2, 112, 7, 112, 2, 113, 7, 113, 2, 114, 7, 114, 2, 115, 7, 115, 2, 116, 7, 116, 2, 117, 7, 117, 2, 118, 7, 118, 2, 119, 7, 119, 2, 120, 7, 120, 2, 121, 7, 121, 2, 122, 7, 122, 2, 123, 7, 123, 2, 124, 7, 124, 2, 125, 7, 125, 2, 126, 7, 126, 2, 127, 7, 127, 2, 128, 7, 128, 2, 129, 7, 129, 2, 130, 7, 130, 2, 131, 7, 131, 2, 132, 7, 132, 2, 133, 7, 133, 2, 134, 7, 134, 2, 135, 7, 135, 2, 136, 7, 136, 2, 137, 7, 137, 2, 138, 7, 138, 2, 139, 7, 139, 2, 140, 7, 140, 2, 141, 7, 141, 2, 142, 7, 142, 2, 143, 7, 143, 2, 144, 7, 144, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 4, 1, 4, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 27, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 31, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 49, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 54, 1, 54, 1, 54, 1, 54, 1, 54, 1, 54, 1, 54, 1, 54, 1, 54, 1, 54, 1, 54, 1, 54, 1, 54, 1, 54, 1, 54, 1, 54, 1, 54, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 55, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 57, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 58, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 61, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 63, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 66, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 69, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 70, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 72, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 75, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 76, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 77, 1, 78, 1, 78, 1, 78, 1, 78, 1, 78, 1, 78, 1, 78, 1, 79, 1, 79, 1, 79, 1, 79, 1, 79, 1, 79, 1, 79, 1, 79, 1, 79, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 81, 1, 81, 1, 81, 1, 81, 1, 81, 1, 81, 1, 81, 1, 81, 1, 81, 1, 81, 1, 81, 1, 81, 1, 81, 1, 81, 1, 81, 1, 81, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 1, 82, 1, 83, 1, 83, 1, 83, 1, 83, 1, 83, 1, 83, 1, 83, 1, 83, 1, 83, 1, 83, 1, 83, 1, 83, 1, 83, 1, 83, 1, 83, 1, 83, 1, 84, 1, 84, 1, 84, 1, 84, 1, 84, 1, 84, 1, 84, 1, 84, 1, 84, 1, 84, 1, 84, 1, 85, 1, 85, 1, 85, 1, 85, 1, 85, 1, 85, 1, 85, 1, 85, 1, 85, 1, 85, 1, 85, 1, 85, 1, 85, 1, 85, 1, 85, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 86, 1, 87, 1, 87, 1, 87, 1, 87, 1, 87, 1, 87, 1, 87, 1, 87, 1, 87, 1, 87, 1, 87, 1, 88, 1, 88, 1, 88, 1, 88, 1, 88, 1, 88, 1, 88, 1, 88, 1, 88, 1, 88, 1, 88, 1, 88, 1, 89, 1, 89, 1, 89, 1, 89, 1, 89, 1, 89, 1, 89, 1, 89, 1, 89, 1, 89, 1, 89, 1, 89, 1, 89, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 1, 91, 1, 91, 1, 91, 1, 91, 1, 91, 1, 91, 1, 91, 1, 91, 1, 91, 1, 91, 1, 91, 1, 91, 1, 91, 1, 92, 1, 92, 1, 92, 1, 92, 1, 92, 1, 92, 1, 92, 1, 92, 1, 92, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 93, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 94, 1, 95, 1, 95, 1, 95, 1, 95, 1, 95, 1, 95, 1, 95, 1, 95, 1, 95, 1, 95, 1, 95, 1, 95, 1, 95, 1, 96, 1, 96, 1, 96, 1, 96, 1, 96, 1, 96, 1, 96, 1, 96, 1, 96, 1, 96, 1, 96, 1, 96, 1, 96, 1, 96, 1, 96, 1, 97, 1, 97, 1, 97, 1, 97, 1, 97, 1, 97, 1, 97, 1, 97, 1, 97, 1, 97, 1, 97, 1, 97, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 98, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 99, 1, 100, 1, 100, 1, 100, 1, 100, 1, 100, 1, 100, 1, 100, 1, 100, 1, 100, 1, 100, 1, 100, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 101, 1, 102, 1, 102, 1, 102, 1, 102, 1, 102, 1, 102, 1, 102, 1, 103, 1, 103, 1, 103, 1, 103, 1, 103, 1, 103, 1, 104, 1, 104, 1, 104, 1, 104, 1, 104, 1, 104, 1, 104, 1, 104, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 105, 1, 106, 1, 106, 1, 106, 1, 106, 1, 106, 1, 106, 1, 106, 1, 106, 1, 107, 1, 107, 1, 107, 1, 107, 1, 107, 1, 107, 1, 107, 1, 107, 1, 107, 1, 107, 1, 107, 1, 107, 1, 108, 1, 108, 1, 108, 1, 108, 1, 108, 1, 108, 1, 108, 1, 108, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 109, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 110, 1, 111, 1, 111, 1, 111, 1, 111, 1, 111, 1, 111, 1, 111, 1, 111, 1, 111, 1, 111, 1, 111, 1, 111, 1, 111, 1, 111, 1, 112, 1, 112, 1, 112, 1, 112, 1, 112, 1, 112, 1, 112, 1, 112, 1, 112, 1, 112, 1, 112, 1, 112, 1, 112, 1, 112, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 113, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 114, 1, 115, 1, 115, 1, 115, 1, 115, 1, 115, 1, 115, 1, 115, 1, 116, 1, 116, 1, 116, 1, 116, 1, 116, 1, 116, 1, 116, 1, 117, 1, 117, 1, 117, 1, 117, 1, 117, 1, 117, 1, 117, 1, 117, 1, 118, 1, 118, 1, 118, 1, 118, 1, 118, 1, 118, 1, 118, 1, 118, 1, 118, 1, 118, 1, 118, 1, 118, 1, 118, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 119, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 120, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 121, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 122, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 123, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 124, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 125, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 126, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 127, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 128, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 129, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 130, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 131, 1, 132, 1, 132, 1, 132, 1, 132, 1, 132, 1, 132, 1, 132, 1, 132, 1, 132, 1, 132, 1, 132, 1, 132, 1, 132, 1, 132, 1, 132, 1, 132, 1, 132, 1, 133, 1, 133, 1, 133, 5, 133, 2346, 8, 133, 10, 133, 12, 133, 2349, 9, 133, 1, 133, 1, 133, 1, 133, 1, 133, 1, 134, 1, 134, 1, 134, 1, 134, 1, 134, 1, 134, 5, 134, 2361, 8, 134, 10, 134, 12, 134, 2364, 9, 134, 1, 134, 1, 134, 1, 135, 1, 135, 1, 135, 1, 135, 1, 135, 5, 135, 2373, 8, 135, 10, 135, 12, 135, 2376, 9, 135, 1, 135, 1, 135, 1, 136, 1, 136, 1, 136, 5, 136, 2383, 8, 136, 10, 136, 12, 136, 2386, 9, 136, 1, 136, 1, 136, 1, 137, 1, 137, 1, 137, 3, 137, 2393, 8, 137, 1, 138, 1, 138, 1, 138, 1, 138, 1, 138, 1, 138, 1, 139, 1, 139, 1, 140, 1, 140, 1, 141, 1, 141, 1, 141, 5, 141, 2408, 8, 141, 10, 141, 12, 141, 2411, 9, 141, 3, 141, 2413, 8, 141, 1, 142, 3, 142, 2416, 8, 142, 1, 142, 1, 142, 1, 142, 4, 142, 2421, 8, 142, 11, 142, 12, 142, 2422, 3, 142, 2425, 8, 142, 1, 142, 3, 142, 2428, 8, 142, 1, 143, 1, 143, 3, 143, 2432, 8, 143, 1, 143, 1, 143, 1, 144, 4, 144, 2437, 8, 144, 11, 144, 12, 144, 2438, 1, 144, 1, 144, 0, 0, 145, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 19, 10, 21, 11, 23, 12, 25, 13, 27, 14, 29, 15, 31, 16, 33, 17, 35, 18, 37, 19, 39, 20, 41, 21, 43, 22, 45, 23, 47, 24, 49, 25, 51, 26, 53, 27, 55, 28, 57, 29, 59, 30, 61, 31, 63, 32, 65, 33, 67, 34, 69, 35, 71, 36, 73, 37, 75, 38, 77, 39, 79, 40, 81, 41, 83, 42, 85, 43, 87, 44, 89, 45, 91, 46, 93, 47, 95, 48, 97, 49, 99, 50, 101, 51, 103, 52, 105, 53, 107, 54, 109, 55, 111, 56, 113, 57, 115, 58, 117, 59, 119, 60, 121, 61, 123, 62, 125, 63, 127, 64, 129, 65, 131, 66, 133, 67, 135, 68, 137, 69, 139, 70, 141, 71, 143, 72, 145, 73, 147, 74, 149, 75, 151, 76, 153, 77, 155, 78, 157, 79, 159, 80, 161, 81, 163, 82, 165, 83, 167, 84, 169, 85, 171, 86, 173, 87, 175, 88, 177, 89, 179, 90, 181, 91, 183, 92, 185, 93, 187, 94, 189, 95, 191, 96, 193, 97, 195, 98, 197, 99, 199, 100, 201, 101, 203, 102, 205, 103, 207, 104, 209, 105, 211, 106, 213, 107, 215, 108, 217, 109, 219, 110, 221, 111, 223, 112, 225, 113, 227, 114, 229, 115, 231, 116, 233, 117, 235, 118, 237, 119, 239, 120, 241, 121, 243, 122, 245, 123, 247, 124, 249, 125, 251, 126, 253, 127, 255, 128, 257, 129, 259, 130, 261, 131, 263, 132, 265, 133, 267, 134, 269, 135, 271, 136, 273, 137, 275, 0, 277, 0, 279, 0, 281, 0, 283, 138, 285, 139, 287, 0, 289, 140, 1, 0, 8, 8, 0, 34, 34, 47, 47, 92, 92, 98, 98, 102, 102, 110, 110, 114, 114, 116, 116, 3, 0, 48, 57, 65, 70, 97, 102, 3, 0, 0, 31, 34, 34, 92, 92, 1, 0, 49, 57, 1, 0, 48, 57, 2, 0, 69, 69, 101, 101, 2, 0, 43, 43, 45, 45, 3, 0, 9, 10, 13, 13, 32, 32, 2453, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 0, 19, 1, 0, 0, 0, 0, 21, 1, 0, 0, 0, 0, 23, 1, 0, 0, 0, 0, 25, 1, 0, 0, 0, 0, 27, 1, 0, 0, 0, 0, 29, 1, 0, 0, 0, 0, 31, 1, 0, 0, 0, 0, 33, 1, 0, 0, 0, 0, 35, 1, 0, 0, 0, 0, 37, 1, 0, 0, 0, 0, 39, 1, 0, 0, 0, 0, 41, 1, 0, 0, 0, 0, 43, 1, 0, 0, 0, 0, 45, 1, 0, 0, 0, 0, 47, 1, 0, 0, 0, 0, 49, 1, 0, 0, 0, 0, 51, 1, 0, 0, 0, 0, 53, 1, 0, 0, 0, 0, 55, 1, 0, 0, 0, 0, 57, 1, 0, 0, 0, 0, 59, 1, 0, 0, 0, 0, 61, 1, 0, 0, 0, 0, 63, 1, 0, 0, 0, 0, 65, 1, 0, 0, 0, 0, 67, 1, 0, 0, 0, 0, 69, 1, 0, 0, 0, 0, 71, 1, 0, 0, 0, 0, 73, 1, 0, 0, 0, 0, 75, 1, 0, 0, 0, 0, 77, 1, 0, 0, 0, 0, 79, 1, 0, 0, 0, 0, 81, 1, 0, 0, 0, 0, 83, 1, 0, 0, 0, 0, 85, 1, 0, 0, 0, 0, 87, 1, 0, 0, 0, 0, 89, 1, 0, 0, 0, 0, 91, 1, 0, 0, 0, 0, 93, 1, 0, 0, 0, 0, 95, 1, 0, 0, 0, 0, 97, 1, 0, 0, 0, 0, 99, 1, 0, 0, 0, 0, 101, 1, 0, 0, 0, 0, 103, 1, 0, 0, 0, 0, 105, 1, 0, 0, 0, 0, 107, 1, 0, 0, 0, 0, 109, 1, 0, 0, 0, 0, 111, 1, 0, 0, 0, 0, 113, 1, 0, 0, 0, 0, 115, 1, 0, 0, 0, 0, 117, 1, 0, 0, 0, 0, 119, 1, 0, 0, 0, 0, 121, 1, 0, 0, 0, 0, 123, 1, 0, 0, 0, 0, 125, 1, 0, 0, 0, 0, 127, 1, 0, 0, 0, 0, 129, 1, 0, 0, 0, 0, 131, 1, 0, 0, 0, 0, 133, 1, 0, 0, 0, 0, 135, 1, 0, 0, 0, 0, 137, 1, 0, 0, 0, 0, 139, 1, 0, 0, 0, 0, 141, 1, 0, 0, 0, 0, 143, 1, 0, 0, 0, 0, 145, 1, 0, 0, 0, 0, 147, 1, 0, 0, 0, 0, 149, 1, 0, 0, 0, 0, 151, 1, 0, 0, 0, 0, 153, 1, 0, 0, 0, 0, 155, 1, 0, 0, 0, 0, 157, 1, 0, 0, 0, 0, 159, 1, 0, 0, 0, 0, 161, 1, 0, 0, 0, 0, 163, 1, 0, 0, 0, 0, 165, 1, 0, 0, 0, 0, 167, 1, 0, 0, 0, 0, 169, 1, 0, 0, 0, 0, 171, 1, 0, 0, 0, 0, 173, 1, 0, 0, 0, 0, 175, 1, 0, 0, 0, 0, 177, 1, 0, 0, 0, 0, 179, 1, 0, 0, 0, 0, 181, 1, 0, 0, 0, 0, 183, 1, 0, 0, 0, 0, 185, 1, 0, 0, 0, 0, 187, 1, 0, 0, 0, 0, 189, 1, 0, 0, 0, 0, 191, 1, 0, 0, 0, 0, 193, 1, 0, 0, 0, 0, 195, 1, 0, 0, 0, 0, 197, 1, 0, 0, 0, 0, 199, 1, 0, 0, 0, 0, 201, 1, 0, 0, 0, 0, 203, 1, 0, 0, 0, 0, 205, 1, 0, 0, 0, 0, 207, 1, 0, 0, 0, 0, 209, 1, 0, 0, 0, 0, 211, 1, 0, 0, 0, 0, 213, 1, 0, 0, 0, 0, 215, 1, 0, 0, 0, 0, 217, 1, 0, 0, 0, 0, 219, 1, 0, 0, 0, 0, 221, 1, 0, 0, 0, 0, 223, 1, 0, 0, 0, 0, 225, 1, 0, 0, 0, 0, 227, 1, 0, 0, 0, 0, 229, 1, 0, 0, 0, 0, 231, 1, 0, 0, 0, 0, 233, 1, 0, 0, 0, 0, 235, 1, 0, 0, 0, 0, 237, 1, 0, 0, 0, 0, 239, 1, 0, 0, 0, 0, 241, 1, 0, 0, 0, 0, 243, 1, 0, 0, 0, 0, 245, 1, 0, 0, 0, 0, 247, 1, 0, 0, 0, 0, 249, 1, 0, 0, 0, 0, 251, 1, 0, 0, 0, 0, 253, 1, 0, 0, 0, 0, 255, 1, 0, 0, 0, 0, 257, 1, 0, 0, 0, 0, 259, 1, 0, 0, 0, 0, 261, 1, 0, 0, 0, 0, 263, 1, 0, 0, 0, 0, 265, 1, 0, 0, 0, 0, 267, 1, 0, 0, 0, 0, 269, 1, 0, 0, 0, 0, 271, 1, 0, 0, 0, 0, 273, 1, 0, 0, 0, 0, 283, 1, 0, 0, 0, 0, 285, 1, 0, 0, 0, 0, 289, 1, 0, 0, 0, 1, 291, 1, 0, 0, 0, 3, 293, 1, 0, 0, 0, 5, 295, 1, 0, 0, 0, 7, 297, 1, 0, 0, 0, 9, 299, 1, 0, 0, 0, 11, 301, 1, 0, 0, 0, 13, 303, 1, 0, 0, 0, 15, 308, 1, 0, 0, 0, 17, 314, 1, 0, 0, 0, 19, 319, 1, 0, 0, 0, 21, 329, 1, 0, 0, 0, 23, 338, 1, 0, 0, 0, 25, 348, 1, 0, 0, 0, 27, 360, 1, 0, 0, 0, 29, 370, 1, 0, 0, 0, 31, 377, 1, 0, 0, 0, 33, 384, 1, 0, 0, 0, 35, 393, 1, 0, 0, 0, 37, 400, 1, 0, 0, 0, 39, 410, 1, 0, 0, 0, 41, 417, 1, 0, 0, 0, 43, 424, 1, 0, 0, 0, 45, 435, 1, 0, 0, 0, 47, 441, 1, 0, 0, 0, 49, 451, 1, 0, 0, 0, 51, 462, 1, 0, 0, 0, 53, 472, 1, 0, 0, 0, 55, 483, 1, 0, 0, 0, 57, 489, 1, 0, 0, 0, 59, 505, 1, 0, 0, 0, 61, 525, 1, 0, 0, 0, 63, 537, 1, 0, 0, 0, 65, 546, 1, 0, 0, 0, 67, 558, 1, 0, 0, 0, 69, 570, 1, 0, 0, 0, 71, 581, 1, 0, 0, 0, 73, 595, 1, 0, 0, 0, 75, 601, 1, 0, 0, 0, 77, 617, 1, 0, 0, 0, 79, 637, 1, 0, 0, 0, 81, 658, 1, 0, 0, 0, 83, 683, 1, 0, 0, 0, 85, 710, 1, 0, 0, 0, 87, 741, 1, 0, 0, 0, 89, 759, 1, 0, 0, 0, 91, 781, 1, 0, 0, 0, 93, 805, 1, 0, 0, 0, 95, 833, 1, 0, 0, 0, 97, 838, 1, 0, 0, 0, 99, 853, 1, 0, 0, 0, 101, 872, 1, 0, 0, 0, 103, 892, 1, 0, 0, 0, 105, 916, 1, 0, 0, 0, 107, 942, 1, 0, 0, 0, 109, 972, 1, 0, 0, 0, 111, 989, 1, 0, 0, 0, 113, 1010, 1, 0, 0, 0, 115, 1033, 1, 0, 0, 0, 117, 1060, 1, 0, 0, 0, 119, 1076, 1, 0, 0, 0, 121, 1094, 1, 0, 0, 0, 123, 1116, 1, 0, 0, 0, 125, 1139, 1, 0, 0, 0, 127, 1166, 1, 0, 0, 0, 129, 1195, 1, 0, 0, 0, 131, 1228, 1, 0, 0, 0, 133, 1248, 1, 0, 0, 0, 135, 1272, 1, 0, 0, 0, 137, 1298, 1, 0, 0, 0, 139, 1328, 1, 0, 0, 0, 141, 1342, 1, 0, 0, 0, 143, 1352, 1, 0, 0, 0, 145, 1368, 1, 0, 0, 0, 147, 1380, 1, 0, 0, 0, 149, 1397, 1, 0, 0, 0, 151, 1418, 1, 0, 0, 0, 153, 1437, 1, 0, 0, 0, 155, 1460, 1, 0, 0, 0, 157, 1478, 1, 0, 0, 0, 159, 1485, 1, 0, 0, 0, 161, 1494, 1, 0, 0, 0, 163, 1508, 1, 0, 0, 0, 165, 1524, 1, 0, 0, 0, 167, 1535, 1, 0, 0, 0, 169, 1551, 1, 0, 0, 0, 171, 1562, 1, 0, 0, 0, 173, 1577, 1, 0, 0, 0, 175, 1594, 1, 0, 0, 0, 177, 1605, 1, 0, 0, 0, 179, 1617, 1, 0, 0, 0, 181, 1630, 1, 0, 0, 0, 183, 1642, 1, 0, 0, 0, 185, 1655, 1, 0, 0, 0, 187, 1664, 1, 0, 0, 0, 189, 1677, 1, 0, 0, 0, 191, 1694, 1, 0, 0, 0, 193, 1707, 1, 0, 0, 0, 195, 1722, 1, 0, 0, 0, 197, 1734, 1, 0, 0, 0, 199, 1754, 1, 0, 0, 0, 201, 1767, 1, 0, 0, 0, 203, 1778, 1, 0, 0, 0, 205, 1793, 1, 0, 0, 0, 207, 1800, 1, 0, 0, 0, 209, 1806, 1, 0, 0, 0, 211, 1814, 1, 0, 0, 0, 213, 1826, 1, 0, 0, 0, 215, 1834, 1, 0, 0, 0, 217, 1846, 1, 0, 0, 0, 219, 1854, 1, 0, 0, 0, 221, 1868, 1, 0, 0, 0, 223, 1886, 1, 0, 0, 0, 225, 1900, 1, 0, 0, 0, 227, 1914, 1, 0, 0, 0, 229, 1932, 1, 0, 0, 0, 231, 1949, 1, 0, 0, 0, 233, 1956, 1, 0, 0, 0, 235, 1963, 1, 0, 0, 0, 237, 1971, 1, 0, 0, 0, 239, 1984, 1, 0, 0, 0, 241, 2011, 1, 0, 0, 0, 243, 2037, 1, 0, 0, 0, 245, 2054, 1, 0, 0, 0, 247, 2074, 1, 0, 0, 0, 249, 2095, 1, 0, 0, 0, 251, 2127, 1, 0, 0, 0, 253, 2157, 1, 0, 0, 0, 255, 2179, 1, 0, 0, 0, 257, 2204, 1, 0, 0, 0, 259, 2230, 1, 0, 0, 0, 261, 2271, 1, 0, 0, 0, 263, 2297, 1, 0, 0, 0, 265, 2325, 1, 0, 0, 0, 267, 2342, 1, 0, 0, 0, 269, 2354, 1, 0, 0, 0, 271, 2367, 1, 0, 0, 0, 273, 2379, 1, 0, 0, 0, 275, 2389, 1, 0, 0, 0, 277, 2394, 1, 0, 0, 0, 279, 2400, 1, 0, 0, 0, 281, 2402, 1, 0, 0, 0, 283, 2412, 1, 0, 0, 0, 285, 2415, 1, 0, 0, 0, 287, 2429, 1, 0, 0, 0, 289, 2436, 1, 0, 0, 0, 291, 292, 5, 44, 0, 0, 292, 2, 1, 0, 0, 0, 293, 294, 5, 58, 0, 0, 294, 4, 1, 0, 0, 0, 295, 296, 5, 91, 0, 0, 296, 6, 1, 0, 0, 0, 297, 298, 5, 93, 0, 0, 298, 8, 1, 0, 0, 0, 299, 300, 5, 123, 0, 0, 300, 10, 1, 0, 0, 0, 301, 302, 5, 125, 0, 0, 302, 12, 1, 0, 0, 0, 303, 304, 5, 116, 0, 0, 304, 305, 5, 114, 0, 0, 305, 306, 5, 117, 0, 0, 306, 307, 5, 101, 0, 0, 307, 14, 1, 0, 0, 0, 308, 309, 5, 102, 0, 0, 309, 310, 5, 97, 0, 0, 310, 311, 5, 108, 0, 0, 311, 312, 5, 115, 0, 0, 312, 313, 5, 101, 0, 0, 313, 16, 1, 0, 0, 0, 314, 315, 5, 110, 0, 0, 315, 316, 5, 117, 0, 0, 316, 317, 5, 108, 0, 0, 317, 318, 5, 108, 0, 0, 318, 18, 1, 0, 0, 0, 319, 320, 5, 34, 0, 0, 320, 321, 5, 67, 0, 0, 321, 322, 5, 111, 0, 0, 322, 323, 5, 109, 0, 0, 323, 324, 5, 109, 0, 0, 324, 325, 5, 101, 0, 0, 325, 326, 5, 110, 0, 0, 326, 327, 5, 116, 0, 0, 327, 328, 5, 34, 0, 0, 328, 20, 1, 0, 0, 0, 329, 330, 5, 34, 0, 0, 330, 331, 5, 83, 0, 0, 331, 332, 5, 116, 0, 0, 332, 333, 5, 97, 0, 0, 333, 334, 5, 116, 0, 0, 334, 335, 5, 101, 0, 0, 335, 336, 5, 115, 0, 0, 336, 337, 5, 34, 0, 0, 337, 22, 1, 0, 0, 0, 338, 339, 5, 34, 0, 0, 339, 340, 5, 83, 0, 0, 340, 341, 5, 116, 0, 0, 341, 342, 5, 97, 0, 0, 342, 343, 5, 114, 0, 0, 343, 344, 5, 116, 0, 0, 344, 345, 5, 65, 0, 0, 345, 346, 5, 116, 0, 0, 346, 347, 5, 34, 0, 0, 347, 24, 1, 0, 0, 0, 348, 349, 5, 34, 0, 0, 349, 350, 5, 78, 0, 0, 350, 351, 5, 101, 0, 0, 351, 352, 5, 120, 0, 0, 352, 353, 5, 116, 0, 0, 353, 354, 5, 83, 0, 0, 354, 355, 5, 116, 0, 0, 355, 356, 5, 97, 0, 0, 356, 357, 5, 116, 0, 0, 357, 358, 5, 101, 0, 0, 358, 359, 5, 34, 0, 0, 359, 26, 1, 0, 0, 0, 360, 361, 5, 34, 0, 0, 361, 362, 5, 86, 0, 0, 362, 363, 5, 101, 0, 0, 363, 364, 5, 114, 0, 0, 364, 365, 5, 115, 0, 0, 365, 366, 5, 105, 0, 0, 366, 367, 5, 111, 0, 0, 367, 368, 5, 110, 0, 0, 368, 369, 5, 34, 0, 0, 369, 28, 1, 0, 0, 0, 370, 371, 5, 34, 0, 0, 371, 372, 5, 84, 0, 0, 372, 373, 5, 121, 0, 0, 373, 374, 5, 112, 0, 0, 374, 375, 5, 101, 0, 0, 375, 376, 5, 34, 0, 0, 376, 30, 1, 0, 0, 0, 377, 378, 5, 34, 0, 0, 378, 379, 5, 84, 0, 0, 379, 380, 5, 97, 0, 0, 380, 381, 5, 115, 0, 0, 381, 382, 5, 107, 0, 0, 382, 383, 5, 34, 0, 0, 383, 32, 1, 0, 0, 0, 384, 385, 5, 34, 0, 0, 385, 386, 5, 67, 0, 0, 386, 387, 5, 104, 0, 0, 387, 388, 5, 111, 0, 0, 388, 389, 5, 105, 0, 0, 389, 390, 5, 99, 0, 0, 390, 391, 5, 101, 0, 0, 391, 392, 5, 34, 0, 0, 392, 34, 1, 0, 0, 0, 393, 394, 5, 34, 0, 0, 394, 395, 5, 70, 0, 0, 395, 396, 5, 97, 0, 0, 396, 397, 5, 105, 0, 0, 397, 398, 5, 108, 0, 0, 398, 399, 5, 34, 0, 0, 399, 36, 1, 0, 0, 0, 400, 401, 5, 34, 0, 0, 401, 402, 5, 83, 0, 0, 402, 403, 5, 117, 0, 0, 403, 404, 5, 99, 0, 0, 404, 405, 5, 99, 0, 0, 405, 406, 5, 101, 0, 0, 406, 407, 5, 101, 0, 0, 407, 408, 5, 100, 0, 0, 408, 409, 5, 34, 0, 0, 409, 38, 1, 0, 0, 0, 410, 411, 5, 34, 0, 0, 411, 412, 5, 80, 0, 0, 412, 413, 5, 97, 0, 0, 413, 414, 5, 115, 0, 0, 414, 415, 5, 115, 0, 0, 415, 416, 5, 34, 0, 0, 416, 40, 1, 0, 0, 0, 417, 418, 5, 34, 0, 0, 418, 419, 5, 87, 0, 0, 419, 420, 5, 97, 0, 0, 420, 421, 5, 105, 0, 0, 421, 422, 5, 116, 0, 0, 422, 423, 5, 34, 0, 0, 423, 42, 1, 0, 0, 0, 424, 425, 5, 34, 0, 0, 425, 426, 5, 80, 0, 0, 426, 427, 5, 97, 0, 0, 427, 428, 5, 114, 0, 0, 428, 429, 5, 97, 0, 0, 429, 430, 5, 108, 0, 0, 430, 431, 5, 108, 0, 0, 431, 432, 5, 101, 0, 0, 432, 433, 5, 108, 0, 0, 433, 434, 5, 34, 0, 0, 434, 44, 1, 0, 0, 0, 435, 436, 5, 34, 0, 0, 436, 437, 5, 77, 0, 0, 437, 438, 5, 97, 0, 0, 438, 439, 5, 112, 0, 0, 439, 440, 5, 34, 0, 0, 440, 46, 1, 0, 0, 0, 441, 442, 5, 34, 0, 0, 442, 443, 5, 67, 0, 0, 443, 444, 5, 104, 0, 0, 444, 445, 5, 111, 0, 0, 445, 446, 5, 105, 0, 0, 446, 447, 5, 99, 0, 0, 447, 448, 5, 101, 0, 0, 448, 449, 5, 115, 0, 0, 449, 450, 5, 34, 0, 0, 450, 48, 1, 0, 0, 0, 451, 452, 5, 34, 0, 0, 452, 453, 5, 86, 0, 0, 453, 454, 5, 97, 0, 0, 454, 455, 5, 114, 0, 0, 455, 456, 5, 105, 0, 0, 456, 457, 5, 97, 0, 0, 457, 458, 5, 98, 0, 0, 458, 459, 5, 108, 0, 0, 459, 460, 5, 101, 0, 0, 460, 461, 5, 34, 0, 0, 461, 50, 1, 0, 0, 0, 462, 463, 5, 34, 0, 0, 463, 464, 5, 68, 0, 0, 464, 465, 5, 101, 0, 0, 465, 466, 5, 102, 0, 0, 466, 467, 5, 97, 0, 0, 467, 468, 5, 117, 0, 0, 468, 469, 5, 108, 0, 0, 469, 470, 5, 116, 0, 0, 470, 471, 5, 34, 0, 0, 471, 52, 1, 0, 0, 0, 472, 473, 5, 34, 0, 0, 473, 474, 5, 66, 0, 0, 474, 475, 5, 114, 0, 0, 475, 476, 5, 97, 0, 0, 476, 477, 5, 110, 0, 0, 477, 478, 5, 99, 0, 0, 478, 479, 5, 104, 0, 0, 479, 480, 5, 101, 0, 0, 480, 481, 5, 115, 0, 0, 481, 482, 5, 34, 0, 0, 482, 54, 1, 0, 0, 0, 483, 484, 5, 34, 0, 0, 484, 485, 5, 65, 0, 0, 485, 486, 5, 110, 0, 0, 486, 487, 5, 100, 0, 0, 487, 488, 5, 34, 0, 0, 488, 56, 1, 0, 0, 0, 489, 490, 5, 34, 0, 0, 490, 491, 5, 66, 0, 0, 491, 492, 5, 111, 0, 0, 492, 493, 5, 111, 0, 0, 493, 494, 5, 108, 0, 0, 494, 495, 5, 101, 0, 0, 495, 496, 5, 97, 0, 0, 496, 497, 5, 110, 0, 0, 497, 498, 5, 69, 0, 0, 498, 499, 5, 113, 0, 0, 499, 500, 5, 117, 0, 0, 500, 501, 5, 97, 0, 0, 501, 502, 5, 108, 0, 0, 502, 503, 5, 115, 0, 0, 503, 504, 5, 34, 0, 0, 504, 58, 1, 0, 0, 0, 505, 506, 5, 34, 0, 0, 506, 507, 5, 66, 0, 0, 507, 508, 5, 111, 0, 0, 508, 509, 5, 111, 0, 0, 509, 510, 5, 108, 0, 0, 510, 511, 5, 101, 0, 0, 511, 512, 5, 97, 0, 0, 512, 513, 5, 110, 0, 0, 513, 514, 5, 69, 0, 0, 514, 515, 5, 113, 0, 0, 515, 516, 5, 117, 0, 0, 516, 517, 5, 97, 0, 0, 517, 518, 5, 108, 0, 0, 518, 519, 5, 115, 0, 0, 519, 520, 5, 80, 0, 0, 520, 521, 5, 97, 0, 0, 521, 522, 5, 116, 0, 0, 522, 523, 5, 104, 0, 0, 523, 524, 5, 34, 0, 0, 524, 60, 1, 0, 0, 0, 525, 526, 5, 34, 0, 0, 526, 527, 5, 73, 0, 0, 527, 528, 5, 115, 0, 0, 528, 529, 5, 66, 0, 0, 529, 530, 5, 111, 0, 0, 530, 531, 5, 111, 0, 0, 531, 532, 5, 108, 0, 0, 532, 533, 5, 101, 0, 0, 533, 534, 5, 97, 0, 0, 534, 535, 5, 110, 0, 0, 535, 536, 5, 34, 0, 0, 536, 62, 1, 0, 0, 0, 537, 538, 5, 34, 0, 0, 538, 539, 5, 73, 0, 0, 539, 540, 5, 115, 0, 0, 540, 541, 5, 78, 0, 0, 541, 542, 5, 117, 0, 0, 542, 543, 5, 108, 0, 0, 543, 544, 5, 108, 0, 0, 544, 545, 5, 34, 0, 0, 545, 64, 1, 0, 0, 0, 546, 547, 5, 34, 0, 0, 547, 548, 5, 73, 0, 0, 548, 549, 5, 115, 0, 0, 549, 550, 5, 78, 0, 0, 550, 551, 5, 117, 0, 0, 551, 552, 5, 109, 0, 0, 552, 553, 5, 101, 0, 0, 553, 554, 5, 114, 0, 0, 554, 555, 5, 105, 0, 0, 555, 556, 5, 99, 0, 0, 556, 557, 5, 34, 0, 0, 557, 66, 1, 0, 0, 0, 558, 559, 5, 34, 0, 0, 559, 560, 5, 73, 0, 0, 560, 561, 5, 115, 0, 0, 561, 562, 5, 80, 0, 0, 562, 563, 5, 114, 0, 0, 563, 564, 5, 101, 0, 0, 564, 565, 5, 115, 0, 0, 565, 566, 5, 101, 0, 0, 566, 567, 5, 110, 0, 0, 567, 568, 5, 116, 0, 0, 568, 569, 5, 34, 0, 0, 569, 68, 1, 0, 0, 0, 570, 571, 5, 34, 0, 0, 571, 572, 5, 73, 0, 0, 572, 573, 5, 115, 0, 0, 573, 574, 5, 83, 0, 0, 574, 575, 5, 116, 0, 0, 575, 576, 5, 114, 0, 0, 576, 577, 5, 105, 0, 0, 577, 578, 5, 110, 0, 0, 578, 579, 5, 103, 0, 0, 579, 580, 5, 34, 0, 0, 580, 70, 1, 0, 0, 0, 581, 582, 5, 34, 0, 0, 582, 583, 5, 73, 0, 0, 583, 584, 5, 115, 0, 0, 584, 585, 5, 84, 0, 0, 585, 586, 5, 105, 0, 0, 586, 587, 5, 109, 0, 0, 587, 588, 5, 101, 0, 0, 588, 589, 5, 115, 0, 0, 589, 590, 5, 116, 0, 0, 590, 591, 5, 97, 0, 0, 591, 592, 5, 109, 0, 0, 592, 593, 5, 112, 0, 0, 593, 594, 5, 34, 0, 0, 594, 72, 1, 0, 0, 0, 595, 596, 5, 34, 0, 0, 596, 597, 5, 78, 0, 0, 597, 598, 5, 111, 0, 0, 598, 599, 5, 116, 0, 0, 599, 600, 5, 34, 0, 0, 600, 74, 1, 0, 0, 0, 601, 602, 5, 34, 0, 0, 602, 603, 5, 78, 0, 0, 603, 604, 5, 117, 0, 0, 604, 605, 5, 109, 0, 0, 605, 606, 5, 101, 0, 0, 606, 607, 5, 114, 0, 0, 607, 608, 5, 105, 0, 0, 608, 609, 5, 99, 0, 0, 609, 610, 5, 69, 0, 0, 610, 611, 5, 113, 0, 0, 611, 612, 5, 117, 0, 0, 612, 613, 5, 97, 0, 0, 613, 614, 5, 108, 0, 0, 614, 615, 5, 115, 0, 0, 615, 616, 5, 34, 0, 0, 616, 76, 1, 0, 0, 0, 617, 618, 5, 34, 0, 0, 618, 619, 5, 78, 0, 0, 619, 620, 5, 117, 0, 0, 620, 621, 5, 109, 0, 0, 621, 622, 5, 101, 0, 0, 622, 623, 5, 114, 0, 0, 623, 624, 5, 105, 0, 0, 624, 625, 5, 99, 0, 0, 625, 626, 5, 69, 0, 0, 626, 627, 5, 113, 0, 0, 627, 628, 5, 117, 0, 0, 628, 629, 5, 97, 0, 0, 629, 630, 5, 108, 0, 0, 630, 631, 5, 115, 0, 0, 631, 632, 5, 80, 0, 0, 632, 633, 5, 97, 0, 0, 633, 634, 5, 116, 0, 0, 634, 635, 5, 104, 0, 0, 635, 636, 5, 34, 0, 0, 636, 78, 1, 0, 0, 0, 637, 638, 5, 34, 0, 0, 638, 639, 5, 78, 0, 0, 639, 640, 5, 117, 0, 0, 640, 641, 5, 109, 0, 0, 641, 642, 5, 101, 0, 0, 642, 643, 5, 114, 0, 0, 643, 644, 5, 105, 0, 0, 644, 645, 5, 99, 0, 0, 645, 646, 5, 71, 0, 0, 646, 647, 5, 114, 0, 0, 647, 648, 5, 101, 0, 0, 648, 649, 5, 97, 0, 0, 649, 650, 5, 116, 0, 0, 650, 651, 5, 101, 0, 0, 651, 652, 5, 114, 0, 0, 652, 653, 5, 84, 0, 0, 653, 654, 5, 104, 0, 0, 654, 655, 5, 97, 0, 0, 655, 656, 5, 110, 0, 0, 656, 657, 5, 34, 0, 0, 657, 80, 1, 0, 0, 0, 658, 659, 5, 34, 0, 0, 659, 660, 5, 78, 0, 0, 660, 661, 5, 117, 0, 0, 661, 662, 5, 109, 0, 0, 662, 663, 5, 101, 0, 0, 663, 664, 5, 114, 0, 0, 664, 665, 5, 105, 0, 0, 665, 666, 5, 99, 0, 0, 666, 667, 5, 71, 0, 0, 667, 668, 5, 114, 0, 0, 668, 669, 5, 101, 0, 0, 669, 670, 5, 97, 0, 0, 670, 671, 5, 116, 0, 0, 671, 672, 5, 101, 0, 0, 672, 673, 5, 114, 0, 0, 673, 674, 5, 84, 0, 0, 674, 675, 5, 104, 0, 0, 675, 676, 5, 97, 0, 0, 676, 677, 5, 110, 0, 0, 677, 678, 5, 80, 0, 0, 678, 679, 5, 97, 0, 0, 679, 680, 5, 116, 0, 0, 680, 681, 5, 104, 0, 0, 681, 682, 5, 34, 0, 0, 682, 82, 1, 0, 0, 0, 683, 684, 5, 34, 0, 0, 684, 685, 5, 78, 0, 0, 685, 686, 5, 117, 0, 0, 686, 687, 5, 109, 0, 0, 687, 688, 5, 101, 0, 0, 688, 689, 5, 114, 0, 0, 689, 690, 5, 105, 0, 0, 690, 691, 5, 99, 0, 0, 691, 692, 5, 71, 0, 0, 692, 693, 5, 114, 0, 0, 693, 694, 5, 101, 0, 0, 694, 695, 5, 97, 0, 0, 695, 696, 5, 116, 0, 0, 696, 697, 5, 101, 0, 0, 697, 698, 5, 114, 0, 0, 698, 699, 5, 84, 0, 0, 699, 700, 5, 104, 0, 0, 700, 701, 5, 97, 0, 0, 701, 702, 5, 110, 0, 0, 702, 703, 5, 69, 0, 0, 703, 704, 5, 113, 0, 0, 704, 705, 5, 117, 0, 0, 705, 706, 5, 97, 0, 0, 706, 707, 5, 108, 0, 0, 707, 708, 5, 115, 0, 0, 708, 709, 5, 34, 0, 0, 709, 84, 1, 0, 0, 0, 710, 711, 5, 34, 0, 0, 711, 712, 5, 78, 0, 0, 712, 713, 5, 117, 0, 0, 713, 714, 5, 109, 0, 0, 714, 715, 5, 101, 0, 0, 715, 716, 5, 114, 0, 0, 716, 717, 5, 105, 0, 0, 717, 718, 5, 99, 0, 0, 718, 719, 5, 71, 0, 0, 719, 720, 5, 114, 0, 0, 720, 721, 5, 101, 0, 0, 721, 722, 5, 97, 0, 0, 722, 723, 5, 116, 0, 0, 723, 724, 5, 101, 0, 0, 724, 725, 5, 114, 0, 0, 725, 726, 5, 84, 0, 0, 726, 727, 5, 104, 0, 0, 727, 728, 5, 97, 0, 0, 728, 729, 5, 110, 0, 0, 729, 730, 5, 69, 0, 0, 730, 731, 5, 113, 0, 0, 731, 732, 5, 117, 0, 0, 732, 733, 5, 97, 0, 0, 733, 734, 5, 108, 0, 0, 734, 735, 5, 115, 0, 0, 735, 736, 5, 80, 0, 0, 736, 737, 5, 97, 0, 0, 737, 738, 5, 116, 0, 0, 738, 739, 5, 104, 0, 0, 739, 740, 5, 34, 0, 0, 740, 86, 1, 0, 0, 0, 741, 742, 5, 34, 0, 0, 742, 743, 5, 78, 0, 0, 743, 744, 5, 117, 0, 0, 744, 745, 5, 109, 0, 0, 745, 746, 5, 101, 0, 0, 746, 747, 5, 114, 0, 0, 747, 748, 5, 105, 0, 0, 748, 749, 5, 99, 0, 0, 749, 750, 5, 76, 0, 0, 750, 751, 5, 101, 0, 0, 751, 752, 5, 115, 0, 0, 752, 753, 5, 115, 0, 0, 753, 754, 5, 84, 0, 0, 754, 755, 5, 104, 0, 0, 755, 756, 5, 97, 0, 0, 756, 757, 5, 110, 0, 0, 757, 758, 5, 34, 0, 0, 758, 88, 1, 0, 0, 0, 759, 760, 5, 34, 0, 0, 760, 761, 5, 78, 0, 0, 761, 762, 5, 117, 0, 0, 762, 763, 5, 109, 0, 0, 763, 764, 5, 101, 0, 0, 764, 765, 5, 114, 0, 0, 765, 766, 5, 105, 0, 0, 766, 767, 5, 99, 0, 0, 767, 768, 5, 76, 0, 0, 768, 769, 5, 101, 0, 0, 769, 770, 5, 115, 0, 0, 770, 771, 5, 115, 0, 0, 771, 772, 5, 84, 0, 0, 772, 773, 5, 104, 0, 0, 773, 774, 5, 97, 0, 0, 774, 775, 5, 110, 0, 0, 775, 776, 5, 80, 0, 0, 776, 777, 5, 97, 0, 0, 777, 778, 5, 116, 0, 0, 778, 779, 5, 104, 0, 0, 779, 780, 5, 34, 0, 0, 780, 90, 1, 0, 0, 0, 781, 782, 5, 34, 0, 0, 782, 783, 5, 78, 0, 0, 783, 784, 5, 117, 0, 0, 784, 785, 5, 109, 0, 0, 785, 786, 5, 101, 0, 0, 786, 787, 5, 114, 0, 0, 787, 788, 5, 105, 0, 0, 788, 789, 5, 99, 0, 0, 789, 790, 5, 76, 0, 0, 790, 791, 5, 101, 0, 0, 791, 792, 5, 115, 0, 0, 792, 793, 5, 115, 0, 0, 793, 794, 5, 84, 0, 0, 794, 795, 5, 104, 0, 0, 795, 796, 5, 97, 0, 0, 796, 797, 5, 110, 0, 0, 797, 798, 5, 69, 0, 0, 798, 799, 5, 113, 0, 0, 799, 800, 5, 117, 0, 0, 800, 801, 5, 97, 0, 0, 801, 802, 5, 108, 0, 0, 802, 803, 5, 115, 0, 0, 803, 804, 5, 34, 0, 0, 804, 92, 1, 0, 0, 0, 805, 806, 5, 34, 0, 0, 806, 807, 5, 78, 0, 0, 807, 808, 5, 117, 0, 0, 808, 809, 5, 109, 0, 0, 809, 810, 5, 101, 0, 0, 810, 811, 5, 114, 0, 0, 811, 812, 5, 105, 0, 0, 812, 813, 5, 99, 0, 0, 813, 814, 5, 76, 0, 0, 814, 815, 5, 101, 0, 0, 815, 816, 5, 115, 0, 0, 816, 817, 5, 115, 0, 0, 817, 818, 5, 84, 0, 0, 818, 819, 5, 104, 0, 0, 819, 820, 5, 97, 0, 0, 820, 821, 5, 110, 0, 0, 821, 822, 5, 69, 0, 0, 822, 823, 5, 113, 0, 0, 823, 824, 5, 117, 0, 0, 824, 825, 5, 97, 0, 0, 825, 826, 5, 108, 0, 0, 826, 827, 5, 115, 0, 0, 827, 828, 5, 80, 0, 0, 828, 829, 5, 97, 0, 0, 829, 830, 5, 116, 0, 0, 830, 831, 5, 104, 0, 0, 831, 832, 5, 34, 0, 0, 832, 94, 1, 0, 0, 0, 833, 834, 5, 34, 0, 0, 834, 835, 5, 79, 0, 0, 835, 836, 5, 114, 0, 0, 836, 837, 5, 34, 0, 0, 837, 96, 1, 0, 0, 0, 838, 839, 5, 34, 0, 0, 839, 840, 5, 83, 0, 0, 840, 841, 5, 116, 0, 0, 841, 842, 5, 114, 0, 0, 842, 843, 5, 105, 0, 0, 843, 844, 5, 110, 0, 0, 844, 845, 5, 103, 0, 0, 845, 846, 5, 69, 0, 0, 846, 847, 5, 113, 0, 0, 847, 848, 5, 117, 0, 0, 848, 849, 5, 97, 0, 0, 849, 850, 5, 108, 0, 0, 850, 851, 5, 115, 0, 0, 851, 852, 5, 34, 0, 0, 852, 98, 1, 0, 0, 0, 853, 854, 5, 34, 0, 0, 854, 855, 5, 83, 0, 0, 855, 856, 5, 116, 0, 0, 856, 857, 5, 114, 0, 0, 857, 858, 5, 105, 0, 0, 858, 859, 5, 110, 0, 0, 859, 860, 5, 103, 0, 0, 860, 861, 5, 69, 0, 0, 861, 862, 5, 113, 0, 0, 862, 863, 5, 117, 0, 0, 863, 864, 5, 97, 0, 0, 864, 865, 5, 108, 0, 0, 865, 866, 5, 115, 0, 0, 866, 867, 5, 80, 0, 0, 867, 868, 5, 97, 0, 0, 868, 869, 5, 116, 0, 0, 869, 870, 5, 104, 0, 0, 870, 871, 5, 34, 0, 0, 871, 100, 1, 0, 0, 0, 872, 873, 5, 34, 0, 0, 873, 874, 5, 83, 0, 0, 874, 875, 5, 116, 0, 0, 875, 876, 5, 114, 0, 0, 876, 877, 5, 105, 0, 0, 877, 878, 5, 110, 0, 0, 878, 879, 5, 103, 0, 0, 879, 880, 5, 71, 0, 0, 880, 881, 5, 114, 0, 0, 881, 882, 5, 101, 0, 0, 882, 883, 5, 97, 0, 0, 883, 884, 5, 116, 0, 0, 884, 885, 5, 101, 0, 0, 885, 886, 5, 114, 0, 0, 886, 887, 5, 84, 0, 0, 887, 888, 5, 104, 0, 0, 888, 889, 5, 97, 0, 0, 889, 890, 5, 110, 0, 0, 890, 891, 5, 34, 0, 0, 891, 102, 1, 0, 0, 0, 892, 893, 5, 34, 0, 0, 893, 894, 5, 83, 0, 0, 894, 895, 5, 116, 0, 0, 895, 896, 5, 114, 0, 0, 896, 897, 5, 105, 0, 0, 897, 898, 5, 110, 0, 0, 898, 899, 5, 103, 0, 0, 899, 900, 5, 71, 0, 0, 900, 901, 5, 114, 0, 0, 901, 902, 5, 101, 0, 0, 902, 903, 5, 97, 0, 0, 903, 904, 5, 116, 0, 0, 904, 905, 5, 101, 0, 0, 905, 906, 5, 114, 0, 0, 906, 907, 5, 84, 0, 0, 907, 908, 5, 104, 0, 0, 908, 909, 5, 97, 0, 0, 909, 910, 5, 110, 0, 0, 910, 911, 5, 80, 0, 0, 911, 912, 5, 97, 0, 0, 912, 913, 5, 116, 0, 0, 913, 914, 5, 104, 0, 0, 914, 915, 5, 34, 0, 0, 915, 104, 1, 0, 0, 0, 916, 917, 5, 34, 0, 0, 917, 918, 5, 83, 0, 0, 918, 919, 5, 116, 0, 0, 919, 920, 5, 114, 0, 0, 920, 921, 5, 105, 0, 0, 921, 922, 5, 110, 0, 0, 922, 923, 5, 103, 0, 0, 923, 924, 5, 71, 0, 0, 924, 925, 5, 114, 0, 0, 925, 926, 5, 101, 0, 0, 926, 927, 5, 97, 0, 0, 927, 928, 5, 116, 0, 0, 928, 929, 5, 101, 0, 0, 929, 930, 5, 114, 0, 0, 930, 931, 5, 84, 0, 0, 931, 932, 5, 104, 0, 0, 932, 933, 5, 97, 0, 0, 933, 934, 5, 110, 0, 0, 934, 935, 5, 69, 0, 0, 935, 936, 5, 113, 0, 0, 936, 937, 5, 117, 0, 0, 937, 938, 5, 97, 0, 0, 938, 939, 5, 108, 0, 0, 939, 940, 5, 115, 0, 0, 940, 941, 5, 34, 0, 0, 941, 106, 1, 0, 0, 0, 942, 943, 5, 34, 0, 0, 943, 944, 5, 83, 0, 0, 944, 945, 5, 116, 0, 0, 945, 946, 5, 114, 0, 0, 946, 947, 5, 105, 0, 0, 947, 948, 5, 110, 0, 0, 948, 949, 5, 103, 0, 0, 949, 950, 5, 71, 0, 0, 950, 951, 5, 114, 0, 0, 951, 952, 5, 101, 0, 0, 952, 953, 5, 97, 0, 0, 953, 954, 5, 116, 0, 0, 954, 955, 5, 101, 0, 0, 955, 956, 5, 114, 0, 0, 956, 957, 5, 84, 0, 0, 957, 958, 5, 104, 0, 0, 958, 959, 5, 97, 0, 0, 959, 960, 5, 110, 0, 0, 960, 961, 5, 69, 0, 0, 961, 962, 5, 113, 0, 0, 962, 963, 5, 117, 0, 0, 963, 964, 5, 97, 0, 0, 964, 965, 5, 108, 0, 0, 965, 966, 5, 115, 0, 0, 966, 967, 5, 80, 0, 0, 967, 968, 5, 97, 0, 0, 968, 969, 5, 116, 0, 0, 969, 970, 5, 104, 0, 0, 970, 971, 5, 34, 0, 0, 971, 108, 1, 0, 0, 0, 972, 973, 5, 34, 0, 0, 973, 974, 5, 83, 0, 0, 974, 975, 5, 116, 0, 0, 975, 976, 5, 114, 0, 0, 976, 977, 5, 105, 0, 0, 977, 978, 5, 110, 0, 0, 978, 979, 5, 103, 0, 0, 979, 980, 5, 76, 0, 0, 980, 981, 5, 101, 0, 0, 981, 982, 5, 115, 0, 0, 982, 983, 5, 115, 0, 0, 983, 984, 5, 84, 0, 0, 984, 985, 5, 104, 0, 0, 985, 986, 5, 97, 0, 0, 986, 987, 5, 110, 0, 0, 987, 988, 5, 34, 0, 0, 988, 110, 1, 0, 0, 0, 989, 990, 5, 34, 0, 0, 990, 991, 5, 83, 0, 0, 991, 992, 5, 116, 0, 0, 992, 993, 5, 114, 0, 0, 993, 994, 5, 105, 0, 0, 994, 995, 5, 110, 0, 0, 995, 996, 5, 103, 0, 0, 996, 997, 5, 76, 0, 0, 997, 998, 5, 101, 0, 0, 998, 999, 5, 115, 0, 0, 999, 1000, 5, 115, 0, 0, 1000, 1001, 5, 84, 0, 0, 1001, 1002, 5, 104, 0, 0, 1002, 1003, 5, 97, 0, 0, 1003, 1004, 5, 110, 0, 0, 1004, 1005, 5, 80, 0, 0, 1005, 1006, 5, 97, 0, 0, 1006, 1007, 5, 116, 0, 0, 1007, 1008, 5, 104, 0, 0, 1008, 1009, 5, 34, 0, 0, 1009, 112, 1, 0, 0, 0, 1010, 1011, 5, 34, 0, 0, 1011, 1012, 5, 83, 0, 0, 1012, 1013, 5, 116, 0, 0, 1013, 1014, 5, 114, 0, 0, 1014, 1015, 5, 105, 0, 0, 1015, 1016, 5, 110, 0, 0, 1016, 1017, 5, 103, 0, 0, 1017, 1018, 5, 76, 0, 0, 1018, 1019, 5, 101, 0, 0, 1019, 1020, 5, 115, 0, 0, 1020, 1021, 5, 115, 0, 0, 1021, 1022, 5, 84, 0, 0, 1022, 1023, 5, 104, 0, 0, 1023, 1024, 5, 97, 0, 0, 1024, 1025, 5, 110, 0, 0, 1025, 1026, 5, 69, 0, 0, 1026, 1027, 5, 113, 0, 0, 1027, 1028, 5, 117, 0, 0, 1028, 1029, 5, 97, 0, 0, 1029, 1030, 5, 108, 0, 0, 1030, 1031, 5, 115, 0, 0, 1031, 1032, 5, 34, 0, 0, 1032, 114, 1, 0, 0, 0, 1033, 1034, 5, 34, 0, 0, 1034, 1035, 5, 83, 0, 0, 1035, 1036, 5, 116, 0, 0, 1036, 1037, 5, 114, 0, 0, 1037, 1038, 5, 105, 0, 0, 1038, 1039, 5, 110, 0, 0, 1039, 1040, 5, 103, 0, 0, 1040, 1041, 5, 76, 0, 0, 1041, 1042, 5, 101, 0, 0, 1042, 1043, 5, 115, 0, 0, 1043, 1044, 5, 115, 0, 0, 1044, 1045, 5, 84, 0, 0, 1045, 1046, 5, 104, 0, 0, 1046, 1047, 5, 97, 0, 0, 1047, 1048, 5, 110, 0, 0, 1048, 1049, 5, 69, 0, 0, 1049, 1050, 5, 113, 0, 0, 1050, 1051, 5, 117, 0, 0, 1051, 1052, 5, 97, 0, 0, 1052, 1053, 5, 108, 0, 0, 1053, 1054, 5, 115, 0, 0, 1054, 1055, 5, 80, 0, 0, 1055, 1056, 5, 97, 0, 0, 1056, 1057, 5, 116, 0, 0, 1057, 1058, 5, 104, 0, 0, 1058, 1059, 5, 34, 0, 0, 1059, 116, 1, 0, 0, 0, 1060, 1061, 5, 34, 0, 0, 1061, 1062, 5, 83, 0, 0, 1062, 1063, 5, 116, 0, 0, 1063, 1064, 5, 114, 0, 0, 1064, 1065, 5, 105, 0, 0, 1065, 1066, 5, 110, 0, 0, 1066, 1067, 5, 103, 0, 0, 1067, 1068, 5, 77, 0, 0, 1068, 1069, 5, 97, 0, 0, 1069, 1070, 5, 116, 0, 0, 1070, 1071, 5, 99, 0, 0, 1071, 1072, 5, 104, 0, 0, 1072, 1073, 5, 101, 0, 0, 1073, 1074, 5, 115, 0, 0, 1074, 1075, 5, 34, 0, 0, 1075, 118, 1, 0, 0, 0, 1076, 1077, 5, 34, 0, 0, 1077, 1078, 5, 84, 0, 0, 1078, 1079, 5, 105, 0, 0, 1079, 1080, 5, 109, 0, 0, 1080, 1081, 5, 101, 0, 0, 1081, 1082, 5, 115, 0, 0, 1082, 1083, 5, 116, 0, 0, 1083, 1084, 5, 97, 0, 0, 1084, 1085, 5, 109, 0, 0, 1085, 1086, 5, 112, 0, 0, 1086, 1087, 5, 69, 0, 0, 1087, 1088, 5, 113, 0, 0, 1088, 1089, 5, 117, 0, 0, 1089, 1090, 5, 97, 0, 0, 1090, 1091, 5, 108, 0, 0, 1091, 1092, 5, 115, 0, 0, 1092, 1093, 5, 34, 0, 0, 1093, 120, 1, 0, 0, 0, 1094, 1095, 5, 34, 0, 0, 1095, 1096, 5, 84, 0, 0, 1096, 1097, 5, 105, 0, 0, 1097, 1098, 5, 109, 0, 0, 1098, 1099, 5, 101, 0, 0, 1099, 1100, 5, 115, 0, 0, 1100, 1101, 5, 116, 0, 0, 1101, 1102, 5, 97, 0, 0, 1102, 1103, 5, 109, 0, 0, 1103, 1104, 5, 112, 0, 0, 1104, 1105, 5, 69, 0, 0, 1105, 1106, 5, 113, 0, 0, 1106, 1107, 5, 117, 0, 0, 1107, 1108, 5, 97, 0, 0, 1108, 1109, 5, 108, 0, 0, 1109, 1110, 5, 115, 0, 0, 1110, 1111, 5, 80, 0, 0, 1111, 1112, 5, 97, 0, 0, 1112, 1113, 5, 116, 0, 0, 1113, 1114, 5, 104, 0, 0, 1114, 1115, 5, 34, 0, 0, 1115, 122, 1, 0, 0, 0, 1116, 1117, 5, 34, 0, 0, 1117, 1118, 5, 84, 0, 0, 1118, 1119, 5, 105, 0, 0, 1119, 1120, 5, 109, 0, 0, 1120, 1121, 5, 101, 0, 0, 1121, 1122, 5, 115, 0, 0, 1122, 1123, 5, 116, 0, 0, 1123, 1124, 5, 97, 0, 0, 1124, 1125, 5, 109, 0, 0, 1125, 1126, 5, 112, 0, 0, 1126, 1127, 5, 71, 0, 0, 1127, 1128, 5, 114, 0, 0, 1128, 1129, 5, 101, 0, 0, 1129, 1130, 5, 97, 0, 0, 1130, 1131, 5, 116, 0, 0, 1131, 1132, 5, 101, 0, 0, 1132, 1133, 5, 114, 0, 0, 1133, 1134, 5, 84, 0, 0, 1134, 1135, 5, 104, 0, 0, 1135, 1136, 5, 97, 0, 0, 1136, 1137, 5, 110, 0, 0, 1137, 1138, 5, 34, 0, 0, 1138, 124, 1, 0, 0, 0, 1139, 1140, 5, 34, 0, 0, 1140, 1141, 5, 84, 0, 0, 1141, 1142, 5, 105, 0, 0, 1142, 1143, 5, 109, 0, 0, 1143, 1144, 5, 101, 0, 0, 1144, 1145, 5, 115, 0, 0, 1145, 1146, 5, 116, 0, 0, 1146, 1147, 5, 97, 0, 0, 1147, 1148, 5, 109, 0, 0, 1148, 1149, 5, 112, 0, 0, 1149, 1150, 5, 71, 0, 0, 1150, 1151, 5, 114, 0, 0, 1151, 1152, 5, 101, 0, 0, 1152, 1153, 5, 97, 0, 0, 1153, 1154, 5, 116, 0, 0, 1154, 1155, 5, 101, 0, 0, 1155, 1156, 5, 114, 0, 0, 1156, 1157, 5, 84, 0, 0, 1157, 1158, 5, 104, 0, 0, 1158, 1159, 5, 97, 0, 0, 1159, 1160, 5, 110, 0, 0, 1160, 1161, 5, 80, 0, 0, 1161, 1162, 5, 97, 0, 0, 1162, 1163, 5, 116, 0, 0, 1163, 1164, 5, 104, 0, 0, 1164, 1165, 5, 34, 0, 0, 1165, 126, 1, 0, 0, 0, 1166, 1167, 5, 34, 0, 0, 1167, 1168, 5, 84, 0, 0, 1168, 1169, 5, 105, 0, 0, 1169, 1170, 5, 109, 0, 0, 1170, 1171, 5, 101, 0, 0, 1171, 1172, 5, 115, 0, 0, 1172, 1173, 5, 116, 0, 0, 1173, 1174, 5, 97, 0, 0, 1174, 1175, 5, 109, 0, 0, 1175, 1176, 5, 112, 0, 0, 1176, 1177, 5, 71, 0, 0, 1177, 1178, 5, 114, 0, 0, 1178, 1179, 5, 101, 0, 0, 1179, 1180, 5, 97, 0, 0, 1180, 1181, 5, 116, 0, 0, 1181, 1182, 5, 101, 0, 0, 1182, 1183, 5, 114, 0, 0, 1183, 1184, 5, 84, 0, 0, 1184, 1185, 5, 104, 0, 0, 1185, 1186, 5, 97, 0, 0, 1186, 1187, 5, 110, 0, 0, 1187, 1188, 5, 69, 0, 0, 1188, 1189, 5, 113, 0, 0, 1189, 1190, 5, 117, 0, 0, 1190, 1191, 5, 97, 0, 0, 1191, 1192, 5, 108, 0, 0, 1192, 1193, 5, 115, 0, 0, 1193, 1194, 5, 34, 0, 0, 1194, 128, 1, 0, 0, 0, 1195, 1196, 5, 34, 0, 0, 1196, 1197, 5, 84, 0, 0, 1197, 1198, 5, 105, 0, 0, 1198, 1199, 5, 109, 0, 0, 1199, 1200, 5, 101, 0, 0, 1200, 1201, 5, 115, 0, 0, 1201, 1202, 5, 116, 0, 0, 1202, 1203, 5, 97, 0, 0, 1203, 1204, 5, 109, 0, 0, 1204, 1205, 5, 112, 0, 0, 1205, 1206, 5, 71, 0, 0, 1206, 1207, 5, 114, 0, 0, 1207, 1208, 5, 101, 0, 0, 1208, 1209, 5, 97, 0, 0, 1209, 1210, 5, 116, 0, 0, 1210, 1211, 5, 101, 0, 0, 1211, 1212, 5, 114, 0, 0, 1212, 1213, 5, 84, 0, 0, 1213, 1214, 5, 104, 0, 0, 1214, 1215, 5, 97, 0, 0, 1215, 1216, 5, 110, 0, 0, 1216, 1217, 5, 69, 0, 0, 1217, 1218, 5, 113, 0, 0, 1218, 1219, 5, 117, 0, 0, 1219, 1220, 5, 97, 0, 0, 1220, 1221, 5, 108, 0, 0, 1221, 1222, 5, 115, 0, 0, 1222, 1223, 5, 80, 0, 0, 1223, 1224, 5, 97, 0, 0, 1224, 1225, 5, 116, 0, 0, 1225, 1226, 5, 104, 0, 0, 1226, 1227, 5, 34, 0, 0, 1227, 130, 1, 0, 0, 0, 1228, 1229, 5, 34, 0, 0, 1229, 1230, 5, 84, 0, 0, 1230, 1231, 5, 105, 0, 0, 1231, 1232, 5, 109, 0, 0, 1232, 1233, 5, 101, 0, 0, 1233, 1234, 5, 115, 0, 0, 1234, 1235, 5, 116, 0, 0, 1235, 1236, 5, 97, 0, 0, 1236, 1237, 5, 109, 0, 0, 1237, 1238, 5, 112, 0, 0, 1238, 1239, 5, 76, 0, 0, 1239, 1240, 5, 101, 0, 0, 1240, 1241, 5, 115, 0, 0, 1241, 1242, 5, 115, 0, 0, 1242, 1243, 5, 84, 0, 0, 1243, 1244, 5, 104, 0, 0, 1244, 1245, 5, 97, 0, 0, 1245, 1246, 5, 110, 0, 0, 1246, 1247, 5, 34, 0, 0, 1247, 132, 1, 0, 0, 0, 1248, 1249, 5, 34, 0, 0, 1249, 1250, 5, 84, 0, 0, 1250, 1251, 5, 105, 0, 0, 1251, 1252, 5, 109, 0, 0, 1252, 1253, 5, 101, 0, 0, 1253, 1254, 5, 115, 0, 0, 1254, 1255, 5, 116, 0, 0, 1255, 1256, 5, 97, 0, 0, 1256, 1257, 5, 109, 0, 0, 1257, 1258, 5, 112, 0, 0, 1258, 1259, 5, 76, 0, 0, 1259, 1260, 5, 101, 0, 0, 1260, 1261, 5, 115, 0, 0, 1261, 1262, 5, 115, 0, 0, 1262, 1263, 5, 84, 0, 0, 1263, 1264, 5, 104, 0, 0, 1264, 1265, 5, 97, 0, 0, 1265, 1266, 5, 110, 0, 0, 1266, 1267, 5, 80, 0, 0, 1267, 1268, 5, 97, 0, 0, 1268, 1269, 5, 116, 0, 0, 1269, 1270, 5, 104, 0, 0, 1270, 1271, 5, 34, 0, 0, 1271, 134, 1, 0, 0, 0, 1272, 1273, 5, 34, 0, 0, 1273, 1274, 5, 84, 0, 0, 1274, 1275, 5, 105, 0, 0, 1275, 1276, 5, 109, 0, 0, 1276, 1277, 5, 101, 0, 0, 1277, 1278, 5, 115, 0, 0, 1278, 1279, 5, 116, 0, 0, 1279, 1280, 5, 97, 0, 0, 1280, 1281, 5, 109, 0, 0, 1281, 1282, 5, 112, 0, 0, 1282, 1283, 5, 76, 0, 0, 1283, 1284, 5, 101, 0, 0, 1284, 1285, 5, 115, 0, 0, 1285, 1286, 5, 115, 0, 0, 1286, 1287, 5, 84, 0, 0, 1287, 1288, 5, 104, 0, 0, 1288, 1289, 5, 97, 0, 0, 1289, 1290, 5, 110, 0, 0, 1290, 1291, 5, 69, 0, 0, 1291, 1292, 5, 113, 0, 0, 1292, 1293, 5, 117, 0, 0, 1293, 1294, 5, 97, 0, 0, 1294, 1295, 5, 108, 0, 0, 1295, 1296, 5, 115, 0, 0, 1296, 1297, 5, 34, 0, 0, 1297, 136, 1, 0, 0, 0, 1298, 1299, 5, 34, 0, 0, 1299, 1300, 5, 84, 0, 0, 1300, 1301, 5, 105, 0, 0, 1301, 1302, 5, 109, 0, 0, 1302, 1303, 5, 101, 0, 0, 1303, 1304, 5, 115, 0, 0, 1304, 1305, 5, 116, 0, 0, 1305, 1306, 5, 97, 0, 0, 1306, 1307, 5, 109, 0, 0, 1307, 1308, 5, 112, 0, 0, 1308, 1309, 5, 76, 0, 0, 1309, 1310, 5, 101, 0, 0, 1310, 1311, 5, 115, 0, 0, 1311, 1312, 5, 115, 0, 0, 1312, 1313, 5, 84, 0, 0, 1313, 1314, 5, 104, 0, 0, 1314, 1315, 5, 97, 0, 0, 1315, 1316, 5, 110, 0, 0, 1316, 1317, 5, 69, 0, 0, 1317, 1318, 5, 113, 0, 0, 1318, 1319, 5, 117, 0, 0, 1319, 1320, 5, 97, 0, 0, 1320, 1321, 5, 108, 0, 0, 1321, 1322, 5, 115, 0, 0, 1322, 1323, 5, 80, 0, 0, 1323, 1324, 5, 97, 0, 0, 1324, 1325, 5, 116, 0, 0, 1325, 1326, 5, 104, 0, 0, 1326, 1327, 5, 34, 0, 0, 1327, 138, 1, 0, 0, 0, 1328, 1329, 5, 34, 0, 0, 1329, 1330, 5, 83, 0, 0, 1330, 1331, 5, 101, 0, 0, 1331, 1332, 5, 99, 0, 0, 1332, 1333, 5, 111, 0, 0, 1333, 1334, 5, 110, 0, 0, 1334, 1335, 5, 100, 0, 0, 1335, 1336, 5, 115, 0, 0, 1336, 1337, 5, 80, 0, 0, 1337, 1338, 5, 97, 0, 0, 1338, 1339, 5, 116, 0, 0, 1339, 1340, 5, 104, 0, 0, 1340, 1341, 5, 34, 0, 0, 1341, 140, 1, 0, 0, 0, 1342, 1343, 5, 34, 0, 0, 1343, 1344, 5, 83, 0, 0, 1344, 1345, 5, 101, 0, 0, 1345, 1346, 5, 99, 0, 0, 1346, 1347, 5, 111, 0, 0, 1347, 1348, 5, 110, 0, 0, 1348, 1349, 5, 100, 0, 0, 1349, 1350, 5, 115, 0, 0, 1350, 1351, 5, 34, 0, 0, 1351, 142, 1, 0, 0, 0, 1352, 1353, 5, 34, 0, 0, 1353, 1354, 5, 84, 0, 0, 1354, 1355, 5, 105, 0, 0, 1355, 1356, 5, 109, 0, 0, 1356, 1357, 5, 101, 0, 0, 1357, 1358, 5, 115, 0, 0, 1358, 1359, 5, 116, 0, 0, 1359, 1360, 5, 97, 0, 0, 1360, 1361, 5, 109, 0, 0, 1361, 1362, 5, 112, 0, 0, 1362, 1363, 5, 80, 0, 0, 1363, 1364, 5, 97, 0, 0, 1364, 1365, 5, 116, 0, 0, 1365, 1366, 5, 104, 0, 0, 1366, 1367, 5, 34, 0, 0, 1367, 144, 1, 0, 0, 0, 1368, 1369, 5, 34, 0, 0, 1369, 1370, 5, 84, 0, 0, 1370, 1371, 5, 105, 0, 0, 1371, 1372, 5, 109, 0, 0, 1372, 1373, 5, 101, 0, 0, 1373, 1374, 5, 115, 0, 0, 1374, 1375, 5, 116, 0, 0, 1375, 1376, 5, 97, 0, 0, 1376, 1377, 5, 109, 0, 0, 1377, 1378, 5, 112, 0, 0, 1378, 1379, 5, 34, 0, 0, 1379, 146, 1, 0, 0, 0, 1380, 1381, 5, 34, 0, 0, 1381, 1382, 5, 84, 0, 0, 1382, 1383, 5, 105, 0, 0, 1383, 1384, 5, 109, 0, 0, 1384, 1385, 5, 101, 0, 0, 1385, 1386, 5, 111, 0, 0, 1386, 1387, 5, 117, 0, 0, 1387, 1388, 5, 116, 0, 0, 1388, 1389, 5, 83, 0, 0, 1389, 1390, 5, 101, 0, 0, 1390, 1391, 5, 99, 0, 0, 1391, 1392, 5, 111, 0, 0, 1392, 1393, 5, 110, 0, 0, 1393, 1394, 5, 100, 0, 0, 1394, 1395, 5, 115, 0, 0, 1395, 1396, 5, 34, 0, 0, 1396, 148, 1, 0, 0, 0, 1397, 1398, 5, 34, 0, 0, 1398, 1399, 5, 84, 0, 0, 1399, 1400, 5, 105, 0, 0, 1400, 1401, 5, 109, 0, 0, 1401, 1402, 5, 101, 0, 0, 1402, 1403, 5, 111, 0, 0, 1403, 1404, 5, 117, 0, 0, 1404, 1405, 5, 116, 0, 0, 1405, 1406, 5, 83, 0, 0, 1406, 1407, 5, 101, 0, 0, 1407, 1408, 5, 99, 0, 0, 1408, 1409, 5, 111, 0, 0, 1409, 1410, 5, 110, 0, 0, 1410, 1411, 5, 100, 0, 0, 1411, 1412, 5, 115, 0, 0, 1412, 1413, 5, 80, 0, 0, 1413, 1414, 5, 97, 0, 0, 1414, 1415, 5, 116, 0, 0, 1415, 1416, 5, 104, 0, 0, 1416, 1417, 5, 34, 0, 0, 1417, 150, 1, 0, 0, 0, 1418, 1419, 5, 34, 0, 0, 1419, 1420, 5, 72, 0, 0, 1420, 1421, 5, 101, 0, 0, 1421, 1422, 5, 97, 0, 0, 1422, 1423, 5, 114, 0, 0, 1423, 1424, 5, 116, 0, 0, 1424, 1425, 5, 98, 0, 0, 1425, 1426, 5, 101, 0, 0, 1426, 1427, 5, 97, 0, 0, 1427, 1428, 5, 116, 0, 0, 1428, 1429, 5, 83, 0, 0, 1429, 1430, 5, 101, 0, 0, 1430, 1431, 5, 99, 0, 0, 1431, 1432, 5, 111, 0, 0, 1432, 1433, 5, 110, 0, 0, 1433, 1434, 5, 100, 0, 0, 1434, 1435, 5, 115, 0, 0, 1435, 1436, 5, 34, 0, 0, 1436, 152, 1, 0, 0, 0, 1437, 1438, 5, 34, 0, 0, 1438, 1439, 5, 72, 0, 0, 1439, 1440, 5, 101, 0, 0, 1440, 1441, 5, 97, 0, 0, 1441, 1442, 5, 114, 0, 0, 1442, 1443, 5, 116, 0, 0, 1443, 1444, 5, 98, 0, 0, 1444, 1445, 5, 101, 0, 0, 1445, 1446, 5, 97, 0, 0, 1446, 1447, 5, 116, 0, 0, 1447, 1448, 5, 83, 0, 0, 1448, 1449, 5, 101, 0, 0, 1449, 1450, 5, 99, 0, 0, 1450, 1451, 5, 111, 0, 0, 1451, 1452, 5, 110, 0, 0, 1452, 1453, 5, 100, 0, 0, 1453, 1454, 5, 115, 0, 0, 1454, 1455, 5, 80, 0, 0, 1455, 1456, 5, 97, 0, 0, 1456, 1457, 5, 116, 0, 0, 1457, 1458, 5, 104, 0, 0, 1458, 1459, 5, 34, 0, 0, 1459, 154, 1, 0, 0, 0, 1460, 1461, 5, 34, 0, 0, 1461, 1462, 5, 80, 0, 0, 1462, 1463, 5, 114, 0, 0, 1463, 1464, 5, 111, 0, 0, 1464, 1465, 5, 99, 0, 0, 1465, 1466, 5, 101, 0, 0, 1466, 1467, 5, 115, 0, 0, 1467, 1468, 5, 115, 0, 0, 1468, 1469, 5, 111, 0, 0, 1469, 1470, 5, 114, 0, 0, 1470, 1471, 5, 67, 0, 0, 1471, 1472, 5, 111, 0, 0, 1472, 1473, 5, 110, 0, 0, 1473, 1474, 5, 102, 0, 0, 1474, 1475, 5, 105, 0, 0, 1475, 1476, 5, 103, 0, 0, 1476, 1477, 5, 34, 0, 0, 1477, 156, 1, 0, 0, 0, 1478, 1479, 5, 34, 0, 0, 1479, 1480, 5, 77, 0, 0, 1480, 1481, 5, 111, 0, 0, 1481, 1482, 5, 100, 0, 0, 1482, 1483, 5, 101, 0, 0, 1483, 1484, 5, 34, 0, 0, 1484, 158, 1, 0, 0, 0, 1485, 1486, 5, 34, 0, 0, 1486, 1487, 5, 73, 0, 0, 1487, 1488, 5, 78, 0, 0, 1488, 1489, 5, 76, 0, 0, 1489, 1490, 5, 73, 0, 0, 1490, 1491, 5, 78, 0, 0, 1491, 1492, 5, 69, 0, 0, 1492, 1493, 5, 34, 0, 0, 1493, 160, 1, 0, 0, 0, 1494, 1495, 5, 34, 0, 0, 1495, 1496, 5, 68, 0, 0, 1496, 1497, 5, 73, 0, 0, 1497, 1498, 5, 83, 0, 0, 1498, 1499, 5, 84, 0, 0, 1499, 1500, 5, 82, 0, 0, 1500, 1501, 5, 73, 0, 0, 1501, 1502, 5, 66, 0, 0, 1502, 1503, 5, 85, 0, 0, 1503, 1504, 5, 84, 0, 0, 1504, 1505, 5, 69, 0, 0, 1505, 1506, 5, 68, 0, 0, 1506, 1507, 5, 34, 0, 0, 1507, 162, 1, 0, 0, 0, 1508, 1509, 5, 34, 0, 0, 1509, 1510, 5, 69, 0, 0, 1510, 1511, 5, 120, 0, 0, 1511, 1512, 5, 101, 0, 0, 1512, 1513, 5, 99, 0, 0, 1513, 1514, 5, 117, 0, 0, 1514, 1515, 5, 116, 0, 0, 1515, 1516, 5, 105, 0, 0, 1516, 1517, 5, 111, 0, 0, 1517, 1518, 5, 110, 0, 0, 1518, 1519, 5, 84, 0, 0, 1519, 1520, 5, 121, 0, 0, 1520, 1521, 5, 112, 0, 0, 1521, 1522, 5, 101, 0, 0, 1522, 1523, 5, 34, 0, 0, 1523, 164, 1, 0, 0, 0, 1524, 1525, 5, 34, 0, 0, 1525, 1526, 5, 83, 0, 0, 1526, 1527, 5, 84, 0, 0, 1527, 1528, 5, 65, 0, 0, 1528, 1529, 5, 78, 0, 0, 1529, 1530, 5, 68, 0, 0, 1530, 1531, 5, 65, 0, 0, 1531, 1532, 5, 82, 0, 0, 1532, 1533, 5, 68, 0, 0, 1533, 1534, 5, 34, 0, 0, 1534, 166, 1, 0, 0, 0, 1535, 1536, 5, 34, 0, 0, 1536, 1537, 5, 73, 0, 0, 1537, 1538, 5, 116, 0, 0, 1538, 1539, 5, 101, 0, 0, 1539, 1540, 5, 109, 0, 0, 1540, 1541, 5, 80, 0, 0, 1541, 1542, 5, 114, 0, 0, 1542, 1543, 5, 111, 0, 0, 1543, 1544, 5, 99, 0, 0, 1544, 1545, 5, 101, 0, 0, 1545, 1546, 5, 115, 0, 0, 1546, 1547, 5, 115, 0, 0, 1547, 1548, 5, 111, 0, 0, 1548, 1549, 5, 114, 0, 0, 1549, 1550, 5, 34, 0, 0, 1550, 168, 1, 0, 0, 0, 1551, 1552, 5, 34, 0, 0, 1552, 1553, 5, 73, 0, 0, 1553, 1554, 5, 116, 0, 0, 1554, 1555, 5, 101, 0, 0, 1555, 1556, 5, 114, 0, 0, 1556, 1557, 5, 97, 0, 0, 1557, 1558, 5, 116, 0, 0, 1558, 1559, 5, 111, 0, 0, 1559, 1560, 5, 114, 0, 0, 1560, 1561, 5, 34, 0, 0, 1561, 170, 1, 0, 0, 0, 1562, 1563, 5, 34, 0, 0, 1563, 1564, 5, 73, 0, 0, 1564, 1565, 5, 116, 0, 0, 1565, 1566, 5, 101, 0, 0, 1566, 1567, 5, 109, 0, 0, 1567, 1568, 5, 83, 0, 0, 1568, 1569, 5, 101, 0, 0, 1569, 1570, 5, 108, 0, 0, 1570, 1571, 5, 101, 0, 0, 1571, 1572, 5, 99, 0, 0, 1572, 1573, 5, 116, 0, 0, 1573, 1574, 5, 111, 0, 0, 1574, 1575, 5, 114, 0, 0, 1575, 1576, 5, 34, 0, 0, 1576, 172, 1, 0, 0, 0, 1577, 1578, 5, 34, 0, 0, 1578, 1579, 5, 77, 0, 0, 1579, 1580, 5, 97, 0, 0, 1580, 1581, 5, 120, 0, 0, 1581, 1582, 5, 67, 0, 0, 1582, 1583, 5, 111, 0, 0, 1583, 1584, 5, 110, 0, 0, 1584, 1585, 5, 99, 0, 0, 1585, 1586, 5, 117, 0, 0, 1586, 1587, 5, 114, 0, 0, 1587, 1588, 5, 114, 0, 0, 1588, 1589, 5, 101, 0, 0, 1589, 1590, 5, 110, 0, 0, 1590, 1591, 5, 99, 0, 0, 1591, 1592, 5, 121, 0, 0, 1592, 1593, 5, 34, 0, 0, 1593, 174, 1, 0, 0, 0, 1594, 1595, 5, 34, 0, 0, 1595, 1596, 5, 82, 0, 0, 1596, 1597, 5, 101, 0, 0, 1597, 1598, 5, 115, 0, 0, 1598, 1599, 5, 111, 0, 0, 1599, 1600, 5, 117, 0, 0, 1600, 1601, 5, 114, 0, 0, 1601, 1602, 5, 99, 0, 0, 1602, 1603, 5, 101, 0, 0, 1603, 1604, 5, 34, 0, 0, 1604, 176, 1, 0, 0, 0, 1605, 1606, 5, 34, 0, 0, 1606, 1607, 5, 73, 0, 0, 1607, 1608, 5, 110, 0, 0, 1608, 1609, 5, 112, 0, 0, 1609, 1610, 5, 117, 0, 0, 1610, 1611, 5, 116, 0, 0, 1611, 1612, 5, 80, 0, 0, 1612, 1613, 5, 97, 0, 0, 1613, 1614, 5, 116, 0, 0, 1614, 1615, 5, 104, 0, 0, 1615, 1616, 5, 34, 0, 0, 1616, 178, 1, 0, 0, 0, 1617, 1618, 5, 34, 0, 0, 1618, 1619, 5, 79, 0, 0, 1619, 1620, 5, 117, 0, 0, 1620, 1621, 5, 116, 0, 0, 1621, 1622, 5, 112, 0, 0, 1622, 1623, 5, 117, 0, 0, 1623, 1624, 5, 116, 0, 0, 1624, 1625, 5, 80, 0, 0, 1625, 1626, 5, 97, 0, 0, 1626, 1627, 5, 116, 0, 0, 1627, 1628, 5, 104, 0, 0, 1628, 1629, 5, 34, 0, 0, 1629, 180, 1, 0, 0, 0, 1630, 1631, 5, 34, 0, 0, 1631, 1632, 5, 73, 0, 0, 1632, 1633, 5, 116, 0, 0, 1633, 1634, 5, 101, 0, 0, 1634, 1635, 5, 109, 0, 0, 1635, 1636, 5, 115, 0, 0, 1636, 1637, 5, 80, 0, 0, 1637, 1638, 5, 97, 0, 0, 1638, 1639, 5, 116, 0, 0, 1639, 1640, 5, 104, 0, 0, 1640, 1641, 5, 34, 0, 0, 1641, 182, 1, 0, 0, 0, 1642, 1643, 5, 34, 0, 0, 1643, 1644, 5, 82, 0, 0, 1644, 1645, 5, 101, 0, 0, 1645, 1646, 5, 115, 0, 0, 1646, 1647, 5, 117, 0, 0, 1647, 1648, 5, 108, 0, 0, 1648, 1649, 5, 116, 0, 0, 1649, 1650, 5, 80, 0, 0, 1650, 1651, 5, 97, 0, 0, 1651, 1652, 5, 116, 0, 0, 1652, 1653, 5, 104, 0, 0, 1653, 1654, 5, 34, 0, 0, 1654, 184, 1, 0, 0, 0, 1655, 1656, 5, 34, 0, 0, 1656, 1657, 5, 82, 0, 0, 1657, 1658, 5, 101, 0, 0, 1658, 1659, 5, 115, 0, 0, 1659, 1660, 5, 117, 0, 0, 1660, 1661, 5, 108, 0, 0, 1661, 1662, 5, 116, 0, 0, 1662, 1663, 5, 34, 0, 0, 1663, 186, 1, 0, 0, 0, 1664, 1665, 5, 34, 0, 0, 1665, 1666, 5, 80, 0, 0, 1666, 1667, 5, 97, 0, 0, 1667, 1668, 5, 114, 0, 0, 1668, 1669, 5, 97, 0, 0, 1669, 1670, 5, 109, 0, 0, 1670, 1671, 5, 101, 0, 0, 1671, 1672, 5, 116, 0, 0, 1672, 1673, 5, 101, 0, 0, 1673, 1674, 5, 114, 0, 0, 1674, 1675, 5, 115, 0, 0, 1675, 1676, 5, 34, 0, 0, 1676, 188, 1, 0, 0, 0, 1677, 1678, 5, 34, 0, 0, 1678, 1679, 5, 82, 0, 0, 1679, 1680, 5, 101, 0, 0, 1680, 1681, 5, 115, 0, 0, 1681, 1682, 5, 117, 0, 0, 1682, 1683, 5, 108, 0, 0, 1683, 1684, 5, 116, 0, 0, 1684, 1685, 5, 83, 0, 0, 1685, 1686, 5, 101, 0, 0, 1686, 1687, 5, 108, 0, 0, 1687, 1688, 5, 101, 0, 0, 1688, 1689, 5, 99, 0, 0, 1689, 1690, 5, 116, 0, 0, 1690, 1691, 5, 111, 0, 0, 1691, 1692, 5, 114, 0, 0, 1692, 1693, 5, 34, 0, 0, 1693, 190, 1, 0, 0, 0, 1694, 1695, 5, 34, 0, 0, 1695, 1696, 5, 73, 0, 0, 1696, 1697, 5, 116, 0, 0, 1697, 1698, 5, 101, 0, 0, 1698, 1699, 5, 109, 0, 0, 1699, 1700, 5, 82, 0, 0, 1700, 1701, 5, 101, 0, 0, 1701, 1702, 5, 97, 0, 0, 1702, 1703, 5, 100, 0, 0, 1703, 1704, 5, 101, 0, 0, 1704, 1705, 5, 114, 0, 0, 1705, 1706, 5, 34, 0, 0, 1706, 192, 1, 0, 0, 0, 1707, 1708, 5, 34, 0, 0, 1708, 1709, 5, 82, 0, 0, 1709, 1710, 5, 101, 0, 0, 1710, 1711, 5, 97, 0, 0, 1711, 1712, 5, 100, 0, 0, 1712, 1713, 5, 101, 0, 0, 1713, 1714, 5, 114, 0, 0, 1714, 1715, 5, 67, 0, 0, 1715, 1716, 5, 111, 0, 0, 1716, 1717, 5, 110, 0, 0, 1717, 1718, 5, 102, 0, 0, 1718, 1719, 5, 105, 0, 0, 1719, 1720, 5, 103, 0, 0, 1720, 1721, 5, 34, 0, 0, 1721, 194, 1, 0, 0, 0, 1722, 1723, 5, 34, 0, 0, 1723, 1724, 5, 73, 0, 0, 1724, 1725, 5, 110, 0, 0, 1725, 1726, 5, 112, 0, 0, 1726, 1727, 5, 117, 0, 0, 1727, 1728, 5, 116, 0, 0, 1728, 1729, 5, 84, 0, 0, 1729, 1730, 5, 121, 0, 0, 1730, 1731, 5, 112, 0, 0, 1731, 1732, 5, 101, 0, 0, 1732, 1733, 5, 34, 0, 0, 1733, 196, 1, 0, 0, 0, 1734, 1735, 5, 34, 0, 0, 1735, 1736, 5, 67, 0, 0, 1736, 1737, 5, 83, 0, 0, 1737, 1738, 5, 86, 0, 0, 1738, 1739, 5, 72, 0, 0, 1739, 1740, 5, 101, 0, 0, 1740, 1741, 5, 97, 0, 0, 1741, 1742, 5, 100, 0, 0, 1742, 1743, 5, 101, 0, 0, 1743, 1744, 5, 114, 0, 0, 1744, 1745, 5, 76, 0, 0, 1745, 1746, 5, 111, 0, 0, 1746, 1747, 5, 99, 0, 0, 1747, 1748, 5, 97, 0, 0, 1748, 1749, 5, 116, 0, 0, 1749, 1750, 5, 105, 0, 0, 1750, 1751, 5, 111, 0, 0, 1751, 1752, 5, 110, 0, 0, 1752, 1753, 5, 34, 0, 0, 1753, 198, 1, 0, 0, 0, 1754, 1755, 5, 34, 0, 0, 1755, 1756, 5, 67, 0, 0, 1756, 1757, 5, 83, 0, 0, 1757, 1758, 5, 86, 0, 0, 1758, 1759, 5, 72, 0, 0, 1759, 1760, 5, 101, 0, 0, 1760, 1761, 5, 97, 0, 0, 1761, 1762, 5, 100, 0, 0, 1762, 1763, 5, 101, 0, 0, 1763, 1764, 5, 114, 0, 0, 1764, 1765, 5, 115, 0, 0, 1765, 1766, 5, 34, 0, 0, 1766, 200, 1, 0, 0, 0, 1767, 1768, 5, 34, 0, 0, 1768, 1769, 5, 77, 0, 0, 1769, 1770, 5, 97, 0, 0, 1770, 1771, 5, 120, 0, 0, 1771, 1772, 5, 73, 0, 0, 1772, 1773, 5, 116, 0, 0, 1773, 1774, 5, 101, 0, 0, 1774, 1775, 5, 109, 0, 0, 1775, 1776, 5, 115, 0, 0, 1776, 1777, 5, 34, 0, 0, 1777, 202, 1, 0, 0, 0, 1778, 1779, 5, 34, 0, 0, 1779, 1780, 5, 77, 0, 0, 1780, 1781, 5, 97, 0, 0, 1781, 1782, 5, 120, 0, 0, 1782, 1783, 5, 73, 0, 0, 1783, 1784, 5, 116, 0, 0, 1784, 1785, 5, 101, 0, 0, 1785, 1786, 5, 109, 0, 0, 1786, 1787, 5, 115, 0, 0, 1787, 1788, 5, 80, 0, 0, 1788, 1789, 5, 97, 0, 0, 1789, 1790, 5, 116, 0, 0, 1790, 1791, 5, 104, 0, 0, 1791, 1792, 5, 34, 0, 0, 1792, 204, 1, 0, 0, 0, 1793, 1794, 5, 34, 0, 0, 1794, 1795, 5, 78, 0, 0, 1795, 1796, 5, 101, 0, 0, 1796, 1797, 5, 120, 0, 0, 1797, 1798, 5, 116, 0, 0, 1798, 1799, 5, 34, 0, 0, 1799, 206, 1, 0, 0, 0, 1800, 1801, 5, 34, 0, 0, 1801, 1802, 5, 69, 0, 0, 1802, 1803, 5, 110, 0, 0, 1803, 1804, 5, 100, 0, 0, 1804, 1805, 5, 34, 0, 0, 1805, 208, 1, 0, 0, 0, 1806, 1807, 5, 34, 0, 0, 1807, 1808, 5, 67, 0, 0, 1808, 1809, 5, 97, 0, 0, 1809, 1810, 5, 117, 0, 0, 1810, 1811, 5, 115, 0, 0, 1811, 1812, 5, 101, 0, 0, 1812, 1813, 5, 34, 0, 0, 1813, 210, 1, 0, 0, 0, 1814, 1815, 5, 34, 0, 0, 1815, 1816, 5, 67, 0, 0, 1816, 1817, 5, 97, 0, 0, 1817, 1818, 5, 117, 0, 0, 1818, 1819, 5, 115, 0, 0, 1819, 1820, 5, 101, 0, 0, 1820, 1821, 5, 80, 0, 0, 1821, 1822, 5, 97, 0, 0, 1822, 1823, 5, 116, 0, 0, 1823, 1824, 5, 104, 0, 0, 1824, 1825, 5, 34, 0, 0, 1825, 212, 1, 0, 0, 0, 1826, 1827, 5, 34, 0, 0, 1827, 1828, 5, 69, 0, 0, 1828, 1829, 5, 114, 0, 0, 1829, 1830, 5, 114, 0, 0, 1830, 1831, 5, 111, 0, 0, 1831, 1832, 5, 114, 0, 0, 1832, 1833, 5, 34, 0, 0, 1833, 214, 1, 0, 0, 0, 1834, 1835, 5, 34, 0, 0, 1835, 1836, 5, 69, 0, 0, 1836, 1837, 5, 114, 0, 0, 1837, 1838, 5, 114, 0, 0, 1838, 1839, 5, 111, 0, 0, 1839, 1840, 5, 114, 0, 0, 1840, 1841, 5, 80, 0, 0, 1841, 1842, 5, 97, 0, 0, 1842, 1843, 5, 116, 0, 0, 1843, 1844, 5, 104, 0, 0, 1844, 1845, 5, 34, 0, 0, 1845, 216, 1, 0, 0, 0, 1846, 1847, 5, 34, 0, 0, 1847, 1848, 5, 82, 0, 0, 1848, 1849, 5, 101, 0, 0, 1849, 1850, 5, 116, 0, 0, 1850, 1851, 5, 114, 0, 0, 1851, 1852, 5, 121, 0, 0, 1852, 1853, 5, 34, 0, 0, 1853, 218, 1, 0, 0, 0, 1854, 1855, 5, 34, 0, 0, 1855, 1856, 5, 69, 0, 0, 1856, 1857, 5, 114, 0, 0, 1857, 1858, 5, 114, 0, 0, 1858, 1859, 5, 111, 0, 0, 1859, 1860, 5, 114, 0, 0, 1860, 1861, 5, 69, 0, 0, 1861, 1862, 5, 113, 0, 0, 1862, 1863, 5, 117, 0, 0, 1863, 1864, 5, 97, 0, 0, 1864, 1865, 5, 108, 0, 0, 1865, 1866, 5, 115, 0, 0, 1866, 1867, 5, 34, 0, 0, 1867, 220, 1, 0, 0, 0, 1868, 1869, 5, 34, 0, 0, 1869, 1870, 5, 73, 0, 0, 1870, 1871, 5, 110, 0, 0, 1871, 1872, 5, 116, 0, 0, 1872, 1873, 5, 101, 0, 0, 1873, 1874, 5, 114, 0, 0, 1874, 1875, 5, 118, 0, 0, 1875, 1876, 5, 97, 0, 0, 1876, 1877, 5, 108, 0, 0, 1877, 1878, 5, 83, 0, 0, 1878, 1879, 5, 101, 0, 0, 1879, 1880, 5, 99, 0, 0, 1880, 1881, 5, 111, 0, 0, 1881, 1882, 5, 110, 0, 0, 1882, 1883, 5, 100, 0, 0, 1883, 1884, 5, 115, 0, 0, 1884, 1885, 5, 34, 0, 0, 1885, 222, 1, 0, 0, 0, 1886, 1887, 5, 34, 0, 0, 1887, 1888, 5, 77, 0, 0, 1888, 1889, 5, 97, 0, 0, 1889, 1890, 5, 120, 0, 0, 1890, 1891, 5, 65, 0, 0, 1891, 1892, 5, 116, 0, 0, 1892, 1893, 5, 116, 0, 0, 1893, 1894, 5, 101, 0, 0, 1894, 1895, 5, 109, 0, 0, 1895, 1896, 5, 112, 0, 0, 1896, 1897, 5, 116, 0, 0, 1897, 1898, 5, 115, 0, 0, 1898, 1899, 5, 34, 0, 0, 1899, 224, 1, 0, 0, 0, 1900, 1901, 5, 34, 0, 0, 1901, 1902, 5, 66, 0, 0, 1902, 1903, 5, 97, 0, 0, 1903, 1904, 5, 99, 0, 0, 1904, 1905, 5, 107, 0, 0, 1905, 1906, 5, 111, 0, 0, 1906, 1907, 5, 102, 0, 0, 1907, 1908, 5, 102, 0, 0, 1908, 1909, 5, 82, 0, 0, 1909, 1910, 5, 97, 0, 0, 1910, 1911, 5, 116, 0, 0, 1911, 1912, 5, 101, 0, 0, 1912, 1913, 5, 34, 0, 0, 1913, 226, 1, 0, 0, 0, 1914, 1915, 5, 34, 0, 0, 1915, 1916, 5, 77, 0, 0, 1916, 1917, 5, 97, 0, 0, 1917, 1918, 5, 120, 0, 0, 1918, 1919, 5, 68, 0, 0, 1919, 1920, 5, 101, 0, 0, 1920, 1921, 5, 108, 0, 0, 1921, 1922, 5, 97, 0, 0, 1922, 1923, 5, 121, 0, 0, 1923, 1924, 5, 83, 0, 0, 1924, 1925, 5, 101, 0, 0, 1925, 1926, 5, 99, 0, 0, 1926, 1927, 5, 111, 0, 0, 1927, 1928, 5, 110, 0, 0, 1928, 1929, 5, 100, 0, 0, 1929, 1930, 5, 115, 0, 0, 1930, 1931, 5, 34, 0, 0, 1931, 228, 1, 0, 0, 0, 1932, 1933, 5, 34, 0, 0, 1933, 1934, 5, 74, 0, 0, 1934, 1935, 5, 105, 0, 0, 1935, 1936, 5, 116, 0, 0, 1936, 1937, 5, 116, 0, 0, 1937, 1938, 5, 101, 0, 0, 1938, 1939, 5, 114, 0, 0, 1939, 1940, 5, 83, 0, 0, 1940, 1941, 5, 116, 0, 0, 1941, 1942, 5, 114, 0, 0, 1942, 1943, 5, 97, 0, 0, 1943, 1944, 5, 116, 0, 0, 1944, 1945, 5, 101, 0, 0, 1945, 1946, 5, 103, 0, 0, 1946, 1947, 5, 121, 0, 0, 1947, 1948, 5, 34, 0, 0, 1948, 230, 1, 0, 0, 0, 1949, 1950, 5, 34, 0, 0, 1950, 1951, 5, 70, 0, 0, 1951, 1952, 5, 85, 0, 0, 1952, 1953, 5, 76, 0, 0, 1953, 1954, 5, 76, 0, 0, 1954, 1955, 5, 34, 0, 0, 1955, 232, 1, 0, 0, 0, 1956, 1957, 5, 34, 0, 0, 1957, 1958, 5, 78, 0, 0, 1958, 1959, 5, 79, 0, 0, 1959, 1960, 5, 78, 0, 0, 1960, 1961, 5, 69, 0, 0, 1961, 1962, 5, 34, 0, 0, 1962, 234, 1, 0, 0, 0, 1963, 1964, 5, 34, 0, 0, 1964, 1965, 5, 67, 0, 0, 1965, 1966, 5, 97, 0, 0, 1966, 1967, 5, 116, 0, 0, 1967, 1968, 5, 99, 0, 0, 1968, 1969, 5, 104, 0, 0, 1969, 1970, 5, 34, 0, 0, 1970, 236, 1, 0, 0, 0, 1971, 1972, 5, 34, 0, 0, 1972, 1973, 5, 83, 0, 0, 1973, 1974, 5, 116, 0, 0, 1974, 1975, 5, 97, 0, 0, 1975, 1976, 5, 116, 0, 0, 1976, 1977, 5, 101, 0, 0, 1977, 1978, 5, 115, 0, 0, 1978, 1979, 5, 46, 0, 0, 1979, 1980, 5, 65, 0, 0, 1980, 1981, 5, 76, 0, 0, 1981, 1982, 5, 76, 0, 0, 1982, 1983, 5, 34, 0, 0, 1983, 238, 1, 0, 0, 0, 1984, 1985, 5, 34, 0, 0, 1985, 1986, 5, 83, 0, 0, 1986, 1987, 5, 116, 0, 0, 1987, 1988, 5, 97, 0, 0, 1988, 1989, 5, 116, 0, 0, 1989, 1990, 5, 101, 0, 0, 1990, 1991, 5, 115, 0, 0, 1991, 1992, 5, 46, 0, 0, 1992, 1993, 5, 68, 0, 0, 1993, 1994, 5, 97, 0, 0, 1994, 1995, 5, 116, 0, 0, 1995, 1996, 5, 97, 0, 0, 1996, 1997, 5, 76, 0, 0, 1997, 1998, 5, 105, 0, 0, 1998, 1999, 5, 109, 0, 0, 1999, 2000, 5, 105, 0, 0, 2000, 2001, 5, 116, 0, 0, 2001, 2002, 5, 69, 0, 0, 2002, 2003, 5, 120, 0, 0, 2003, 2004, 5, 99, 0, 0, 2004, 2005, 5, 101, 0, 0, 2005, 2006, 5, 101, 0, 0, 2006, 2007, 5, 100, 0, 0, 2007, 2008, 5, 101, 0, 0, 2008, 2009, 5, 100, 0, 0, 2009, 2010, 5, 34, 0, 0, 2010, 240, 1, 0, 0, 0, 2011, 2012, 5, 34, 0, 0, 2012, 2013, 5, 83, 0, 0, 2013, 2014, 5, 116, 0, 0, 2014, 2015, 5, 97, 0, 0, 2015, 2016, 5, 116, 0, 0, 2016, 2017, 5, 101, 0, 0, 2017, 2018, 5, 115, 0, 0, 2018, 2019, 5, 46, 0, 0, 2019, 2020, 5, 72, 0, 0, 2020, 2021, 5, 101, 0, 0, 2021, 2022, 5, 97, 0, 0, 2022, 2023, 5, 114, 0, 0, 2023, 2024, 5, 116, 0, 0, 2024, 2025, 5, 98, 0, 0, 2025, 2026, 5, 101, 0, 0, 2026, 2027, 5, 97, 0, 0, 2027, 2028, 5, 116, 0, 0, 2028, 2029, 5, 84, 0, 0, 2029, 2030, 5, 105, 0, 0, 2030, 2031, 5, 109, 0, 0, 2031, 2032, 5, 101, 0, 0, 2032, 2033, 5, 111, 0, 0, 2033, 2034, 5, 117, 0, 0, 2034, 2035, 5, 116, 0, 0, 2035, 2036, 5, 34, 0, 0, 2036, 242, 1, 0, 0, 0, 2037, 2038, 5, 34, 0, 0, 2038, 2039, 5, 83, 0, 0, 2039, 2040, 5, 116, 0, 0, 2040, 2041, 5, 97, 0, 0, 2041, 2042, 5, 116, 0, 0, 2042, 2043, 5, 101, 0, 0, 2043, 2044, 5, 115, 0, 0, 2044, 2045, 5, 46, 0, 0, 2045, 2046, 5, 84, 0, 0, 2046, 2047, 5, 105, 0, 0, 2047, 2048, 5, 109, 0, 0, 2048, 2049, 5, 101, 0, 0, 2049, 2050, 5, 111, 0, 0, 2050, 2051, 5, 117, 0, 0, 2051, 2052, 5, 116, 0, 0, 2052, 2053, 5, 34, 0, 0, 2053, 244, 1, 0, 0, 0, 2054, 2055, 5, 34, 0, 0, 2055, 2056, 5, 83, 0, 0, 2056, 2057, 5, 116, 0, 0, 2057, 2058, 5, 97, 0, 0, 2058, 2059, 5, 116, 0, 0, 2059, 2060, 5, 101, 0, 0, 2060, 2061, 5, 115, 0, 0, 2061, 2062, 5, 46, 0, 0, 2062, 2063, 5, 84, 0, 0, 2063, 2064, 5, 97, 0, 0, 2064, 2065, 5, 115, 0, 0, 2065, 2066, 5, 107, 0, 0, 2066, 2067, 5, 70, 0, 0, 2067, 2068, 5, 97, 0, 0, 2068, 2069, 5, 105, 0, 0, 2069, 2070, 5, 108, 0, 0, 2070, 2071, 5, 101, 0, 0, 2071, 2072, 5, 100, 0, 0, 2072, 2073, 5, 34, 0, 0, 2073, 246, 1, 0, 0, 0, 2074, 2075, 5, 34, 0, 0, 2075, 2076, 5, 83, 0, 0, 2076, 2077, 5, 116, 0, 0, 2077, 2078, 5, 97, 0, 0, 2078, 2079, 5, 116, 0, 0, 2079, 2080, 5, 101, 0, 0, 2080, 2081, 5, 115, 0, 0, 2081, 2082, 5, 46, 0, 0, 2082, 2083, 5, 80, 0, 0, 2083, 2084, 5, 101, 0, 0, 2084, 2085, 5, 114, 0, 0, 2085, 2086, 5, 109, 0, 0, 2086, 2087, 5, 105, 0, 0, 2087, 2088, 5, 115, 0, 0, 2088, 2089, 5, 115, 0, 0, 2089, 2090, 5, 105, 0, 0, 2090, 2091, 5, 111, 0, 0, 2091, 2092, 5, 110, 0, 0, 2092, 2093, 5, 115, 0, 0, 2093, 2094, 5, 34, 0, 0, 2094, 248, 1, 0, 0, 0, 2095, 2096, 5, 34, 0, 0, 2096, 2097, 5, 83, 0, 0, 2097, 2098, 5, 116, 0, 0, 2098, 2099, 5, 97, 0, 0, 2099, 2100, 5, 116, 0, 0, 2100, 2101, 5, 101, 0, 0, 2101, 2102, 5, 115, 0, 0, 2102, 2103, 5, 46, 0, 0, 2103, 2104, 5, 82, 0, 0, 2104, 2105, 5, 101, 0, 0, 2105, 2106, 5, 115, 0, 0, 2106, 2107, 5, 117, 0, 0, 2107, 2108, 5, 108, 0, 0, 2108, 2109, 5, 116, 0, 0, 2109, 2110, 5, 80, 0, 0, 2110, 2111, 5, 97, 0, 0, 2111, 2112, 5, 116, 0, 0, 2112, 2113, 5, 104, 0, 0, 2113, 2114, 5, 77, 0, 0, 2114, 2115, 5, 97, 0, 0, 2115, 2116, 5, 116, 0, 0, 2116, 2117, 5, 99, 0, 0, 2117, 2118, 5, 104, 0, 0, 2118, 2119, 5, 70, 0, 0, 2119, 2120, 5, 97, 0, 0, 2120, 2121, 5, 105, 0, 0, 2121, 2122, 5, 108, 0, 0, 2122, 2123, 5, 117, 0, 0, 2123, 2124, 5, 114, 0, 0, 2124, 2125, 5, 101, 0, 0, 2125, 2126, 5, 34, 0, 0, 2126, 250, 1, 0, 0, 0, 2127, 2128, 5, 34, 0, 0, 2128, 2129, 5, 83, 0, 0, 2129, 2130, 5, 116, 0, 0, 2130, 2131, 5, 97, 0, 0, 2131, 2132, 5, 116, 0, 0, 2132, 2133, 5, 101, 0, 0, 2133, 2134, 5, 115, 0, 0, 2134, 2135, 5, 46, 0, 0, 2135, 2136, 5, 80, 0, 0, 2136, 2137, 5, 97, 0, 0, 2137, 2138, 5, 114, 0, 0, 2138, 2139, 5, 97, 0, 0, 2139, 2140, 5, 109, 0, 0, 2140, 2141, 5, 101, 0, 0, 2141, 2142, 5, 116, 0, 0, 2142, 2143, 5, 101, 0, 0, 2143, 2144, 5, 114, 0, 0, 2144, 2145, 5, 80, 0, 0, 2145, 2146, 5, 97, 0, 0, 2146, 2147, 5, 116, 0, 0, 2147, 2148, 5, 104, 0, 0, 2148, 2149, 5, 70, 0, 0, 2149, 2150, 5, 97, 0, 0, 2150, 2151, 5, 105, 0, 0, 2151, 2152, 5, 108, 0, 0, 2152, 2153, 5, 117, 0, 0, 2153, 2154, 5, 114, 0, 0, 2154, 2155, 5, 101, 0, 0, 2155, 2156, 5, 34, 0, 0, 2156, 252, 1, 0, 0, 0, 2157, 2158, 5, 34, 0, 0, 2158, 2159, 5, 83, 0, 0, 2159, 2160, 5, 116, 0, 0, 2160, 2161, 5, 97, 0, 0, 2161, 2162, 5, 116, 0, 0, 2162, 2163, 5, 101, 0, 0, 2163, 2164, 5, 115, 0, 0, 2164, 2165, 5, 46, 0, 0, 2165, 2166, 5, 66, 0, 0, 2166, 2167, 5, 114, 0, 0, 2167, 2168, 5, 97, 0, 0, 2168, 2169, 5, 110, 0, 0, 2169, 2170, 5, 99, 0, 0, 2170, 2171, 5, 104, 0, 0, 2171, 2172, 5, 70, 0, 0, 2172, 2173, 5, 97, 0, 0, 2173, 2174, 5, 105, 0, 0, 2174, 2175, 5, 108, 0, 0, 2175, 2176, 5, 101, 0, 0, 2176, 2177, 5, 100, 0, 0, 2177, 2178, 5, 34, 0, 0, 2178, 254, 1, 0, 0, 0, 2179, 2180, 5, 34, 0, 0, 2180, 2181, 5, 83, 0, 0, 2181, 2182, 5, 116, 0, 0, 2182, 2183, 5, 97, 0, 0, 2183, 2184, 5, 116, 0, 0, 2184, 2185, 5, 101, 0, 0, 2185, 2186, 5, 115, 0, 0, 2186, 2187, 5, 46, 0, 0, 2187, 2188, 5, 78, 0, 0, 2188, 2189, 5, 111, 0, 0, 2189, 2190, 5, 67, 0, 0, 2190, 2191, 5, 104, 0, 0, 2191, 2192, 5, 111, 0, 0, 2192, 2193, 5, 105, 0, 0, 2193, 2194, 5, 99, 0, 0, 2194, 2195, 5, 101, 0, 0, 2195, 2196, 5, 77, 0, 0, 2196, 2197, 5, 97, 0, 0, 2197, 2198, 5, 116, 0, 0, 2198, 2199, 5, 99, 0, 0, 2199, 2200, 5, 104, 0, 0, 2200, 2201, 5, 101, 0, 0, 2201, 2202, 5, 100, 0, 0, 2202, 2203, 5, 34, 0, 0, 2203, 256, 1, 0, 0, 0, 2204, 2205, 5, 34, 0, 0, 2205, 2206, 5, 83, 0, 0, 2206, 2207, 5, 116, 0, 0, 2207, 2208, 5, 97, 0, 0, 2208, 2209, 5, 116, 0, 0, 2209, 2210, 5, 101, 0, 0, 2210, 2211, 5, 115, 0, 0, 2211, 2212, 5, 46, 0, 0, 2212, 2213, 5, 73, 0, 0, 2213, 2214, 5, 110, 0, 0, 2214, 2215, 5, 116, 0, 0, 2215, 2216, 5, 114, 0, 0, 2216, 2217, 5, 105, 0, 0, 2217, 2218, 5, 110, 0, 0, 2218, 2219, 5, 115, 0, 0, 2219, 2220, 5, 105, 0, 0, 2220, 2221, 5, 99, 0, 0, 2221, 2222, 5, 70, 0, 0, 2222, 2223, 5, 97, 0, 0, 2223, 2224, 5, 105, 0, 0, 2224, 2225, 5, 108, 0, 0, 2225, 2226, 5, 117, 0, 0, 2226, 2227, 5, 114, 0, 0, 2227, 2228, 5, 101, 0, 0, 2228, 2229, 5, 34, 0, 0, 2229, 258, 1, 0, 0, 0, 2230, 2231, 5, 34, 0, 0, 2231, 2232, 5, 83, 0, 0, 2232, 2233, 5, 116, 0, 0, 2233, 2234, 5, 97, 0, 0, 2234, 2235, 5, 116, 0, 0, 2235, 2236, 5, 101, 0, 0, 2236, 2237, 5, 115, 0, 0, 2237, 2238, 5, 46, 0, 0, 2238, 2239, 5, 69, 0, 0, 2239, 2240, 5, 120, 0, 0, 2240, 2241, 5, 99, 0, 0, 2241, 2242, 5, 101, 0, 0, 2242, 2243, 5, 101, 0, 0, 2243, 2244, 5, 100, 0, 0, 2244, 2245, 5, 84, 0, 0, 2245, 2246, 5, 111, 0, 0, 2246, 2247, 5, 108, 0, 0, 2247, 2248, 5, 101, 0, 0, 2248, 2249, 5, 114, 0, 0, 2249, 2250, 5, 97, 0, 0, 2250, 2251, 5, 116, 0, 0, 2251, 2252, 5, 101, 0, 0, 2252, 2253, 5, 100, 0, 0, 2253, 2254, 5, 70, 0, 0, 2254, 2255, 5, 97, 0, 0, 2255, 2256, 5, 105, 0, 0, 2256, 2257, 5, 108, 0, 0, 2257, 2258, 5, 117, 0, 0, 2258, 2259, 5, 114, 0, 0, 2259, 2260, 5, 101, 0, 0, 2260, 2261, 5, 84, 0, 0, 2261, 2262, 5, 104, 0, 0, 2262, 2263, 5, 114, 0, 0, 2263, 2264, 5, 101, 0, 0, 2264, 2265, 5, 115, 0, 0, 2265, 2266, 5, 104, 0, 0, 2266, 2267, 5, 111, 0, 0, 2267, 2268, 5, 108, 0, 0, 2268, 2269, 5, 100, 0, 0, 2269, 2270, 5, 34, 0, 0, 2270, 260, 1, 0, 0, 0, 2271, 2272, 5, 34, 0, 0, 2272, 2273, 5, 83, 0, 0, 2273, 2274, 5, 116, 0, 0, 2274, 2275, 5, 97, 0, 0, 2275, 2276, 5, 116, 0, 0, 2276, 2277, 5, 101, 0, 0, 2277, 2278, 5, 115, 0, 0, 2278, 2279, 5, 46, 0, 0, 2279, 2280, 5, 73, 0, 0, 2280, 2281, 5, 116, 0, 0, 2281, 2282, 5, 101, 0, 0, 2282, 2283, 5, 109, 0, 0, 2283, 2284, 5, 82, 0, 0, 2284, 2285, 5, 101, 0, 0, 2285, 2286, 5, 97, 0, 0, 2286, 2287, 5, 100, 0, 0, 2287, 2288, 5, 101, 0, 0, 2288, 2289, 5, 114, 0, 0, 2289, 2290, 5, 70, 0, 0, 2290, 2291, 5, 97, 0, 0, 2291, 2292, 5, 105, 0, 0, 2292, 2293, 5, 108, 0, 0, 2293, 2294, 5, 101, 0, 0, 2294, 2295, 5, 100, 0, 0, 2295, 2296, 5, 34, 0, 0, 2296, 262, 1, 0, 0, 0, 2297, 2298, 5, 34, 0, 0, 2298, 2299, 5, 83, 0, 0, 2299, 2300, 5, 116, 0, 0, 2300, 2301, 5, 97, 0, 0, 2301, 2302, 5, 116, 0, 0, 2302, 2303, 5, 101, 0, 0, 2303, 2304, 5, 115, 0, 0, 2304, 2305, 5, 46, 0, 0, 2305, 2306, 5, 82, 0, 0, 2306, 2307, 5, 101, 0, 0, 2307, 2308, 5, 115, 0, 0, 2308, 2309, 5, 117, 0, 0, 2309, 2310, 5, 108, 0, 0, 2310, 2311, 5, 116, 0, 0, 2311, 2312, 5, 87, 0, 0, 2312, 2313, 5, 114, 0, 0, 2313, 2314, 5, 105, 0, 0, 2314, 2315, 5, 116, 0, 0, 2315, 2316, 5, 101, 0, 0, 2316, 2317, 5, 114, 0, 0, 2317, 2318, 5, 70, 0, 0, 2318, 2319, 5, 97, 0, 0, 2319, 2320, 5, 105, 0, 0, 2320, 2321, 5, 108, 0, 0, 2321, 2322, 5, 101, 0, 0, 2322, 2323, 5, 100, 0, 0, 2323, 2324, 5, 34, 0, 0, 2324, 264, 1, 0, 0, 0, 2325, 2326, 5, 34, 0, 0, 2326, 2327, 5, 83, 0, 0, 2327, 2328, 5, 116, 0, 0, 2328, 2329, 5, 97, 0, 0, 2329, 2330, 5, 116, 0, 0, 2330, 2331, 5, 101, 0, 0, 2331, 2332, 5, 115, 0, 0, 2332, 2333, 5, 46, 0, 0, 2333, 2334, 5, 82, 0, 0, 2334, 2335, 5, 117, 0, 0, 2335, 2336, 5, 110, 0, 0, 2336, 2337, 5, 116, 0, 0, 2337, 2338, 5, 105, 0, 0, 2338, 2339, 5, 109, 0, 0, 2339, 2340, 5, 101, 0, 0, 2340, 2341, 5, 34, 0, 0, 2341, 266, 1, 0, 0, 0, 2342, 2347, 5, 34, 0, 0, 2343, 2346, 3, 275, 137, 0, 2344, 2346, 3, 281, 140, 0, 2345, 2343, 1, 0, 0, 0, 2345, 2344, 1, 0, 0, 0, 2346, 2349, 1, 0, 0, 0, 2347, 2345, 1, 0, 0, 0, 2347, 2348, 1, 0, 0, 0, 2348, 2350, 1, 0, 0, 0, 2349, 2347, 1, 0, 0, 0, 2350, 2351, 5, 46, 0, 0, 2351, 2352, 5, 36, 0, 0, 2352, 2353, 5, 34, 0, 0, 2353, 268, 1, 0, 0, 0, 2354, 2355, 5, 34, 0, 0, 2355, 2356, 5, 36, 0, 0, 2356, 2357, 5, 36, 0, 0, 2357, 2362, 1, 0, 0, 0, 2358, 2361, 3, 275, 137, 0, 2359, 2361, 3, 281, 140, 0, 2360, 2358, 1, 0, 0, 0, 2360, 2359, 1, 0, 0, 0, 2361, 2364, 1, 0, 0, 0, 2362, 2360, 1, 0, 0, 0, 2362, 2363, 1, 0, 0, 0, 2363, 2365, 1, 0, 0, 0, 2364, 2362, 1, 0, 0, 0, 2365, 2366, 5, 34, 0, 0, 2366, 270, 1, 0, 0, 0, 2367, 2368, 5, 34, 0, 0, 2368, 2369, 5, 36, 0, 0, 2369, 2374, 1, 0, 0, 0, 2370, 2373, 3, 275, 137, 0, 2371, 2373, 3, 281, 140, 0, 2372, 2370, 1, 0, 0, 0, 2372, 2371, 1, 0, 0, 0, 2373, 2376, 1, 0, 0, 0, 2374, 2372, 1, 0, 0, 0, 2374, 2375, 1, 0, 0, 0, 2375, 2377, 1, 0, 0, 0, 2376, 2374, 1, 0, 0, 0, 2377, 2378, 5, 34, 0, 0, 2378, 272, 1, 0, 0, 0, 2379, 2384, 5, 34, 0, 0, 2380, 2383, 3, 275, 137, 0, 2381, 2383, 3, 281, 140, 0, 2382, 2380, 1, 0, 0, 0, 2382, 2381, 1, 0, 0, 0, 2383, 2386, 1, 0, 0, 0, 2384, 2382, 1, 0, 0, 0, 2384, 2385, 1, 0, 0, 0, 2385, 2387, 1, 0, 0, 0, 2386, 2384, 1, 0, 0, 0, 2387, 2388, 5, 34, 0, 0, 2388, 274, 1, 0, 0, 0, 2389, 2392, 5, 92, 0, 0, 2390, 2393, 7, 0, 0, 0, 2391, 2393, 3, 277, 138, 0, 2392, 2390, 1, 0, 0, 0, 2392, 2391, 1, 0, 0, 0, 2393, 276, 1, 0, 0, 0, 2394, 2395, 5, 117, 0, 0, 2395, 2396, 3, 279, 139, 0, 2396, 2397, 3, 279, 139, 0, 2397, 2398, 3, 279, 139, 0, 2398, 2399, 3, 279, 139, 0, 2399, 278, 1, 0, 0, 0, 2400, 2401, 7, 1, 0, 0, 2401, 280, 1, 0, 0, 0, 2402, 2403, 8, 2, 0, 0, 2403, 282, 1, 0, 0, 0, 2404, 2413, 5, 48, 0, 0, 2405, 2409, 7, 3, 0, 0, 2406, 2408, 7, 4, 0, 0, 2407, 2406, 1, 0, 0, 0, 2408, 2411, 1, 0, 0, 0, 2409, 2407, 1, 0, 0, 0, 2409, 2410, 1, 0, 0, 0, 2410, 2413, 1, 0, 0, 0, 2411, 2409, 1, 0, 0, 0, 2412, 2404, 1, 0, 0, 0, 2412, 2405, 1, 0, 0, 0, 2413, 284, 1, 0, 0, 0, 2414, 2416, 5, 45, 0, 0, 2415, 2414, 1, 0, 0, 0, 2415, 2416, 1, 0, 0, 0, 2416, 2417, 1, 0, 0, 0, 2417, 2424, 3, 283, 141, 0, 2418, 2420, 5, 46, 0, 0, 2419, 2421, 7, 4, 0, 0, 2420, 2419, 1, 0, 0, 0, 2421, 2422, 1, 0, 0, 0, 2422, 2420, 1, 0, 0, 0, 2422, 2423, 1, 0, 0, 0, 2423, 2425, 1, 0, 0, 0, 2424, 2418, 1, 0, 0, 0, 2424, 2425, 1, 0, 0, 0, 2425, 2427, 1, 0, 0, 0, 2426, 2428, 3, 287, 143, 0, 2427, 2426, 1, 0, 0, 0, 2427, 2428, 1, 0, 0, 0, 2428, 286, 1, 0, 0, 0, 2429, 2431, 7, 5, 0, 0, 2430, 2432, 7, 6, 0, 0, 2431, 2430, 1, 0, 0, 0, 2431, 2432, 1, 0, 0, 0, 2432, 2433, 1, 0, 0, 0, 2433, 2434, 3, 283, 141, 0, 2434, 288, 1, 0, 0, 0, 2435, 2437, 7, 7, 0, 0, 2436, 2435, 1, 0, 0, 0, 2437, 2438, 1, 0, 0, 0, 2438, 2436, 1, 0, 0, 0, 2438, 2439, 1, 0, 0, 0, 2439, 2440, 1, 0, 0, 0, 2440, 2441, 6, 144, 0, 0, 2441, 290, 1, 0, 0, 0, 18, 0, 2345, 2347, 2360, 2362, 2372, 2374, 2382, 2384, 2392, 2409, 2412, 2415, 2422, 2424, 2427, 2431, 2438, 1, 6, 0, 0] \ No newline at end of file diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLLexer.py b/localstack/services/stepfunctions/asl/antlr/runtime/ASLLexer.py index 8e467ec8a8b04..4033681ec5367 100644 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLLexer.py +++ b/localstack/services/stepfunctions/asl/antlr/runtime/ASLLexer.py @@ -1,4 +1,4 @@ -# Generated from /Users/mep/LocalStack/localstack/localstack/services/stepfunctions/asl/antlr/ASLLexer.g4 by ANTLR 4.13.1 +# Generated from ASLLexer.g4 by ANTLR 4.13.1 from antlr4 import * from io import StringIO import sys diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLLexer.tokens b/localstack/services/stepfunctions/asl/antlr/runtime/ASLLexer.tokens deleted file mode 100644 index a5fdbc70bd29d..0000000000000 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLLexer.tokens +++ /dev/null @@ -1,273 +0,0 @@ -COMMA=1 -COLON=2 -LBRACK=3 -RBRACK=4 -LBRACE=5 -RBRACE=6 -TRUE=7 -FALSE=8 -NULL=9 -COMMENT=10 -STATES=11 -STARTAT=12 -NEXTSTATE=13 -VERSION=14 -TYPE=15 -TASK=16 -CHOICE=17 -FAIL=18 -SUCCEED=19 -PASS=20 -WAIT=21 -PARALLEL=22 -MAP=23 -CHOICES=24 -VARIABLE=25 -DEFAULT=26 -BRANCHES=27 -AND=28 -BOOLEANEQUALS=29 -BOOLEANQUALSPATH=30 -ISBOOLEAN=31 -ISNULL=32 -ISNUMERIC=33 -ISPRESENT=34 -ISSTRING=35 -ISTIMESTAMP=36 -NOT=37 -NUMERICEQUALS=38 -NUMERICEQUALSPATH=39 -NUMERICGREATERTHAN=40 -NUMERICGREATERTHANPATH=41 -NUMERICGREATERTHANEQUALS=42 -NUMERICGREATERTHANEQUALSPATH=43 -NUMERICLESSTHAN=44 -NUMERICLESSTHANPATH=45 -NUMERICLESSTHANEQUALS=46 -NUMERICLESSTHANEQUALSPATH=47 -OR=48 -STRINGEQUALS=49 -STRINGEQUALSPATH=50 -STRINGGREATERTHAN=51 -STRINGGREATERTHANPATH=52 -STRINGGREATERTHANEQUALS=53 -STRINGGREATERTHANEQUALSPATH=54 -STRINGLESSTHAN=55 -STRINGLESSTHANPATH=56 -STRINGLESSTHANEQUALS=57 -STRINGLESSTHANEQUALSPATH=58 -STRINGMATCHES=59 -TIMESTAMPEQUALS=60 -TIMESTAMPEQUALSPATH=61 -TIMESTAMPGREATERTHAN=62 -TIMESTAMPGREATERTHANPATH=63 -TIMESTAMPGREATERTHANEQUALS=64 -TIMESTAMPGREATERTHANEQUALSPATH=65 -TIMESTAMPLESSTHAN=66 -TIMESTAMPLESSTHANPATH=67 -TIMESTAMPLESSTHANEQUALS=68 -TIMESTAMPLESSTHANEQUALSPATH=69 -SECONDSPATH=70 -SECONDS=71 -TIMESTAMPPATH=72 -TIMESTAMP=73 -TIMEOUTSECONDS=74 -TIMEOUTSECONDSPATH=75 -HEARTBEATSECONDS=76 -HEARTBEATSECONDSPATH=77 -PROCESSORCONFIG=78 -MODE=79 -INLINE=80 -DISTRIBUTED=81 -EXECUTIONTYPE=82 -STANDARD=83 -ITEMPROCESSOR=84 -ITERATOR=85 -ITEMSELECTOR=86 -MAXCONCURRENCY=87 -RESOURCE=88 -INPUTPATH=89 -OUTPUTPATH=90 -ITEMSPATH=91 -RESULTPATH=92 -RESULT=93 -PARAMETERS=94 -RESULTSELECTOR=95 -ITEMREADER=96 -READERCONFIG=97 -INPUTTYPE=98 -CSVHEADERLOCATION=99 -CSVHEADERS=100 -MAXITEMS=101 -MAXITEMSPATH=102 -NEXT=103 -END=104 -CAUSE=105 -CAUSEPATH=106 -ERROR=107 -ERRORPATH=108 -RETRY=109 -ERROREQUALS=110 -INTERVALSECONDS=111 -MAXATTEMPTS=112 -BACKOFFRATE=113 -MAXDELAYSECONDS=114 -JITTERSTRATEGY=115 -FULL=116 -NONE=117 -CATCH=118 -ERRORNAMEStatesALL=119 -ERRORNAMEStatesDataLimitExceeded=120 -ERRORNAMEStatesHeartbeatTimeout=121 -ERRORNAMEStatesTimeout=122 -ERRORNAMEStatesTaskFailed=123 -ERRORNAMEStatesPermissions=124 -ERRORNAMEStatesResultPathMatchFailure=125 -ERRORNAMEStatesParameterPathFailure=126 -ERRORNAMEStatesBranchFailed=127 -ERRORNAMEStatesNoChoiceMatched=128 -ERRORNAMEStatesIntrinsicFailure=129 -ERRORNAMEStatesExceedToleratedFailureThreshold=130 -ERRORNAMEStatesItemReaderFailed=131 -ERRORNAMEStatesResultWriterFailed=132 -ERRORNAMEStatesRuntime=133 -STRINGDOLLAR=134 -STRINGPATHCONTEXTOBJ=135 -STRINGPATH=136 -STRING=137 -INT=138 -NUMBER=139 -WS=140 -','=1 -':'=2 -'['=3 -']'=4 -'{'=5 -'}'=6 -'true'=7 -'false'=8 -'null'=9 -'"Comment"'=10 -'"States"'=11 -'"StartAt"'=12 -'"NextState"'=13 -'"Version"'=14 -'"Type"'=15 -'"Task"'=16 -'"Choice"'=17 -'"Fail"'=18 -'"Succeed"'=19 -'"Pass"'=20 -'"Wait"'=21 -'"Parallel"'=22 -'"Map"'=23 -'"Choices"'=24 -'"Variable"'=25 -'"Default"'=26 -'"Branches"'=27 -'"And"'=28 -'"BooleanEquals"'=29 -'"BooleanEqualsPath"'=30 -'"IsBoolean"'=31 -'"IsNull"'=32 -'"IsNumeric"'=33 -'"IsPresent"'=34 -'"IsString"'=35 -'"IsTimestamp"'=36 -'"Not"'=37 -'"NumericEquals"'=38 -'"NumericEqualsPath"'=39 -'"NumericGreaterThan"'=40 -'"NumericGreaterThanPath"'=41 -'"NumericGreaterThanEquals"'=42 -'"NumericGreaterThanEqualsPath"'=43 -'"NumericLessThan"'=44 -'"NumericLessThanPath"'=45 -'"NumericLessThanEquals"'=46 -'"NumericLessThanEqualsPath"'=47 -'"Or"'=48 -'"StringEquals"'=49 -'"StringEqualsPath"'=50 -'"StringGreaterThan"'=51 -'"StringGreaterThanPath"'=52 -'"StringGreaterThanEquals"'=53 -'"StringGreaterThanEqualsPath"'=54 -'"StringLessThan"'=55 -'"StringLessThanPath"'=56 -'"StringLessThanEquals"'=57 -'"StringLessThanEqualsPath"'=58 -'"StringMatches"'=59 -'"TimestampEquals"'=60 -'"TimestampEqualsPath"'=61 -'"TimestampGreaterThan"'=62 -'"TimestampGreaterThanPath"'=63 -'"TimestampGreaterThanEquals"'=64 -'"TimestampGreaterThanEqualsPath"'=65 -'"TimestampLessThan"'=66 -'"TimestampLessThanPath"'=67 -'"TimestampLessThanEquals"'=68 -'"TimestampLessThanEqualsPath"'=69 -'"SecondsPath"'=70 -'"Seconds"'=71 -'"TimestampPath"'=72 -'"Timestamp"'=73 -'"TimeoutSeconds"'=74 -'"TimeoutSecondsPath"'=75 -'"HeartbeatSeconds"'=76 -'"HeartbeatSecondsPath"'=77 -'"ProcessorConfig"'=78 -'"Mode"'=79 -'"INLINE"'=80 -'"DISTRIBUTED"'=81 -'"ExecutionType"'=82 -'"STANDARD"'=83 -'"ItemProcessor"'=84 -'"Iterator"'=85 -'"ItemSelector"'=86 -'"MaxConcurrency"'=87 -'"Resource"'=88 -'"InputPath"'=89 -'"OutputPath"'=90 -'"ItemsPath"'=91 -'"ResultPath"'=92 -'"Result"'=93 -'"Parameters"'=94 -'"ResultSelector"'=95 -'"ItemReader"'=96 -'"ReaderConfig"'=97 -'"InputType"'=98 -'"CSVHeaderLocation"'=99 -'"CSVHeaders"'=100 -'"MaxItems"'=101 -'"MaxItemsPath"'=102 -'"Next"'=103 -'"End"'=104 -'"Cause"'=105 -'"CausePath"'=106 -'"Error"'=107 -'"ErrorPath"'=108 -'"Retry"'=109 -'"ErrorEquals"'=110 -'"IntervalSeconds"'=111 -'"MaxAttempts"'=112 -'"BackoffRate"'=113 -'"MaxDelaySeconds"'=114 -'"JitterStrategy"'=115 -'"FULL"'=116 -'"NONE"'=117 -'"Catch"'=118 -'"States.ALL"'=119 -'"States.DataLimitExceeded"'=120 -'"States.HeartbeatTimeout"'=121 -'"States.Timeout"'=122 -'"States.TaskFailed"'=123 -'"States.Permissions"'=124 -'"States.ResultPathMatchFailure"'=125 -'"States.ParameterPathFailure"'=126 -'"States.BranchFailed"'=127 -'"States.NoChoiceMatched"'=128 -'"States.IntrinsicFailure"'=129 -'"States.ExceedToleratedFailureThreshold"'=130 -'"States.ItemReaderFailed"'=131 -'"States.ResultWriterFailed"'=132 -'"States.Runtime"'=133 diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLParser.interp b/localstack/services/stepfunctions/asl/antlr/runtime/ASLParser.interp deleted file mode 100644 index 86919b59baac2..0000000000000 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLParser.interp +++ /dev/null @@ -1,383 +0,0 @@ -token literal names: -null -',' -':' -'[' -']' -'{' -'}' -'true' -'false' -'null' -'"Comment"' -'"States"' -'"StartAt"' -'"NextState"' -'"Version"' -'"Type"' -'"Task"' -'"Choice"' -'"Fail"' -'"Succeed"' -'"Pass"' -'"Wait"' -'"Parallel"' -'"Map"' -'"Choices"' -'"Variable"' -'"Default"' -'"Branches"' -'"And"' -'"BooleanEquals"' -'"BooleanEqualsPath"' -'"IsBoolean"' -'"IsNull"' -'"IsNumeric"' -'"IsPresent"' -'"IsString"' -'"IsTimestamp"' -'"Not"' -'"NumericEquals"' -'"NumericEqualsPath"' -'"NumericGreaterThan"' -'"NumericGreaterThanPath"' -'"NumericGreaterThanEquals"' -'"NumericGreaterThanEqualsPath"' -'"NumericLessThan"' -'"NumericLessThanPath"' -'"NumericLessThanEquals"' -'"NumericLessThanEqualsPath"' -'"Or"' -'"StringEquals"' -'"StringEqualsPath"' -'"StringGreaterThan"' -'"StringGreaterThanPath"' -'"StringGreaterThanEquals"' -'"StringGreaterThanEqualsPath"' -'"StringLessThan"' -'"StringLessThanPath"' -'"StringLessThanEquals"' -'"StringLessThanEqualsPath"' -'"StringMatches"' -'"TimestampEquals"' -'"TimestampEqualsPath"' -'"TimestampGreaterThan"' -'"TimestampGreaterThanPath"' -'"TimestampGreaterThanEquals"' -'"TimestampGreaterThanEqualsPath"' -'"TimestampLessThan"' -'"TimestampLessThanPath"' -'"TimestampLessThanEquals"' -'"TimestampLessThanEqualsPath"' -'"SecondsPath"' -'"Seconds"' -'"TimestampPath"' -'"Timestamp"' -'"TimeoutSeconds"' -'"TimeoutSecondsPath"' -'"HeartbeatSeconds"' -'"HeartbeatSecondsPath"' -'"ProcessorConfig"' -'"Mode"' -'"INLINE"' -'"DISTRIBUTED"' -'"ExecutionType"' -'"STANDARD"' -'"ItemProcessor"' -'"Iterator"' -'"ItemSelector"' -'"MaxConcurrency"' -'"Resource"' -'"InputPath"' -'"OutputPath"' -'"ItemsPath"' -'"ResultPath"' -'"Result"' -'"Parameters"' -'"ResultSelector"' -'"ItemReader"' -'"ReaderConfig"' -'"InputType"' -'"CSVHeaderLocation"' -'"CSVHeaders"' -'"MaxItems"' -'"MaxItemsPath"' -'"Next"' -'"End"' -'"Cause"' -'"CausePath"' -'"Error"' -'"ErrorPath"' -'"Retry"' -'"ErrorEquals"' -'"IntervalSeconds"' -'"MaxAttempts"' -'"BackoffRate"' -'"MaxDelaySeconds"' -'"JitterStrategy"' -'"FULL"' -'"NONE"' -'"Catch"' -'"States.ALL"' -'"States.DataLimitExceeded"' -'"States.HeartbeatTimeout"' -'"States.Timeout"' -'"States.TaskFailed"' -'"States.Permissions"' -'"States.ResultPathMatchFailure"' -'"States.ParameterPathFailure"' -'"States.BranchFailed"' -'"States.NoChoiceMatched"' -'"States.IntrinsicFailure"' -'"States.ExceedToleratedFailureThreshold"' -'"States.ItemReaderFailed"' -'"States.ResultWriterFailed"' -'"States.Runtime"' -null -null -null -null -null -null -null - -token symbolic names: -null -COMMA -COLON -LBRACK -RBRACK -LBRACE -RBRACE -TRUE -FALSE -NULL -COMMENT -STATES -STARTAT -NEXTSTATE -VERSION -TYPE -TASK -CHOICE -FAIL -SUCCEED -PASS -WAIT -PARALLEL -MAP -CHOICES -VARIABLE -DEFAULT -BRANCHES -AND -BOOLEANEQUALS -BOOLEANQUALSPATH -ISBOOLEAN -ISNULL -ISNUMERIC -ISPRESENT -ISSTRING -ISTIMESTAMP -NOT -NUMERICEQUALS -NUMERICEQUALSPATH -NUMERICGREATERTHAN -NUMERICGREATERTHANPATH -NUMERICGREATERTHANEQUALS -NUMERICGREATERTHANEQUALSPATH -NUMERICLESSTHAN -NUMERICLESSTHANPATH -NUMERICLESSTHANEQUALS -NUMERICLESSTHANEQUALSPATH -OR -STRINGEQUALS -STRINGEQUALSPATH -STRINGGREATERTHAN -STRINGGREATERTHANPATH -STRINGGREATERTHANEQUALS -STRINGGREATERTHANEQUALSPATH -STRINGLESSTHAN -STRINGLESSTHANPATH -STRINGLESSTHANEQUALS -STRINGLESSTHANEQUALSPATH -STRINGMATCHES -TIMESTAMPEQUALS -TIMESTAMPEQUALSPATH -TIMESTAMPGREATERTHAN -TIMESTAMPGREATERTHANPATH -TIMESTAMPGREATERTHANEQUALS -TIMESTAMPGREATERTHANEQUALSPATH -TIMESTAMPLESSTHAN -TIMESTAMPLESSTHANPATH -TIMESTAMPLESSTHANEQUALS -TIMESTAMPLESSTHANEQUALSPATH -SECONDSPATH -SECONDS -TIMESTAMPPATH -TIMESTAMP -TIMEOUTSECONDS -TIMEOUTSECONDSPATH -HEARTBEATSECONDS -HEARTBEATSECONDSPATH -PROCESSORCONFIG -MODE -INLINE -DISTRIBUTED -EXECUTIONTYPE -STANDARD -ITEMPROCESSOR -ITERATOR -ITEMSELECTOR -MAXCONCURRENCY -RESOURCE -INPUTPATH -OUTPUTPATH -ITEMSPATH -RESULTPATH -RESULT -PARAMETERS -RESULTSELECTOR -ITEMREADER -READERCONFIG -INPUTTYPE -CSVHEADERLOCATION -CSVHEADERS -MAXITEMS -MAXITEMSPATH -NEXT -END -CAUSE -CAUSEPATH -ERROR -ERRORPATH -RETRY -ERROREQUALS -INTERVALSECONDS -MAXATTEMPTS -BACKOFFRATE -MAXDELAYSECONDS -JITTERSTRATEGY -FULL -NONE -CATCH -ERRORNAMEStatesALL -ERRORNAMEStatesDataLimitExceeded -ERRORNAMEStatesHeartbeatTimeout -ERRORNAMEStatesTimeout -ERRORNAMEStatesTaskFailed -ERRORNAMEStatesPermissions -ERRORNAMEStatesResultPathMatchFailure -ERRORNAMEStatesParameterPathFailure -ERRORNAMEStatesBranchFailed -ERRORNAMEStatesNoChoiceMatched -ERRORNAMEStatesIntrinsicFailure -ERRORNAMEStatesExceedToleratedFailureThreshold -ERRORNAMEStatesItemReaderFailed -ERRORNAMEStatesResultWriterFailed -ERRORNAMEStatesRuntime -STRINGDOLLAR -STRINGPATHCONTEXTOBJ -STRINGPATH -STRING -INT -NUMBER -WS - -rule names: -state_machine -program_decl -top_layer_stmt -startat_decl -comment_decl -version_decl -state_stmt -states_decl -state_name -state_decl -state_decl_body -type_decl -next_decl -resource_decl -input_path_decl -result_decl -result_path_decl -output_path_decl -end_decl -default_decl -error_decl -error_path_decl -cause_decl -cause_path_decl -seconds_decl -seconds_path_decl -timestamp_decl -timestamp_path_decl -items_path_decl -max_concurrency_decl -parameters_decl -timeout_seconds_decl -timeout_seconds_path_decl -heartbeat_seconds_decl -heartbeat_seconds_path_decl -payload_tmpl_decl -payload_binding -intrinsic_func -payload_arr_decl -payload_value_decl -payload_value_lit -result_selector_decl -state_type -choices_decl -choice_rule -comparison_variable_stmt -comparison_composite_stmt -comparison_composite -variable_decl -comparison_func -branches_decl -item_processor_decl -item_processor_item -processor_config_decl -processor_config_field -mode_decl -mode_type -execution_decl -execution_type -iterator_decl -iterator_decl_item -item_selector_decl -item_reader_decl -items_reader_field -reader_config_decl -reader_config_field -input_type_decl -csv_header_location_decl -csv_headers_decl -max_items_decl -max_items_path_decl -retry_decl -retrier_decl -retrier_stmt -error_equals_decl -interval_seconds_decl -max_attempts_decl -backoff_rate_decl -max_delay_seconds_decl -jitter_strategy_decl -catch_decl -catcher_decl -catcher_stmt -comparison_op -choice_operator -states_error_name -error_name -json_obj_decl -json_binding -json_arr_decl -json_value_decl -keyword_or_string - - -atn: -[4, 1, 140, 838, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 2, 61, 7, 61, 2, 62, 7, 62, 2, 63, 7, 63, 2, 64, 7, 64, 2, 65, 7, 65, 2, 66, 7, 66, 2, 67, 7, 67, 2, 68, 7, 68, 2, 69, 7, 69, 2, 70, 7, 70, 2, 71, 7, 71, 2, 72, 7, 72, 2, 73, 7, 73, 2, 74, 7, 74, 2, 75, 7, 75, 2, 76, 7, 76, 2, 77, 7, 77, 2, 78, 7, 78, 2, 79, 7, 79, 2, 80, 7, 80, 2, 81, 7, 81, 2, 82, 7, 82, 2, 83, 7, 83, 2, 84, 7, 84, 2, 85, 7, 85, 2, 86, 7, 86, 2, 87, 7, 87, 2, 88, 7, 88, 2, 89, 7, 89, 2, 90, 7, 90, 2, 91, 7, 91, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 192, 8, 1, 10, 1, 12, 1, 195, 9, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 204, 8, 2, 1, 3, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 1, 6, 3, 6, 252, 8, 6, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 7, 5, 7, 260, 8, 7, 10, 7, 12, 7, 263, 9, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 10, 5, 10, 277, 8, 10, 10, 10, 12, 10, 280, 9, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 12, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 3, 14, 300, 8, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 1, 16, 3, 16, 310, 8, 16, 1, 17, 1, 17, 1, 17, 1, 17, 3, 17, 316, 8, 17, 1, 18, 1, 18, 1, 18, 1, 18, 1, 19, 1, 19, 1, 19, 1, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 3, 21, 336, 8, 21, 1, 22, 1, 22, 1, 22, 1, 22, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 1, 23, 3, 23, 348, 8, 23, 1, 24, 1, 24, 1, 24, 1, 24, 1, 25, 1, 25, 1, 25, 1, 25, 1, 26, 1, 26, 1, 26, 1, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 28, 1, 28, 1, 28, 1, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 1, 32, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 34, 1, 34, 1, 34, 1, 34, 1, 35, 1, 35, 1, 35, 1, 35, 5, 35, 398, 8, 35, 10, 35, 12, 35, 401, 9, 35, 1, 35, 1, 35, 1, 35, 1, 35, 3, 35, 407, 8, 35, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 3, 36, 422, 8, 36, 1, 37, 1, 37, 1, 38, 1, 38, 1, 38, 1, 38, 5, 38, 430, 8, 38, 10, 38, 12, 38, 433, 9, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 439, 8, 38, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 445, 8, 39, 1, 40, 1, 40, 1, 40, 1, 40, 1, 40, 3, 40, 452, 8, 40, 1, 41, 1, 41, 1, 41, 1, 41, 1, 42, 1, 42, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 1, 43, 5, 43, 466, 8, 43, 10, 43, 12, 43, 469, 9, 43, 1, 43, 1, 43, 1, 44, 1, 44, 1, 44, 1, 44, 4, 44, 477, 8, 44, 11, 44, 12, 44, 478, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 5, 44, 487, 8, 44, 10, 44, 12, 44, 490, 9, 44, 1, 44, 1, 44, 3, 44, 494, 8, 44, 1, 45, 1, 45, 1, 45, 1, 45, 3, 45, 500, 8, 45, 1, 46, 1, 46, 3, 46, 504, 8, 46, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, 1, 47, 5, 47, 513, 8, 47, 10, 47, 12, 47, 516, 9, 47, 1, 47, 1, 47, 3, 47, 520, 8, 47, 1, 48, 1, 48, 1, 48, 1, 48, 1, 49, 1, 49, 1, 49, 1, 49, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 5, 50, 536, 8, 50, 10, 50, 12, 50, 539, 9, 50, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 5, 51, 549, 8, 51, 10, 51, 12, 51, 552, 9, 51, 1, 51, 1, 51, 1, 52, 1, 52, 1, 52, 1, 52, 3, 52, 560, 8, 52, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 1, 53, 5, 53, 568, 8, 53, 10, 53, 12, 53, 571, 9, 53, 1, 53, 1, 53, 1, 54, 1, 54, 3, 54, 577, 8, 54, 1, 55, 1, 55, 1, 55, 1, 55, 1, 56, 1, 56, 1, 57, 1, 57, 1, 57, 1, 57, 1, 58, 1, 58, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 5, 59, 597, 8, 59, 10, 59, 12, 59, 600, 9, 59, 1, 59, 1, 59, 1, 60, 1, 60, 1, 60, 1, 60, 3, 60, 608, 8, 60, 1, 61, 1, 61, 1, 61, 1, 61, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 1, 62, 5, 62, 620, 8, 62, 10, 62, 12, 62, 623, 9, 62, 1, 62, 1, 62, 1, 63, 1, 63, 1, 63, 3, 63, 630, 8, 63, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 1, 64, 5, 64, 638, 8, 64, 10, 64, 12, 64, 641, 9, 64, 1, 64, 1, 64, 1, 65, 1, 65, 1, 65, 1, 65, 1, 65, 3, 65, 650, 8, 65, 1, 66, 1, 66, 1, 66, 1, 66, 1, 67, 1, 67, 1, 67, 1, 67, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 1, 68, 5, 68, 666, 8, 68, 10, 68, 12, 68, 669, 9, 68, 1, 68, 1, 68, 1, 69, 1, 69, 1, 69, 1, 69, 1, 70, 1, 70, 1, 70, 1, 70, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 1, 71, 5, 71, 687, 8, 71, 10, 71, 12, 71, 690, 9, 71, 3, 71, 692, 8, 71, 1, 71, 1, 71, 1, 72, 1, 72, 1, 72, 1, 72, 5, 72, 700, 8, 72, 10, 72, 12, 72, 703, 9, 72, 1, 72, 1, 72, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 1, 73, 3, 73, 714, 8, 73, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 1, 74, 5, 74, 722, 8, 74, 10, 74, 12, 74, 725, 9, 74, 1, 74, 1, 74, 1, 75, 1, 75, 1, 75, 1, 75, 1, 76, 1, 76, 1, 76, 1, 76, 1, 77, 1, 77, 1, 77, 1, 77, 1, 78, 1, 78, 1, 78, 1, 78, 1, 79, 1, 79, 1, 79, 1, 79, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 1, 80, 5, 80, 755, 8, 80, 10, 80, 12, 80, 758, 9, 80, 3, 80, 760, 8, 80, 1, 80, 1, 80, 1, 81, 1, 81, 1, 81, 1, 81, 5, 81, 768, 8, 81, 10, 81, 12, 81, 771, 9, 81, 1, 81, 1, 81, 1, 82, 1, 82, 1, 82, 1, 82, 3, 82, 779, 8, 82, 1, 83, 1, 83, 1, 84, 1, 84, 1, 85, 1, 85, 1, 86, 1, 86, 3, 86, 789, 8, 86, 1, 87, 1, 87, 1, 87, 1, 87, 5, 87, 795, 8, 87, 10, 87, 12, 87, 798, 9, 87, 1, 87, 1, 87, 1, 87, 1, 87, 3, 87, 804, 8, 87, 1, 88, 1, 88, 1, 88, 1, 88, 1, 89, 1, 89, 1, 89, 1, 89, 5, 89, 814, 8, 89, 10, 89, 12, 89, 817, 9, 89, 1, 89, 1, 89, 1, 89, 1, 89, 3, 89, 823, 8, 89, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 1, 90, 3, 90, 834, 8, 90, 1, 91, 1, 91, 1, 91, 0, 0, 92, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 160, 162, 164, 166, 168, 170, 172, 174, 176, 178, 180, 182, 0, 9, 1, 0, 7, 8, 1, 0, 16, 23, 1, 0, 80, 81, 1, 0, 138, 139, 1, 0, 116, 117, 3, 0, 29, 36, 38, 47, 49, 69, 3, 0, 28, 28, 37, 37, 48, 48, 1, 0, 119, 133, 5, 0, 10, 13, 15, 105, 107, 107, 109, 119, 121, 137, 863, 0, 184, 1, 0, 0, 0, 2, 187, 1, 0, 0, 0, 4, 203, 1, 0, 0, 0, 6, 205, 1, 0, 0, 0, 8, 209, 1, 0, 0, 0, 10, 213, 1, 0, 0, 0, 12, 251, 1, 0, 0, 0, 14, 253, 1, 0, 0, 0, 16, 266, 1, 0, 0, 0, 18, 268, 1, 0, 0, 0, 20, 272, 1, 0, 0, 0, 22, 283, 1, 0, 0, 0, 24, 287, 1, 0, 0, 0, 26, 291, 1, 0, 0, 0, 28, 295, 1, 0, 0, 0, 30, 301, 1, 0, 0, 0, 32, 305, 1, 0, 0, 0, 34, 311, 1, 0, 0, 0, 36, 317, 1, 0, 0, 0, 38, 321, 1, 0, 0, 0, 40, 325, 1, 0, 0, 0, 42, 335, 1, 0, 0, 0, 44, 337, 1, 0, 0, 0, 46, 347, 1, 0, 0, 0, 48, 349, 1, 0, 0, 0, 50, 353, 1, 0, 0, 0, 52, 357, 1, 0, 0, 0, 54, 361, 1, 0, 0, 0, 56, 365, 1, 0, 0, 0, 58, 369, 1, 0, 0, 0, 60, 373, 1, 0, 0, 0, 62, 377, 1, 0, 0, 0, 64, 381, 1, 0, 0, 0, 66, 385, 1, 0, 0, 0, 68, 389, 1, 0, 0, 0, 70, 406, 1, 0, 0, 0, 72, 421, 1, 0, 0, 0, 74, 423, 1, 0, 0, 0, 76, 438, 1, 0, 0, 0, 78, 444, 1, 0, 0, 0, 80, 451, 1, 0, 0, 0, 82, 453, 1, 0, 0, 0, 84, 457, 1, 0, 0, 0, 86, 459, 1, 0, 0, 0, 88, 493, 1, 0, 0, 0, 90, 499, 1, 0, 0, 0, 92, 503, 1, 0, 0, 0, 94, 505, 1, 0, 0, 0, 96, 521, 1, 0, 0, 0, 98, 525, 1, 0, 0, 0, 100, 529, 1, 0, 0, 0, 102, 542, 1, 0, 0, 0, 104, 559, 1, 0, 0, 0, 106, 561, 1, 0, 0, 0, 108, 576, 1, 0, 0, 0, 110, 578, 1, 0, 0, 0, 112, 582, 1, 0, 0, 0, 114, 584, 1, 0, 0, 0, 116, 588, 1, 0, 0, 0, 118, 590, 1, 0, 0, 0, 120, 607, 1, 0, 0, 0, 122, 609, 1, 0, 0, 0, 124, 613, 1, 0, 0, 0, 126, 629, 1, 0, 0, 0, 128, 631, 1, 0, 0, 0, 130, 649, 1, 0, 0, 0, 132, 651, 1, 0, 0, 0, 134, 655, 1, 0, 0, 0, 136, 659, 1, 0, 0, 0, 138, 672, 1, 0, 0, 0, 140, 676, 1, 0, 0, 0, 142, 680, 1, 0, 0, 0, 144, 695, 1, 0, 0, 0, 146, 713, 1, 0, 0, 0, 148, 715, 1, 0, 0, 0, 150, 728, 1, 0, 0, 0, 152, 732, 1, 0, 0, 0, 154, 736, 1, 0, 0, 0, 156, 740, 1, 0, 0, 0, 158, 744, 1, 0, 0, 0, 160, 748, 1, 0, 0, 0, 162, 763, 1, 0, 0, 0, 164, 778, 1, 0, 0, 0, 166, 780, 1, 0, 0, 0, 168, 782, 1, 0, 0, 0, 170, 784, 1, 0, 0, 0, 172, 788, 1, 0, 0, 0, 174, 803, 1, 0, 0, 0, 176, 805, 1, 0, 0, 0, 178, 822, 1, 0, 0, 0, 180, 833, 1, 0, 0, 0, 182, 835, 1, 0, 0, 0, 184, 185, 3, 2, 1, 0, 185, 186, 5, 0, 0, 1, 186, 1, 1, 0, 0, 0, 187, 188, 5, 5, 0, 0, 188, 193, 3, 4, 2, 0, 189, 190, 5, 1, 0, 0, 190, 192, 3, 4, 2, 0, 191, 189, 1, 0, 0, 0, 192, 195, 1, 0, 0, 0, 193, 191, 1, 0, 0, 0, 193, 194, 1, 0, 0, 0, 194, 196, 1, 0, 0, 0, 195, 193, 1, 0, 0, 0, 196, 197, 5, 6, 0, 0, 197, 3, 1, 0, 0, 0, 198, 204, 3, 8, 4, 0, 199, 204, 3, 10, 5, 0, 200, 204, 3, 6, 3, 0, 201, 204, 3, 14, 7, 0, 202, 204, 3, 62, 31, 0, 203, 198, 1, 0, 0, 0, 203, 199, 1, 0, 0, 0, 203, 200, 1, 0, 0, 0, 203, 201, 1, 0, 0, 0, 203, 202, 1, 0, 0, 0, 204, 5, 1, 0, 0, 0, 205, 206, 5, 12, 0, 0, 206, 207, 5, 2, 0, 0, 207, 208, 3, 182, 91, 0, 208, 7, 1, 0, 0, 0, 209, 210, 5, 10, 0, 0, 210, 211, 5, 2, 0, 0, 211, 212, 3, 182, 91, 0, 212, 9, 1, 0, 0, 0, 213, 214, 5, 14, 0, 0, 214, 215, 5, 2, 0, 0, 215, 216, 3, 182, 91, 0, 216, 11, 1, 0, 0, 0, 217, 252, 3, 8, 4, 0, 218, 252, 3, 22, 11, 0, 219, 252, 3, 28, 14, 0, 220, 252, 3, 26, 13, 0, 221, 252, 3, 24, 12, 0, 222, 252, 3, 30, 15, 0, 223, 252, 3, 32, 16, 0, 224, 252, 3, 34, 17, 0, 225, 252, 3, 36, 18, 0, 226, 252, 3, 38, 19, 0, 227, 252, 3, 86, 43, 0, 228, 252, 3, 40, 20, 0, 229, 252, 3, 42, 21, 0, 230, 252, 3, 44, 22, 0, 231, 252, 3, 46, 23, 0, 232, 252, 3, 48, 24, 0, 233, 252, 3, 50, 25, 0, 234, 252, 3, 52, 26, 0, 235, 252, 3, 54, 27, 0, 236, 252, 3, 56, 28, 0, 237, 252, 3, 102, 51, 0, 238, 252, 3, 118, 59, 0, 239, 252, 3, 122, 61, 0, 240, 252, 3, 124, 62, 0, 241, 252, 3, 58, 29, 0, 242, 252, 3, 62, 31, 0, 243, 252, 3, 64, 32, 0, 244, 252, 3, 66, 33, 0, 245, 252, 3, 68, 34, 0, 246, 252, 3, 100, 50, 0, 247, 252, 3, 60, 30, 0, 248, 252, 3, 142, 71, 0, 249, 252, 3, 160, 80, 0, 250, 252, 3, 82, 41, 0, 251, 217, 1, 0, 0, 0, 251, 218, 1, 0, 0, 0, 251, 219, 1, 0, 0, 0, 251, 220, 1, 0, 0, 0, 251, 221, 1, 0, 0, 0, 251, 222, 1, 0, 0, 0, 251, 223, 1, 0, 0, 0, 251, 224, 1, 0, 0, 0, 251, 225, 1, 0, 0, 0, 251, 226, 1, 0, 0, 0, 251, 227, 1, 0, 0, 0, 251, 228, 1, 0, 0, 0, 251, 229, 1, 0, 0, 0, 251, 230, 1, 0, 0, 0, 251, 231, 1, 0, 0, 0, 251, 232, 1, 0, 0, 0, 251, 233, 1, 0, 0, 0, 251, 234, 1, 0, 0, 0, 251, 235, 1, 0, 0, 0, 251, 236, 1, 0, 0, 0, 251, 237, 1, 0, 0, 0, 251, 238, 1, 0, 0, 0, 251, 239, 1, 0, 0, 0, 251, 240, 1, 0, 0, 0, 251, 241, 1, 0, 0, 0, 251, 242, 1, 0, 0, 0, 251, 243, 1, 0, 0, 0, 251, 244, 1, 0, 0, 0, 251, 245, 1, 0, 0, 0, 251, 246, 1, 0, 0, 0, 251, 247, 1, 0, 0, 0, 251, 248, 1, 0, 0, 0, 251, 249, 1, 0, 0, 0, 251, 250, 1, 0, 0, 0, 252, 13, 1, 0, 0, 0, 253, 254, 5, 11, 0, 0, 254, 255, 5, 2, 0, 0, 255, 256, 5, 5, 0, 0, 256, 261, 3, 18, 9, 0, 257, 258, 5, 1, 0, 0, 258, 260, 3, 18, 9, 0, 259, 257, 1, 0, 0, 0, 260, 263, 1, 0, 0, 0, 261, 259, 1, 0, 0, 0, 261, 262, 1, 0, 0, 0, 262, 264, 1, 0, 0, 0, 263, 261, 1, 0, 0, 0, 264, 265, 5, 6, 0, 0, 265, 15, 1, 0, 0, 0, 266, 267, 3, 182, 91, 0, 267, 17, 1, 0, 0, 0, 268, 269, 3, 16, 8, 0, 269, 270, 5, 2, 0, 0, 270, 271, 3, 20, 10, 0, 271, 19, 1, 0, 0, 0, 272, 273, 5, 5, 0, 0, 273, 278, 3, 12, 6, 0, 274, 275, 5, 1, 0, 0, 275, 277, 3, 12, 6, 0, 276, 274, 1, 0, 0, 0, 277, 280, 1, 0, 0, 0, 278, 276, 1, 0, 0, 0, 278, 279, 1, 0, 0, 0, 279, 281, 1, 0, 0, 0, 280, 278, 1, 0, 0, 0, 281, 282, 5, 6, 0, 0, 282, 21, 1, 0, 0, 0, 283, 284, 5, 15, 0, 0, 284, 285, 5, 2, 0, 0, 285, 286, 3, 84, 42, 0, 286, 23, 1, 0, 0, 0, 287, 288, 5, 103, 0, 0, 288, 289, 5, 2, 0, 0, 289, 290, 3, 182, 91, 0, 290, 25, 1, 0, 0, 0, 291, 292, 5, 88, 0, 0, 292, 293, 5, 2, 0, 0, 293, 294, 3, 182, 91, 0, 294, 27, 1, 0, 0, 0, 295, 296, 5, 89, 0, 0, 296, 299, 5, 2, 0, 0, 297, 300, 5, 9, 0, 0, 298, 300, 3, 182, 91, 0, 299, 297, 1, 0, 0, 0, 299, 298, 1, 0, 0, 0, 300, 29, 1, 0, 0, 0, 301, 302, 5, 93, 0, 0, 302, 303, 5, 2, 0, 0, 303, 304, 3, 180, 90, 0, 304, 31, 1, 0, 0, 0, 305, 306, 5, 92, 0, 0, 306, 309, 5, 2, 0, 0, 307, 310, 5, 9, 0, 0, 308, 310, 3, 182, 91, 0, 309, 307, 1, 0, 0, 0, 309, 308, 1, 0, 0, 0, 310, 33, 1, 0, 0, 0, 311, 312, 5, 90, 0, 0, 312, 315, 5, 2, 0, 0, 313, 316, 5, 9, 0, 0, 314, 316, 3, 182, 91, 0, 315, 313, 1, 0, 0, 0, 315, 314, 1, 0, 0, 0, 316, 35, 1, 0, 0, 0, 317, 318, 5, 104, 0, 0, 318, 319, 5, 2, 0, 0, 319, 320, 7, 0, 0, 0, 320, 37, 1, 0, 0, 0, 321, 322, 5, 26, 0, 0, 322, 323, 5, 2, 0, 0, 323, 324, 3, 182, 91, 0, 324, 39, 1, 0, 0, 0, 325, 326, 5, 107, 0, 0, 326, 327, 5, 2, 0, 0, 327, 328, 3, 182, 91, 0, 328, 41, 1, 0, 0, 0, 329, 330, 5, 108, 0, 0, 330, 331, 5, 2, 0, 0, 331, 336, 5, 136, 0, 0, 332, 333, 5, 108, 0, 0, 333, 334, 5, 2, 0, 0, 334, 336, 3, 74, 37, 0, 335, 329, 1, 0, 0, 0, 335, 332, 1, 0, 0, 0, 336, 43, 1, 0, 0, 0, 337, 338, 5, 105, 0, 0, 338, 339, 5, 2, 0, 0, 339, 340, 3, 182, 91, 0, 340, 45, 1, 0, 0, 0, 341, 342, 5, 106, 0, 0, 342, 343, 5, 2, 0, 0, 343, 348, 5, 136, 0, 0, 344, 345, 5, 106, 0, 0, 345, 346, 5, 2, 0, 0, 346, 348, 3, 74, 37, 0, 347, 341, 1, 0, 0, 0, 347, 344, 1, 0, 0, 0, 348, 47, 1, 0, 0, 0, 349, 350, 5, 71, 0, 0, 350, 351, 5, 2, 0, 0, 351, 352, 5, 138, 0, 0, 352, 49, 1, 0, 0, 0, 353, 354, 5, 70, 0, 0, 354, 355, 5, 2, 0, 0, 355, 356, 3, 182, 91, 0, 356, 51, 1, 0, 0, 0, 357, 358, 5, 73, 0, 0, 358, 359, 5, 2, 0, 0, 359, 360, 3, 182, 91, 0, 360, 53, 1, 0, 0, 0, 361, 362, 5, 72, 0, 0, 362, 363, 5, 2, 0, 0, 363, 364, 3, 182, 91, 0, 364, 55, 1, 0, 0, 0, 365, 366, 5, 91, 0, 0, 366, 367, 5, 2, 0, 0, 367, 368, 3, 182, 91, 0, 368, 57, 1, 0, 0, 0, 369, 370, 5, 87, 0, 0, 370, 371, 5, 2, 0, 0, 371, 372, 5, 138, 0, 0, 372, 59, 1, 0, 0, 0, 373, 374, 5, 94, 0, 0, 374, 375, 5, 2, 0, 0, 375, 376, 3, 70, 35, 0, 376, 61, 1, 0, 0, 0, 377, 378, 5, 74, 0, 0, 378, 379, 5, 2, 0, 0, 379, 380, 5, 138, 0, 0, 380, 63, 1, 0, 0, 0, 381, 382, 5, 75, 0, 0, 382, 383, 5, 2, 0, 0, 383, 384, 5, 136, 0, 0, 384, 65, 1, 0, 0, 0, 385, 386, 5, 76, 0, 0, 386, 387, 5, 2, 0, 0, 387, 388, 5, 138, 0, 0, 388, 67, 1, 0, 0, 0, 389, 390, 5, 77, 0, 0, 390, 391, 5, 2, 0, 0, 391, 392, 5, 136, 0, 0, 392, 69, 1, 0, 0, 0, 393, 394, 5, 5, 0, 0, 394, 399, 3, 72, 36, 0, 395, 396, 5, 1, 0, 0, 396, 398, 3, 72, 36, 0, 397, 395, 1, 0, 0, 0, 398, 401, 1, 0, 0, 0, 399, 397, 1, 0, 0, 0, 399, 400, 1, 0, 0, 0, 400, 402, 1, 0, 0, 0, 401, 399, 1, 0, 0, 0, 402, 403, 5, 6, 0, 0, 403, 407, 1, 0, 0, 0, 404, 405, 5, 5, 0, 0, 405, 407, 5, 6, 0, 0, 406, 393, 1, 0, 0, 0, 406, 404, 1, 0, 0, 0, 407, 71, 1, 0, 0, 0, 408, 409, 5, 134, 0, 0, 409, 410, 5, 2, 0, 0, 410, 422, 5, 136, 0, 0, 411, 412, 5, 134, 0, 0, 412, 413, 5, 2, 0, 0, 413, 422, 5, 135, 0, 0, 414, 415, 5, 134, 0, 0, 415, 416, 5, 2, 0, 0, 416, 422, 3, 74, 37, 0, 417, 418, 3, 182, 91, 0, 418, 419, 5, 2, 0, 0, 419, 420, 3, 78, 39, 0, 420, 422, 1, 0, 0, 0, 421, 408, 1, 0, 0, 0, 421, 411, 1, 0, 0, 0, 421, 414, 1, 0, 0, 0, 421, 417, 1, 0, 0, 0, 422, 73, 1, 0, 0, 0, 423, 424, 5, 137, 0, 0, 424, 75, 1, 0, 0, 0, 425, 426, 5, 3, 0, 0, 426, 431, 3, 78, 39, 0, 427, 428, 5, 1, 0, 0, 428, 430, 3, 78, 39, 0, 429, 427, 1, 0, 0, 0, 430, 433, 1, 0, 0, 0, 431, 429, 1, 0, 0, 0, 431, 432, 1, 0, 0, 0, 432, 434, 1, 0, 0, 0, 433, 431, 1, 0, 0, 0, 434, 435, 5, 4, 0, 0, 435, 439, 1, 0, 0, 0, 436, 437, 5, 3, 0, 0, 437, 439, 5, 4, 0, 0, 438, 425, 1, 0, 0, 0, 438, 436, 1, 0, 0, 0, 439, 77, 1, 0, 0, 0, 440, 445, 3, 72, 36, 0, 441, 445, 3, 76, 38, 0, 442, 445, 3, 70, 35, 0, 443, 445, 3, 80, 40, 0, 444, 440, 1, 0, 0, 0, 444, 441, 1, 0, 0, 0, 444, 442, 1, 0, 0, 0, 444, 443, 1, 0, 0, 0, 445, 79, 1, 0, 0, 0, 446, 452, 5, 139, 0, 0, 447, 452, 5, 138, 0, 0, 448, 452, 7, 0, 0, 0, 449, 452, 5, 9, 0, 0, 450, 452, 3, 182, 91, 0, 451, 446, 1, 0, 0, 0, 451, 447, 1, 0, 0, 0, 451, 448, 1, 0, 0, 0, 451, 449, 1, 0, 0, 0, 451, 450, 1, 0, 0, 0, 452, 81, 1, 0, 0, 0, 453, 454, 5, 95, 0, 0, 454, 455, 5, 2, 0, 0, 455, 456, 3, 70, 35, 0, 456, 83, 1, 0, 0, 0, 457, 458, 7, 1, 0, 0, 458, 85, 1, 0, 0, 0, 459, 460, 5, 24, 0, 0, 460, 461, 5, 2, 0, 0, 461, 462, 5, 3, 0, 0, 462, 467, 3, 88, 44, 0, 463, 464, 5, 1, 0, 0, 464, 466, 3, 88, 44, 0, 465, 463, 1, 0, 0, 0, 466, 469, 1, 0, 0, 0, 467, 465, 1, 0, 0, 0, 467, 468, 1, 0, 0, 0, 468, 470, 1, 0, 0, 0, 469, 467, 1, 0, 0, 0, 470, 471, 5, 4, 0, 0, 471, 87, 1, 0, 0, 0, 472, 473, 5, 5, 0, 0, 473, 476, 3, 90, 45, 0, 474, 475, 5, 1, 0, 0, 475, 477, 3, 90, 45, 0, 476, 474, 1, 0, 0, 0, 477, 478, 1, 0, 0, 0, 478, 476, 1, 0, 0, 0, 478, 479, 1, 0, 0, 0, 479, 480, 1, 0, 0, 0, 480, 481, 5, 6, 0, 0, 481, 494, 1, 0, 0, 0, 482, 483, 5, 5, 0, 0, 483, 488, 3, 92, 46, 0, 484, 485, 5, 1, 0, 0, 485, 487, 3, 92, 46, 0, 486, 484, 1, 0, 0, 0, 487, 490, 1, 0, 0, 0, 488, 486, 1, 0, 0, 0, 488, 489, 1, 0, 0, 0, 489, 491, 1, 0, 0, 0, 490, 488, 1, 0, 0, 0, 491, 492, 5, 6, 0, 0, 492, 494, 1, 0, 0, 0, 493, 472, 1, 0, 0, 0, 493, 482, 1, 0, 0, 0, 494, 89, 1, 0, 0, 0, 495, 500, 3, 96, 48, 0, 496, 500, 3, 98, 49, 0, 497, 500, 3, 24, 12, 0, 498, 500, 3, 8, 4, 0, 499, 495, 1, 0, 0, 0, 499, 496, 1, 0, 0, 0, 499, 497, 1, 0, 0, 0, 499, 498, 1, 0, 0, 0, 500, 91, 1, 0, 0, 0, 501, 504, 3, 94, 47, 0, 502, 504, 3, 24, 12, 0, 503, 501, 1, 0, 0, 0, 503, 502, 1, 0, 0, 0, 504, 93, 1, 0, 0, 0, 505, 506, 3, 168, 84, 0, 506, 519, 5, 2, 0, 0, 507, 520, 3, 88, 44, 0, 508, 509, 5, 3, 0, 0, 509, 514, 3, 88, 44, 0, 510, 511, 5, 1, 0, 0, 511, 513, 3, 88, 44, 0, 512, 510, 1, 0, 0, 0, 513, 516, 1, 0, 0, 0, 514, 512, 1, 0, 0, 0, 514, 515, 1, 0, 0, 0, 515, 517, 1, 0, 0, 0, 516, 514, 1, 0, 0, 0, 517, 518, 5, 4, 0, 0, 518, 520, 1, 0, 0, 0, 519, 507, 1, 0, 0, 0, 519, 508, 1, 0, 0, 0, 520, 95, 1, 0, 0, 0, 521, 522, 5, 25, 0, 0, 522, 523, 5, 2, 0, 0, 523, 524, 3, 182, 91, 0, 524, 97, 1, 0, 0, 0, 525, 526, 3, 166, 83, 0, 526, 527, 5, 2, 0, 0, 527, 528, 3, 180, 90, 0, 528, 99, 1, 0, 0, 0, 529, 530, 5, 27, 0, 0, 530, 531, 5, 2, 0, 0, 531, 532, 5, 3, 0, 0, 532, 537, 3, 2, 1, 0, 533, 534, 5, 1, 0, 0, 534, 536, 3, 2, 1, 0, 535, 533, 1, 0, 0, 0, 536, 539, 1, 0, 0, 0, 537, 535, 1, 0, 0, 0, 537, 538, 1, 0, 0, 0, 538, 540, 1, 0, 0, 0, 539, 537, 1, 0, 0, 0, 540, 541, 5, 4, 0, 0, 541, 101, 1, 0, 0, 0, 542, 543, 5, 84, 0, 0, 543, 544, 5, 2, 0, 0, 544, 545, 5, 5, 0, 0, 545, 550, 3, 104, 52, 0, 546, 547, 5, 1, 0, 0, 547, 549, 3, 104, 52, 0, 548, 546, 1, 0, 0, 0, 549, 552, 1, 0, 0, 0, 550, 548, 1, 0, 0, 0, 550, 551, 1, 0, 0, 0, 551, 553, 1, 0, 0, 0, 552, 550, 1, 0, 0, 0, 553, 554, 5, 6, 0, 0, 554, 103, 1, 0, 0, 0, 555, 560, 3, 106, 53, 0, 556, 560, 3, 6, 3, 0, 557, 560, 3, 14, 7, 0, 558, 560, 3, 8, 4, 0, 559, 555, 1, 0, 0, 0, 559, 556, 1, 0, 0, 0, 559, 557, 1, 0, 0, 0, 559, 558, 1, 0, 0, 0, 560, 105, 1, 0, 0, 0, 561, 562, 5, 78, 0, 0, 562, 563, 5, 2, 0, 0, 563, 564, 5, 5, 0, 0, 564, 569, 3, 108, 54, 0, 565, 566, 5, 1, 0, 0, 566, 568, 3, 108, 54, 0, 567, 565, 1, 0, 0, 0, 568, 571, 1, 0, 0, 0, 569, 567, 1, 0, 0, 0, 569, 570, 1, 0, 0, 0, 570, 572, 1, 0, 0, 0, 571, 569, 1, 0, 0, 0, 572, 573, 5, 6, 0, 0, 573, 107, 1, 0, 0, 0, 574, 577, 3, 110, 55, 0, 575, 577, 3, 114, 57, 0, 576, 574, 1, 0, 0, 0, 576, 575, 1, 0, 0, 0, 577, 109, 1, 0, 0, 0, 578, 579, 5, 79, 0, 0, 579, 580, 5, 2, 0, 0, 580, 581, 3, 112, 56, 0, 581, 111, 1, 0, 0, 0, 582, 583, 7, 2, 0, 0, 583, 113, 1, 0, 0, 0, 584, 585, 5, 82, 0, 0, 585, 586, 5, 2, 0, 0, 586, 587, 3, 116, 58, 0, 587, 115, 1, 0, 0, 0, 588, 589, 5, 83, 0, 0, 589, 117, 1, 0, 0, 0, 590, 591, 5, 85, 0, 0, 591, 592, 5, 2, 0, 0, 592, 593, 5, 5, 0, 0, 593, 598, 3, 120, 60, 0, 594, 595, 5, 1, 0, 0, 595, 597, 3, 120, 60, 0, 596, 594, 1, 0, 0, 0, 597, 600, 1, 0, 0, 0, 598, 596, 1, 0, 0, 0, 598, 599, 1, 0, 0, 0, 599, 601, 1, 0, 0, 0, 600, 598, 1, 0, 0, 0, 601, 602, 5, 6, 0, 0, 602, 119, 1, 0, 0, 0, 603, 608, 3, 6, 3, 0, 604, 608, 3, 14, 7, 0, 605, 608, 3, 8, 4, 0, 606, 608, 3, 106, 53, 0, 607, 603, 1, 0, 0, 0, 607, 604, 1, 0, 0, 0, 607, 605, 1, 0, 0, 0, 607, 606, 1, 0, 0, 0, 608, 121, 1, 0, 0, 0, 609, 610, 5, 86, 0, 0, 610, 611, 5, 2, 0, 0, 611, 612, 3, 70, 35, 0, 612, 123, 1, 0, 0, 0, 613, 614, 5, 96, 0, 0, 614, 615, 5, 2, 0, 0, 615, 616, 5, 5, 0, 0, 616, 621, 3, 126, 63, 0, 617, 618, 5, 1, 0, 0, 618, 620, 3, 126, 63, 0, 619, 617, 1, 0, 0, 0, 620, 623, 1, 0, 0, 0, 621, 619, 1, 0, 0, 0, 621, 622, 1, 0, 0, 0, 622, 624, 1, 0, 0, 0, 623, 621, 1, 0, 0, 0, 624, 625, 5, 6, 0, 0, 625, 125, 1, 0, 0, 0, 626, 630, 3, 26, 13, 0, 627, 630, 3, 60, 30, 0, 628, 630, 3, 128, 64, 0, 629, 626, 1, 0, 0, 0, 629, 627, 1, 0, 0, 0, 629, 628, 1, 0, 0, 0, 630, 127, 1, 0, 0, 0, 631, 632, 5, 97, 0, 0, 632, 633, 5, 2, 0, 0, 633, 634, 5, 5, 0, 0, 634, 639, 3, 130, 65, 0, 635, 636, 5, 1, 0, 0, 636, 638, 3, 130, 65, 0, 637, 635, 1, 0, 0, 0, 638, 641, 1, 0, 0, 0, 639, 637, 1, 0, 0, 0, 639, 640, 1, 0, 0, 0, 640, 642, 1, 0, 0, 0, 641, 639, 1, 0, 0, 0, 642, 643, 5, 6, 0, 0, 643, 129, 1, 0, 0, 0, 644, 650, 3, 132, 66, 0, 645, 650, 3, 134, 67, 0, 646, 650, 3, 136, 68, 0, 647, 650, 3, 138, 69, 0, 648, 650, 3, 140, 70, 0, 649, 644, 1, 0, 0, 0, 649, 645, 1, 0, 0, 0, 649, 646, 1, 0, 0, 0, 649, 647, 1, 0, 0, 0, 649, 648, 1, 0, 0, 0, 650, 131, 1, 0, 0, 0, 651, 652, 5, 98, 0, 0, 652, 653, 5, 2, 0, 0, 653, 654, 3, 182, 91, 0, 654, 133, 1, 0, 0, 0, 655, 656, 5, 99, 0, 0, 656, 657, 5, 2, 0, 0, 657, 658, 3, 182, 91, 0, 658, 135, 1, 0, 0, 0, 659, 660, 5, 100, 0, 0, 660, 661, 5, 2, 0, 0, 661, 662, 5, 3, 0, 0, 662, 667, 3, 182, 91, 0, 663, 664, 5, 1, 0, 0, 664, 666, 3, 182, 91, 0, 665, 663, 1, 0, 0, 0, 666, 669, 1, 0, 0, 0, 667, 665, 1, 0, 0, 0, 667, 668, 1, 0, 0, 0, 668, 670, 1, 0, 0, 0, 669, 667, 1, 0, 0, 0, 670, 671, 5, 4, 0, 0, 671, 137, 1, 0, 0, 0, 672, 673, 5, 101, 0, 0, 673, 674, 5, 2, 0, 0, 674, 675, 5, 138, 0, 0, 675, 139, 1, 0, 0, 0, 676, 677, 5, 102, 0, 0, 677, 678, 5, 2, 0, 0, 678, 679, 5, 136, 0, 0, 679, 141, 1, 0, 0, 0, 680, 681, 5, 109, 0, 0, 681, 682, 5, 2, 0, 0, 682, 691, 5, 3, 0, 0, 683, 688, 3, 144, 72, 0, 684, 685, 5, 1, 0, 0, 685, 687, 3, 144, 72, 0, 686, 684, 1, 0, 0, 0, 687, 690, 1, 0, 0, 0, 688, 686, 1, 0, 0, 0, 688, 689, 1, 0, 0, 0, 689, 692, 1, 0, 0, 0, 690, 688, 1, 0, 0, 0, 691, 683, 1, 0, 0, 0, 691, 692, 1, 0, 0, 0, 692, 693, 1, 0, 0, 0, 693, 694, 5, 4, 0, 0, 694, 143, 1, 0, 0, 0, 695, 696, 5, 5, 0, 0, 696, 701, 3, 146, 73, 0, 697, 698, 5, 1, 0, 0, 698, 700, 3, 146, 73, 0, 699, 697, 1, 0, 0, 0, 700, 703, 1, 0, 0, 0, 701, 699, 1, 0, 0, 0, 701, 702, 1, 0, 0, 0, 702, 704, 1, 0, 0, 0, 703, 701, 1, 0, 0, 0, 704, 705, 5, 6, 0, 0, 705, 145, 1, 0, 0, 0, 706, 714, 3, 148, 74, 0, 707, 714, 3, 150, 75, 0, 708, 714, 3, 152, 76, 0, 709, 714, 3, 154, 77, 0, 710, 714, 3, 156, 78, 0, 711, 714, 3, 158, 79, 0, 712, 714, 3, 8, 4, 0, 713, 706, 1, 0, 0, 0, 713, 707, 1, 0, 0, 0, 713, 708, 1, 0, 0, 0, 713, 709, 1, 0, 0, 0, 713, 710, 1, 0, 0, 0, 713, 711, 1, 0, 0, 0, 713, 712, 1, 0, 0, 0, 714, 147, 1, 0, 0, 0, 715, 716, 5, 110, 0, 0, 716, 717, 5, 2, 0, 0, 717, 718, 5, 3, 0, 0, 718, 723, 3, 172, 86, 0, 719, 720, 5, 1, 0, 0, 720, 722, 3, 172, 86, 0, 721, 719, 1, 0, 0, 0, 722, 725, 1, 0, 0, 0, 723, 721, 1, 0, 0, 0, 723, 724, 1, 0, 0, 0, 724, 726, 1, 0, 0, 0, 725, 723, 1, 0, 0, 0, 726, 727, 5, 4, 0, 0, 727, 149, 1, 0, 0, 0, 728, 729, 5, 111, 0, 0, 729, 730, 5, 2, 0, 0, 730, 731, 5, 138, 0, 0, 731, 151, 1, 0, 0, 0, 732, 733, 5, 112, 0, 0, 733, 734, 5, 2, 0, 0, 734, 735, 5, 138, 0, 0, 735, 153, 1, 0, 0, 0, 736, 737, 5, 113, 0, 0, 737, 738, 5, 2, 0, 0, 738, 739, 7, 3, 0, 0, 739, 155, 1, 0, 0, 0, 740, 741, 5, 114, 0, 0, 741, 742, 5, 2, 0, 0, 742, 743, 5, 138, 0, 0, 743, 157, 1, 0, 0, 0, 744, 745, 5, 115, 0, 0, 745, 746, 5, 2, 0, 0, 746, 747, 7, 4, 0, 0, 747, 159, 1, 0, 0, 0, 748, 749, 5, 118, 0, 0, 749, 750, 5, 2, 0, 0, 750, 759, 5, 3, 0, 0, 751, 756, 3, 162, 81, 0, 752, 753, 5, 1, 0, 0, 753, 755, 3, 162, 81, 0, 754, 752, 1, 0, 0, 0, 755, 758, 1, 0, 0, 0, 756, 754, 1, 0, 0, 0, 756, 757, 1, 0, 0, 0, 757, 760, 1, 0, 0, 0, 758, 756, 1, 0, 0, 0, 759, 751, 1, 0, 0, 0, 759, 760, 1, 0, 0, 0, 760, 761, 1, 0, 0, 0, 761, 762, 5, 4, 0, 0, 762, 161, 1, 0, 0, 0, 763, 764, 5, 5, 0, 0, 764, 769, 3, 164, 82, 0, 765, 766, 5, 1, 0, 0, 766, 768, 3, 164, 82, 0, 767, 765, 1, 0, 0, 0, 768, 771, 1, 0, 0, 0, 769, 767, 1, 0, 0, 0, 769, 770, 1, 0, 0, 0, 770, 772, 1, 0, 0, 0, 771, 769, 1, 0, 0, 0, 772, 773, 5, 6, 0, 0, 773, 163, 1, 0, 0, 0, 774, 779, 3, 148, 74, 0, 775, 779, 3, 32, 16, 0, 776, 779, 3, 24, 12, 0, 777, 779, 3, 8, 4, 0, 778, 774, 1, 0, 0, 0, 778, 775, 1, 0, 0, 0, 778, 776, 1, 0, 0, 0, 778, 777, 1, 0, 0, 0, 779, 165, 1, 0, 0, 0, 780, 781, 7, 5, 0, 0, 781, 167, 1, 0, 0, 0, 782, 783, 7, 6, 0, 0, 783, 169, 1, 0, 0, 0, 784, 785, 7, 7, 0, 0, 785, 171, 1, 0, 0, 0, 786, 789, 3, 170, 85, 0, 787, 789, 3, 182, 91, 0, 788, 786, 1, 0, 0, 0, 788, 787, 1, 0, 0, 0, 789, 173, 1, 0, 0, 0, 790, 791, 5, 5, 0, 0, 791, 796, 3, 176, 88, 0, 792, 793, 5, 1, 0, 0, 793, 795, 3, 176, 88, 0, 794, 792, 1, 0, 0, 0, 795, 798, 1, 0, 0, 0, 796, 794, 1, 0, 0, 0, 796, 797, 1, 0, 0, 0, 797, 799, 1, 0, 0, 0, 798, 796, 1, 0, 0, 0, 799, 800, 5, 6, 0, 0, 800, 804, 1, 0, 0, 0, 801, 802, 5, 5, 0, 0, 802, 804, 5, 6, 0, 0, 803, 790, 1, 0, 0, 0, 803, 801, 1, 0, 0, 0, 804, 175, 1, 0, 0, 0, 805, 806, 3, 182, 91, 0, 806, 807, 5, 2, 0, 0, 807, 808, 3, 180, 90, 0, 808, 177, 1, 0, 0, 0, 809, 810, 5, 3, 0, 0, 810, 815, 3, 180, 90, 0, 811, 812, 5, 1, 0, 0, 812, 814, 3, 180, 90, 0, 813, 811, 1, 0, 0, 0, 814, 817, 1, 0, 0, 0, 815, 813, 1, 0, 0, 0, 815, 816, 1, 0, 0, 0, 816, 818, 1, 0, 0, 0, 817, 815, 1, 0, 0, 0, 818, 819, 5, 4, 0, 0, 819, 823, 1, 0, 0, 0, 820, 821, 5, 3, 0, 0, 821, 823, 5, 4, 0, 0, 822, 809, 1, 0, 0, 0, 822, 820, 1, 0, 0, 0, 823, 179, 1, 0, 0, 0, 824, 834, 5, 139, 0, 0, 825, 834, 5, 138, 0, 0, 826, 834, 5, 7, 0, 0, 827, 834, 5, 8, 0, 0, 828, 834, 5, 9, 0, 0, 829, 834, 3, 176, 88, 0, 830, 834, 3, 178, 89, 0, 831, 834, 3, 174, 87, 0, 832, 834, 3, 182, 91, 0, 833, 824, 1, 0, 0, 0, 833, 825, 1, 0, 0, 0, 833, 826, 1, 0, 0, 0, 833, 827, 1, 0, 0, 0, 833, 828, 1, 0, 0, 0, 833, 829, 1, 0, 0, 0, 833, 830, 1, 0, 0, 0, 833, 831, 1, 0, 0, 0, 833, 832, 1, 0, 0, 0, 834, 181, 1, 0, 0, 0, 835, 836, 7, 8, 0, 0, 836, 183, 1, 0, 0, 0, 52, 193, 203, 251, 261, 278, 299, 309, 315, 335, 347, 399, 406, 421, 431, 438, 444, 451, 467, 478, 488, 493, 499, 503, 514, 519, 537, 550, 559, 569, 576, 598, 607, 621, 629, 639, 649, 667, 688, 691, 701, 713, 723, 756, 759, 769, 778, 788, 796, 803, 815, 822, 833] \ No newline at end of file diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLParser.py b/localstack/services/stepfunctions/asl/antlr/runtime/ASLParser.py index af2303869cd40..6630597378de2 100644 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLParser.py +++ b/localstack/services/stepfunctions/asl/antlr/runtime/ASLParser.py @@ -1,4 +1,4 @@ -# Generated from /Users/mep/LocalStack/localstack/localstack/services/stepfunctions/asl/antlr/ASLParser.g4 by ANTLR 4.13.1 +# Generated from ASLParser.g4 by ANTLR 4.13.1 # encoding: utf-8 from antlr4 import * from io import StringIO diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLParser.tokens b/localstack/services/stepfunctions/asl/antlr/runtime/ASLParser.tokens deleted file mode 100644 index a5fdbc70bd29d..0000000000000 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLParser.tokens +++ /dev/null @@ -1,273 +0,0 @@ -COMMA=1 -COLON=2 -LBRACK=3 -RBRACK=4 -LBRACE=5 -RBRACE=6 -TRUE=7 -FALSE=8 -NULL=9 -COMMENT=10 -STATES=11 -STARTAT=12 -NEXTSTATE=13 -VERSION=14 -TYPE=15 -TASK=16 -CHOICE=17 -FAIL=18 -SUCCEED=19 -PASS=20 -WAIT=21 -PARALLEL=22 -MAP=23 -CHOICES=24 -VARIABLE=25 -DEFAULT=26 -BRANCHES=27 -AND=28 -BOOLEANEQUALS=29 -BOOLEANQUALSPATH=30 -ISBOOLEAN=31 -ISNULL=32 -ISNUMERIC=33 -ISPRESENT=34 -ISSTRING=35 -ISTIMESTAMP=36 -NOT=37 -NUMERICEQUALS=38 -NUMERICEQUALSPATH=39 -NUMERICGREATERTHAN=40 -NUMERICGREATERTHANPATH=41 -NUMERICGREATERTHANEQUALS=42 -NUMERICGREATERTHANEQUALSPATH=43 -NUMERICLESSTHAN=44 -NUMERICLESSTHANPATH=45 -NUMERICLESSTHANEQUALS=46 -NUMERICLESSTHANEQUALSPATH=47 -OR=48 -STRINGEQUALS=49 -STRINGEQUALSPATH=50 -STRINGGREATERTHAN=51 -STRINGGREATERTHANPATH=52 -STRINGGREATERTHANEQUALS=53 -STRINGGREATERTHANEQUALSPATH=54 -STRINGLESSTHAN=55 -STRINGLESSTHANPATH=56 -STRINGLESSTHANEQUALS=57 -STRINGLESSTHANEQUALSPATH=58 -STRINGMATCHES=59 -TIMESTAMPEQUALS=60 -TIMESTAMPEQUALSPATH=61 -TIMESTAMPGREATERTHAN=62 -TIMESTAMPGREATERTHANPATH=63 -TIMESTAMPGREATERTHANEQUALS=64 -TIMESTAMPGREATERTHANEQUALSPATH=65 -TIMESTAMPLESSTHAN=66 -TIMESTAMPLESSTHANPATH=67 -TIMESTAMPLESSTHANEQUALS=68 -TIMESTAMPLESSTHANEQUALSPATH=69 -SECONDSPATH=70 -SECONDS=71 -TIMESTAMPPATH=72 -TIMESTAMP=73 -TIMEOUTSECONDS=74 -TIMEOUTSECONDSPATH=75 -HEARTBEATSECONDS=76 -HEARTBEATSECONDSPATH=77 -PROCESSORCONFIG=78 -MODE=79 -INLINE=80 -DISTRIBUTED=81 -EXECUTIONTYPE=82 -STANDARD=83 -ITEMPROCESSOR=84 -ITERATOR=85 -ITEMSELECTOR=86 -MAXCONCURRENCY=87 -RESOURCE=88 -INPUTPATH=89 -OUTPUTPATH=90 -ITEMSPATH=91 -RESULTPATH=92 -RESULT=93 -PARAMETERS=94 -RESULTSELECTOR=95 -ITEMREADER=96 -READERCONFIG=97 -INPUTTYPE=98 -CSVHEADERLOCATION=99 -CSVHEADERS=100 -MAXITEMS=101 -MAXITEMSPATH=102 -NEXT=103 -END=104 -CAUSE=105 -CAUSEPATH=106 -ERROR=107 -ERRORPATH=108 -RETRY=109 -ERROREQUALS=110 -INTERVALSECONDS=111 -MAXATTEMPTS=112 -BACKOFFRATE=113 -MAXDELAYSECONDS=114 -JITTERSTRATEGY=115 -FULL=116 -NONE=117 -CATCH=118 -ERRORNAMEStatesALL=119 -ERRORNAMEStatesDataLimitExceeded=120 -ERRORNAMEStatesHeartbeatTimeout=121 -ERRORNAMEStatesTimeout=122 -ERRORNAMEStatesTaskFailed=123 -ERRORNAMEStatesPermissions=124 -ERRORNAMEStatesResultPathMatchFailure=125 -ERRORNAMEStatesParameterPathFailure=126 -ERRORNAMEStatesBranchFailed=127 -ERRORNAMEStatesNoChoiceMatched=128 -ERRORNAMEStatesIntrinsicFailure=129 -ERRORNAMEStatesExceedToleratedFailureThreshold=130 -ERRORNAMEStatesItemReaderFailed=131 -ERRORNAMEStatesResultWriterFailed=132 -ERRORNAMEStatesRuntime=133 -STRINGDOLLAR=134 -STRINGPATHCONTEXTOBJ=135 -STRINGPATH=136 -STRING=137 -INT=138 -NUMBER=139 -WS=140 -','=1 -':'=2 -'['=3 -']'=4 -'{'=5 -'}'=6 -'true'=7 -'false'=8 -'null'=9 -'"Comment"'=10 -'"States"'=11 -'"StartAt"'=12 -'"NextState"'=13 -'"Version"'=14 -'"Type"'=15 -'"Task"'=16 -'"Choice"'=17 -'"Fail"'=18 -'"Succeed"'=19 -'"Pass"'=20 -'"Wait"'=21 -'"Parallel"'=22 -'"Map"'=23 -'"Choices"'=24 -'"Variable"'=25 -'"Default"'=26 -'"Branches"'=27 -'"And"'=28 -'"BooleanEquals"'=29 -'"BooleanEqualsPath"'=30 -'"IsBoolean"'=31 -'"IsNull"'=32 -'"IsNumeric"'=33 -'"IsPresent"'=34 -'"IsString"'=35 -'"IsTimestamp"'=36 -'"Not"'=37 -'"NumericEquals"'=38 -'"NumericEqualsPath"'=39 -'"NumericGreaterThan"'=40 -'"NumericGreaterThanPath"'=41 -'"NumericGreaterThanEquals"'=42 -'"NumericGreaterThanEqualsPath"'=43 -'"NumericLessThan"'=44 -'"NumericLessThanPath"'=45 -'"NumericLessThanEquals"'=46 -'"NumericLessThanEqualsPath"'=47 -'"Or"'=48 -'"StringEquals"'=49 -'"StringEqualsPath"'=50 -'"StringGreaterThan"'=51 -'"StringGreaterThanPath"'=52 -'"StringGreaterThanEquals"'=53 -'"StringGreaterThanEqualsPath"'=54 -'"StringLessThan"'=55 -'"StringLessThanPath"'=56 -'"StringLessThanEquals"'=57 -'"StringLessThanEqualsPath"'=58 -'"StringMatches"'=59 -'"TimestampEquals"'=60 -'"TimestampEqualsPath"'=61 -'"TimestampGreaterThan"'=62 -'"TimestampGreaterThanPath"'=63 -'"TimestampGreaterThanEquals"'=64 -'"TimestampGreaterThanEqualsPath"'=65 -'"TimestampLessThan"'=66 -'"TimestampLessThanPath"'=67 -'"TimestampLessThanEquals"'=68 -'"TimestampLessThanEqualsPath"'=69 -'"SecondsPath"'=70 -'"Seconds"'=71 -'"TimestampPath"'=72 -'"Timestamp"'=73 -'"TimeoutSeconds"'=74 -'"TimeoutSecondsPath"'=75 -'"HeartbeatSeconds"'=76 -'"HeartbeatSecondsPath"'=77 -'"ProcessorConfig"'=78 -'"Mode"'=79 -'"INLINE"'=80 -'"DISTRIBUTED"'=81 -'"ExecutionType"'=82 -'"STANDARD"'=83 -'"ItemProcessor"'=84 -'"Iterator"'=85 -'"ItemSelector"'=86 -'"MaxConcurrency"'=87 -'"Resource"'=88 -'"InputPath"'=89 -'"OutputPath"'=90 -'"ItemsPath"'=91 -'"ResultPath"'=92 -'"Result"'=93 -'"Parameters"'=94 -'"ResultSelector"'=95 -'"ItemReader"'=96 -'"ReaderConfig"'=97 -'"InputType"'=98 -'"CSVHeaderLocation"'=99 -'"CSVHeaders"'=100 -'"MaxItems"'=101 -'"MaxItemsPath"'=102 -'"Next"'=103 -'"End"'=104 -'"Cause"'=105 -'"CausePath"'=106 -'"Error"'=107 -'"ErrorPath"'=108 -'"Retry"'=109 -'"ErrorEquals"'=110 -'"IntervalSeconds"'=111 -'"MaxAttempts"'=112 -'"BackoffRate"'=113 -'"MaxDelaySeconds"'=114 -'"JitterStrategy"'=115 -'"FULL"'=116 -'"NONE"'=117 -'"Catch"'=118 -'"States.ALL"'=119 -'"States.DataLimitExceeded"'=120 -'"States.HeartbeatTimeout"'=121 -'"States.Timeout"'=122 -'"States.TaskFailed"'=123 -'"States.Permissions"'=124 -'"States.ResultPathMatchFailure"'=125 -'"States.ParameterPathFailure"'=126 -'"States.BranchFailed"'=127 -'"States.NoChoiceMatched"'=128 -'"States.IntrinsicFailure"'=129 -'"States.ExceedToleratedFailureThreshold"'=130 -'"States.ItemReaderFailed"'=131 -'"States.ResultWriterFailed"'=132 -'"States.Runtime"'=133 diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLParserListener.py b/localstack/services/stepfunctions/asl/antlr/runtime/ASLParserListener.py index af9e8d57daef4..71098ac4228d2 100644 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLParserListener.py +++ b/localstack/services/stepfunctions/asl/antlr/runtime/ASLParserListener.py @@ -1,4 +1,4 @@ -# Generated from /Users/mep/LocalStack/localstack/localstack/services/stepfunctions/asl/antlr/ASLParser.g4 by ANTLR 4.13.1 +# Generated from ASLParser.g4 by ANTLR 4.13.1 from antlr4 import * if "." in __name__: from .ASLParser import ASLParser diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLParserVisitor.py b/localstack/services/stepfunctions/asl/antlr/runtime/ASLParserVisitor.py index ad511273e0e79..9e2dd9184ea23 100644 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLParserVisitor.py +++ b/localstack/services/stepfunctions/asl/antlr/runtime/ASLParserVisitor.py @@ -1,4 +1,4 @@ -# Generated from /Users/mep/LocalStack/localstack/localstack/services/stepfunctions/asl/antlr/ASLParser.g4 by ANTLR 4.13.1 +# Generated from ASLParser.g4 by ANTLR 4.13.1 from antlr4 import * if "." in __name__: from .ASLParser import ASLParser From 84ce11b686cb95af18f506786ba21e077dbf268d Mon Sep 17 00:00:00 2001 From: Sannya Singal <32308435+sannya-singal@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:37:48 +0530 Subject: [PATCH 022/169] fix function get azs cloudformation template (#10595) --- .../cloudformation/test_template_engine.py | 2 ++ .../test_template_engine.snapshot.json | 2 +- .../test_template_engine.validation.json | 2 +- tests/aws/templates/functions_get_azs.yml | 14 +++++++++++++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/aws/services/cloudformation/test_template_engine.py b/tests/aws/services/cloudformation/test_template_engine.py index ebdec4cd2e15d..61c33d39dc769 100644 --- a/tests/aws/services/cloudformation/test_template_engine.py +++ b/tests/aws/services/cloudformation/test_template_engine.py @@ -8,6 +8,7 @@ import yaml from localstack.aws.api.lambda_ import Runtime +from localstack.constants import AWS_REGION_US_EAST_1 from localstack.services.cloudformation.engine.yaml_parser import parse_yaml from localstack.testing.aws.cloudformation_utils import load_template_file, load_template_raw from localstack.testing.pytest import markers @@ -250,6 +251,7 @@ def test_get_azs_function(self, deploy_cfn_template, snapshot): template_path=template_path, ) + snapshot.add_transformer(snapshot.transform.regex(AWS_REGION_US_EAST_1, "")) snapshot.match("azs", deployed.outputs["Zones"].split(";")) @markers.aws.validated diff --git a/tests/aws/services/cloudformation/test_template_engine.snapshot.json b/tests/aws/services/cloudformation/test_template_engine.snapshot.json index 63c01573fbc06..04a85b9b88777 100644 --- a/tests/aws/services/cloudformation/test_template_engine.snapshot.json +++ b/tests/aws/services/cloudformation/test_template_engine.snapshot.json @@ -601,7 +601,7 @@ } }, "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_get_azs_function": { - "recorded-date": "27-11-2023, 13:14:35", + "recorded-date": "03-04-2024, 07:12:29", "recorded-content": { "azs": [ "a", diff --git a/tests/aws/services/cloudformation/test_template_engine.validation.json b/tests/aws/services/cloudformation/test_template_engine.validation.json index 20d3cbb9a37b0..744147553fae6 100644 --- a/tests/aws/services/cloudformation/test_template_engine.validation.json +++ b/tests/aws/services/cloudformation/test_template_engine.validation.json @@ -1,6 +1,6 @@ { "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_get_azs_function": { - "last_validated_date": "2023-11-27T12:14:35+00:00" + "last_validated_date": "2024-04-03T07:12:29+00:00" }, "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_capabilities_requirements": { "last_validated_date": "2023-01-30T19:15:46+00:00" diff --git a/tests/aws/templates/functions_get_azs.yml b/tests/aws/templates/functions_get_azs.yml index bcc7763f83ca9..fba83c9c46a3e 100644 --- a/tests/aws/templates/functions_get_azs.yml +++ b/tests/aws/templates/functions_get_azs.yml @@ -1,12 +1,24 @@ +Parameters: + DeployRegion: + Type: String + Default: us-east-1 + +Conditions: + DeployInUSEast1: + Fn::Equals: + - !Ref DeployRegion + - us-east-1 + Resources: SsmParameter: Type: AWS::SSM::Parameter + Condition: DeployInUSEast1 Properties: Type: String Value: Fn::Join: - ";" - - Fn::GetAZs: !Ref "AWS::Region" + - Fn::GetAZs: !Ref DeployRegion Outputs: Zones: Value: From 16cd8de250487b25ab00dfb4054e00569c89d4dd Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:39:22 +0200 Subject: [PATCH 023/169] fix DDB issue when TransactWriteItems targets tables with and without streams (#10593) --- localstack/services/dynamodb/provider.py | 10 +- tests/aws/services/dynamodb/test_dynamodb.py | 74 ++++++++++ .../dynamodb/test_dynamodb.snapshot.json | 138 ++++++++++++++++++ .../dynamodb/test_dynamodb.validation.json | 3 + 4 files changed, 222 insertions(+), 3 deletions(-) diff --git a/localstack/services/dynamodb/provider.py b/localstack/services/dynamodb/provider.py index f041fb4afad96..c364c2f6aea99 100644 --- a/localstack/services/dynamodb/provider.py +++ b/localstack/services/dynamodb/provider.py @@ -1760,6 +1760,8 @@ def prepare_transact_write_item_records( record = self.get_record_template(region_name) match request: case {"Put": {"TableName": table_name, "Item": new_item}}: + if not (stream_type := tables_stream_type.get(table_name)): + continue keys = SchemaExtractor.extract_keys( item=new_item, table_name=table_name, @@ -1772,7 +1774,6 @@ def prepare_transact_write_item_records( if existing_item == new_item: continue - stream_type = tables_stream_type[table_name] if stream_type.stream_view_type: record["dynamodb"]["StreamViewType"] = stream_type.stream_view_type @@ -1790,6 +1791,8 @@ def prepare_transact_write_item_records( continue case {"Update": {"TableName": table_name, "Key": keys}}: + if not (stream_type := tables_stream_type.get(table_name)): + continue updated_item = find_item_for_keys_values_in_batch( table_name, keys, updated_items ) @@ -1800,7 +1803,6 @@ def prepare_transact_write_item_records( table_name, keys, existing_items ) - stream_type = tables_stream_type[table_name] if stream_type.stream_view_type: record["dynamodb"]["StreamViewType"] = stream_type.stream_view_type @@ -1818,13 +1820,15 @@ def prepare_transact_write_item_records( continue case {"Delete": {"TableName": table_name, "Key": keys}}: + if not (stream_type := tables_stream_type.get(table_name)): + continue + existing_item = find_item_for_keys_values_in_batch( table_name, keys, existing_items ) if not existing_item: continue - stream_type = tables_stream_type[table_name] if stream_type.stream_view_type: record["dynamodb"]["StreamViewType"] = stream_type.stream_view_type diff --git a/tests/aws/services/dynamodb/test_dynamodb.py b/tests/aws/services/dynamodb/test_dynamodb.py index 9118789e18678..55028239a62ea 100644 --- a/tests/aws/services/dynamodb/test_dynamodb.py +++ b/tests/aws/services/dynamodb/test_dynamodb.py @@ -2054,3 +2054,77 @@ def _get_records_amount(record_amount: int): retry(lambda: _get_records_amount(5), sleep=1, retries=3) snapshot.match("get-records", {"Records": records}) + + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + paths=[ + "$..SizeBytes", + "$..DeletionProtectionEnabled", + "$..ProvisionedThroughput.NumberOfDecreasesToday", + "$..StreamDescription.CreationRequestDateTime", + ] + ) + def test_transact_write_items_streaming_for_different_tables( + self, + dynamodb_create_table_with_parameters, + wait_for_dynamodb_stream_ready, + snapshot, + aws_client, + dynamodbstreams_snapshot_transformers, + ): + # TODO: add a test with both Kinesis and DDBStreams destinations + table_name_stream = f"test-ddb-table-{short_uid()}" + table_name_no_stream = f"test-ddb-table-{short_uid()}" + create_table_stream = dynamodb_create_table_with_parameters( + TableName=table_name_stream, + KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + StreamSpecification={"StreamEnabled": True, "StreamViewType": "NEW_AND_OLD_IMAGES"}, + ) + snapshot.match("create-table-stream", create_table_stream) + + create_table_no_stream = dynamodb_create_table_with_parameters( + TableName=table_name_no_stream, + KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "id", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + snapshot.match("create-table-no-stream", create_table_no_stream) + + stream_arn = create_table_stream["TableDescription"]["LatestStreamArn"] + wait_for_dynamodb_stream_ready(stream_arn=stream_arn) + + describe_stream_result = aws_client.dynamodbstreams.describe_stream(StreamArn=stream_arn) + snapshot.match("describe-stream", describe_stream_result) + + shard_id = describe_stream_result["StreamDescription"]["Shards"][0]["ShardId"] + shard_iterator = aws_client.dynamodbstreams.get_shard_iterator( + StreamArn=stream_arn, ShardId=shard_id, ShardIteratorType="TRIM_HORIZON" + )["ShardIterator"] + + # Call TransactWriteItems on the 2 different tables at once + response = aws_client.dynamodb.transact_write_items( + TransactItems=[ + {"Put": {"TableName": table_name_no_stream, "Item": {"id": {"S": "Fred"}}}}, + {"Put": {"TableName": table_name_stream, "Item": {"id": {"S": "Fred"}}}}, + ] + ) + snapshot.match("transact-write-two-tables", response) + + # Total amount of records should be 1: + # - TransactWriteItem on Fred insert for TableStream + records = [] + + def _get_records_amount(record_amount: int): + nonlocal shard_iterator + if len(records) < record_amount: + _resp = aws_client.dynamodbstreams.get_records(ShardIterator=shard_iterator) + records.extend(_resp["Records"]) + if next_shard_iterator := _resp.get("NextShardIterator"): + shard_iterator = next_shard_iterator + + assert len(records) >= record_amount + + retry(lambda: _get_records_amount(1), sleep=1, retries=3) + snapshot.match("get-records", {"Records": records}) diff --git a/tests/aws/services/dynamodb/test_dynamodb.snapshot.json b/tests/aws/services/dynamodb/test_dynamodb.snapshot.json index d44cdc9c7963d..057c97c4782eb 100644 --- a/tests/aws/services/dynamodb/test_dynamodb.snapshot.json +++ b/tests/aws/services/dynamodb/test_dynamodb.snapshot.json @@ -1095,5 +1095,143 @@ ] } } + }, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_transact_write_items_streaming_for_different_tables": { + "recorded-date": "02-04-2024, 21:45:36", + "recorded-content": { + "create-table-stream": { + "TableDescription": { + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "CreationDateTime": "datetime", + "DeletionProtectionEnabled": false, + "ItemCount": 0, + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "LatestStreamArn": "arn:aws:dynamodb::111111111111:table//stream/", + "LatestStreamLabel": "", + "ProvisionedThroughput": { + "NumberOfDecreasesToday": 0, + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + }, + "StreamSpecification": { + "StreamEnabled": true, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "TableArn": "arn:aws:dynamodb::111111111111:table/", + "TableId": "", + "TableName": "", + "TableSizeBytes": 0, + "TableStatus": "" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "create-table-no-stream": { + "TableDescription": { + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "CreationDateTime": "datetime", + "DeletionProtectionEnabled": false, + "ItemCount": 0, + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "ProvisionedThroughput": { + "NumberOfDecreasesToday": 0, + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + }, + "TableArn": "arn:aws:dynamodb::111111111111:table/", + "TableId": "", + "TableName": "", + "TableSizeBytes": 0, + "TableStatus": "" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-stream": { + "StreamDescription": { + "CreationRequestDateTime": "datetime", + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "Shards": [ + { + "SequenceNumberRange": { + "StartingSequenceNumber": "starting-sequence-number" + }, + "ShardId": "" + } + ], + "StreamArn": "arn:aws:dynamodb::111111111111:table//stream/", + "StreamLabel": "", + "StreamStatus": "ENABLED", + "StreamViewType": "NEW_AND_OLD_IMAGES", + "TableName": "" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "transact-write-two-tables": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "get-records": { + "Records": [ + { + "awsRegion": "", + "dynamodb": { + "ApproximateCreationDateTime": "datetime", + "Keys": { + "id": { + "S": "Fred" + } + }, + "NewImage": { + "id": { + "S": "Fred" + } + }, + "SequenceNumber": "", + "SizeBytes": 12, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventID": "", + "eventName": "INSERT", + "eventSource": "aws:dynamodb", + "eventVersion": "1.1" + } + ] + } + } } } diff --git a/tests/aws/services/dynamodb/test_dynamodb.validation.json b/tests/aws/services/dynamodb/test_dynamodb.validation.json index 20f142d944708..f2eb8bd4ec09f 100644 --- a/tests/aws/services/dynamodb/test_dynamodb.validation.json +++ b/tests/aws/services/dynamodb/test_dynamodb.validation.json @@ -65,6 +65,9 @@ "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_transact_write_items_streaming": { "last_validated_date": "2024-03-15T01:54:32+00:00" }, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_transact_write_items_streaming_for_different_tables": { + "last_validated_date": "2024-04-02T21:45:36+00:00" + }, "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_transaction_write_binary_data": { "last_validated_date": "2023-08-23T14:33:31+00:00" }, From f1fbc5e6b7bcde48862e45973a42d5f58dde134f Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:46:53 +0200 Subject: [PATCH 024/169] extend pinning in ASF update action (#10537) --- .github/workflows/asf-updates.yml | 21 ++++++++++++++------- Makefile | 12 ++++++------ bin/release-helper.sh | 4 ++-- pyproject.toml | 12 ++++++++---- requirements-base-runtime.txt | 2 +- requirements-dev.txt | 8 ++++---- requirements-runtime.txt | 4 ++-- requirements-test.txt | 8 ++++---- requirements-typehint.txt | 14 +++++++------- 9 files changed, 48 insertions(+), 37 deletions(-) diff --git a/.github/workflows/asf-updates.yml b/.github/workflows/asf-updates.yml index 741c864ac5723..fd72300378ca4 100644 --- a/.github/workflows/asf-updates.yml +++ b/.github/workflows/asf-updates.yml @@ -66,23 +66,30 @@ jobs: echo "$(git diff --name-only origin/master localstack/aws/api/ | sed 's#localstack/aws/api/#- #g' | sed 's#/__init__.py##g' | sed 's/_/-/g')" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - - name: Update botocore pin + - name: Update botocore and transitive pins # only update the pin if we have updates in the ASF code if: ${{ success() && steps.check-for-changes.outputs.diff-count != '0' && steps.check-for-changes.outputs.diff-count != '' }} run: | source .venv/bin/activate # determine botocore version in venv BOTOCORE_VERSION=$(python -c "import botocore; print(botocore.__version__)"); - echo "Pinning botocore to version $BOTOCORE_VERSION" + echo "Pinning botocore, boto3, and boto3-stubs to version $BOTOCORE_VERSION" bin/release-helper.sh set-dep-ver botocore "==$BOTOCORE_VERSION" + bin/release-helper.sh set-dep-ver boto3 "==$BOTOCORE_VERSION" + bin/release-helper.sh set-dep-ver boto3-stubs "==$BOTOCORE_VERSION" + + # aws-cli is two minor versions behind botocore + AWSCLI_VERSION=$(python -c 'import botocore; from packaging.version import Version; version = Version(botocore.__version__); print(f"{version.major}.{version.minor - 2}.{version.micro}")') + echo "Pinning aws-cli to version $AWSCLI_VERSION" + bin/release-helper.sh set-dep-ver awscli "==$AWSCLI_VERSION" # upgrade the requirements files only for the botocore package pip install pip-tools - pip-compile --upgrade-package botocore --upgrade-package boto3 --extra base-runtime -o requirements-base-runtime.txt pyproject.toml - pip-compile --upgrade-package botocore --upgrade-package boto3 --upgrade-package awscli --extra runtime -o requirements-runtime.txt pyproject.toml - pip-compile --upgrade-package botocore --upgrade-package boto3 --upgrade-package awscli --extra test -o requirements-test.txt pyproject.toml - pip-compile --upgrade-package botocore --upgrade-package boto3 --upgrade-package awscli --extra dev -o requirements-dev.txt pyproject.toml - pip-compile --upgrade-package botocore --upgrade-package boto3 --upgrade-package awscli --extra typehint -o requirements-typehint.txt pyproject.toml + pip-compile --strip-extras --upgrade-package "botocore==$BOTOCORE_VERSION" --upgrade-package "boto3==$BOTOCORE_VERSION" --extra base-runtime -o requirements-base-runtime.txt pyproject.toml + pip-compile --strip-extras --upgrade-package "botocore==$BOTOCORE_VERSION" --upgrade-package "boto3==$BOTOCORE_VERSION" --upgrade-package "awscli==$AWSCLI_VERSION" --extra runtime -o requirements-runtime.txt pyproject.toml + pip-compile --strip-extras --upgrade-package "botocore==$BOTOCORE_VERSION" --upgrade-package "boto3==$BOTOCORE_VERSION" --upgrade-package "awscli==$AWSCLI_VERSION" --extra test -o requirements-test.txt pyproject.toml + pip-compile --strip-extras --upgrade-package "botocore==$BOTOCORE_VERSION" --upgrade-package "boto3==$BOTOCORE_VERSION" --upgrade-package "awscli==$AWSCLI_VERSION" --extra dev -o requirements-dev.txt pyproject.toml + pip-compile --strip-extras --upgrade-package "botocore==$BOTOCORE_VERSION" --upgrade-package "boto3==$BOTOCORE_VERSION" --upgrade-package "awscli==$AWSCLI_VERSION" --upgrade-package "boto3-stubs==$BOTOCORE_VERSION" --extra typehint -o requirements-typehint.txt pyproject.toml - name: Read PR markdown template if: ${{ success() && steps.check-for-changes.outputs.diff-count != '0' && steps.check-for-changes.outputs.diff-count != '' }} diff --git a/Makefile b/Makefile index bf534375573e0..e573d6087019f 100644 --- a/Makefile +++ b/Makefile @@ -35,12 +35,12 @@ freeze: ## Run pip freeze -l in the virtual environment upgrade-pinned-dependencies: venv $(VENV_RUN); $(PIP_CMD) install --upgrade pip-tools pre-commit - $(VENV_RUN); pip-compile --upgrade --strip-extras -o requirements-basic.txt pyproject.toml - $(VENV_RUN); pip-compile --upgrade --extra runtime -o requirements-runtime.txt pyproject.toml - $(VENV_RUN); pip-compile --upgrade --extra test -o requirements-test.txt pyproject.toml - $(VENV_RUN); pip-compile --upgrade --extra dev -o requirements-dev.txt pyproject.toml - $(VENV_RUN); pip-compile --upgrade --extra typehint -o requirements-typehint.txt pyproject.toml - $(VENV_RUN); pip-compile --upgrade --extra base-runtime -o requirements-base-runtime.txt pyproject.toml + $(VENV_RUN); pip-compile --strip-extras --upgrade --strip-extras -o requirements-basic.txt pyproject.toml + $(VENV_RUN); pip-compile --strip-extras --upgrade --extra runtime -o requirements-runtime.txt pyproject.toml + $(VENV_RUN); pip-compile --strip-extras --upgrade --extra test -o requirements-test.txt pyproject.toml + $(VENV_RUN); pip-compile --strip-extras --upgrade --extra dev -o requirements-dev.txt pyproject.toml + $(VENV_RUN); pip-compile --strip-extras --upgrade --extra typehint -o requirements-typehint.txt pyproject.toml + $(VENV_RUN); pip-compile --strip-extras --upgrade --extra base-runtime -o requirements-base-runtime.txt pyproject.toml $(VENV_RUN); pre-commit autoupdate install-basic: venv ## Install basic dependencies for CLI usage into venv diff --git a/bin/release-helper.sh b/bin/release-helper.sh index 8668160dfca0a..7b32733765397 100755 --- a/bin/release-helper.sh +++ b/bin/release-helper.sh @@ -159,8 +159,8 @@ function cmd-set-dep-ver() { dep=$1 ver=$2 - egrep -h "^(\s*\"?)(${dep})(\[[a-zA-Z0-9,]+\])?(>=|==|<=)([^\"]*)(\")?(,)?$" ${DEPENDENCY_FILE} || { echo "dependency ${dep} not found in ${DEPENDENCY_FILE}"; return 1; } - sed -i -r "s/^(\s*\"?)(${dep})(\[[a-zA-Z0-9,]+\])?(>=|==|<=)([^\"]*)(\")?(,)?$/\1\2\3${ver}\6\7/g" ${DEPENDENCY_FILE} + egrep -h "^(\s*\"?)(${dep})(\[[a-zA-Z0-9,\-]+\])?(>=|==|<=)([^\"]*)(\")?(,)?$" ${DEPENDENCY_FILE} || { echo "dependency ${dep} not found in ${DEPENDENCY_FILE}"; return 1; } + sed -i -r "s/^(\s*\"?)(${dep})(\[[a-zA-Z0-9,\-]+\])?(>=|==|<=)([^\"]*)(\")?(,)?$/\1\2\3${ver}\6\7/g" ${DEPENDENCY_FILE} } function cmd-github-outputs() { diff --git a/pyproject.toml b/pyproject.toml index d05517389e797..cc15a130e4887 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,9 +47,11 @@ Issues = "https://github.com/localstack/localstack/issues" [project.optional-dependencies] # minimal required to actually run localstack on the host for services natively implemented in python base-runtime = [ - "awscrt>=0.13.14", - "boto3>=1.26.121", + # pinned / updated by ASF update action + "boto3==1.34.74", + # pinned / updated by ASF update action "botocore==1.34.74", + "awscrt>=0.13.14", "cbor2>=5.2.0", "dnspython>=1.16.0", # TODO tag incompatibility introduced in 7.0.0 with https://github.com/docker/docker-py/pull/3191 @@ -73,13 +75,14 @@ base-runtime = [ # required to actually run localstack on the host runtime = [ "localstack-core[base-runtime]", + # pinned / updated by ASF update action + "awscli==1.32.74", "airspeed-ext>=0.6.3", "amazon_kclpy>=2.0.6,!=2.1.0", # antlr4-python3-runtime: exact pin because antlr4 runtime is tightly coupled to the generated parser code "antlr4-python3-runtime==4.13.1", "apispec>=5.1.1", "aws-sam-translator>=1.15.1", - "awscli>=1.32.69", "crontab>=0.22.6", "cryptography>=41.0.5", "json5>=0.9.11", @@ -127,7 +130,8 @@ dev = [ typehint = [ # typehint is an optional extension of the dev dependencies "localstack-core[dev]", - "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]>=1.34.69", + # pinned / updated by ASF update action + "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.74", ] [tool.setuptools] diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index 8cc77ac915705..bb4ba9b4fd604 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --extra=base-runtime --output-file=requirements-base-runtime.txt --unsafe-package=distribute --unsafe-package=localstack-core --unsafe-package=pip --unsafe-package=setuptools pyproject.toml +# pip-compile --extra=base-runtime --output-file=requirements-base-runtime.txt --strip-extras --unsafe-package=distribute --unsafe-package=localstack-core --unsafe-package=pip --unsafe-package=setuptools pyproject.toml # aiofiles==23.2.1 # via quart diff --git a/requirements-dev.txt b/requirements-dev.txt index 5f30b4fb3b7fd..eb4d276934b25 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --extra=dev --output-file=requirements-dev.txt --unsafe-package=distribute --unsafe-package=localstack-core --unsafe-package=pip --unsafe-package=setuptools pyproject.toml +# pip-compile --extra=dev --output-file=requirements-dev.txt --strip-extras --unsafe-package=distribute --unsafe-package=localstack-core --unsafe-package=pip --unsafe-package=setuptools pyproject.toml # aiofiles==23.2.1 # via quart @@ -108,7 +108,7 @@ constantly==23.10.4 # via localstack-twisted constructs==10.3.0 # via aws-cdk-lib -coverage[toml]==6.5.0 +coverage==6.5.0 # via # coveralls # localstack-core @@ -176,7 +176,7 @@ hpack==4.0.0 # via h2 httpcore==1.0.5 # via httpx -httpx[http2]==0.27.0 +httpx==0.27.0 # via localstack-core hypercorn==0.16.0 # via @@ -273,7 +273,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext[all]==5.0.4.post1 +moto-ext==5.0.4.post1 # via localstack-core mpmath==1.3.0 # via sympy diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 6d319cb3d92b2..a81a0f5b30eef 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --extra=runtime --output-file=requirements-runtime.txt --unsafe-package=distribute --unsafe-package=localstack-core --unsafe-package=pip --unsafe-package=setuptools pyproject.toml +# pip-compile --extra=runtime --output-file=requirements-runtime.txt --strip-extras --unsafe-package=distribute --unsafe-package=localstack-core --unsafe-package=pip --unsafe-package=setuptools pyproject.toml # aiofiles==23.2.1 # via quart @@ -210,7 +210,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext[all]==5.0.4.post1 +moto-ext==5.0.4.post1 # via localstack-core (pyproject.toml) mpmath==1.3.0 # via sympy diff --git a/requirements-test.txt b/requirements-test.txt index ae0f714d9cf49..4387b3c4e8fc9 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --extra=test --output-file=requirements-test.txt --unsafe-package=distribute --unsafe-package=localstack-core --unsafe-package=pip --unsafe-package=setuptools pyproject.toml +# pip-compile --extra=test --output-file=requirements-test.txt --strip-extras --unsafe-package=distribute --unsafe-package=localstack-core --unsafe-package=pip --unsafe-package=setuptools pyproject.toml # aiofiles==23.2.1 # via quart @@ -106,7 +106,7 @@ constantly==23.10.4 # via localstack-twisted constructs==10.3.0 # via aws-cdk-lib -coverage[toml]==7.4.4 +coverage==7.4.4 # via localstack-core (pyproject.toml) crontab==1.0.1 # via localstack-core @@ -162,7 +162,7 @@ hpack==4.0.0 # via h2 httpcore==1.0.5 # via httpx -httpx[http2]==0.27.0 +httpx==0.27.0 # via localstack-core (pyproject.toml) hypercorn==0.16.0 # via @@ -257,7 +257,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext[all]==5.0.4.post1 +moto-ext==5.0.4.post1 # via localstack-core mpmath==1.3.0 # via sympy diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 453ed24b2c156..0261106070039 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.11 # by the following command: # -# pip-compile --extra=typehint --output-file=requirements-typehint.txt --unsafe-package=distribute --unsafe-package=localstack-core --unsafe-package=pip --unsafe-package=setuptools pyproject.toml +# pip-compile --extra=typehint --output-file=requirements-typehint.txt --strip-extras --unsafe-package=distribute --unsafe-package=localstack-core --unsafe-package=pip --unsafe-package=setuptools pyproject.toml # aiofiles==23.2.1 # via quart @@ -60,7 +60,7 @@ boto3==1.34.74 # aws-sam-translator # localstack-core # moto-ext -boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.75 +boto3-stubs==1.34.74 # via localstack-core (pyproject.toml) botocore==1.34.74 # via @@ -112,7 +112,7 @@ constantly==23.10.4 # via localstack-twisted constructs==10.3.0 # via aws-cdk-lib -coverage[toml]==6.5.0 +coverage==6.5.0 # via # coveralls # localstack-core @@ -180,7 +180,7 @@ hpack==4.0.0 # via h2 httpcore==1.0.5 # via httpx -httpx[http2]==0.27.0 +httpx==0.27.0 # via localstack-core hypercorn==0.16.0 # via @@ -277,7 +277,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext[all]==5.0.4.post1 +moto-ext==5.0.4.post1 # via localstack-core mpmath==1.3.0 # via sympy @@ -339,7 +339,7 @@ mypy-boto3-ec2==1.34.73 # via boto3-stubs mypy-boto3-ecr==1.34.0 # via boto3-stubs -mypy-boto3-ecs==1.34.71 +mypy-boto3-ecs==1.34.76 # via boto3-stubs mypy-boto3-efs==1.34.0 # via boto3-stubs @@ -365,7 +365,7 @@ mypy-boto3-fis==1.34.63 # via boto3-stubs mypy-boto3-glacier==1.34.0 # via boto3-stubs -mypy-boto3-glue==1.34.35 +mypy-boto3-glue==1.34.76 # via boto3-stubs mypy-boto3-iam==1.34.8 # via boto3-stubs From acd38e462a6bb398d8585b0e18338e5c42ae2e59 Mon Sep 17 00:00:00 2001 From: Smith Chang <164195759+veryyet@users.noreply.github.com> Date: Thu, 4 Apr 2024 00:11:50 +0800 Subject: [PATCH 025/169] chore: remove repetitive words (#10507) Signed-off-by: veryyet --- localstack/services/lambda_/invocation/version_manager.py | 2 +- localstack/utils/aws/request_context.py | 2 +- localstack/utils/http.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/localstack/services/lambda_/invocation/version_manager.py b/localstack/services/lambda_/invocation/version_manager.py index a59c31195886e..23553984fbe91 100644 --- a/localstack/services/lambda_/invocation/version_manager.py +++ b/localstack/services/lambda_/invocation/version_manager.py @@ -203,7 +203,7 @@ def invoke(self, *, invocation: Invocation) -> InvocationResult: self.function, self.function_version ) as provisioning_type: # TODO: potential race condition when changing provisioned concurrency after getting the lease but before - # getting an an environment + # getting an environment try: # Blocks and potentially creates a new execution environment for this invocation with self.assignment_service.get_environment( diff --git a/localstack/utils/aws/request_context.py b/localstack/utils/aws/request_context.py index 1d04ada49c26a..c018242e09108 100644 --- a/localstack/utils/aws/request_context.py +++ b/localstack/utils/aws/request_context.py @@ -48,7 +48,7 @@ def get_proxy_request_for_thread(): def get_flask_request_for_thread(): try: - # Append/cache a converted request (requests.Request) to the the thread-local Flask request. + # Append/cache a converted request (requests.Request) to the thread-local Flask request. # We use this request object as the invocation context, which may be modified in other places, # e.g., when manually configuring the region in the request context of an incoming API call. if not hasattr(request, "_converted_request"): diff --git a/localstack/utils/http.py b/localstack/utils/http.py index da307bc13e521..5c16444bdf031 100644 --- a/localstack/utils/http.py +++ b/localstack/utils/http.py @@ -292,7 +292,7 @@ def do_download( except Exception as e: if print_error: LOG.info( - "Unable to download Github artifact from from %s to %s: %s %s" + "Unable to download Github artifact from %s to %s: %s %s" % (url, target_file, e, traceback.format_exc()) ) From aeb04b64f78e06ea3c64a3dadd60c9b1ecf621ed Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Wed, 3 Apr 2024 22:03:22 +0200 Subject: [PATCH 026/169] fix SNS MessageBody filtering when value is a list (#10594) --- localstack/services/sns/publisher.py | 3 + tests/aws/services/sns/test_sns.py | 172 +++++++++++++++ tests/aws/services/sns/test_sns.snapshot.json | 199 ++++++++++++++++++ .../aws/services/sns/test_sns.validation.json | 6 + tests/unit/test_sns.py | 8 + 5 files changed, 388 insertions(+) diff --git a/localstack/services/sns/publisher.py b/localstack/services/sns/publisher.py index cc87acf36f586..022766ce4c1b4 100644 --- a/localstack/services/sns/publisher.py +++ b/localstack/services/sns/publisher.py @@ -1179,6 +1179,9 @@ def _evaluate_filter_policy_conditions_on_attribute( return False def _evaluate_condition(self, value, condition, field_exists: bool): + if isinstance(value, list): + return any(self._evaluate_condition(val, condition, field_exists) for val in value) + if not isinstance(condition, dict): return field_exists and value == condition elif (must_exist := condition.get("exists")) is not None: diff --git a/tests/aws/services/sns/test_sns.py b/tests/aws/services/sns/test_sns.py index e6c6ad599f1e7..e5456308f8b0c 100644 --- a/tests/aws/services/sns/test_sns.py +++ b/tests/aws/services/sns/test_sns.py @@ -3043,6 +3043,84 @@ def get_filter_policy(): num_msgs_4 = len(response_4["Messages"]) assert num_msgs_4 == num_msgs_3 + @markers.aws.validated + def test_exists_filter_policy_attributes_array( + self, sqs_create_queue, sns_create_topic, sns_create_sqs_subscription, snapshot, aws_client + ): + topic_arn = sns_create_topic()["TopicArn"] + queue_url = sqs_create_queue() + subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) + subscription_arn = subscription["SubscriptionArn"] + + filter_policy = {"store": ["value1"]} + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=json.dumps(filter_policy), + ) + + response_attributes = aws_client.sns.get_subscription_attributes( + SubscriptionArn=subscription_arn + ) + snapshot.match("subscription-attributes-policy", response_attributes) + + response_0 = aws_client.sqs.receive_message(QueueUrl=queue_url, VisibilityTimeout=0) + snapshot.match("messages-init", response_0) + + # publish message that satisfies the filter policy, assert that message is received + message = "message-1" + aws_client.sns.publish( + TopicArn=topic_arn, + Message=message, + MessageAttributes={ + "store": {"DataType": "String", "StringValue": "value1"}, + }, + ) + response_1 = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 + ) + aws_client.sqs.delete_message( + QueueUrl=queue_url, ReceiptHandle=response_1["Messages"][0]["ReceiptHandle"] + ) + snapshot.match("messages-1", response_1) + + # publish message that satisfies the filter policy but with String.Array + message = "message-2" + aws_client.sns.publish( + TopicArn=topic_arn, + Message=message, + MessageAttributes={ + "store": { + "DataType": "String.Array", + "StringValue": json.dumps(["value1", "value2"]), + }, + }, + ) + response_2 = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 + ) + aws_client.sqs.delete_message( + QueueUrl=queue_url, ReceiptHandle=response_2["Messages"][0]["ReceiptHandle"] + ) + snapshot.match("messages-2", response_2) + + # publish message that does not satisfy the filter policy with String.Array + message = "message-3" + aws_client.sns.publish( + TopicArn=topic_arn, + Message=message, + MessageAttributes={ + "store": { + "DataType": "String.Array", + "StringValue": json.dumps(["value2", "value3"]), + }, + }, + ) + response_3 = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 + ) + snapshot.match("messages-3", response_3) + @markers.aws.validated def test_set_subscription_filter_policy_scope( self, sqs_create_queue, sns_create_topic, sns_create_sqs_subscription, snapshot, aws_client @@ -3535,6 +3613,100 @@ def _verify_and_snapshot_sqs_messages(msg_to_send: list[dict], snapshot_prefix: # assert there are no messages in the queue assert "Messages" not in response or response["Messages"] == [] + @markers.aws.validated + def test_filter_policy_on_message_body_array_attributes( + self, + sqs_create_queue, + sns_create_topic, + sns_create_sqs_subscription, + snapshot, + aws_client, + ): + topic_arn = sns_create_topic()["TopicArn"] + queue_url_1 = sqs_create_queue() + queue_url_2 = sqs_create_queue() + subscription_1 = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url_1) + subscription_2 = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url_2) + subscription_arn_1 = subscription_1["SubscriptionArn"] + subscription_arn_2 = subscription_2["SubscriptionArn"] + + filter_policy_1 = {"headers": {"route-to": ["queue1"]}} + filter_policy_2 = {"headers": {"route-to": ["queue2"]}} + for sub_arn, filter_policy in ( + (subscription_arn_1, filter_policy_1), + (subscription_arn_2, filter_policy_2), + ): + aws_client.sns.set_subscription_attributes( + SubscriptionArn=sub_arn, + AttributeName="FilterPolicyScope", + AttributeValue="MessageBody", + ) + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=sub_arn, + AttributeName="FilterPolicy", + AttributeValue=json.dumps(filter_policy), + ) + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=sub_arn, + AttributeName="RawMessageDelivery", + AttributeValue="true", + ) + + queues = [queue_url_1, queue_url_2] + + for i, queue_url in enumerate(queues): + response = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=1 + ) + snapshot.match(f"recv-init-{i}", response) + # assert there are no messages in the queue + assert "Messages" not in response or response["Messages"] == [] + + # publish messages that satisfies the filter policy, assert that messages are received + messages = [ + {"headers": {"route-to": ["queue3"]}}, + {"headers": {"route-to": ["queue1"]}}, + {"headers": {"route-to": ["queue2"]}}, + {"headers": {"route-to": ["queue1", "queue2"]}}, + ] + for i, message in enumerate(messages): + aws_client.sns.publish( + TopicArn=topic_arn, + Message=json.dumps(message), + ) + + def get_messages(_queue_url: str, _recv_messages: list): + # due to the random nature of receiving SQS messages, we need to consolidate a single object to match + sqs_response = aws_client.sqs.receive_message( + QueueUrl=_queue_url, + WaitTimeSeconds=1, + VisibilityTimeout=0, + MessageAttributeNames=["All"], + AttributeNames=["All"], + ) + for _message in sqs_response["Messages"]: + _recv_messages.append(_message) + aws_client.sqs.delete_message( + QueueUrl=_queue_url, ReceiptHandle=_message["ReceiptHandle"] + ) + + assert len(_recv_messages) == 2 + + for i, queue_url in enumerate(queues): + recv_messages = [] + retry( + get_messages, + retries=10, + sleep=0.1, + _queue_url=queue_url, + _recv_messages=recv_messages, + ) + # we need to sort the list (the order does not matter as we're not using FIFO) + recv_messages.sort(key=itemgetter("Body")) + snapshot.match(f"messages-queue-{i}", {"Messages": recv_messages}) + class TestSNSPlatformEndpoint: @markers.aws.only_localstack diff --git a/tests/aws/services/sns/test_sns.snapshot.json b/tests/aws/services/sns/test_sns.snapshot.json index cd020730a1406..6eb7a3fe5cfc4 100644 --- a/tests/aws/services/sns/test_sns.snapshot.json +++ b/tests/aws/services/sns/test_sns.snapshot.json @@ -5022,5 +5022,204 @@ "UnsubscribeUrl": "/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns::111111111111::" } } + }, + "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_exists_filter_policy_attributes_array": { + "recorded-date": "02-04-2024, 22:27:18", + "recorded-content": { + "subscription-attributes-policy": { + "Attributes": { + "ConfirmationWasAuthenticated": "true", + "Endpoint": "arn:aws:sqs::111111111111:", + "FilterPolicy": { + "store": [ + "value1" + ] + }, + "FilterPolicyScope": "MessageAttributes", + "Owner": "111111111111", + "PendingConfirmation": "false", + "Protocol": "sqs", + "RawMessageDelivery": "false", + "SubscriptionArn": "arn:aws:sns::111111111111::", + "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/", + "TopicArn": "arn:aws:sns::111111111111:" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-init": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-1": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "", + "TopicArn": "arn:aws:sns::111111111111:", + "Message": "message-1", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "", + "SigningCertURL": "/SimpleNotificationService-", + "UnsubscribeURL": "/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns::111111111111::", + "MessageAttributes": { + "store": { + "Type": "String", + "Value": "value1" + } + } + }, + "MD5OfBody": "", + "MessageId": "", + "ReceiptHandle": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-2": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "", + "TopicArn": "arn:aws:sns::111111111111:", + "Message": "message-2", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "", + "SigningCertURL": "/SimpleNotificationService-", + "UnsubscribeURL": "/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns::111111111111::", + "MessageAttributes": { + "store": { + "Type": "String.Array", + "Value": "[\"value1\", \"value2\"]" + } + } + }, + "MD5OfBody": "", + "MessageId": "", + "ReceiptHandle": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-3": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body_array_attributes": { + "recorded-date": "02-04-2024, 22:36:25", + "recorded-content": { + "recv-init-0": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "recv-init-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-queue-0": { + "Messages": [ + { + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "1", + "SenderId": "", + "SentTimestamp": "timestamp" + }, + "Body": { + "headers": { + "route-to": [ + "queue1", + "queue2" + ] + } + }, + "MD5OfBody": "", + "MessageId": "", + "ReceiptHandle": "" + }, + { + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "1", + "SenderId": "", + "SentTimestamp": "timestamp" + }, + "Body": { + "headers": { + "route-to": [ + "queue1" + ] + } + }, + "MD5OfBody": "", + "MessageId": "", + "ReceiptHandle": "" + } + ] + }, + "messages-queue-1": { + "Messages": [ + { + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "1", + "SenderId": "", + "SentTimestamp": "timestamp" + }, + "Body": { + "headers": { + "route-to": [ + "queue1", + "queue2" + ] + } + }, + "MD5OfBody": "", + "MessageId": "", + "ReceiptHandle": "" + }, + { + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "1", + "SenderId": "", + "SentTimestamp": "timestamp" + }, + "Body": { + "headers": { + "route-to": [ + "queue2" + ] + } + }, + "MD5OfBody": "", + "MessageId": "", + "ReceiptHandle": "" + } + ] + } + } } } diff --git a/tests/aws/services/sns/test_sns.validation.json b/tests/aws/services/sns/test_sns.validation.json index a581ab159b6e0..d122a43bb9c54 100644 --- a/tests/aws/services/sns/test_sns.validation.json +++ b/tests/aws/services/sns/test_sns.validation.json @@ -2,6 +2,9 @@ "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_exists_filter_policy": { "last_validated_date": "2023-11-09T20:04:02+00:00" }, + "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_exists_filter_policy_attributes_array": { + "last_validated_date": "2024-04-02T22:27:17+00:00" + }, "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy": { "last_validated_date": "2024-01-25T18:07:57+00:00" }, @@ -14,6 +17,9 @@ "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body[True]": { "last_validated_date": "2023-11-09T19:58:29+00:00" }, + "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body_array_attributes": { + "last_validated_date": "2024-04-02T22:36:24+00:00" + }, "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body_dot_attribute": { "last_validated_date": "2023-11-09T17:50:59+00:00" }, diff --git a/tests/unit/test_sns.py b/tests/unit/test_sns.py index 34a5f73b3750c..4a9820486555e 100644 --- a/tests/unit/test_sns.py +++ b/tests/unit/test_sns.py @@ -601,6 +601,14 @@ def test_filter_policy_on_message_body(self): ({"f1": "v3", "f2": "v5"}, False), ), ), + ( + {"f1": ["v1", "v2"]}, # f1 must be v1 OR v2 (f1=v1 OR f1=v2) + ( + ({"f1": ["v1"], "f2": "v4"}, True), + ({"f1": ["v2", "v3"], "f2": "v5"}, True), + ({"f1": ["v3", "v4"], "f2": "v5"}, False), + ), + ), ] sub_filter = SubscriptionFilter() From 904e8c4cb313bc2f159c9475b94ffd9294289c31 Mon Sep 17 00:00:00 2001 From: Macwan Nevil Date: Fri, 5 Apr 2024 16:05:54 +0530 Subject: [PATCH 027/169] added aws validate test case for ListSecrets filtering (#10520) --- .../secretsmanager/test_secretsmanager.py | 102 ++++++++++++++++++ .../test_secretsmanager.validation.json | 3 + 2 files changed, 105 insertions(+) diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.py b/tests/aws/services/secretsmanager/test_secretsmanager.py index 30765250f45ca..c053318914dca 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.py +++ b/tests/aws/services/secretsmanager/test_secretsmanager.py @@ -261,6 +261,108 @@ def test_call_lists_secrets_multiple_times_snapshots( ) sm_snapshot.match("delete_secret_res_1", delete_secret_res_1) + @markers.aws.validated + def test_list_secrets_filtering(self, aws_client, create_secret): + secret_name_1 = "testing1/one" + secret_name_2 = "/testing2/two" + secret_name_3 = "testing3/three" + secret_name_4 = "/testing4/four" + + secret_1 = create_secret(Name=secret_name_1, SecretString="secret", Description="a secret") + secret_2 = create_secret(Name=secret_name_2, SecretString="secret", Description="an secret") + secret_3 = create_secret(Name=secret_name_3, SecretString="secret", Description="asecret") + secret_4 = create_secret(Name=secret_name_4, SecretString="secret", Description="thesecret") + + self._wait_created_is_listed(aws_client.secretsmanager, secret_id=secret_name_1) + self._wait_created_is_listed(aws_client.secretsmanager, secret_id=secret_name_2) + self._wait_created_is_listed(aws_client.secretsmanager, secret_id=secret_name_3) + self._wait_created_is_listed(aws_client.secretsmanager, secret_id=secret_name_4) + + # name based filtering + response = aws_client.secretsmanager.list_secrets( + Filters=[{"Key": "name", "Values": ["/"]}] + ) + assert len(response["SecretList"]) == 2 + for secret in response["SecretList"]: + assert secret["Name"] in [secret_name_2, secret_name_4] + assert secret["ARN"] in [secret_2["ARN"], secret_4["ARN"]] + + response = aws_client.secretsmanager.list_secrets( + Filters=[{"Key": "name", "Values": ["!/"]}] + ) + assert len(response["SecretList"]) == 2 + for secret in response["SecretList"]: + assert secret["Name"] in [secret_name_1, secret_name_3] + assert secret["ARN"] in [secret_1["ARN"], secret_3["ARN"]] + + response = aws_client.secretsmanager.list_secrets( + Filters=[{"Key": "name", "Values": ["testing1 one"]}] + ) + assert len(response["SecretList"]) == 0 + + # description based filtering + response = aws_client.secretsmanager.list_secrets( + Filters=[{"Key": "description", "Values": ["a"]}] + ) + assert len(response["SecretList"]) == 3 + for secret in response["SecretList"]: + assert secret["Description"].startswith("a") + + response = aws_client.secretsmanager.list_secrets( + Filters=[{"Key": "description", "Values": ["!a"]}] + ) + assert len(response["SecretList"]) == 1 + assert response["SecretList"][0]["Description"] == "thesecret" + + response = aws_client.secretsmanager.list_secrets( + Filters=[{"Key": "description", "Values": ["a secret"]}] + ) + assert len(response["SecretList"]) == 2 + + # name and description based filtering + response = aws_client.secretsmanager.list_secrets( + Filters=[ + {"Key": "description", "Values": ["a"]}, + {"Key": "name", "Values": ["secret"]}, + ] + ) + assert len(response["SecretList"]) == 0 + + response = aws_client.secretsmanager.list_secrets( + Filters=[ + {"Key": "description", "Values": ["a"]}, + {"Key": "name", "Values": ["an"]}, + ] + ) + assert len(response["SecretList"]) == 0 + + response = aws_client.secretsmanager.list_secrets( + Filters=[ + {"Key": "description", "Values": ["a secret"]}, + ] + ) + assert len(response["SecretList"]) == 2 + + for secret in response["SecretList"]: + assert secret["Name"] in [secret_name_1, secret_name_2] + + response = aws_client.secretsmanager.list_secrets( + Filters=[ + {"Key": "description", "Values": ["!a"]}, + ] + ) + assert len(response["SecretList"]) == 1 + + response = aws_client.secretsmanager.list_secrets( + Filters=[{"Key": "description", "Values": ["!c"]}] + ) + assert len(response["SecretList"]) == 4 + + response = aws_client.secretsmanager.list_secrets( + Filters=[{"Key": "name", "Values": ["testing1 one"]}] + ) + assert len(response["SecretList"]) == 0 + @markers.aws.validated def test_create_multi_secrets(self, cleanups, aws_client): secret_names = [short_uid(), short_uid(), short_uid()] diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json index 00c51c648d5d8..abc38119cbd08 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json @@ -56,6 +56,9 @@ "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_last_updated_date": { "last_validated_date": "2024-03-15T08:12:47+00:00" }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_list_secrets_filtering": { + "last_validated_date": "2024-03-26T09:40:54+00:00" + }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_no_client_request_token[CreateSecret]": { "last_validated_date": "2024-03-15T08:14:56+00:00" }, From 8f6fa077271ceff48aad7b058f9f827e3d9ce217 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:37:42 +0200 Subject: [PATCH 028/169] fix s3 create bucket empty constraint for us-east-1 (#10604) --- localstack/aws/api/s3/__init__.py | 7 ++++ localstack/aws/spec-patches.json | 14 +++++++ localstack/services/s3/exceptions.py | 5 --- localstack/services/s3/v3/provider.py | 16 ++++++-- tests/aws/services/s3/test_s3.py | 39 ++++++++++++++++--- tests/aws/services/s3/test_s3.snapshot.json | 27 +++++++++++-- tests/aws/services/s3/test_s3.validation.json | 2 +- 7 files changed, 93 insertions(+), 17 deletions(-) diff --git a/localstack/aws/api/s3/__init__.py b/localstack/aws/api/s3/__init__.py index bd7279f9ab5fb..39ff9d944af3b 100644 --- a/localstack/aws/api/s3/__init__.py +++ b/localstack/aws/api/s3/__init__.py @@ -925,6 +925,13 @@ class KeyTooLongError(ServiceException): Size: Optional[KeyLength] +class InvalidLocationConstraint(ServiceException): + code: str = "InvalidLocationConstraint" + sender_fault: bool = False + status_code: int = 400 + LocationConstraint: Optional[BucketRegion] + + AbortDate = datetime diff --git a/localstack/aws/spec-patches.json b/localstack/aws/spec-patches.json index e57f884dd865d..8354cd50f541e 100644 --- a/localstack/aws/spec-patches.json +++ b/localstack/aws/spec-patches.json @@ -1176,6 +1176,20 @@ "documentation": "

Your key is too long

", "exception": true } + }, + { + "op": "add", + "path": "/shapes/InvalidLocationConstraint", + "value": { + "type": "structure", + "members": { + "LocationConstraint": { + "shape": "BucketRegion" + } + }, + "documentation": "

The specified location-constraint is not valid

", + "exception": true + } } ] } diff --git a/localstack/services/s3/exceptions.py b/localstack/services/s3/exceptions.py index da76db00f82b3..e87356e24e3f6 100644 --- a/localstack/services/s3/exceptions.py +++ b/localstack/services/s3/exceptions.py @@ -1,11 +1,6 @@ from localstack.aws.api import CommonServiceException -class InvalidLocationConstraint(CommonServiceException): - def __init__(self, message=None): - super().__init__("InvalidLocationConstraint", status_code=400, message=message) - - class MalformedXML(CommonServiceException): def __init__(self, message=None): if not message: diff --git a/localstack/services/s3/v3/provider.py b/localstack/services/s3/v3/provider.py index 3ba3512700ae6..d8e1b4afec7e1 100644 --- a/localstack/services/s3/v3/provider.py +++ b/localstack/services/s3/v3/provider.py @@ -101,6 +101,7 @@ InvalidArgument, InvalidBucketName, InvalidDigest, + InvalidLocationConstraint, InvalidObjectState, InvalidPartNumber, InvalidPartOrder, @@ -211,7 +212,6 @@ from localstack.services.s3.cors import S3CorsHandler, s3_cors_request_handler from localstack.services.s3.exceptions import ( InvalidBucketState, - InvalidLocationConstraint, InvalidRequest, MalformedPolicy, MalformedXML, @@ -421,12 +421,22 @@ def create_bucket( if not is_bucket_name_valid(bucket_name): raise InvalidBucketName("The specified bucket is not valid.", BucketName=bucket_name) - if create_bucket_configuration := request.get("CreateBucketConfiguration"): + + # the XML parser returns an empty dict if the body contains the following: + # + # but it also returns an empty dict if the body is fully empty. We need to differentiate the 2 cases by checking + # if the body is empty or not + if context.request.data and ( + (create_bucket_configuration := request.get("CreateBucketConfiguration")) is not None + ): if not (bucket_region := create_bucket_configuration.get("LocationConstraint")): raise MalformedXML() if bucket_region == "us-east-1": - raise InvalidLocationConstraint("The specified location-constraint is not valid") + raise InvalidLocationConstraint( + "The specified location-constraint is not valid", + LocationConstraint=bucket_region, + ) else: bucket_region = "us-east-1" if context.region != bucket_region: diff --git a/tests/aws/services/s3/test_s3.py b/tests/aws/services/s3/test_s3.py index adf358d546795..30c63e65695b9 100644 --- a/tests/aws/services/s3/test_s3.py +++ b/tests/aws/services/s3/test_s3.py @@ -2540,6 +2540,10 @@ def test_bucket_availability(self, snapshot, aws_client): snapshot.match("bucket-replication", e.value.response) @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + condition=is_v2_provider, + paths=["$..Error.LocationConstraint"], # not returned by Moto + ) def test_different_location_constraint( self, s3_create_bucket, @@ -2550,12 +2554,19 @@ def test_different_location_constraint( aws_client, ): snapshot.add_transformer(snapshot.transform.s3_api()) - snapshot.add_transformer( - snapshot.transform.key_value("Location", "", reference_replacement=False) + snapshot.add_transformers_list( + [ + snapshot.transform.key_value("Location", "", reference_replacement=False), + snapshot.transform.key_value( + "LocationConstraint", "", reference_replacement=False + ), + ] ) bucket_1_name = f"bucket-{short_uid()}" - region_1 = "us-east-1" - client_1 = aws_client_factory(region_name=region_1).s3 + region_us_east_1 = "us-east-1" + client_1 = aws_client_factory( + region_name=region_us_east_1, config=Config(parameter_validation=False) + ).s3 s3_create_bucket_with_client( client_1, Bucket=bucket_1_name, @@ -2563,6 +2574,24 @@ def test_different_location_constraint( response = client_1.get_bucket_location(Bucket=bucket_1_name) snapshot.match("get_bucket_location_bucket_1", response) + # assert creation fails with location constraint for us-east-1 region + with pytest.raises(Exception) as exc: + client_1.create_bucket( + Bucket=f"bucket-{short_uid()}", + CreateBucketConfiguration={"LocationConstraint": region_us_east_1}, + ) + snapshot.match("create-bucket-constraint-us-east-1", exc.value.response) + if is_aws_cloud() or not is_v2_provider(): + assert exc.value.response["Error"]["LocationConstraint"] == region_us_east_1 + + # assert creation fails with location constraint with the region unset + with pytest.raises(Exception) as exc: + client_1.create_bucket( + Bucket=f"bucket-{short_uid()}", + CreateBucketConfiguration={"LocationConstraint": None}, + ) + snapshot.match("create-bucket-constraint-us-east-1-with-None", exc.value.response) + region_2 = "us-east-2" snapshot.add_transformer(RegexTransformer(region_2, "")) client_2 = aws_client_factory(region_name=region_2).s3 @@ -10413,7 +10442,7 @@ def test_post_object_with_storage_class(self, s3_bucket, aws_client, snapshot): reason="not implemented in moto", ) @markers.snapshot.skip_snapshot_verify( - paths=["$..HostId"], # FIXME: in CI, it fails sporadically and the form is empty + paths=["$..HostId"], ) def test_post_object_with_wrong_content_type(self, s3_bucket, aws_client, snapshot): snapshot.add_transformers_list( diff --git a/tests/aws/services/s3/test_s3.snapshot.json b/tests/aws/services/s3/test_s3.snapshot.json index 207fecd06c3ad..fc69023424cbb 100644 --- a/tests/aws/services/s3/test_s3.snapshot.json +++ b/tests/aws/services/s3/test_s3.snapshot.json @@ -1078,7 +1078,7 @@ } }, "tests/aws/services/s3/test_s3.py::TestS3::test_different_location_constraint": { - "recorded-date": "25-03-2024, 17:20:27", + "recorded-date": "04-04-2024, 17:13:12", "recorded-content": { "get_bucket_location_bucket_1": { "LocationConstraint": null, @@ -1087,8 +1087,29 @@ "HTTPStatusCode": 200 } }, + "create-bucket-constraint-us-east-1": { + "Error": { + "Code": "InvalidLocationConstraint", + "LocationConstraint": "", + "Message": "The specified location-constraint is not valid" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "create-bucket-constraint-us-east-1-with-None": { + "Error": { + "Code": "MalformedXML", + "Message": "The XML you provided was not well-formed or did not validate against our published schema" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, "get_bucket_location_bucket_2": { - "LocationConstraint": "", + "LocationConstraint": "", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -1112,7 +1133,7 @@ } }, "get_bucket_location_bucket_3": { - "LocationConstraint": "", + "LocationConstraint": "", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 diff --git a/tests/aws/services/s3/test_s3.validation.json b/tests/aws/services/s3/test_s3.validation.json index 16966ad0f1618..e94b24584e521 100644 --- a/tests/aws/services/s3/test_s3.validation.json +++ b/tests/aws/services/s3/test_s3.validation.json @@ -63,7 +63,7 @@ "last_validated_date": "2023-10-22T02:25:14+00:00" }, "tests/aws/services/s3/test_s3.py::TestS3::test_different_location_constraint": { - "last_validated_date": "2024-03-25T17:20:27+00:00" + "last_validated_date": "2024-04-04T17:13:12+00:00" }, "tests/aws/services/s3/test_s3.py::TestS3::test_empty_bucket_fixture": { "last_validated_date": "2023-09-08T16:52:15+00:00" From a3b9adeb05e6bd3f0c4b5d8ab106d1f52cae1ee6 Mon Sep 17 00:00:00 2001 From: Macwan Nevil Date: Fri, 5 Apr 2024 21:14:16 +0530 Subject: [PATCH 029/169] Revert "added aws validate test case for ListSecrets filtering" (#10607) --- .../secretsmanager/test_secretsmanager.py | 102 ------------------ .../test_secretsmanager.validation.json | 3 - 2 files changed, 105 deletions(-) diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.py b/tests/aws/services/secretsmanager/test_secretsmanager.py index c053318914dca..30765250f45ca 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.py +++ b/tests/aws/services/secretsmanager/test_secretsmanager.py @@ -261,108 +261,6 @@ def test_call_lists_secrets_multiple_times_snapshots( ) sm_snapshot.match("delete_secret_res_1", delete_secret_res_1) - @markers.aws.validated - def test_list_secrets_filtering(self, aws_client, create_secret): - secret_name_1 = "testing1/one" - secret_name_2 = "/testing2/two" - secret_name_3 = "testing3/three" - secret_name_4 = "/testing4/four" - - secret_1 = create_secret(Name=secret_name_1, SecretString="secret", Description="a secret") - secret_2 = create_secret(Name=secret_name_2, SecretString="secret", Description="an secret") - secret_3 = create_secret(Name=secret_name_3, SecretString="secret", Description="asecret") - secret_4 = create_secret(Name=secret_name_4, SecretString="secret", Description="thesecret") - - self._wait_created_is_listed(aws_client.secretsmanager, secret_id=secret_name_1) - self._wait_created_is_listed(aws_client.secretsmanager, secret_id=secret_name_2) - self._wait_created_is_listed(aws_client.secretsmanager, secret_id=secret_name_3) - self._wait_created_is_listed(aws_client.secretsmanager, secret_id=secret_name_4) - - # name based filtering - response = aws_client.secretsmanager.list_secrets( - Filters=[{"Key": "name", "Values": ["/"]}] - ) - assert len(response["SecretList"]) == 2 - for secret in response["SecretList"]: - assert secret["Name"] in [secret_name_2, secret_name_4] - assert secret["ARN"] in [secret_2["ARN"], secret_4["ARN"]] - - response = aws_client.secretsmanager.list_secrets( - Filters=[{"Key": "name", "Values": ["!/"]}] - ) - assert len(response["SecretList"]) == 2 - for secret in response["SecretList"]: - assert secret["Name"] in [secret_name_1, secret_name_3] - assert secret["ARN"] in [secret_1["ARN"], secret_3["ARN"]] - - response = aws_client.secretsmanager.list_secrets( - Filters=[{"Key": "name", "Values": ["testing1 one"]}] - ) - assert len(response["SecretList"]) == 0 - - # description based filtering - response = aws_client.secretsmanager.list_secrets( - Filters=[{"Key": "description", "Values": ["a"]}] - ) - assert len(response["SecretList"]) == 3 - for secret in response["SecretList"]: - assert secret["Description"].startswith("a") - - response = aws_client.secretsmanager.list_secrets( - Filters=[{"Key": "description", "Values": ["!a"]}] - ) - assert len(response["SecretList"]) == 1 - assert response["SecretList"][0]["Description"] == "thesecret" - - response = aws_client.secretsmanager.list_secrets( - Filters=[{"Key": "description", "Values": ["a secret"]}] - ) - assert len(response["SecretList"]) == 2 - - # name and description based filtering - response = aws_client.secretsmanager.list_secrets( - Filters=[ - {"Key": "description", "Values": ["a"]}, - {"Key": "name", "Values": ["secret"]}, - ] - ) - assert len(response["SecretList"]) == 0 - - response = aws_client.secretsmanager.list_secrets( - Filters=[ - {"Key": "description", "Values": ["a"]}, - {"Key": "name", "Values": ["an"]}, - ] - ) - assert len(response["SecretList"]) == 0 - - response = aws_client.secretsmanager.list_secrets( - Filters=[ - {"Key": "description", "Values": ["a secret"]}, - ] - ) - assert len(response["SecretList"]) == 2 - - for secret in response["SecretList"]: - assert secret["Name"] in [secret_name_1, secret_name_2] - - response = aws_client.secretsmanager.list_secrets( - Filters=[ - {"Key": "description", "Values": ["!a"]}, - ] - ) - assert len(response["SecretList"]) == 1 - - response = aws_client.secretsmanager.list_secrets( - Filters=[{"Key": "description", "Values": ["!c"]}] - ) - assert len(response["SecretList"]) == 4 - - response = aws_client.secretsmanager.list_secrets( - Filters=[{"Key": "name", "Values": ["testing1 one"]}] - ) - assert len(response["SecretList"]) == 0 - @markers.aws.validated def test_create_multi_secrets(self, cleanups, aws_client): secret_names = [short_uid(), short_uid(), short_uid()] diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json index abc38119cbd08..00c51c648d5d8 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json @@ -56,9 +56,6 @@ "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_last_updated_date": { "last_validated_date": "2024-03-15T08:12:47+00:00" }, - "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_list_secrets_filtering": { - "last_validated_date": "2024-03-26T09:40:54+00:00" - }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_no_client_request_token[CreateSecret]": { "last_validated_date": "2024-03-15T08:14:56+00:00" }, From c2e179dfc638e974d21c367f5f321d88d2b9a86b Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Mon, 8 Apr 2024 08:09:41 +0200 Subject: [PATCH 030/169] Update ASF APIs (#10611) Co-authored-by: LocalStack Bot --- localstack/aws/api/cloudformation/__init__.py | 10 ++++++++++ localstack/aws/api/cloudwatch/__init__.py | 1 + localstack/aws/api/ec2/__init__.py | 10 ++++++++++ localstack/aws/api/lambda_/__init__.py | 1 + localstack/aws/api/resource_groups/__init__.py | 1 + pyproject.toml | 8 ++++---- requirements-base-runtime.txt | 4 ++-- requirements-dev.txt | 6 +++--- requirements-runtime.txt | 6 +++--- requirements-test.txt | 6 +++--- requirements-typehint.txt | 8 ++++---- 11 files changed, 42 insertions(+), 19 deletions(-) diff --git a/localstack/aws/api/cloudformation/__init__.py b/localstack/aws/api/cloudformation/__init__.py index b42cd52dd71ae..794124f2606b1 100644 --- a/localstack/aws/api/cloudformation/__init__.py +++ b/localstack/aws/api/cloudformation/__init__.py @@ -392,6 +392,15 @@ class PermissionModels(str): SELF_MANAGED = "SELF_MANAGED" +class PolicyAction(str): + Delete = "Delete" + Retain = "Retain" + Snapshot = "Snapshot" + ReplaceAndDelete = "ReplaceAndDelete" + ReplaceAndRetain = "ReplaceAndRetain" + ReplaceAndSnapshot = "ReplaceAndSnapshot" + + class ProvisioningType(str): NON_PROVISIONABLE = "NON_PROVISIONABLE" IMMUTABLE = "IMMUTABLE" @@ -923,6 +932,7 @@ class ResourceChangeDetail(TypedDict, total=False): class ResourceChange(TypedDict, total=False): + PolicyAction: Optional[PolicyAction] Action: Optional[ChangeAction] LogicalResourceId: Optional[LogicalResourceId] PhysicalResourceId: Optional[PhysicalResourceId] diff --git a/localstack/aws/api/cloudwatch/__init__.py b/localstack/aws/api/cloudwatch/__init__.py index aca2aea040046..27d2d1b9d523d 100644 --- a/localstack/aws/api/cloudwatch/__init__.py +++ b/localstack/aws/api/cloudwatch/__init__.py @@ -341,6 +341,7 @@ class MetricMathAnomalyDetector(TypedDict, total=False): class SingleMetricAnomalyDetector(TypedDict, total=False): + AccountId: Optional[AccountId] Namespace: Optional[Namespace] MetricName: Optional[MetricName] Dimensions: Optional[Dimensions] diff --git a/localstack/aws/api/ec2/__init__.py b/localstack/aws/api/ec2/__init__.py index 91c906955e57b..5a913b3036d63 100644 --- a/localstack/aws/api/ec2/__init__.py +++ b/localstack/aws/api/ec2/__init__.py @@ -2022,6 +2022,16 @@ class InstanceType(str): c7gd_metal = "c7gd.metal" m7gd_metal = "m7gd.metal" r7gd_metal = "r7gd.metal" + g6_xlarge = "g6.xlarge" + g6_2xlarge = "g6.2xlarge" + g6_4xlarge = "g6.4xlarge" + g6_8xlarge = "g6.8xlarge" + g6_12xlarge = "g6.12xlarge" + g6_16xlarge = "g6.16xlarge" + g6_24xlarge = "g6.24xlarge" + g6_48xlarge = "g6.48xlarge" + gr6_4xlarge = "gr6.4xlarge" + gr6_8xlarge = "gr6.8xlarge" class InstanceTypeHypervisor(str): diff --git a/localstack/aws/api/lambda_/__init__.py b/localstack/aws/api/lambda_/__init__.py index 9e788a19a72d2..12eb99b311a57 100644 --- a/localstack/aws/api/lambda_/__init__.py +++ b/localstack/aws/api/lambda_/__init__.py @@ -246,6 +246,7 @@ class Runtime(str): python3_10 = "python3.10" java17 = "java17" ruby3_2 = "ruby3.2" + ruby3_3 = "ruby3.3" python3_11 = "python3.11" nodejs20_x = "nodejs20.x" provided_al2023 = "provided.al2023" diff --git a/localstack/aws/api/resource_groups/__init__.py b/localstack/aws/api/resource_groups/__init__.py index 9b97428670313..36e8140f49c73 100644 --- a/localstack/aws/api/resource_groups/__init__.py +++ b/localstack/aws/api/resource_groups/__init__.py @@ -52,6 +52,7 @@ class QueryErrorCode(str): CLOUDFORMATION_STACK_INACTIVE = "CLOUDFORMATION_STACK_INACTIVE" CLOUDFORMATION_STACK_NOT_EXISTING = "CLOUDFORMATION_STACK_NOT_EXISTING" CLOUDFORMATION_STACK_UNASSUMABLE_ROLE = "CLOUDFORMATION_STACK_UNASSUMABLE_ROLE" + RESOURCE_TYPE_NOT_SUPPORTED = "RESOURCE_TYPE_NOT_SUPPORTED" class QueryType(str): diff --git a/pyproject.toml b/pyproject.toml index cc15a130e4887..99b49bc5b1913 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,9 +48,9 @@ Issues = "https://github.com/localstack/localstack/issues" # minimal required to actually run localstack on the host for services natively implemented in python base-runtime = [ # pinned / updated by ASF update action - "boto3==1.34.74", + "boto3==1.34.79", # pinned / updated by ASF update action - "botocore==1.34.74", + "botocore==1.34.79", "awscrt>=0.13.14", "cbor2>=5.2.0", "dnspython>=1.16.0", @@ -76,7 +76,7 @@ base-runtime = [ runtime = [ "localstack-core[base-runtime]", # pinned / updated by ASF update action - "awscli==1.32.74", + "awscli==1.32.79", "airspeed-ext>=0.6.3", "amazon_kclpy>=2.0.6,!=2.1.0", # antlr4-python3-runtime: exact pin because antlr4 runtime is tightly coupled to the generated parser code @@ -131,7 +131,7 @@ typehint = [ # typehint is an optional extension of the dev dependencies "localstack-core[dev]", # pinned / updated by ASF update action - "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.74", + "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.79", ] [tool.setuptools] diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index bb4ba9b4fd604..a848b1d303b4a 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -14,9 +14,9 @@ blinker==1.7.0 # via # flask # quart -boto3==1.34.74 +boto3==1.34.79 # via localstack-core (pyproject.toml) -botocore==1.34.74 +botocore==1.34.79 # via # boto3 # localstack-core (pyproject.toml) diff --git a/requirements-dev.txt b/requirements-dev.txt index eb4d276934b25..c2d787a04ac1f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.86.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.74 +awscli==1.32.79 # via localstack-core awscrt==0.20.6 # via localstack-core @@ -55,12 +55,12 @@ blinker==1.7.0 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.74 +boto3==1.34.79 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.74 +botocore==1.34.79 # via # aws-xray-sdk # awscli diff --git a/requirements-runtime.txt b/requirements-runtime.txt index a81a0f5b30eef..d8efd434ec59b 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -33,7 +33,7 @@ aws-sam-translator==1.86.0 # localstack-core (pyproject.toml) aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.74 +awscli==1.32.79 # via localstack-core (pyproject.toml) awscrt==0.20.6 # via localstack-core @@ -43,12 +43,12 @@ blinker==1.7.0 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.74 +boto3==1.34.79 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.74 +botocore==1.34.79 # via # aws-xray-sdk # awscli diff --git a/requirements-test.txt b/requirements-test.txt index 4387b3c4e8fc9..3f605da692398 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.86.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.74 +awscli==1.32.79 # via localstack-core awscrt==0.20.6 # via localstack-core @@ -55,12 +55,12 @@ blinker==1.7.0 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.74 +boto3==1.34.79 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.74 +botocore==1.34.79 # via # aws-xray-sdk # awscli diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 0261106070039..9469165dc781d 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.86.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.74 +awscli==1.32.79 # via localstack-core awscrt==0.20.6 # via localstack-core @@ -55,14 +55,14 @@ blinker==1.7.0 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.74 +boto3==1.34.79 # via # aws-sam-translator # localstack-core # moto-ext -boto3-stubs==1.34.74 +boto3-stubs==1.34.79 # via localstack-core (pyproject.toml) -botocore==1.34.74 +botocore==1.34.79 # via # aws-xray-sdk # awscli From 389d942ac65caf2f824d2ed88fbbe628d1a28b83 Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Mon, 8 Apr 2024 09:37:12 +0200 Subject: [PATCH 031/169] Add event matching test suite (#10599) --- localstack/services/events/provider.py | 236 ++-------- localstack/services/events/utils.py | 278 ++++++++++++ .../lambda_/event_source_listeners/utils.py | 152 ++++--- .../event_pattern_templates/arrays.json5 | 22 + .../event_pattern_templates/arrays_NEG.json5 | 21 + .../arrays_empty_EXC.json5 | 15 + .../arrays_empty_null_NEG.json5 | 15 + .../event_pattern_templates/boolean.json5 | 14 + .../event_pattern_templates/boolean_NEG.json5 | 14 + .../complex_many_rules.json5 | 58 +++ .../complex_multi_key_event.json | 11 + .../complex_multi_key_event_pattern.json | 6 + .../complex_multi_match.json5 | 26 ++ .../complex_multi_match_NEG.json5 | 27 ++ .../event_pattern_templates/complex_or.json5 | 26 ++ .../complex_or_NEG.json5 | 25 ++ .../content_anything_but_ignorecase.json5 | 19 + .../content_anything_but_ignorecase_NEG.json5 | 19 + ...content_anything_but_ignorecase_list.json5 | 19 + ...ent_anything_but_ignorecase_list_NEG.json5 | 19 + .../content_anything_but_number.json5 | 19 + .../content_anything_but_number_NEG.json5 | 19 + .../content_anything_but_number_list.json5 | 19 + ...content_anything_but_number_list_NEG.json5 | 19 + .../content_anything_but_string.json5 | 19 + .../content_anything_but_string_NEG.json5 | 19 + .../content_anything_but_string_list.json5 | 19 + ...content_anything_but_string_list_NEG.json5 | 19 + .../content_anything_prefix.json5 | 19 + .../content_anything_prefix_NEG.json5 | 19 + .../content_anything_suffix.json5 | 19 + .../content_anything_suffix_NEG.json5 | 19 + .../content_exists.json5 | 20 + .../content_exists_NEG.json5 | 22 + .../content_exists_false.json5 | 27 ++ .../content_exists_false_NEG.json5 | 31 ++ .../content_ignorecase.json5 | 14 + .../content_ignorecase_NEG.json5 | 14 + .../content_ip_address.json5 | 19 + .../content_ip_address_NEG.json5 | 19 + .../content_numeric_EXC.json5 | 19 + .../content_numeric_and.json5 | 23 + .../content_numeric_and_NEG.json5 | 23 + .../content_numeric_operatorcasing_EXC.json5 | 19 + .../content_numeric_syntax_EXC.json5 | 19 + .../content_prefix.json5 | 14 + .../content_prefix_NEG.json5 | 14 + .../content_prefix_ignorecase.json5 | 17 + .../content_suffix.json5 | 15 + .../content_suffix_NEG.json5 | 15 + .../content_suffix_ignorecase.json5 | 17 + .../content_suffix_ignorecase_NEG.json5 | 17 + .../content_wildcard_complex_EXC.json5 | 15 + .../content_wildcard_nonrepeating.json5 | 15 + .../content_wildcard_nonrepeating_NEG.json5 | 15 + .../content_wildcard_repeating.json5 | 15 + .../content_wildcard_repeating_NEG.json5 | 15 + .../content_wildcard_simplified.json5 | 15 + .../dot_joining_event.json5 | 19 + .../dot_joining_event_NEG.json5 | 21 + .../dot_joining_pattern.json5 | 21 + .../dot_joining_pattern_NEG.json5 | 23 + .../event_pattern_templates/dynamodb.json5 | 27 ++ .../int_nolist_EXC.json5 | 15 + .../key_case_sensitive_NEG.json5 | 14 + .../event_pattern_templates/minimal.json5 | 15 + .../nested_json_NEG.json5 | 16 + .../event_pattern_templates/null_value.json5 | 23 + .../null_value_NEG.json5 | 23 + .../number_comparison_float.json5 | 20 + .../operator_case_sensitive_EXC.json5 | 14 + .../operator_multiple_list.json5 | 15 + .../or-anything-but.json5 | 38 ++ .../or-exists-parent.json5 | 38 ++ .../event_pattern_templates/or-exists.json5 | 37 ++ .../event_pattern_templates/prefix.json5 | 40 ++ .../event_pattern_templates/sample1.json5 | 20 + .../event_pattern_templates/string.json5 | 16 + .../string_empty.json5 | 23 + .../string_nolist_EXC.json5 | 16 + .../services/events/test_event_patterns.py | 160 +++++++ .../events/test_event_patterns.snapshot.json | 414 ++++++++++++++++++ .../test_event_patterns.validation.json | 233 ++++++++++ .../aws/services/events/test_events_rules.py | 13 +- 84 files changed, 2760 insertions(+), 272 deletions(-) create mode 100644 localstack/services/events/utils.py create mode 100644 tests/aws/services/events/event_pattern_templates/arrays.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/arrays_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/arrays_empty_EXC.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/arrays_empty_null_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/boolean.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/boolean_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/complex_many_rules.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/complex_multi_key_event.json create mode 100644 tests/aws/services/events/event_pattern_templates/complex_multi_key_event_pattern.json create mode 100644 tests/aws/services/events/event_pattern_templates/complex_multi_match.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/complex_multi_match_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/complex_or.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/complex_or_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_anything_but_ignorecase.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_anything_but_ignorecase_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_anything_but_ignorecase_list.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_anything_but_ignorecase_list_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_anything_but_number.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_anything_but_number_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_anything_but_number_list.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_anything_but_number_list_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_anything_but_string.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_anything_but_string_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_anything_but_string_list.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_anything_but_string_list_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_anything_prefix.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_anything_prefix_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_anything_suffix.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_anything_suffix_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_exists.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_exists_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_exists_false.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_exists_false_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_ignorecase.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_ignorecase_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_ip_address.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_ip_address_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_numeric_EXC.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_numeric_and.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_numeric_and_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_numeric_operatorcasing_EXC.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_numeric_syntax_EXC.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_prefix.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_prefix_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_prefix_ignorecase.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_suffix.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_suffix_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_suffix_ignorecase.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_suffix_ignorecase_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_wildcard_complex_EXC.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_wildcard_nonrepeating.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_wildcard_nonrepeating_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_wildcard_repeating.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_wildcard_repeating_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/content_wildcard_simplified.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/dot_joining_event.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/dot_joining_event_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/dot_joining_pattern.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/dot_joining_pattern_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/dynamodb.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/int_nolist_EXC.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/key_case_sensitive_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/minimal.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/nested_json_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/null_value.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/null_value_NEG.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/number_comparison_float.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/operator_case_sensitive_EXC.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/operator_multiple_list.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/or-anything-but.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/or-exists-parent.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/or-exists.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/prefix.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/sample1.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/string.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/string_empty.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/string_nolist_EXC.json5 create mode 100644 tests/aws/services/events/test_event_patterns.py create mode 100644 tests/aws/services/events/test_event_patterns.snapshot.json create mode 100644 tests/aws/services/events/test_event_patterns.validation.json diff --git a/localstack/services/events/provider.py b/localstack/services/events/provider.py index 056a2fc226727..1d8b93a1c7bef 100644 --- a/localstack/services/events/provider.py +++ b/localstack/services/events/provider.py @@ -1,11 +1,10 @@ import datetime -import ipaddress import json import logging import os import re import time -from typing import Any, Dict, List, Optional +from typing import Any, Dict, Optional from moto.events import events_backends from moto.events.responses import EventsHandler as MotoEventsHandler @@ -42,6 +41,7 @@ from localstack.services.edge import ROUTER from localstack.services.events.models import EventsStore, events_stores from localstack.services.events.scheduler import JobScheduler +from localstack.services.events.utils import matches_event from localstack.services.moto import call_moto from localstack.services.plugins import ServiceLifecycleHook from localstack.utils.aws.arns import event_bus_arn, parse_arn @@ -59,7 +59,6 @@ TEST_EVENTS_CACHE = [] EVENTS_TMP_DIR = "cw_events" DEFAULT_EVENT_BUS_NAME = "default" -CONTENT_BASE_FILTER_KEYWORDS = ["prefix", "anything-but", "numeric", "cidr", "exists"] CONNECTION_NAME_PATTERN = re.compile("^[\\.\\-_A-Za-z0-9]+$") @@ -108,16 +107,32 @@ def get_store(context: RequestContext) -> EventsStore: def test_event_pattern( self, context: RequestContext, event_pattern: EventPattern, event: String, **kwargs ) -> TestEventPatternResponse: - # https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_TestEventPattern.html - # Test event pattern uses event pattern to match against event. - # So event pattern keys must be in the event keys and values must match. - # If event pattern has a key that event does not have, it is not a match. - evt_pattern = json.loads(str(event_pattern)) - evt = json.loads(str(event)) + """Test event pattern uses EventBridge event pattern matching: + https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html + """ + event_pattern_dict = json.loads(event_pattern) + event_dict = json.loads(event) - if any(key not in evt or evt[key] not in values for key, values in evt_pattern.items()): - return TestEventPatternResponse(Result=False) - return TestEventPatternResponse(Result=True) + # TODO: unify all these different implementation below ;) + + # EventBridge implementation: + result = matches_event(event_pattern_dict, event_dict) + + # EventSourceMapping implementation: + # result = does_match_event(event_pattern_dict, event_dict) + + # moto implementation: + # from moto.events.models import EventPattern as EventPatternMoto + # + # event_pattern = EventPatternMoto.load(event_pattern) + # result = event_pattern.matches_event(event_dict) + + # SNS: + # from localstack.services.sns.publisher import SubscriptionFilter + # subscription_filter = SubscriptionFilter() + # result = subscription_filter._evaluate_nested_filter_policy_on_dict(event_pattern_dict, event_dict) + + return TestEventPatternResponse(Result=result) @staticmethod def get_scheduled_rule_func( @@ -382,204 +397,9 @@ def _dump_events_to_files(events_with_added_uuid): LOG.info("Unable to dump events to tmp dir %s: %s", _get_events_tmp_dir(), e) -def handle_numeric_conditions(conditions: list[Any], value: float): - for i in range(0, len(conditions), 2): - if conditions[i] == "<" and not (value < conditions[i + 1]): - return False - if conditions[i] == ">" and not (value > conditions[i + 1]): - return False - if conditions[i] == "<=" and not (value <= conditions[i + 1]): - return False - if conditions[i] == ">=" and not (value >= conditions[i + 1]): - return False - return True - - -def check_valid_numeric_content_base_rule(list_of_operators): - if len(list_of_operators) > 4: - return False - - if "=" in list_of_operators: - return False - - if len(list_of_operators) > 2: - upper_limit = None - lower_limit = None - for index in range(len(list_of_operators)): - if not isinstance(list_of_operators[index], int) and "<" in list_of_operators[index]: - upper_limit = list_of_operators[index + 1] - if not isinstance(list_of_operators[index], int) and ">" in list_of_operators[index]: - lower_limit = list_of_operators[index + 1] - if upper_limit and lower_limit and upper_limit < lower_limit: - return False - index = index + 1 - return True - - -def filter_event_with_content_base_parameter(pattern_value: list, event_value: str | int): - for element in pattern_value: - if (isinstance(element, (str, int))) and (event_value == element or element in event_value): - return True - elif isinstance(element, dict): - element_key = list(element.keys())[0] - element_value = element.get(element_key) - if element_key.lower() == "prefix": - if isinstance(event_value, str) and event_value.startswith(element_value): - return True - elif element_key.lower() == "exists": - if element_value and event_value: - return True - elif not element_value and isinstance(event_value, object): - return True - elif element_key.lower() == "cidr": - ips = [str(ip) for ip in ipaddress.IPv4Network(element_value)] - if event_value in ips: - return True - elif element_key.lower() == "numeric": - if check_valid_numeric_content_base_rule(element_value): - for index in range(len(element_value)): - if isinstance(element_value[index], int): - continue - if ( - element_value[index] == ">" - and isinstance(element_value[index + 1], int) - and event_value <= element_value[index + 1] - ): - break - elif ( - element_value[index] == ">=" - and isinstance(element_value[index + 1], int) - and event_value < element_value[index + 1] - ): - break - elif ( - element_value[index] == "<" - and isinstance(element_value[index + 1], int) - and event_value >= element_value[index + 1] - ): - break - elif ( - element_value[index] == "<=" - and isinstance(element_value[index + 1], int) - and event_value > element_value[index + 1] - ): - break - else: - return True - - elif element_key.lower() == "anything-but": - if isinstance(element_value, list) and event_value not in element_value: - return True - elif (isinstance(element_value, (str, int))) and event_value != element_value: - return True - elif isinstance(element_value, dict): - nested_key = list(element_value)[0] - if nested_key == "prefix" and not re.match( - r"^{}".format(element_value.get(nested_key)), event_value - ): - return True - return False - - -# TODO: unclear shared responsibility for filtering with filter_event_with_content_base_parameter -def handle_prefix_filtering(event_pattern, value): - for element in event_pattern: - if isinstance(element, (int, str)): - if str(element) == str(value): - return True - if element in value: - return True - elif isinstance(element, dict) and "prefix" in element: - if value.startswith(element.get("prefix")): - return True - elif isinstance(element, dict) and "anything-but" in element: - if element.get("anything-but") != value: - return True - elif isinstance(element, dict) and "exists" in element: - if element.get("exists") and value: - return True - elif "numeric" in element: - return handle_numeric_conditions(element.get("numeric"), value) - elif isinstance(element, list): - if value in list: - return True - return False - - -def identify_content_base_parameter_in_pattern(parameters) -> bool: - return any( - list(param.keys())[0] in CONTENT_BASE_FILTER_KEYWORDS - for param in parameters - if isinstance(param, dict) - ) - - -def get_two_lists_intersection(lst1: List, lst2: List) -> List: - lst3 = [value for value in lst1 if value in lst2] - return lst3 - - -def event_pattern_prefix_bool_filter(event_pattern_filter_value_list: list[dict[str, Any]]) -> bool: - for event_pattern_filter_value in event_pattern_filter_value_list: - if "exists" in event_pattern_filter_value: - return event_pattern_filter_value.get("exists") - else: - return True - - -# TODO: refactor/simplify! def filter_event_based_on_event_format( self, rule_name: str, event_bus_name: str, event: dict[str, Any] ): - def filter_event(event_pattern_filter: dict[str, Any], event: dict[str, Any]): - for key, value in event_pattern_filter.items(): - fallback = object() - event_value = event.get(key.lower(), event.get(key, fallback)) - if event_value is fallback and event_pattern_prefix_bool_filter(value): - return False - - # 1. check if certain values in the event do not match the expected pattern - if event_value and isinstance(event_value, dict): - for key_a, value_a in event_value.items(): - if key_a == "ip": - # TODO add IP-Address check here - continue - if isinstance(value.get(key_a), (int, str)): - if value_a != value.get(key_a): - return False - if isinstance(value.get(key_a), list) and value_a not in value.get(key_a): - if not handle_prefix_filtering(value.get(key_a), value_a): - return False - - # 2. check if the pattern is a list and event values are not contained in it - if isinstance(value, list): - if identify_content_base_parameter_in_pattern(value): - if not filter_event_with_content_base_parameter(value, event_value): - return False - else: - if ( - isinstance(event_value, list) - and get_two_lists_intersection(value, event_value) == [] - ): - return False - if ( - not isinstance(event_value, list) - and isinstance(event_value, (str, int)) - and event_value not in value - ): - return False - - # 3. recursively call filter_event(..) for dict types - elif isinstance(value, (str, dict)): - try: - value = json.loads(value) if isinstance(value, str) else value - if isinstance(value, dict) and not filter_event(value, event_value): - return False - except json.decoder.JSONDecodeError: - return False - - return True - rule_information = self.events_backend.describe_rule( rule_name, event_bus_arn(event_bus_name, self.current_account, self.region) ) @@ -589,7 +409,7 @@ def filter_event(event_pattern_filter: dict[str, Any], event: dict[str, Any]): return False if rule_information.event_pattern._pattern: event_pattern = rule_information.event_pattern._pattern - if not filter_event(event_pattern, event): + if not matches_event(event_pattern, event): return False return True diff --git a/localstack/services/events/utils.py b/localstack/services/events/utils.py new file mode 100644 index 0000000000000..428ba2a5b2331 --- /dev/null +++ b/localstack/services/events/utils.py @@ -0,0 +1,278 @@ +import ipaddress +import json +import logging +import re +from typing import Any + +CONTENT_BASE_FILTER_KEYWORDS = ["prefix", "anything-but", "numeric", "cidr", "exists"] + +LOG = logging.getLogger(__name__) + + +class InvalidEventPatternException(Exception): + reason: str + + def __init__(self, reason=None, message=None) -> None: + self.reason = reason + self.message = message or f"Event pattern is not valid. Reason: {reason}" + + +def matches_event(event_pattern: dict[str, any], event: dict[str, Any]) -> bool: + """Decides whether an event pattern matches an event or not. + Returns True if the `event_pattern` matches the given `event` and False otherwise. + + Implements "Amazon EventBridge event patterns": + https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html + Used in different places: + * EventBridge: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html + * Lambda ESM: https://docs.aws.amazon.com/lambda/latest/dg/invocation-eventfiltering.html + * EventBridge Pipes: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-pipes-event-filtering.html + * SNS: https://docs.aws.amazon.com/sns/latest/dg/sns-subscription-filter-policies.html + + Open source AWS rule engine: https://github.com/aws/event-ruler + """ + for key, value in event_pattern.items(): + fallback = object() + # Keys are case-sensitive according to the test case `key_case_sensitive_NEG` + event_value = event.get(key, fallback) + if event_value is fallback and event_pattern_prefix_bool_filter(value): + return False + + # 1. check if certain values in the event do not match the expected pattern + if event_value and isinstance(event_value, dict): + for key_a, value_a in event_value.items(): + # TODO: why does the ip part appear here again, while cidr is handled in filter_event_with_content_base_parameter? + if key_a == "cidr": + # TODO add IP-Address check here + LOG.warning( + "Unsupported filter operator cidr. Please create a feature request." + ) + continue + if isinstance(value.get(key_a), (int, str)): + if value_a != value.get(key_a): + return False + if isinstance(value.get(key_a), list) and value_a not in value.get(key_a): + if not handle_prefix_filtering(value.get(key_a), value_a): + return False + + # 2. check if the pattern is a list and event values are not contained in it + if isinstance(value, list): + if identify_content_base_parameter_in_pattern(value): + if not filter_event_with_content_base_parameter(value, event_value): + return False + else: + if isinstance(event_value, list) and is_list_intersection_empty(value, event_value): + return False + if ( + not isinstance(event_value, list) + and isinstance(event_value, (str, int)) + and event_value not in value + ): + return False + + # 3. recursively call filter_event(..) for dict types + elif isinstance(value, (str, dict)): + try: + # TODO: validate whether inner JSON-encoded strings actually get decoded recursively + value = json.loads(value) if isinstance(value, str) else value + if isinstance(value, dict) and not matches_event(value, event_value): + return False + except json.decoder.JSONDecodeError: + return False + + return True + + +def event_pattern_prefix_bool_filter(event_pattern_filter_value_list: list[dict[str, Any]]) -> bool: + for event_pattern_filter_value in event_pattern_filter_value_list: + if "exists" in event_pattern_filter_value: + return event_pattern_filter_value.get("exists") + else: + return True + + +def filter_event_with_content_base_parameter(pattern_value: list, event_value: str | int): + for element in pattern_value: + if (isinstance(element, (str, int))) and (event_value == element or element in event_value): + return True + elif isinstance(element, dict): + # Only the first operator gets evaluated and further operators in the list are silently ignored + operator = list(element.keys())[0] + element_value = element.get(operator) + # TODO: why do we implement the operators here again? They are already in handle_prefix_filtering?! + if operator == "prefix": + if isinstance(event_value, str) and event_value.startswith(element_value): + return True + elif operator == "exists": + if element_value and event_value: + return True + elif not element_value and isinstance(event_value, object): + return True + elif operator == "cidr": + ips = [str(ip) for ip in ipaddress.IPv4Network(element_value)] + if event_value in ips: + return True + elif operator == "numeric": + if check_valid_numeric_content_base_rule(element_value): + for index in range(len(element_value)): + if isinstance(element_value[index], int): + continue + if ( + element_value[index] == ">" + and isinstance(element_value[index + 1], int) + and event_value <= element_value[index + 1] + ): + break + elif ( + element_value[index] == ">=" + and isinstance(element_value[index + 1], int) + and event_value < element_value[index + 1] + ): + break + elif ( + element_value[index] == "<" + and isinstance(element_value[index + 1], int) + and event_value >= element_value[index + 1] + ): + break + elif ( + element_value[index] == "<=" + and isinstance(element_value[index + 1], int) + and event_value > element_value[index + 1] + ): + break + elif ( + element_value[index] == "=" + and isinstance(element_value[index + 1], int) + and event_value == element_value[index + 1] + ): + break + else: + return True + + elif operator == "anything-but": + if isinstance(element_value, list) and event_value not in element_value: + return True + elif (isinstance(element_value, (str, int))) and event_value != element_value: + return True + elif isinstance(element_value, dict): + nested_key = list(element_value)[0] + if nested_key == "prefix" and not re.match( + r"^{}".format(element_value.get(nested_key)), event_value + ): + return True + return False + + +def is_list_intersection_empty(list1: list, list2: list) -> bool: + """Checks if the intersection of two lists is empty. + + Example: is_list_intersection_empty([1, 2, None], [None]) == False + + Following the definition from AWS: + "If the value in the event is an array, then the event pattern matches if the intersection of the + event pattern array and the event array is non-empty." + https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-arrays.html + + Implementation: set operations are more efficient than using lists + """ + return len(set(list1) & set(list2)) == 0 + + +# TODO: unclear shared responsibility for filtering with filter_event_with_content_base_parameter +def handle_prefix_filtering(event_pattern, value): + for element in event_pattern: + # TODO: fix direct int or string matching, which is not allowed. A list with possible values is required. + if isinstance(element, (int, str)): + if str(element) == str(value): + return True + if element in value: + return True + elif isinstance(element, dict) and "prefix" in element: + if value.startswith(element.get("prefix")): + return True + elif isinstance(element, dict) and "anything-but" in element: + if element.get("anything-but") != value: + return True + elif isinstance(element, dict) and "exists" in element: + if element.get("exists") and value: + return True + elif isinstance(element, dict) and "numeric" in element: + return handle_numeric_conditions(element.get("numeric"), value) + elif isinstance(element, list): + if value in element: + return True + return False + + +def identify_content_base_parameter_in_pattern(parameters) -> bool: + return any( + list(param.keys())[0] in CONTENT_BASE_FILTER_KEYWORDS + for param in parameters + if isinstance(param, dict) + ) + + +def check_valid_numeric_content_base_rule(list_of_operators): + # TODO: validate? + if len(list_of_operators) > 4: + return False + + # TODO: Why? + if "=" in list_of_operators: + return False + + if len(list_of_operators) > 2: + upper_limit = None + lower_limit = None + # TODO: what is this for, why another operator check? + for index in range(len(list_of_operators)): + if not isinstance(list_of_operators[index], int) and "<" in list_of_operators[index]: + upper_limit = list_of_operators[index + 1] + if not isinstance(list_of_operators[index], int) and ">" in list_of_operators[index]: + lower_limit = list_of_operators[index + 1] + if upper_limit and lower_limit and upper_limit < lower_limit: + return False + return True + + +def handle_numeric_conditions(conditions: list[any], value: int | float): + """Implements numeric matching for a given list of conditions. + Example: { "numeric": [ ">", 0, "<=", 5 ] } + + Numeric matching works with values that are JSON numbers. + It is limited to values between -5.0e9 and +5.0e9 inclusive, with 15 digits of precision, + or six digits to the right of the decimal point. + https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#filtering-numeric-matchinghttps://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#filtering-numeric-matching + """ + + # Invalid example for uneven list: { "numeric": [ ">", 0, "<" ] } + if len(conditions) % 2 > 0: + raise InvalidEventPatternException("Bad numeric range operator") + + if not isinstance(value, (int, float)): + raise InvalidEventPatternException( + f"The value {value} for the numeric comparison {conditions} is not a valid number" + ) + + for i in range(0, len(conditions), 2): + operator = conditions[i] + second_operand_str = conditions[i + 1] + try: + second_operand = float(second_operand_str) + except ValueError: + raise InvalidEventPatternException( + f"Could not convert filter value {second_operand_str} to a valid number" + ) + + if operator == "<" and not (value < second_operand): + return False + if operator == ">" and not (value > second_operand): + return False + if operator == "<=" and not (value <= second_operand): + return False + if operator == ">=" and not (value >= second_operand): + return False + if operator == "=" and not (value == second_operand): + return False + return True diff --git a/localstack/services/lambda_/event_source_listeners/utils.py b/localstack/services/lambda_/event_source_listeners/utils.py index bd26585f184cc..e32c604e70bcd 100644 --- a/localstack/services/lambda_/event_source_listeners/utils.py +++ b/localstack/services/lambda_/event_source_listeners/utils.py @@ -1,7 +1,6 @@ import json import logging import re -from typing import Dict, List, Union from localstack.aws.api.lambda_ import FilterCriteria from localstack.utils.strings import first_char_to_lower @@ -9,40 +8,60 @@ LOG = logging.getLogger(__name__) -def filter_stream_records(records, filters: List[FilterCriteria]): +class InvalidEventPatternException(Exception): + reason: str + + def __init__(self, reason=None, message=None) -> None: + self.reason = reason + self.message = message or f"Event pattern is not valid. Reason: {reason}" + + +def filter_stream_records(records, filters: list[FilterCriteria]): filtered_records = [] for record in records: for filter in filters: for rule in filter["Filters"]: - if filter_stream_record(json.loads(rule["Pattern"]), record): + filter_pattern: dict[str, any] = json.loads(rule["Pattern"]) + if does_match_event(filter_pattern, record): filtered_records.append(record) break return filtered_records -def filter_stream_record(filter_rule: Dict[str, any], record: Dict[str, any]) -> bool: - if not filter_rule: +def does_match_event(event_pattern: dict[str, any], event: dict[str, any]) -> bool: + """Decides whether an event pattern matches an event or not. + Returns True if the `event_pattern` matches the given `event` and False otherwise. + + Implements "Amazon EventBridge event patterns": + https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html + Used in different places: + * EventBridge: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html + * Lambda ESM: https://docs.aws.amazon.com/lambda/latest/dg/invocation-eventfiltering.html + * EventBridge Pipes: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-pipes-event-filtering.html + * SNS: https://docs.aws.amazon.com/sns/latest/dg/sns-subscription-filter-policies.html + + Open source AWS rule engine: https://github.com/aws/event-ruler + """ + # TODO: test this conditional: https://coveralls.io/builds/66584026/source?filename=localstack%2Fservices%2Flambda_%2Fevent_source_listeners%2Futils.py#L25 + if not event_pattern: return True - # https://docs.aws.amazon.com/lambda/latest/dg/invocation-eventfiltering.html#filtering-syntax - filter_results = [] - for key, value in filter_rule.items(): + does_match_results = [] + for key, value in event_pattern.items(): # check if rule exists in event - record_value = ( - record.get(key.lower(), record.get(key)) if isinstance(record, Dict) else None - ) - append_record = False - if record_value is not None: - # check if filter rule value is a list (leaf of rule tree) or a dict (rescursively call function) + event_value = event.get(key) if isinstance(event, dict) else None + does_pattern_match = False + if event_value is not None: + # check if filter rule value is a list (leaf of rule tree) or a dict (recursively call function) if isinstance(value, list): if len(value) > 0: if isinstance(value[0], (str, int)): - append_record = record_value in value + does_pattern_match = event_value in value if isinstance(value[0], dict): - append_record = verify_dict_filter(record_value, value[0]) + does_pattern_match = verify_dict_filter(event_value, value[0]) else: LOG.warning(f"Empty lambda filter: {key}") elif isinstance(value, dict): - append_record = filter_stream_record(value, record_value) + does_pattern_match = does_match_event(value, event_value) else: # special case 'exists' def _filter_rule_value_list(val): @@ -62,69 +81,84 @@ def _filter_rule_value_dict(val): return True if isinstance(value, list) and len(value) > 0: - append_record = _filter_rule_value_list(value) + does_pattern_match = _filter_rule_value_list(value) elif isinstance(value, dict): # special case 'exists' for S type, e.g. {"S": [{"exists": false}]} # https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.Lambda.Tutorial2.html - append_record = _filter_rule_value_dict(value) + does_pattern_match = _filter_rule_value_dict(value) - filter_results.append(append_record) - return all(filter_results) + does_match_results.append(does_pattern_match) + return all(does_match_results) -def verify_dict_filter(record_value: any, dict_filter: Dict[str, any]) -> bool: +def verify_dict_filter(record_value: any, dict_filter: dict[str, any]) -> bool: # https://docs.aws.amazon.com/lambda/latest/dg/invocation-eventfiltering.html#filtering-syntax - fits_filter = False + does_match_filter = False for key, filter_value in dict_filter.items(): - if key.lower() == "anything-but": - fits_filter = record_value not in filter_value - elif key.lower() == "numeric": - fits_filter = parse_and_apply_numeric_filter(record_value, filter_value) - elif key.lower() == "exists": - fits_filter = bool(filter_value) # exists means that the key exists in the event record - elif key.lower() == "prefix": + if key == "anything-but": + does_match_filter = record_value not in filter_value + elif key == "numeric": + does_match_filter = handle_numeric_conditions(record_value, filter_value) + elif key == "exists": + does_match_filter = bool( + filter_value + ) # exists means that the key exists in the event record + elif key == "prefix": if not isinstance(record_value, str): LOG.warning(f"Record Value {record_value} does not seem to be a valid string.") - fits_filter = isinstance(record_value, str) and record_value.startswith( + does_match_filter = isinstance(record_value, str) and record_value.startswith( str(filter_value) ) - - if fits_filter: + if does_match_filter: return True - return fits_filter + return does_match_filter -def parse_and_apply_numeric_filter( - record_value: Dict, numeric_filter: List[Union[str, int]] -) -> bool: - if len(numeric_filter) % 2 > 0: - LOG.warning("Invalid numeric lambda filter given") - return True - if not isinstance(record_value, (int, float)): - LOG.warning(f"Record {record_value} seem not to be a valid number") - return False +def handle_numeric_conditions( + first_operand: int | float, conditions: list[str | int | float] +) -> bool: + """Implements numeric matching for a given list of conditions. + Example: { "numeric": [ ">", 0, "<=", 5 ] } + + Numeric matching works with values that are JSON numbers. + It is limited to values between -5.0e9 and +5.0e9 inclusive, with 15 digits of precision, + or six digits to the right of the decimal point. + https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#filtering-numeric-matchinghttps://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#filtering-numeric-matching + """ + # Invalid example for uneven list: { "numeric": [ ">", 0, "<" ] } + if len(conditions) % 2 > 0: + raise InvalidEventPatternException("Bad numeric range operator") + + if not isinstance(first_operand, (int, float)): + raise InvalidEventPatternException( + f"The value {first_operand} for the numeric comparison {conditions} is not a valid number" + ) - for idx in range(0, len(numeric_filter), 2): + for i in range(0, len(conditions), 2): + operator = conditions[i] + second_operand_str = conditions[i + 1] try: - if numeric_filter[idx] == ">" and not (record_value > float(numeric_filter[idx + 1])): - return False - if numeric_filter[idx] == ">=" and not (record_value >= float(numeric_filter[idx + 1])): - return False - if numeric_filter[idx] == "=" and not (record_value == float(numeric_filter[idx + 1])): - return False - if numeric_filter[idx] == "<" and not (record_value < float(numeric_filter[idx + 1])): - return False - if numeric_filter[idx] == "<=" and not (record_value <= float(numeric_filter[idx + 1])): - return False + second_operand = float(second_operand_str) except ValueError: - LOG.warning( - f"Could not convert filter value {numeric_filter[idx + 1]} to a valid number value for filtering" - ) + raise InvalidEventPatternException( + f"Could not convert filter value {second_operand_str} to a valid number" + ) from ValueError + + if operator == ">" and not (first_operand > second_operand): + return False + if operator == ">=" and not (first_operand >= second_operand): + return False + if operator == "=" and not (first_operand == second_operand): + return False + if operator == "<" and not (first_operand < second_operand): + return False + if operator == "<=" and not (first_operand <= second_operand): + return False return True -def contains_list(filter: Dict) -> bool: +def contains_list(filter: dict) -> bool: if isinstance(filter, dict): for key, value in filter.items(): if isinstance(value, list) and len(value) > 0: @@ -178,7 +212,7 @@ def event_source_arn_matches(mapped: str, searched: str) -> bool: return False -def has_data_filter_criteria(filters: List[FilterCriteria]) -> bool: +def has_data_filter_criteria(filters: list[FilterCriteria]) -> bool: for filter in filters: for rule in filter.get("Filters", []): parsed_pattern = json.loads(rule["Pattern"]) diff --git a/tests/aws/services/events/event_pattern_templates/arrays.json5 b/tests/aws/services/events/event_pattern_templates/arrays.json5 new file mode 100644 index 0000000000000..7a103bd30e3b9 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/arrays.json5 @@ -0,0 +1,22 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-arrays.html +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "resources": [ + "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:eb56d16b-bbf0-401d-b893-d5978ed4a025:autoScalingGroupName/ASGTerminate", + "arn:aws:ec2:us-east-1:123456789012:instance/i-b188560f" + ] + }, + "EventPattern": { + "resources": [ + "arn:aws:ec2:us-east-1:123456789012:instance/i-b188560f", + "arn:aws:ec2:us-east-1:111122223333:instance/i-b188560f", + "arn:aws:ec2:us-east-1:444455556666:instance/i-b188560f", + ] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/arrays_NEG.json5 b/tests/aws/services/events/event_pattern_templates/arrays_NEG.json5 new file mode 100644 index 0000000000000..140f8d319bc9c --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/arrays_NEG.json5 @@ -0,0 +1,21 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-arrays.html +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "resources": [ + "arn:aws:autoscaling:us-east-1:123456789012:autoScalingGroup:eb56d16b-bbf0-401d-b893-d5978ed4a025:autoScalingGroupName/ASGTerminate", + ] + }, + "EventPattern": { + "resources": [ + "arn:aws:ec2:us-east-1:123456789012:instance/i-b188560f", + "arn:aws:ec2:us-east-1:111122223333:instance/i-b188560f", + "arn:aws:ec2:us-east-1:444455556666:instance/i-b188560f", + ] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/arrays_empty_EXC.json5 b/tests/aws/services/events/event_pattern_templates/arrays_empty_EXC.json5 new file mode 100644 index 0000000000000..30be2359d3b03 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/arrays_empty_EXC.json5 @@ -0,0 +1,15 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-arrays.html +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "resources": [] + }, + "EventPattern": { + "resources": [] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/arrays_empty_null_NEG.json5 b/tests/aws/services/events/event_pattern_templates/arrays_empty_null_NEG.json5 new file mode 100644 index 0000000000000..63263844de317 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/arrays_empty_null_NEG.json5 @@ -0,0 +1,15 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-arrays.html +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "resources": [] + }, + "EventPattern": { + "resources": [null] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/boolean.json5 b/tests/aws/services/events/event_pattern_templates/boolean.json5 new file mode 100644 index 0000000000000..00af9a445c8af --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/boolean.json5 @@ -0,0 +1,14 @@ +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "boolean": false + }, + "EventPattern": { + "boolean": [false] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/boolean_NEG.json5 b/tests/aws/services/events/event_pattern_templates/boolean_NEG.json5 new file mode 100644 index 0000000000000..66078368fbe6d --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/boolean_NEG.json5 @@ -0,0 +1,14 @@ +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "boolean": true + }, + "EventPattern": { + "boolean": [false] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/complex_many_rules.json5 b/tests/aws/services/events/event_pattern_templates/complex_many_rules.json5 new file mode 100644 index 0000000000000..b9a9b636b29ca --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/complex_many_rules.json5 @@ -0,0 +1,58 @@ +// Based on the test case (not AWS validated!): +// tests.aws.services.events.test_events_rules.test_put_event_with_content_base_rule_in_pattern +{ + "Event": { + "id": "1", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "EventBusName": "my-event-bus-name", + "source": "core.update-account-command", + "detail-type": "core.app.backend", + "detail": { + "description": "this-is-event-details", + "amount": 200, + "salary": 2000, + "env": "prod", + "user": "user3", + "admins": "admin", + "test1": 300, + "test2": "test22", + "test3": "test333", + "test4": "this test4", + "ip": "10.102.1.100", + "num-test1": 100, + "num-test2": 200, + "num-test3": 300, + "num-test4": 200, + "num-test5": 500, + "num-test6": 300, + "num-test7": 300, + } + }, + "EventPattern": { + "source": [{"exists": true}], + "detail-type": [{"prefix": "core.app"}], + "detail": { + "description": ["this-is-event-details"], + "amount": [200], + "salary": [2000, 4000], + "env": ["dev", "prod"], + "user": ["user1", "user2", "user3"], + "admins": ["skyli", {"prefix": "hey"}, {"prefix": "ad"}], + "test1": [{"anything-but": 200}], + "test2": [{"anything-but": "test2"}], + "test3": [{"anything-but": ["test3", "test33"]}], + "test4": [{"anything-but": {"prefix": "test4"}}], +// TODO: implement IP matching in LocalStack +// "ip": [{"cidr": "10.102.1.0/24"}], + "num-test1": [{"numeric": ["<", 200]}], + "num-test2": [{"numeric": ["<=", 200]}], + "num-test3": [{"numeric": [">", 200]}], + "num-test4": [{"numeric": [">=", 200]}], + "num-test5": [{"numeric": [">=", 200, "<=", 500]}], + "num-test6": [{"numeric": [">", 200, "<", 500]}], + "num-test7": [{"numeric": [">=", 200, "<", 500]}], + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/complex_multi_key_event.json b/tests/aws/services/events/event_pattern_templates/complex_multi_key_event.json new file mode 100644 index 0000000000000..dd385c4694e21 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/complex_multi_key_event.json @@ -0,0 +1,11 @@ +{ + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "location": "eu-central-1" + } +} diff --git a/tests/aws/services/events/event_pattern_templates/complex_multi_key_event_pattern.json b/tests/aws/services/events/event_pattern_templates/complex_multi_key_event_pattern.json new file mode 100644 index 0000000000000..5676987a9d287 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/complex_multi_key_event_pattern.json @@ -0,0 +1,6 @@ +{ + "detail": { + "location": [ { "prefix": "us-" } ], + "location": [ { "anything-but": "us-east" } ] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/complex_multi_match.json5 b/tests/aws/services/events/event_pattern_templates/complex_multi_match.json5 new file mode 100644 index 0000000000000..3bd0dd29cf286 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/complex_multi_match.json5 @@ -0,0 +1,26 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-complex-example +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "state": "processing", + "c-count": 3, + "d-count": 9, + "x-limit": 999 + } + }, + "EventPattern": { + "time": [ { "prefix": "2022-07-13" } ], + "detail": { + "state": [ { "anything-but": "initializing" } ], + "c-count": [ { "numeric": [ ">", 0, "<=", 5 ] } ], + "d-count": [ { "numeric": [ "<", 10 ] } ], + "x-limit": [ { "anything-but": [ 100, 200, 300 ] } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/complex_multi_match_NEG.json5 b/tests/aws/services/events/event_pattern_templates/complex_multi_match_NEG.json5 new file mode 100644 index 0000000000000..60a0a1a53713e --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/complex_multi_match_NEG.json5 @@ -0,0 +1,27 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-complex-example +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "state": "processing", + "c-count": 3, + "d-count": 9, + // Matches 300 + "x-limit": 300 + } + }, + "EventPattern": { + "time": [ { "prefix": "2022-07-13" } ], + "detail": { + "state": [ { "anything-but": "initializing" } ], + "c-count": [ { "numeric": [ ">", 0, "<=", 5 ] } ], + "d-count": [ { "numeric": [ "<", 10 ] } ], + "x-limit": [ { "anything-but": [ 100, 200, 300 ] } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/complex_or.json5 b/tests/aws/services/events/event_pattern_templates/complex_or.json5 new file mode 100644 index 0000000000000..fc5721986d059 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/complex_or.json5 @@ -0,0 +1,26 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-complex-example-or +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "c-count": 0, + // Matches <10 + "d-count": 0, + "x-limit": 9.018e2 + } + }, + "EventPattern": { + "detail": { + "$or": [ + { "c-count": [ { "numeric": [ ">", 0, "<=", 5 ] } ] }, + { "d-count": [ { "numeric": [ "<", 10 ] } ] }, + { "x-limit": [ { "numeric": [ "=", 3.018e2 ] } ] } + ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/complex_or_NEG.json5 b/tests/aws/services/events/event_pattern_templates/complex_or_NEG.json5 new file mode 100644 index 0000000000000..85798d4c20d7c --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/complex_or_NEG.json5 @@ -0,0 +1,25 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-complex-example-or +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "c-count": 0, + "d-count": 10, + "x-limit": 9.018e2 + } + }, + "EventPattern": { + "detail": { + "$or": [ + { "c-count": [ { "numeric": [ ">", 0, "<=", 5 ] } ] }, + { "d-count": [ { "numeric": [ "<", 10 ] } ] }, + { "x-limit": [ { "numeric": [ "=", 3.018e2 ] } ] } + ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_anything_but_ignorecase.json5 b/tests/aws/services/events/event_pattern_templates/content_anything_but_ignorecase.json5 new file mode 100644 index 0000000000000..9f73a6360e99e --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_anything_but_ignorecase.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "state": "pending" + } + }, + "EventPattern": { + "detail": { + "state" : [{ "anything-but": { "equals-ignore-case": "initializing" }}] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_anything_but_ignorecase_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_anything_but_ignorecase_NEG.json5 new file mode 100644 index 0000000000000..d80b8eef4bed1 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_anything_but_ignorecase_NEG.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "state": "INITIALIZING" + } + }, + "EventPattern": { + "detail": { + "state" : [{ "anything-but": { "equals-ignore-case": "initializing" }}] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_anything_but_ignorecase_list.json5 b/tests/aws/services/events/event_pattern_templates/content_anything_but_ignorecase_list.json5 new file mode 100644 index 0000000000000..f7185bb85a7b2 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_anything_but_ignorecase_list.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "state": "pending" + } + }, + "EventPattern": { + "detail": { + "state" : [{ "anything-but": { "equals-ignore-case": ["initializing", "stopped"] }}] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_anything_but_ignorecase_list_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_anything_but_ignorecase_list_NEG.json5 new file mode 100644 index 0000000000000..6f112c3ab7a36 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_anything_but_ignorecase_list_NEG.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "state": "Stopped" + } + }, + "EventPattern": { + "detail": { + "state" : [{ "anything-but": { "equals-ignore-case": ["initializing", "stopped"] }}] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_anything_but_number.json5 b/tests/aws/services/events/event_pattern_templates/content_anything_but_number.json5 new file mode 100644 index 0000000000000..8746fa73d1ce8 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_anything_but_number.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "x-limit": 789 + } + }, + "EventPattern": { + "detail": { + "x-limit": [ { "anything-but": 123 } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_anything_but_number_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_anything_but_number_NEG.json5 new file mode 100644 index 0000000000000..34fe749dd5ee4 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_anything_but_number_NEG.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "x-limit": 123 + } + }, + "EventPattern": { + "detail": { + "x-limit": [ { "anything-but": 123 } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_anything_but_number_list.json5 b/tests/aws/services/events/event_pattern_templates/content_anything_but_number_list.json5 new file mode 100644 index 0000000000000..a455273290fab --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_anything_but_number_list.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "x-limit": 999 + } + }, + "EventPattern": { + "detail": { + "x-limit": [ { "anything-but": [ 100, 200, 300 ] } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_anything_but_number_list_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_anything_but_number_list_NEG.json5 new file mode 100644 index 0000000000000..577d58ecbb666 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_anything_but_number_list_NEG.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "x-limit": 100 + } + }, + "EventPattern": { + "detail": { + "x-limit": [ { "anything-but": [ 100, 200, 300 ] } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_anything_but_string.json5 b/tests/aws/services/events/event_pattern_templates/content_anything_but_string.json5 new file mode 100644 index 0000000000000..f3ba506fb8ae7 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_anything_but_string.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "state": "pending" + } + }, + "EventPattern": { + "detail": { + "state": [ { "anything-but": "initializing" } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_anything_but_string_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_anything_but_string_NEG.json5 new file mode 100644 index 0000000000000..eead136029933 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_anything_but_string_NEG.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "state": "initializing" + } + }, + "EventPattern": { + "detail": { + "state": [ { "anything-but": "initializing" } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_anything_but_string_list.json5 b/tests/aws/services/events/event_pattern_templates/content_anything_but_string_list.json5 new file mode 100644 index 0000000000000..ac73eb8670a44 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_anything_but_string_list.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "state": "pending" + } + }, + "EventPattern": { + "detail": { + "state": [ { "anything-but": [ "stopped", "overloaded" ] } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_anything_but_string_list_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_anything_but_string_list_NEG.json5 new file mode 100644 index 0000000000000..35e3432aab7d6 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_anything_but_string_list_NEG.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "state": "stopped" + } + }, + "EventPattern": { + "detail": { + "state": [ { "anything-but": [ "stopped", "overloaded" ] } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_anything_prefix.json5 b/tests/aws/services/events/event_pattern_templates/content_anything_prefix.json5 new file mode 100644 index 0000000000000..a0e90f42dded2 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_anything_prefix.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "state": "post-init" + } + }, + "EventPattern": { + "detail": { + "state": [ { "anything-but": { "prefix": "init" } } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_anything_prefix_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_anything_prefix_NEG.json5 new file mode 100644 index 0000000000000..7e08802a9815d --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_anything_prefix_NEG.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "state": "init-prefix" + } + }, + "EventPattern": { + "detail": { + "state": [ { "anything-but": { "prefix": "init" } } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_anything_suffix.json5 b/tests/aws/services/events/event_pattern_templates/content_anything_suffix.json5 new file mode 100644 index 0000000000000..1d8e1403cbd73 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_anything_suffix.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "FileName": "file.txt.bak" + } + }, + "EventPattern": { + "detail": { + "FileName": [ { "anything-but": { "suffix": ".txt" } } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_anything_suffix_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_anything_suffix_NEG.json5 new file mode 100644 index 0000000000000..836005720a425 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_anything_suffix_NEG.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-anything-but +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "FileName": "file.txt" + } + }, + "EventPattern": { + "detail": { + "FileName": [ { "anything-but": { "suffix": ".txt" } } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_exists.json5 b/tests/aws/services/events/event_pattern_templates/content_exists.json5 new file mode 100644 index 0000000000000..fb11fb313307e --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_exists.json5 @@ -0,0 +1,20 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-exists-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "instance-id": "i-abcd1111", + "state": "pending" + } + }, + "EventPattern": { + "detail": { + "state": [ { "exists": true } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_exists_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_exists_NEG.json5 new file mode 100644 index 0000000000000..615636d453443 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_exists_NEG.json5 @@ -0,0 +1,22 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-exists-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + // Does NOT match because the detail.state field is missing + "detail": { + "c-count" : { + "c1": 100 + } + } + }, + "EventPattern": { + "detail": { + "state": [ { "exists": true } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_exists_false.json5 b/tests/aws/services/events/event_pattern_templates/content_exists_false.json5 new file mode 100644 index 0000000000000..04adeda7a97fa --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_exists_false.json5 @@ -0,0 +1,27 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-exists-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "level1": { + "level2": "l2 value" + } + } + }, + "EventPattern": { + "detail": { + "level1": { + "level2:": { + "level3": { + "level4": [ { "exists": false } ] + } + } + } + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_exists_false_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_exists_false_NEG.json5 new file mode 100644 index 0000000000000..6723c62a52aab --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_exists_false_NEG.json5 @@ -0,0 +1,31 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-exists-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "level1": { + "level2": { + "level3": { + "level4": "l4 value" + } + } + } + } + }, + "EventPattern": { + "detail": { + "level1": { + "level2": { + "level3": { + "level4": [ { "exists": false } ] + } + } + } + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_ignorecase.json5 b/tests/aws/services/events/event_pattern_templates/content_ignorecase.json5 new file mode 100644 index 0000000000000..edcf8fa574f07 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_ignorecase.json5 @@ -0,0 +1,14 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-equals-ignore-case-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": [ "EC2 Instance State-change Notification" ], + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + }, + "EventPattern": { + "detail-type": [ { "equals-ignore-case": "ec2 instance state-change notification" } ] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_ignorecase_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_ignorecase_NEG.json5 new file mode 100644 index 0000000000000..72a7fd59e7294 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_ignorecase_NEG.json5 @@ -0,0 +1,14 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-equals-ignore-case-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": [ "EC2 Instance State-change Notification" ], + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + }, + "EventPattern": { + "detail-type": [ { "equals-ignore-case": "I do not match" } ] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_ip_address.json5 b/tests/aws/services/events/event_pattern_templates/content_ip_address.json5 new file mode 100644 index 0000000000000..cb66ade6e382c --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_ip_address.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-ip-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "sourceIPAddress": "10.0.0.255" + } + }, + "EventPattern": { + "detail": { + "sourceIPAddress": [ { "cidr": "10.0.0.0/24" } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_ip_address_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_ip_address_NEG.json5 new file mode 100644 index 0000000000000..af00a054883ee --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_ip_address_NEG.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-ip-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "sourceIPAddress": "10.0.0.256" + } + }, + "EventPattern": { + "detail": { + "sourceIPAddress": [ { "cidr": "10.0.0.0/24" } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_numeric_EXC.json5 b/tests/aws/services/events/event_pattern_templates/content_numeric_EXC.json5 new file mode 100644 index 0000000000000..66bea44174ffc --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_numeric_EXC.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#filtering-numeric-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "c-count": 3, + } + }, + "EventPattern": { + "detail": { + "c-count": [ { "numeric": [ ">", 0, ">", 0 ] } ], + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_numeric_and.json5 b/tests/aws/services/events/event_pattern_templates/content_numeric_and.json5 new file mode 100644 index 0000000000000..159d52aad325e --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_numeric_and.json5 @@ -0,0 +1,23 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#filtering-numeric-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "c-count": 3, + "d-count": 9, + "x-limit": 3.018e2 + } + }, + "EventPattern": { + "detail": { + "c-count": [ { "numeric": [ ">", 0, "<=", 5 ] } ], + "d-count": [ { "numeric": [ "<", 10 ] } ], + "x-limit": [ { "numeric": [ "=", 3.018e2 ] } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_numeric_and_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_numeric_and_NEG.json5 new file mode 100644 index 0000000000000..d147cab87a66f --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_numeric_and_NEG.json5 @@ -0,0 +1,23 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#filtering-numeric-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "c-count": 42, + "d-count": 9, + "x-limit": 3.018e2 + } + }, + "EventPattern": { + "detail": { + "c-count": [ { "numeric": [ ">", 0, "<=", 5 ] } ], + "d-count": [ { "numeric": [ "<", 10 ] } ], + "x-limit": [ { "numeric": [ "=", 3.018e2 ] } ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_numeric_operatorcasing_EXC.json5 b/tests/aws/services/events/event_pattern_templates/content_numeric_operatorcasing_EXC.json5 new file mode 100644 index 0000000000000..18027381ff697 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_numeric_operatorcasing_EXC.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#filtering-numeric-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "equal": 5, + } + }, + "EventPattern": { + "detail": { + "equal": [ { "NUMERIC": [ "=", 5 ] } ], + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_numeric_syntax_EXC.json5 b/tests/aws/services/events/event_pattern_templates/content_numeric_syntax_EXC.json5 new file mode 100644 index 0000000000000..7dcc348c6ab08 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_numeric_syntax_EXC.json5 @@ -0,0 +1,19 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#filtering-numeric-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "c-count": 3, + } + }, + "EventPattern": { + "detail": { + "c-count": [ { "numeric": [ ">", 0, "<" ] } ], + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_prefix.json5 b/tests/aws/services/events/event_pattern_templates/content_prefix.json5 new file mode 100644 index 0000000000000..361e52ff7f997 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_prefix.json5 @@ -0,0 +1,14 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-prefix-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + }, + "EventPattern": { + "time": [ { "prefix": "2022-07-13" } ] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_prefix_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_prefix_NEG.json5 new file mode 100644 index 0000000000000..094135ffdfd71 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_prefix_NEG.json5 @@ -0,0 +1,14 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-prefix-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + }, + "EventPattern": { + "time": [ { "prefix": "2022-07-99" } ] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_prefix_ignorecase.json5 b/tests/aws/services/events/event_pattern_templates/content_prefix_ignorecase.json5 new file mode 100644 index 0000000000000..4cfd3523f4d02 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_prefix_ignorecase.json5 @@ -0,0 +1,17 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-prefix-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "service": "EvEnTb" + } + }, + "EventPattern": { + "detail": {"service" : [{ "prefix": { "equals-ignore-case": "EventB" }}]} + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_suffix.json5 b/tests/aws/services/events/event_pattern_templates/content_suffix.json5 new file mode 100644 index 0000000000000..f699b596c7683 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_suffix.json5 @@ -0,0 +1,15 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-suffix-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "FileName": "image.png" + }, + "EventPattern": { + "FileName": [ { "suffix": ".png" } ] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_suffix_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_suffix_NEG.json5 new file mode 100644 index 0000000000000..70cc5f9b9883b --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_suffix_NEG.json5 @@ -0,0 +1,15 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-suffix-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "FileName": "image.png" + }, + "EventPattern": { + "FileName": [ { "suffix": ".PNG" } ] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_suffix_ignorecase.json5 b/tests/aws/services/events/event_pattern_templates/content_suffix_ignorecase.json5 new file mode 100644 index 0000000000000..bc1704ea1831b --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_suffix_ignorecase.json5 @@ -0,0 +1,17 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-suffix-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "FileName": "image.PNG" + }, + }, + "EventPattern": { + "detail": {"FileName" : [{ "suffix": { "equals-ignore-case": ".png" }}]} + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_suffix_ignorecase_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_suffix_ignorecase_NEG.json5 new file mode 100644 index 0000000000000..6537fe76a5a17 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_suffix_ignorecase_NEG.json5 @@ -0,0 +1,17 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-suffix-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "FileName": "image.jpg" + }, + }, + "EventPattern": { + "detail": {"FileName" : [{ "suffix": { "equals-ignore-case": ".png" }}]} + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_wildcard_complex_EXC.json5 b/tests/aws/services/events/event_pattern_templates/content_wildcard_complex_EXC.json5 new file mode 100644 index 0000000000000..754f3e3fbb4e4 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_wildcard_complex_EXC.json5 @@ -0,0 +1,15 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-wildcard-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "EventBusArn": "arn:aws:events:us-east-1:123456789012:event-bus/myEventBus" + }, + "EventPattern": { + "EventBusArn": [ { "wildcard": "*:*:*:*:*:event-bus/*" } ] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_wildcard_nonrepeating.json5 b/tests/aws/services/events/event_pattern_templates/content_wildcard_nonrepeating.json5 new file mode 100644 index 0000000000000..ab6b58ad794df --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_wildcard_nonrepeating.json5 @@ -0,0 +1,15 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-wildcard-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "FileName": "/Users/ls_user/dir/dir/dir/dir/dir/doc.txt" + }, + "EventPattern": { + "FileName": [ { "wildcard": "/Users/*/doc.txt" } ] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_wildcard_nonrepeating_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_wildcard_nonrepeating_NEG.json5 new file mode 100644 index 0000000000000..d2911b623f345 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_wildcard_nonrepeating_NEG.json5 @@ -0,0 +1,15 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-wildcard-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "FileName": "/Users/ls_user/dir/dir/dir/dir/dir/notmatchingdoc.txt" + }, + "EventPattern": { + "FileName": [ { "wildcard": "/Users/*/doc.txt" } ] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_wildcard_repeating.json5 b/tests/aws/services/events/event_pattern_templates/content_wildcard_repeating.json5 new file mode 100644 index 0000000000000..c46dfa1262ec4 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_wildcard_repeating.json5 @@ -0,0 +1,15 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-wildcard-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "FileName": "/Users/ls_user/dir/dir/dir/dir/dir/doc.txt" + }, + "EventPattern": { + "FileName": [ { "wildcard": "/Users/*/dir/dir/dir/dir/dir/doc.txt" } ] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_wildcard_repeating_NEG.json5 b/tests/aws/services/events/event_pattern_templates/content_wildcard_repeating_NEG.json5 new file mode 100644 index 0000000000000..ce36c4d593f6e --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_wildcard_repeating_NEG.json5 @@ -0,0 +1,15 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-wildcard-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "FileName": "/Users/ls_user/dir/dir/dir/dir/otherdir/doc.txt" + }, + "EventPattern": { + "FileName": [ { "wildcard": "/Users/*/dir/dir/dir/dir/dir/doc.txt" } ] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/content_wildcard_simplified.json5 b/tests/aws/services/events/event_pattern_templates/content_wildcard_simplified.json5 new file mode 100644 index 0000000000000..e90d3964174ca --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/content_wildcard_simplified.json5 @@ -0,0 +1,15 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-wildcard-matching +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "EventBusArn": "arn:aws:events:us-east-1:123456789012:event-bus/myEventBus" + }, + "EventPattern": { + "EventBusArn": [ { "wildcard": "arn:aws:events:us-east-1:*:event-bus/*" } ] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/dot_joining_event.json5 b/tests/aws/services/events/event_pattern_templates/dot_joining_event.json5 new file mode 100644 index 0000000000000..a416f130b162e --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/dot_joining_event.json5 @@ -0,0 +1,19 @@ +// Based on "Considerations when creating event patterns" from https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "state.status": "running" + } + }, + "EventPattern": { + "detail": { + "state": { "status": [ "running" ] } + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/dot_joining_event_NEG.json5 b/tests/aws/services/events/event_pattern_templates/dot_joining_event_NEG.json5 new file mode 100644 index 0000000000000..a007e949bf28f --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/dot_joining_event_NEG.json5 @@ -0,0 +1,21 @@ +// Based on "Considerations when creating event patterns" from https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "state.status": { + "status": "running" + } + } + }, + "EventPattern": { + "detail": { + "state": { "status": [ "running" ] } + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/dot_joining_pattern.json5 b/tests/aws/services/events/event_pattern_templates/dot_joining_pattern.json5 new file mode 100644 index 0000000000000..ba158dcba3d5f --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/dot_joining_pattern.json5 @@ -0,0 +1,21 @@ +// Based on "Considerations when creating event patterns" from https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "state": { + "status": "running" + } + } + }, + "EventPattern": { + "detail" : { + "state.status": [ "running" ] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/dot_joining_pattern_NEG.json5 b/tests/aws/services/events/event_pattern_templates/dot_joining_pattern_NEG.json5 new file mode 100644 index 0000000000000..90f70c344c38b --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/dot_joining_pattern_NEG.json5 @@ -0,0 +1,23 @@ +// Based on "Considerations when creating event patterns" from https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "state": { + "status": "running" + } + } + }, + "EventPattern": { + "detail" : { + "state.status": { + "status": [ "running" ] + } + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/dynamodb.json5 b/tests/aws/services/events/event_pattern_templates/dynamodb.json5 new file mode 100644 index 0000000000000..bccf084d5103f --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/dynamodb.json5 @@ -0,0 +1,27 @@ +{ + "Event": { + "id": "1", + "source": "order", + "detail-type": "Test", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "dynamodb": { + "NewImage": { + "homemade": { + "S": "ABCD", + "N": "1234" + } + } + } + }, + "EventPattern": { + "dynamodb": { + "NewImage": { + "homemade": { + "N": ["1234"] + } + } + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/int_nolist_EXC.json5 b/tests/aws/services/events/event_pattern_templates/int_nolist_EXC.json5 new file mode 100644 index 0000000000000..ccb92aa68573f --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/int_nolist_EXC.json5 @@ -0,0 +1,15 @@ +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "int": 42 + }, + "EventPattern": { + // Without the list + "int": 42 + } +} diff --git a/tests/aws/services/events/event_pattern_templates/key_case_sensitive_NEG.json5 b/tests/aws/services/events/event_pattern_templates/key_case_sensitive_NEG.json5 new file mode 100644 index 0000000000000..7b811554810ac --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/key_case_sensitive_NEG.json5 @@ -0,0 +1,14 @@ +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "my_key": "my-value", + }, + "EventPattern": { + "MY_KEY": ["my-value"] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/minimal.json5 b/tests/aws/services/events/event_pattern_templates/minimal.json5 new file mode 100644 index 0000000000000..5de636d785e50 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/minimal.json5 @@ -0,0 +1,15 @@ +// API: https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_TestEventPattern.html +// Mandatory fields for events: id, account, source, time, region, resources, detail-type +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + }, + "EventPattern": { + "id": ["1"] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/nested_json_NEG.json5 b/tests/aws/services/events/event_pattern_templates/nested_json_NEG.json5 new file mode 100644 index 0000000000000..5ed5f2bc46671 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/nested_json_NEG.json5 @@ -0,0 +1,16 @@ +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": "{\"my-key\": \"my-value\"}", + }, + "EventPattern": { + "detail": { + "my-key": ["my-value"] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/null_value.json5 b/tests/aws/services/events/event_pattern_templates/null_value.json5 new file mode 100644 index 0000000000000..28d2d55af637b --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/null_value.json5 @@ -0,0 +1,23 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-null-values.html +{ + "Event": { + "version": "0", + "id": "3e3c153a-8339-4e30-8c35-687ebef853fe", + "detail-type": "EC2 Instance Launch Successful", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "2015-11-11T21:31:47Z", + "region": "us-east-1", + "resources": [ + ], + "detail": { + "eventVersion": "", + "responseElements": null + } + }, + "EventPattern": { + "detail": { + "responseElements": [null] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/null_value_NEG.json5 b/tests/aws/services/events/event_pattern_templates/null_value_NEG.json5 new file mode 100644 index 0000000000000..9f4caa79d8b1b --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/null_value_NEG.json5 @@ -0,0 +1,23 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-null-values.html +{ + "Event": { + "version": "0", + "id": "3e3c153a-8339-4e30-8c35-687ebef853fe", + "detail-type": "EC2 Instance Launch Successful", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "2015-11-11T21:31:47Z", + "region": "us-east-1", + "resources": [ + ], + "detail": { + "eventVersion": "", + "responseElements": "null" + } + }, + "EventPattern": { + "detail": { + "responseElements": [null] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/number_comparison_float.json5 b/tests/aws/services/events/event_pattern_templates/number_comparison_float.json5 new file mode 100644 index 0000000000000..67a1852d10c68 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/number_comparison_float.json5 @@ -0,0 +1,20 @@ +// Based on "Considerations when creating event patterns" from https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + // The scientific notation 3.0e2 requires different testing because it gets serialized into 300.0 + // Gets serialized into the string '... "number": 300.0}' + "number": 300.0 + }, + "EventPattern": { + // This behavior contradicts the AWS documentation: + // For numbers, EventBridge uses string representation. For example, 300, 300.0, and 3.0e2 are not considered equal. + // Gets serialized into the string '{"number": [300]}' + "number": [300] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/operator_case_sensitive_EXC.json5 b/tests/aws/services/events/event_pattern_templates/operator_case_sensitive_EXC.json5 new file mode 100644 index 0000000000000..46a1a4befaa97 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/operator_case_sensitive_EXC.json5 @@ -0,0 +1,14 @@ +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "my_key": "my-value", + }, + "EventPattern": { + "my_key": [{ "EXISTS": true }] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/operator_multiple_list.json5 b/tests/aws/services/events/event_pattern_templates/operator_multiple_list.json5 new file mode 100644 index 0000000000000..fe780a0ccdc4b --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/operator_multiple_list.json5 @@ -0,0 +1,15 @@ +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "my_key": "my-value", + }, + "EventPattern": { + // Only the first operator in the list gets evaluated, others are ignored without raising an exception + "my_key": [{ "exists": true }, {"prefix": "IGNORED" }, {"suffix": "IGNORED"}] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/or-anything-but.json5 b/tests/aws/services/events/event_pattern_templates/or-anything-but.json5 new file mode 100644 index 0000000000000..2208a36f9fb58 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/or-anything-but.json5 @@ -0,0 +1,38 @@ +{ + "Event": { + "id": "1", + "source": "order", + "detail-type": "Test", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "dynamodb": { + "NewImage": { + "homemade": { + "S": "ABCD", + "N": "1234" + } + } + } + }, + "EventPattern": { + "dynamodb": { + "NewImage": { + "homemade": { + "S": [ + // Matches this filter because ABCD is not "roses" + { + "anything-but": [ + "roses" + ] + }, + // Does NOT match this filter because S exists + { + "exists": false + } + ] + } + } + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/or-exists-parent.json5 b/tests/aws/services/events/event_pattern_templates/or-exists-parent.json5 new file mode 100644 index 0000000000000..90b31da0ba3e5 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/or-exists-parent.json5 @@ -0,0 +1,38 @@ +{ + "Event": { + "id": "1", + "source": "order", + "detail-type": "Test", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "dynamodb": { + "NewImage": { + // "homemade>S" does NOT exist + "purchased": { + "N": "789" + } + } + } + }, + "EventPattern": { + "dynamodb": { + "NewImage": { + "homemade": { + "S": [ + // Does NOT match this filter because "homemade" is not present + { + "anything-but": [ + "roses" + ] + }, + // Matches this filter because "homemade>S" does not exist + { + "exists": false + } + ] + } + } + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/or-exists.json5 b/tests/aws/services/events/event_pattern_templates/or-exists.json5 new file mode 100644 index 0000000000000..0128eecb034f9 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/or-exists.json5 @@ -0,0 +1,37 @@ +{ + "Event": { + "id": "1", + "source": "order", + "detail-type": "Test", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "dynamodb": { + "NewImage": { + "homemade": { + "N": "789" + } + } + } + }, + "EventPattern": { + "dynamodb": { + "NewImage": { + "homemade": { + "S": [ + // Does NOT match this filter because "homemade" is not present + { + "anything-but": [ + "roses" + ] + }, + // Matches this filter because S does not exist + { + "exists": false + } + ] + } + } + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/prefix.json5 b/tests/aws/services/events/event_pattern_templates/prefix.json5 new file mode 100644 index 0000000000000..d901062ac1058 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/prefix.json5 @@ -0,0 +1,40 @@ +{ + "Event": { + "id": "1", + "source": "order", + "detail-type": "Test", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "dynamodb": { + "ApproximateCreationDateTime": 1664559083.0, + "Keys": { + "SK": { "S": "PRODUCT#CHOCOLATE#DARK#1000" }, + "PK": { "S": "COMPANY#1000" } + }, + "NewImage": { + "quantity": { "N": "50" }, + "company_id": { "S": "1000" }, + "fabric": { "S": "Florida Chocolates" }, + "price": { "N": "15" }, + "stores": { "N": "5" }, + "product_id": { "S": "1000" }, + "SK": { "S": "PRODUCT#CHOCOLATE#DARK#1000" }, + "PK": { "S": "COMPANY#1000" }, + "state": { "S": "FL" }, + "type": { "S": "" } + }, + "SequenceNumber": "700000000000888747038", + "SizeBytes": 174, + "StreamViewType": "NEW_AND_OLD_IMAGES" + } + }, + "EventPattern": { + "dynamodb": { + "Keys": { + "PK": { "S": [{ "prefix": "COMPANY" }] }, + "SK": { "S": [{ "prefix": "PRODUCT" }] } + } + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/sample1.json5 b/tests/aws/services/events/event_pattern_templates/sample1.json5 new file mode 100644 index 0000000000000..dd44d17ecf669 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/sample1.json5 @@ -0,0 +1,20 @@ +// This sample is based on our API tests +// API: https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_TestEventPattern.html +{ + "Event": { + "id": "1", + "source": "order", + "detail-type": "Test", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z" + }, + "EventPattern": { + "source": [ + "order" + ], + "detail-type": [ + "Test" + ], + } +} diff --git a/tests/aws/services/events/event_pattern_templates/string.json5 b/tests/aws/services/events/event_pattern_templates/string.json5 new file mode 100644 index 0000000000000..710c5774c36f1 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/string.json5 @@ -0,0 +1,16 @@ +// Based on "Considerations when creating event patterns" from https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "string": "A CamelCase string with an emoji 🚀 and trailing whitespace " + }, + "EventPattern": { + // For strings, EventBridge uses exact character-by-character matching without case-folding or any other string normalization. + "string": ["A CamelCase string with an emoji 🚀 and trailing whitespace "] + } +} diff --git a/tests/aws/services/events/event_pattern_templates/string_empty.json5 b/tests/aws/services/events/event_pattern_templates/string_empty.json5 new file mode 100644 index 0000000000000..356df68168dfb --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/string_empty.json5 @@ -0,0 +1,23 @@ +// Based on https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-null-values.html +{ + "Event": { + "version": "0", + "id": "3e3c153a-8339-4e30-8c35-687ebef853fe", + "detail-type": "EC2 Instance Launch Successful", + "source": "aws.autoscaling", + "account": "123456789012", + "time": "2015-11-11T21:31:47Z", + "region": "us-east-1", + "resources": [ + ], + "detail": { + "eventVersion": "", + "responseElements": null + } + }, + "EventPattern": { + "detail": { + "eventVersion": [""] + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/string_nolist_EXC.json5 b/tests/aws/services/events/event_pattern_templates/string_nolist_EXC.json5 new file mode 100644 index 0000000000000..8dc89de8c3ba2 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/string_nolist_EXC.json5 @@ -0,0 +1,16 @@ +// Based on "Considerations when creating event patterns" from https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "string": "my-value", + }, + "EventPattern": { + // Without the list + "string": "my-value", + } +} diff --git a/tests/aws/services/events/test_event_patterns.py b/tests/aws/services/events/test_event_patterns.py new file mode 100644 index 0000000000000..6e3a0a49a3f99 --- /dev/null +++ b/tests/aws/services/events/test_event_patterns.py @@ -0,0 +1,160 @@ +import json +import os +from pathlib import Path +from typing import List, Tuple + +import json5 +import pytest + +from localstack.testing.aws.util import is_aws_cloud +from localstack.testing.pytest import markers +from tests.aws.services.events.helper_functions import is_v2_provider + +THIS_FOLDER: str = os.path.dirname(os.path.realpath(__file__)) +REQUEST_TEMPLATE_DIR = os.path.join(THIS_FOLDER, "event_pattern_templates") +COMPLEX_MULTI_KEY_EVENT_PATTERN = os.path.join( + REQUEST_TEMPLATE_DIR, "complex_multi_key_event_pattern.json" +) +COMPLEX_MULTI_KEY_EVENT = os.path.join(REQUEST_TEMPLATE_DIR, "complex_multi_key_event.json") + + +def load_request_templates(directory_path: str) -> List[Tuple[dict, str]]: + json5_files = list_files_with_suffix(directory_path, ".json5") + return [load_request_template(file_path) for file_path in json5_files] + + +def load_request_template(file_path: str) -> Tuple[dict, str]: + with open(file_path, "r") as df: + template = json5.load(df) + return template, Path(file_path).stem + + +def list_files_with_suffix(directory_path: str, suffix: str) -> List[str]: + files = [] + for root, _, filenames in os.walk(directory_path): + for filename in filenames: + if filename.endswith(suffix): + absolute_filepath = os.path.join(root, filename) + files.append(absolute_filepath) + + return files + + +request_template_tuples = load_request_templates(REQUEST_TEMPLATE_DIR) + +SKIP_LABELS = [ + # Failing exception tests: + "arrays_empty_EXC", + "content_numeric_EXC", + "content_numeric_operatorcasing_EXC", + "content_numeric_syntax_EXC", + "content_wildcard_complex_EXC", + "int_nolist_EXC", + "operator_case_sensitive_EXC", + "string_nolist_EXC", + # Failing tests: + "complex_or", + "content_anything_but_ignorecase", + "content_anything_but_ignorecase_list", + "content_anything_suffix", + "content_exists_false", + "content_ignorecase", + "content_ignorecase_NEG", + "content_ip_address", + "content_numeric_and", + "content_prefix_ignorecase", + "content_suffix", + "content_suffix_ignorecase", + "content_wildcard_nonrepeating", + "content_wildcard_repeating", + "content_wildcard_simplified", + "dot_joining_event", + "dot_joining_pattern", + "nested_json_NEG", + "or-exists", + "or-exists-parent", +] + + +# TODO: extend these test cases based on the open source docs + tests: https://github.com/aws/event-ruler +# For example, "JSON Array Matching", "And and Or Relationship among fields with Ruler", rule validation, +# and exception handling. +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") +@pytest.mark.parametrize( + "request_template,label", request_template_tuples, ids=[t[1] for t in request_template_tuples] +) +@markers.aws.validated +def test_test_event_pattern(aws_client, snapshot, request_template, label): + """This parametrized test handles three outcomes: + a) MATCH (default): The EventPattern matches the Event yielding true as result. + b) NO MATCH (_NEG suffix): The EventPattern does NOT match the Event yielding false as result. + c) EXCEPTION (_EXC suffix): The EventPattern is invalid and raises an exception. + """ + if label in SKIP_LABELS and not is_aws_cloud(): + pytest.skip("Not yet implemented") + + event = request_template["Event"] + event_pattern = request_template["EventPattern"] + + if label.endswith("_EXC"): + with pytest.raises(Exception) as e: + aws_client.events.test_event_pattern( + Event=json.dumps(event), + EventPattern=json.dumps(event_pattern), + ) + exception_info = {"exception_type": type(e.value), "exception_message": e.value.response} + snapshot.match(label, exception_info) + else: + response = aws_client.events.test_event_pattern( + Event=json.dumps(event), + EventPattern=json.dumps(event_pattern), + ) + + # Validate the test intention: The _NEG suffix indicates negative tests (i.e., a pattern not matching the event) + if label.endswith("_NEG"): + assert not response["Result"] + else: + assert response["Result"] + + +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") +@markers.aws.validated +def test_test_event_pattern_with_multi_key(aws_client): + """Test the special case of a duplicate JSON key separately because it requires working around the + uniqueness constraints of the JSON5 library and Python dicts, which would already de-deduplicate the key "location". + This example is based on the following AWS documentation: + https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-complex-example + """ + + with open(COMPLEX_MULTI_KEY_EVENT, "r") as event_file, open( + COMPLEX_MULTI_KEY_EVENT_PATTERN, "r" + ) as event_pattern_file: + event = event_file.read() + event_pattern = event_pattern_file.read() + + response = aws_client.events.test_event_pattern( + Event=event, + EventPattern=event_pattern, + ) + assert response["Result"] + + +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") +@markers.aws.validated +def test_test_event_pattern_with_escape_characters(aws_client): + r"""Test the special case of using escape characters separately because it requires working around JSON escaping. + Escape characters are explained in the AWS documentation: + https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-wildcard-matching + * "The string \* represents the literal * character" + * "The string \\ represents the literal \ character" + """ + + event = r'{"id": "1", "source": "test-source", "detail-type": "test-detail-type", "account": "123456789012", "region": "us-east-2", "time": "2022-07-13T13:48:01Z", "detail": {"escape_star": "*", "escape_backslash": "\\"}}' + # TODO: devise better testing strategy for * because the wildcard matches everything and "\\*" does not match. + event_pattern = r'{"detail": {"escape_star": ["*"], "escape_backslash": ["\\"]}}' + + response = aws_client.events.test_event_pattern( + Event=event, + EventPattern=event_pattern, + ) + assert response["Result"] diff --git a/tests/aws/services/events/test_event_patterns.snapshot.json b/tests/aws/services/events/test_event_patterns.snapshot.json new file mode 100644 index 0000000000000..e828c53c14a61 --- /dev/null +++ b/tests/aws/services/events/test_event_patterns.snapshot.json @@ -0,0 +1,414 @@ +{ + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_repeating]": { + "recorded-date": "04-04-2024, 08:07:53", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix_NEG]": { + "recorded-date": "04-04-2024, 08:07:54", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_multi_match]": { + "recorded-date": "04-04-2024, 08:07:54", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[int_nolist_EXC]": { + "recorded-date": "04-04-2024, 08:07:54", + "recorded-content": { + "int_nolist_EXC": { + "exception_message": { + "Error": { + "Code": "InvalidEventPatternException", + "Message": "Event pattern is not valid. Reason: \"int\" must be an object or an array\n at [Source: (String)\"{\"int\": 42}\"; line: 1, column: 11]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "exception_type": "" + } + } + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays]": { + "recorded-date": "04-04-2024, 08:07:54", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_repeating_NEG]": { + "recorded-date": "04-04-2024, 08:07:54", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase_list_NEG]": { + "recorded-date": "04-04-2024, 08:07:55", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_simplified]": { + "recorded-date": "04-04-2024, 08:07:55", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[or-exists]": { + "recorded-date": "04-04-2024, 08:07:55", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_nonrepeating]": { + "recorded-date": "04-04-2024, 08:07:55", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_multi_match_NEG]": { + "recorded-date": "04-04-2024, 08:07:55", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string]": { + "recorded-date": "04-04-2024, 08:07:55", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_operatorcasing_EXC]": { + "recorded-date": "04-04-2024, 08:07:55", + "recorded-content": { + "content_numeric_operatorcasing_EXC": { + "exception_message": { + "Error": { + "Code": "InvalidEventPatternException", + "Message": "Event pattern is not valid. Reason: Unrecognized match type NUMERIC\n at [Source: (String)\"{\"detail\": {\"equal\": [{\"NUMERIC\": [\"=\", 5]}]}}\"; line: 1, column: 36]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "exception_type": "" + } + } + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number_list_NEG]": { + "recorded-date": "04-04-2024, 08:07:56", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ip_address]": { + "recorded-date": "04-04-2024, 08:07:56", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_prefix_NEG]": { + "recorded-date": "04-04-2024, 08:07:56", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[string_nolist_EXC]": { + "recorded-date": "04-04-2024, 08:07:56", + "recorded-content": { + "string_nolist_EXC": { + "exception_message": { + "Error": { + "Code": "InvalidEventPatternException", + "Message": "Event pattern is not valid. Reason: \"string\" must be an object or an array\n at [Source: (String)\"{\"string\": \"my-value\"}\"; line: 1, column: 13]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "exception_type": "" + } + } + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string_NEG]": { + "recorded-date": "04-04-2024, 08:07:57", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[boolean_NEG]": { + "recorded-date": "04-04-2024, 08:07:57", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[or-exists-parent]": { + "recorded-date": "04-04-2024, 08:07:57", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays_empty_EXC]": { + "recorded-date": "04-04-2024, 08:07:57", + "recorded-content": { + "arrays_empty_EXC": { + "exception_message": { + "Error": { + "Code": "InvalidEventPatternException", + "Message": "Event pattern is not valid. Reason: Empty arrays are not allowed\n at [Source: (String)\"{\"resources\": []}\"; line: 1, column: 17]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "exception_type": "" + } + } + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[nested_json_NEG]": { + "recorded-date": "04-04-2024, 08:07:58", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_pattern]": { + "recorded-date": "04-04-2024, 08:07:58", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[operator_multiple_list]": { + "recorded-date": "04-04-2024, 08:07:58", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string_list]": { + "recorded-date": "04-04-2024, 08:07:58", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dynamodb]": { + "recorded-date": "04-04-2024, 08:07:58", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase]": { + "recorded-date": "04-04-2024, 08:07:58", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[string_empty]": { + "recorded-date": "04-04-2024, 08:07:58", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_many_rules]": { + "recorded-date": "04-04-2024, 08:11:59", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[null_value]": { + "recorded-date": "04-04-2024, 08:07:59", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number_list]": { + "recorded-date": "04-04-2024, 08:07:59", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_nonrepeating_NEG]": { + "recorded-date": "04-04-2024, 08:07:59", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number_NEG]": { + "recorded-date": "04-04-2024, 08:07:59", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[operator_case_sensitive_EXC]": { + "recorded-date": "04-04-2024, 08:07:59", + "recorded-content": { + "operator_case_sensitive_EXC": { + "exception_message": { + "Error": { + "Code": "InvalidEventPatternException", + "Message": "Event pattern is not valid. Reason: Unrecognized match type EXISTS\n at [Source: (String)\"{\"my_key\": [{\"EXISTS\": true}]}\"; line: 1, column: 28]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "exception_type": "" + } + } + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists]": { + "recorded-date": "04-04-2024, 08:08:00", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ip_address_NEG]": { + "recorded-date": "04-04-2024, 08:08:00", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_EXC]": { + "recorded-date": "04-04-2024, 08:08:00", + "recorded-content": { + "content_numeric_EXC": { + "exception_message": { + "Error": { + "Code": "InvalidEventPatternException", + "Message": "Event pattern is not valid. Reason: Bad numeric range operator: >\n at [Source: (String)\"{\"detail\": {\"c-count\": [{\"numeric\": [\">\", 0, \">\", 0]}]}}\"; line: 1, column: 49]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "exception_type": "" + } + } + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_or_NEG]": { + "recorded-date": "04-04-2024, 08:08:00", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_or]": { + "recorded-date": "04-04-2024, 08:08:00", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_and_NEG]": { + "recorded-date": "04-04-2024, 08:08:01", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_event]": { + "recorded-date": "04-04-2024, 08:08:01", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_suffix_NEG]": { + "recorded-date": "04-04-2024, 08:08:01", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_prefix_ignorecase]": { + "recorded-date": "04-04-2024, 08:08:01", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix_ignorecase]": { + "recorded-date": "04-04-2024, 08:08:01", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[null_value_NEG]": { + "recorded-date": "04-04-2024, 08:08:01", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_pattern_NEG]": { + "recorded-date": "04-04-2024, 08:08:01", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_prefix]": { + "recorded-date": "04-04-2024, 08:08:01", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[sample1]": { + "recorded-date": "04-04-2024, 08:08:02", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[key_case_sensitive_NEG]": { + "recorded-date": "04-04-2024, 08:08:02", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_event_NEG]": { + "recorded-date": "04-04-2024, 08:08:02", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[prefix]": { + "recorded-date": "04-04-2024, 08:08:02", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_suffix]": { + "recorded-date": "04-04-2024, 08:08:02", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase_list]": { + "recorded-date": "04-04-2024, 08:08:02", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_prefix_NEG]": { + "recorded-date": "04-04-2024, 08:08:02", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[string]": { + "recorded-date": "04-04-2024, 08:08:02", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays_empty_null_NEG]": { + "recorded-date": "04-04-2024, 08:08:03", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix_ignorecase_NEG]": { + "recorded-date": "04-04-2024, 08:08:03", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_complex_EXC]": { + "recorded-date": "04-04-2024, 08:08:03", + "recorded-content": { + "content_wildcard_complex_EXC": { + "exception_message": { + "Error": { + "Code": "InvalidEventPatternException", + "Message": "Event pattern is not valid. Reason: Rule is too complex - try using fewer wildcard characters or fewer repeating character sequences after a wildcard character" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "exception_type": "" + } + } + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string_list_NEG]": { + "recorded-date": "04-04-2024, 08:08:04", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_prefix]": { + "recorded-date": "04-04-2024, 08:08:04", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ignorecase_NEG]": { + "recorded-date": "04-04-2024, 08:08:04", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[minimal]": { + "recorded-date": "04-04-2024, 08:08:04", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists_false]": { + "recorded-date": "04-04-2024, 08:08:04", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[number_comparison_float]": { + "recorded-date": "04-04-2024, 08:08:04", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase_NEG]": { + "recorded-date": "04-04-2024, 08:08:04", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix]": { + "recorded-date": "04-04-2024, 08:08:04", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays_NEG]": { + "recorded-date": "04-04-2024, 08:08:05", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ignorecase]": { + "recorded-date": "04-04-2024, 08:08:05", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists_false_NEG]": { + "recorded-date": "04-04-2024, 08:08:05", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists_NEG]": { + "recorded-date": "04-04-2024, 08:08:05", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_syntax_EXC]": { + "recorded-date": "04-04-2024, 08:08:05", + "recorded-content": { + "content_numeric_syntax_EXC": { + "exception_message": { + "Error": { + "Code": "InvalidEventPatternException", + "Message": "Event pattern is not valid. Reason: Value of < must be numeric\n at [Source: (String)\"{\"detail\": {\"c-count\": [{\"numeric\": [\">\", 0, \"<\"]}]}}\"; line: 1, column: 50]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "exception_type": "" + } + } + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_and]": { + "recorded-date": "04-04-2024, 08:08:06", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[boolean]": { + "recorded-date": "04-04-2024, 08:08:06", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number]": { + "recorded-date": "04-04-2024, 08:08:06", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[or-anything-but]": { + "recorded-date": "04-04-2024, 08:08:06", + "recorded-content": {} + } +} diff --git a/tests/aws/services/events/test_event_patterns.validation.json b/tests/aws/services/events/test_event_patterns.validation.json new file mode 100644 index 0000000000000..c620718600098 --- /dev/null +++ b/tests/aws/services/events/test_event_patterns.validation.json @@ -0,0 +1,233 @@ +{ + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays]": { + "last_validated_date": "2024-04-04T08:07:54+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays_NEG]": { + "last_validated_date": "2024-04-04T08:08:05+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays_empty_EXC]": { + "last_validated_date": "2024-04-04T08:07:57+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays_empty_null_NEG]": { + "last_validated_date": "2024-04-04T08:08:03+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[boolean]": { + "last_validated_date": "2024-04-04T08:08:06+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[boolean_NEG]": { + "last_validated_date": "2024-04-04T08:07:57+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_many_rules]": { + "last_validated_date": "2024-04-04T08:11:59+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_multi_match]": { + "last_validated_date": "2024-04-04T08:07:54+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_multi_match_NEG]": { + "last_validated_date": "2024-04-04T08:07:55+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_or]": { + "last_validated_date": "2024-04-04T08:08:00+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_or_NEG]": { + "last_validated_date": "2024-04-04T08:08:00+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase]": { + "last_validated_date": "2024-04-04T08:07:58+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase_NEG]": { + "last_validated_date": "2024-04-04T08:08:04+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase_list]": { + "last_validated_date": "2024-04-04T08:08:02+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase_list_NEG]": { + "last_validated_date": "2024-04-04T08:07:55+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number]": { + "last_validated_date": "2024-04-04T08:08:06+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number_NEG]": { + "last_validated_date": "2024-04-04T08:07:59+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number_list]": { + "last_validated_date": "2024-04-04T08:07:59+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number_list_NEG]": { + "last_validated_date": "2024-04-04T08:07:56+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string]": { + "last_validated_date": "2024-04-04T08:07:55+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string_NEG]": { + "last_validated_date": "2024-04-04T08:07:57+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string_list]": { + "last_validated_date": "2024-04-04T08:07:58+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string_list_NEG]": { + "last_validated_date": "2024-04-04T08:08:04+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_prefix]": { + "last_validated_date": "2024-04-04T08:08:01+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_prefix_NEG]": { + "last_validated_date": "2024-04-04T08:07:56+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_suffix]": { + "last_validated_date": "2024-04-04T08:08:02+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_suffix_NEG]": { + "last_validated_date": "2024-04-04T08:08:01+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists]": { + "last_validated_date": "2024-04-04T08:08:00+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists_NEG]": { + "last_validated_date": "2024-04-04T08:08:05+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists_false]": { + "last_validated_date": "2024-04-04T08:08:04+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists_false_NEG]": { + "last_validated_date": "2024-04-04T08:08:05+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ignorecase]": { + "last_validated_date": "2024-04-04T08:08:05+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ignorecase_NEG]": { + "last_validated_date": "2024-04-04T08:08:04+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ip_address]": { + "last_validated_date": "2024-04-04T08:07:56+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ip_address_NEG]": { + "last_validated_date": "2024-04-04T08:08:00+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_EXC]": { + "last_validated_date": "2024-04-04T08:08:00+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_and]": { + "last_validated_date": "2024-04-04T08:08:06+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_and_NEG]": { + "last_validated_date": "2024-04-04T08:08:01+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_operatorcasing_EXC]": { + "last_validated_date": "2024-04-04T08:07:55+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_syntax_EXC]": { + "last_validated_date": "2024-04-04T08:08:05+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_prefix]": { + "last_validated_date": "2024-04-04T08:08:04+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_prefix_NEG]": { + "last_validated_date": "2024-04-04T08:08:02+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_prefix_ignorecase]": { + "last_validated_date": "2024-04-04T08:08:01+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix]": { + "last_validated_date": "2024-04-04T08:08:04+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix_NEG]": { + "last_validated_date": "2024-04-04T08:07:54+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix_ignorecase]": { + "last_validated_date": "2024-04-04T08:08:01+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix_ignorecase_NEG]": { + "last_validated_date": "2024-04-04T08:08:03+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_complex_EXC]": { + "last_validated_date": "2024-04-04T08:08:03+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_nonrepeating]": { + "last_validated_date": "2024-04-04T08:07:55+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_nonrepeating_NEG]": { + "last_validated_date": "2024-04-04T08:07:59+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_repeating]": { + "last_validated_date": "2024-04-04T08:07:53+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_repeating_NEG]": { + "last_validated_date": "2024-04-04T08:07:54+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_simplified]": { + "last_validated_date": "2024-04-04T08:07:55+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_event]": { + "last_validated_date": "2024-04-04T08:08:01+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_event_NEG]": { + "last_validated_date": "2024-04-04T08:08:02+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_pattern]": { + "last_validated_date": "2024-04-04T08:07:58+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_pattern_NEG]": { + "last_validated_date": "2024-04-04T08:08:01+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dynamodb]": { + "last_validated_date": "2024-04-04T08:07:58+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[int_nolist_EXC]": { + "last_validated_date": "2024-04-04T08:07:54+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[key_case_sensitive_NEG]": { + "last_validated_date": "2024-04-04T08:08:02+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[minimal]": { + "last_validated_date": "2024-04-04T08:08:04+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[nested_json_NEG]": { + "last_validated_date": "2024-04-04T08:07:58+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[null_value]": { + "last_validated_date": "2024-04-04T08:07:59+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[null_value_NEG]": { + "last_validated_date": "2024-04-04T08:08:01+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[number_comparison_float]": { + "last_validated_date": "2024-04-04T08:08:04+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[operator_case_sensitive_EXC]": { + "last_validated_date": "2024-04-04T08:07:59+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[operator_multiple_list]": { + "last_validated_date": "2024-04-04T08:07:58+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[or-anything-but]": { + "last_validated_date": "2024-04-04T08:08:06+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[or-exists-parent]": { + "last_validated_date": "2024-04-04T08:07:57+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[or-exists]": { + "last_validated_date": "2024-04-04T08:07:55+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[prefix]": { + "last_validated_date": "2024-04-04T08:08:02+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[sample1]": { + "last_validated_date": "2024-04-04T08:08:02+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[string]": { + "last_validated_date": "2024-04-04T08:08:02+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[string_empty]": { + "last_validated_date": "2024-04-04T08:07:58+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[string_nolist_EXC]": { + "last_validated_date": "2024-04-04T08:07:56+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern_with_escape_characters": { + "last_validated_date": "2024-04-04T08:08:06+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern_with_multi_key": { + "last_validated_date": "2024-04-04T08:08:06+00:00" + } +} diff --git a/tests/aws/services/events/test_events_rules.py b/tests/aws/services/events/test_events_rules.py index 28e4747bf8147..63758ca8db630 100644 --- a/tests/aws/services/events/test_events_rules.py +++ b/tests/aws/services/events/test_events_rules.py @@ -247,11 +247,13 @@ def test_put_event_with_content_base_rule_in_pattern(aws_client, clean_up): queue_url = aws_client.sqs.create_queue(QueueName=queue_name)["QueueUrl"] queue_arn = arns.sqs_queue_arn(queue_name, TEST_AWS_ACCOUNT_ID, TEST_AWS_REGION_NAME) + # EventBridge apparently converts some fields, for example: Source=>source, DetailType=>detail-type + # but the actual pattern matching is case-sensitive by key! pattern = { - "Source": [{"exists": True}], + "source": [{"exists": True}], "detail-type": [{"prefix": "core.app"}], - "Detail": { - "decription": ["this-is-event-details"], + "detail": { + "description": ["this-is-event-details"], "amount": [200], "salary": [2000, 4000], "env": ["dev", "prod"], @@ -261,7 +263,8 @@ def test_put_event_with_content_base_rule_in_pattern(aws_client, clean_up): "test2": [{"anything-but": "test2"}], "test3": [{"anything-but": ["test3", "test33"]}], "test4": [{"anything-but": {"prefix": "test4"}}], - "ip": [{"cidr": "10.102.1.0/24"}], + # TODO: unsupported in LocalStack + # "ip": [{"cidr": "10.102.1.0/24"}], "num-test1": [{"numeric": ["<", 200]}], "num-test2": [{"numeric": ["<=", 200]}], "num-test3": [{"numeric": [">", 200]}], @@ -278,7 +281,7 @@ def test_put_event_with_content_base_rule_in_pattern(aws_client, clean_up): "DetailType": "core.app.backend", "Detail": json.dumps( { - "decription": "this-is-event-details", + "description": "this-is-event-details", "amount": 200, "salary": 2000, "env": "prod", From a4161c4c52202f9775068a73467e276914fb85d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristopher=20Pinz=C3=B3n?= Date: Mon, 8 Apr 2024 15:39:09 -0500 Subject: [PATCH 032/169] add script for generating Kubernetes dev files (#10560) --- localstack/dev/kubernetes/__init__.py | 0 localstack/dev/kubernetes/__main__.py | 204 ++++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 localstack/dev/kubernetes/__init__.py create mode 100644 localstack/dev/kubernetes/__main__.py diff --git a/localstack/dev/kubernetes/__init__.py b/localstack/dev/kubernetes/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/localstack/dev/kubernetes/__main__.py b/localstack/dev/kubernetes/__main__.py new file mode 100644 index 0000000000000..67348cba10bf9 --- /dev/null +++ b/localstack/dev/kubernetes/__main__.py @@ -0,0 +1,204 @@ +import os + +import click +import yaml + +from localstack import version as localstack_version + + +def generate_k8s_cluster_config(pro: bool = False, mount_moto: bool = False): + volumes = [] + root_path = os.path.join(os.path.dirname(__file__), "..", "..", "..") + localstack_code_path = os.path.join(root_path, "localstack") + volumes.append( + { + "volume": f"{os.path.normpath(localstack_code_path)}:/code/localstack", + "nodeFilters": ["server:*", "agent:*"], + } + ) + + egg_path = os.path.join(root_path, "localstack_core.egg-info/entry_points.txt") + if pro: + ext_path = os.path.join(root_path, "..", "localstack-ext") + ext_code_path = os.path.join(ext_path, "localstack_ext") + egg_path = os.path.join(ext_path, "localstack_ext.egg-info/entry_points.txt") + + volumes.append( + { + "volume": f"{os.path.normpath(ext_code_path)}:/code/localstack_ext", + "nodeFilters": ["server:*", "agent:*"], + } + ) + + volumes.append( + { + "volume": f"{os.path.normpath(egg_path)}:/code/entry_points", + "nodeFilters": ["server:*", "agent:*"], + } + ) + + if mount_moto: + moto_path = os.path.join(root_path, "..", "moto", "moto") + volumes.append( + {"volume": f"{moto_path}:/code/moto", "nodeFilters": ["server:*", "agent:*"]} + ) + + config = {"apiVersion": "k3d.io/v1alpha3", "kind": "Simple", "volumes": volumes} + + return config + + +def snake_to_kebab_case(string: str): + return string.lower().replace("_", "-") + + +def generate_k8s_cluster_overrides( + pro: bool = False, cluster_config: dict = None, write: bool = False +): + volumes = [] + for volume in cluster_config["volumes"]: + name = snake_to_kebab_case(volume["volume"].split(":")[-1].split("/")[-1]) + volume_type = "Directory" if name != "entry-points" else "File" + volumes.append( + { + "name": name, + "hostPath": {"path": volume["volume"].split(":")[-1]}, + "type": volume_type, + } + ) + + volume_mounts = [] + target_path = "/opt/code/localstack/" + venv_path = os.path.join(target_path, ".venv", "lib", "python3.11", "site-packages") + for volume in volumes: + if volume["name"] == "entry-points": + entry_points_path = os.path.join( + target_path, "localstack_core.egg-info", "entry_points.txt" + ) + if pro: + project = "localstack_ext-" + version = localstack_version.__version__ + dist_info = f"{project}{version}0.dist-info" + entry_points_path = os.path.join(venv_path, dist_info, "entry_points.txt") + + volume_mounts.append( + { + "name": volume["name"], + "readOnly": True, + "mountPath": entry_points_path, + } + ) + continue + + volume_mounts.append( + { + "name": volume["name"], + "readOnly": True, + "mountPath": os.path.join(venv_path, volume["hostPath"]["path"].split("/")[-1]), + } + ) + + overrides = { + "debug": True, + "volumes": volumes, + "volumeMounts": volume_mounts, + } + + return overrides + + +def write_file(content: dict, output_path: str, file_name: str): + path = os.path.join(output_path, file_name) + with open(path, "w") as f: + f.write(yaml.dump(content)) + f.close() + print(f"Generated file at {path}") + + +def print_file(content: dict, file_name: str): + print(f"Generated file:\t{file_name}") + print("=====================================") + print(yaml.dump(content)) + print("=====================================") + + +@click.command("run") +@click.option( + "--pro", is_flag=True, default=None, help="Mount the localstack-ext code into the cluster." +) +@click.option( + "--mount-moto", is_flag=True, default=None, help="Mount the moto code into the cluster." +) +@click.option( + "--write", + is_flag=True, + default=None, + help="Write the configuration and overrides to files.", +) +@click.option( + "--output-dir", + "-o", + type=click.Path(exists=True, file_okay=False, resolve_path=True), + help="Output directory for generated files.", +) +@click.option( + "--overrides-file", + "-of", + default=None, + help="Name of the overrides file (default: overrides.yml).", +) +@click.option( + "--config-file", + "-cf", + default=None, + help="Name of the configuration file (default: configuration.yml).", +) +@click.argument("command", nargs=-1, required=False) +def run( + pro: bool = None, + mount_moto: bool = False, + write: bool = False, + output_dir=None, + overrides_file: str = None, + config_file: str = None, + command: str = None, +): + """ + A tool for localstack developers to generate the kubernetes cluster configuration file and the overrides to mount the localstack code into the cluster. + """ + + config = generate_k8s_cluster_config(pro=pro, mount_moto=mount_moto) + + overrides = generate_k8s_cluster_overrides(pro, config) + + output_dir = output_dir or os.getcwd() + overrides_file = overrides_file or "overrides.yml" + config_file = config_file or "configuration.yml" + + if write: + write_file(config, output_dir, config_file) + write_file(overrides, output_dir, overrides_file) + else: + print_file(config, config_file) + print_file(overrides, overrides_file) + + overrides_file_path = os.path.join(output_dir, overrides_file) + config_file_path = os.path.join(output_dir, config_file) + + print("\nTo create a k3d cluster with the generated configuration, follow these steps:") + print("1. Run the following command to create the cluster:") + print(f"\n k3d cluster create --config {config_file_path}\n") + + print("2. Once the cluster is created, start LocalStack with the generated overrides:") + print("\n helm repo add localstack https://localstack.github.io/helm-charts # (if required)") + print( + f"\n helm upgrade --install localstack localstack/localstack -f {overrides_file_path}\n" + ) + + +def main(): + run() + + +if __name__ == "__main__": + main() From 5b15be19fc316a3f14887c5fe4ac3bd9c54bd8a0 Mon Sep 17 00:00:00 2001 From: Bernhard Matyas <90144234+baermat@users.noreply.github.com> Date: Mon, 8 Apr 2024 22:50:59 +0200 Subject: [PATCH 033/169] eventbridge: fix handling of list elements (#10600) Co-authored-by: Joel Scheuner --- localstack/services/events/utils.py | 9 +- localstack/utils/testutil.py | 10 +- .../list_within_dict.json5 | 26 +++ .../events/test_event_patterns.snapshot.json | 154 +++++++------- .../test_event_patterns.validation.json | 153 +++++++------- .../events/test_events_integrations.py | 196 ++++++++++++++++++ .../test_events_integrations.snapshot.json | 104 ++++++++++ .../test_events_integrations.validation.json | 6 + 8 files changed, 501 insertions(+), 157 deletions(-) create mode 100644 tests/aws/services/events/event_pattern_templates/list_within_dict.json5 create mode 100644 tests/aws/services/events/test_events_integrations.snapshot.json diff --git a/localstack/services/events/utils.py b/localstack/services/events/utils.py index 428ba2a5b2331..a93fdcc07b895 100644 --- a/localstack/services/events/utils.py +++ b/localstack/services/events/utils.py @@ -70,13 +70,16 @@ def matches_event(event_pattern: dict[str, any], event: dict[str, Any]) -> bool: ): return False - # 3. recursively call filter_event(..) for dict types + # 3. recursively call matches_event(..) for dict types elif isinstance(value, (str, dict)): try: # TODO: validate whether inner JSON-encoded strings actually get decoded recursively value = json.loads(value) if isinstance(value, str) else value - if isinstance(value, dict) and not matches_event(value, event_value): - return False + if isinstance(event_value, list): + return any(matches_event(value, ev) for ev in event_value) + else: + if isinstance(value, dict) and not matches_event(value, event_value): + return False except json.decoder.JSONDecodeError: return False diff --git a/localstack/utils/testutil.py b/localstack/utils/testutil.py index 3e67ed5cf739e..08aa26282c110 100644 --- a/localstack/utils/testutil.py +++ b/localstack/utils/testutil.py @@ -573,10 +573,12 @@ def get_log_events(func_name, delay): raw_message = event["message"] if ( not raw_message - or "START" in raw_message - or "END" in raw_message - or "REPORT" in raw_message - # necessary until tail is updated in docker images. See this PR: + or raw_message.startswith("INIT_START") + or raw_message.startswith("START") + or raw_message.startswith("END") + or raw_message.startswith( + "REPORT" + ) # necessary until tail is updated in docker images. See this PR: # http://git.savannah.gnu.org/gitweb/?p=coreutils.git;a=commitdiff;h=v8.24-111-g1118f32 or "tail: unrecognized file system type" in raw_message or regex_filter diff --git a/tests/aws/services/events/event_pattern_templates/list_within_dict.json5 b/tests/aws/services/events/event_pattern_templates/list_within_dict.json5 new file mode 100644 index 0000000000000..23f811ef54d91 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/list_within_dict.json5 @@ -0,0 +1,26 @@ +// Motivated by https://github.com/localstack/localstack/pull/10600 +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "detail": { + "automations": [ + {"key1": "value1"}, + // the "exists" operator matches because at least one element of the list matches + {"id": "match-does-exist"}, + {"key2": "value2"} + ] + } + }, + "EventPattern": { + "detail": { + "automations": { + "id": [{"exists": true}] + } + } + } +} diff --git a/tests/aws/services/events/test_event_patterns.snapshot.json b/tests/aws/services/events/test_event_patterns.snapshot.json index e828c53c14a61..1fe5309849faa 100644 --- a/tests/aws/services/events/test_event_patterns.snapshot.json +++ b/tests/aws/services/events/test_event_patterns.snapshot.json @@ -1,18 +1,18 @@ { "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_repeating]": { - "recorded-date": "04-04-2024, 08:07:53", + "recorded-date": "08-04-2024, 19:33:54", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix_NEG]": { - "recorded-date": "04-04-2024, 08:07:54", + "recorded-date": "08-04-2024, 19:33:54", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_multi_match]": { - "recorded-date": "04-04-2024, 08:07:54", + "recorded-date": "08-04-2024, 19:33:55", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[int_nolist_EXC]": { - "recorded-date": "04-04-2024, 08:07:54", + "recorded-date": "08-04-2024, 19:33:55", "recorded-content": { "int_nolist_EXC": { "exception_message": { @@ -30,39 +30,39 @@ } }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays]": { - "recorded-date": "04-04-2024, 08:07:54", + "recorded-date": "08-04-2024, 19:33:55", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_repeating_NEG]": { - "recorded-date": "04-04-2024, 08:07:54", + "recorded-date": "08-04-2024, 19:33:56", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase_list_NEG]": { - "recorded-date": "04-04-2024, 08:07:55", + "recorded-date": "08-04-2024, 19:33:56", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_simplified]": { - "recorded-date": "04-04-2024, 08:07:55", + "recorded-date": "08-04-2024, 19:33:56", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[or-exists]": { - "recorded-date": "04-04-2024, 08:07:55", + "recorded-date": "08-04-2024, 19:33:56", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_nonrepeating]": { - "recorded-date": "04-04-2024, 08:07:55", + "recorded-date": "08-04-2024, 19:33:56", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_multi_match_NEG]": { - "recorded-date": "04-04-2024, 08:07:55", + "recorded-date": "08-04-2024, 19:33:56", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string]": { - "recorded-date": "04-04-2024, 08:07:55", + "recorded-date": "08-04-2024, 19:33:56", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_operatorcasing_EXC]": { - "recorded-date": "04-04-2024, 08:07:55", + "recorded-date": "08-04-2024, 19:33:56", "recorded-content": { "content_numeric_operatorcasing_EXC": { "exception_message": { @@ -80,19 +80,19 @@ } }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number_list_NEG]": { - "recorded-date": "04-04-2024, 08:07:56", + "recorded-date": "08-04-2024, 19:33:57", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ip_address]": { - "recorded-date": "04-04-2024, 08:07:56", + "recorded-date": "08-04-2024, 19:33:57", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_prefix_NEG]": { - "recorded-date": "04-04-2024, 08:07:56", + "recorded-date": "08-04-2024, 19:33:57", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[string_nolist_EXC]": { - "recorded-date": "04-04-2024, 08:07:56", + "recorded-date": "08-04-2024, 19:33:57", "recorded-content": { "string_nolist_EXC": { "exception_message": { @@ -110,19 +110,19 @@ } }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string_NEG]": { - "recorded-date": "04-04-2024, 08:07:57", + "recorded-date": "08-04-2024, 19:33:58", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[boolean_NEG]": { - "recorded-date": "04-04-2024, 08:07:57", + "recorded-date": "08-04-2024, 19:33:58", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[or-exists-parent]": { - "recorded-date": "04-04-2024, 08:07:57", + "recorded-date": "08-04-2024, 19:33:58", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays_empty_EXC]": { - "recorded-date": "04-04-2024, 08:07:57", + "recorded-date": "08-04-2024, 19:33:58", "recorded-content": { "arrays_empty_EXC": { "exception_message": { @@ -140,55 +140,55 @@ } }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[nested_json_NEG]": { - "recorded-date": "04-04-2024, 08:07:58", + "recorded-date": "08-04-2024, 19:33:59", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_pattern]": { - "recorded-date": "04-04-2024, 08:07:58", + "recorded-date": "08-04-2024, 19:33:59", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[operator_multiple_list]": { - "recorded-date": "04-04-2024, 08:07:58", + "recorded-date": "08-04-2024, 19:33:59", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string_list]": { - "recorded-date": "04-04-2024, 08:07:58", + "recorded-date": "08-04-2024, 19:33:59", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dynamodb]": { - "recorded-date": "04-04-2024, 08:07:58", + "recorded-date": "08-04-2024, 19:33:59", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase]": { - "recorded-date": "04-04-2024, 08:07:58", + "recorded-date": "08-04-2024, 19:33:59", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[string_empty]": { - "recorded-date": "04-04-2024, 08:07:58", + "recorded-date": "08-04-2024, 19:33:59", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_many_rules]": { - "recorded-date": "04-04-2024, 08:11:59", + "recorded-date": "08-04-2024, 19:34:00", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[null_value]": { - "recorded-date": "04-04-2024, 08:07:59", + "recorded-date": "08-04-2024, 19:34:00", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number_list]": { - "recorded-date": "04-04-2024, 08:07:59", + "recorded-date": "08-04-2024, 19:34:00", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_nonrepeating_NEG]": { - "recorded-date": "04-04-2024, 08:07:59", + "recorded-date": "08-04-2024, 19:34:00", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number_NEG]": { - "recorded-date": "04-04-2024, 08:07:59", + "recorded-date": "08-04-2024, 19:34:00", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[operator_case_sensitive_EXC]": { - "recorded-date": "04-04-2024, 08:07:59", + "recorded-date": "08-04-2024, 19:34:00", "recorded-content": { "operator_case_sensitive_EXC": { "exception_message": { @@ -206,15 +206,15 @@ } }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists]": { - "recorded-date": "04-04-2024, 08:08:00", + "recorded-date": "08-04-2024, 19:34:01", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ip_address_NEG]": { - "recorded-date": "04-04-2024, 08:08:00", + "recorded-date": "08-04-2024, 19:34:01", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_EXC]": { - "recorded-date": "04-04-2024, 08:08:00", + "recorded-date": "08-04-2024, 19:34:01", "recorded-content": { "content_numeric_EXC": { "exception_message": { @@ -232,87 +232,87 @@ } }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_or_NEG]": { - "recorded-date": "04-04-2024, 08:08:00", + "recorded-date": "08-04-2024, 19:34:01", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_or]": { - "recorded-date": "04-04-2024, 08:08:00", + "recorded-date": "08-04-2024, 19:34:01", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_and_NEG]": { - "recorded-date": "04-04-2024, 08:08:01", + "recorded-date": "08-04-2024, 19:34:02", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_event]": { - "recorded-date": "04-04-2024, 08:08:01", + "recorded-date": "08-04-2024, 19:34:02", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_suffix_NEG]": { - "recorded-date": "04-04-2024, 08:08:01", + "recorded-date": "08-04-2024, 19:34:02", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_prefix_ignorecase]": { - "recorded-date": "04-04-2024, 08:08:01", + "recorded-date": "08-04-2024, 19:34:02", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix_ignorecase]": { - "recorded-date": "04-04-2024, 08:08:01", + "recorded-date": "08-04-2024, 19:34:02", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[null_value_NEG]": { - "recorded-date": "04-04-2024, 08:08:01", + "recorded-date": "08-04-2024, 19:34:02", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_pattern_NEG]": { - "recorded-date": "04-04-2024, 08:08:01", + "recorded-date": "08-04-2024, 19:34:02", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_prefix]": { - "recorded-date": "04-04-2024, 08:08:01", + "recorded-date": "08-04-2024, 19:34:02", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[sample1]": { - "recorded-date": "04-04-2024, 08:08:02", + "recorded-date": "08-04-2024, 19:34:03", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[key_case_sensitive_NEG]": { - "recorded-date": "04-04-2024, 08:08:02", + "recorded-date": "08-04-2024, 19:34:03", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_event_NEG]": { - "recorded-date": "04-04-2024, 08:08:02", + "recorded-date": "08-04-2024, 19:34:03", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[prefix]": { - "recorded-date": "04-04-2024, 08:08:02", + "recorded-date": "08-04-2024, 19:34:03", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_suffix]": { - "recorded-date": "04-04-2024, 08:08:02", + "recorded-date": "08-04-2024, 19:34:03", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase_list]": { - "recorded-date": "04-04-2024, 08:08:02", + "recorded-date": "08-04-2024, 19:34:03", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_prefix_NEG]": { - "recorded-date": "04-04-2024, 08:08:02", + "recorded-date": "08-04-2024, 19:34:03", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[string]": { - "recorded-date": "04-04-2024, 08:08:02", + "recorded-date": "08-04-2024, 19:34:03", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays_empty_null_NEG]": { - "recorded-date": "04-04-2024, 08:08:03", + "recorded-date": "08-04-2024, 19:34:04", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix_ignorecase_NEG]": { - "recorded-date": "04-04-2024, 08:08:03", + "recorded-date": "08-04-2024, 19:34:04", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_complex_EXC]": { - "recorded-date": "04-04-2024, 08:08:03", + "recorded-date": "08-04-2024, 19:34:04", "recorded-content": { "content_wildcard_complex_EXC": { "exception_message": { @@ -330,55 +330,55 @@ } }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string_list_NEG]": { - "recorded-date": "04-04-2024, 08:08:04", + "recorded-date": "08-04-2024, 19:34:04", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_prefix]": { - "recorded-date": "04-04-2024, 08:08:04", + "recorded-date": "08-04-2024, 19:34:04", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ignorecase_NEG]": { - "recorded-date": "04-04-2024, 08:08:04", + "recorded-date": "08-04-2024, 19:34:05", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[minimal]": { - "recorded-date": "04-04-2024, 08:08:04", + "recorded-date": "08-04-2024, 19:34:05", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists_false]": { - "recorded-date": "04-04-2024, 08:08:04", + "recorded-date": "08-04-2024, 19:34:05", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[number_comparison_float]": { - "recorded-date": "04-04-2024, 08:08:04", + "recorded-date": "08-04-2024, 19:34:05", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase_NEG]": { - "recorded-date": "04-04-2024, 08:08:04", + "recorded-date": "08-04-2024, 19:34:05", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix]": { - "recorded-date": "04-04-2024, 08:08:04", + "recorded-date": "08-04-2024, 19:34:05", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays_NEG]": { - "recorded-date": "04-04-2024, 08:08:05", + "recorded-date": "08-04-2024, 19:34:05", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ignorecase]": { - "recorded-date": "04-04-2024, 08:08:05", + "recorded-date": "08-04-2024, 19:34:05", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists_false_NEG]": { - "recorded-date": "04-04-2024, 08:08:05", + "recorded-date": "08-04-2024, 19:34:06", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists_NEG]": { - "recorded-date": "04-04-2024, 08:08:05", + "recorded-date": "08-04-2024, 19:34:06", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_syntax_EXC]": { - "recorded-date": "04-04-2024, 08:08:05", + "recorded-date": "08-04-2024, 19:34:06", "recorded-content": { "content_numeric_syntax_EXC": { "exception_message": { @@ -396,19 +396,23 @@ } }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_and]": { - "recorded-date": "04-04-2024, 08:08:06", + "recorded-date": "08-04-2024, 19:34:06", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[boolean]": { - "recorded-date": "04-04-2024, 08:08:06", + "recorded-date": "08-04-2024, 19:34:06", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number]": { - "recorded-date": "04-04-2024, 08:08:06", + "recorded-date": "08-04-2024, 19:34:06", "recorded-content": {} }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[or-anything-but]": { - "recorded-date": "04-04-2024, 08:08:06", + "recorded-date": "08-04-2024, 19:34:07", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[list_within_dict]": { + "recorded-date": "08-04-2024, 19:33:54", "recorded-content": {} } } diff --git a/tests/aws/services/events/test_event_patterns.validation.json b/tests/aws/services/events/test_event_patterns.validation.json index c620718600098..12606f07e3c66 100644 --- a/tests/aws/services/events/test_event_patterns.validation.json +++ b/tests/aws/services/events/test_event_patterns.validation.json @@ -1,228 +1,231 @@ { "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays]": { - "last_validated_date": "2024-04-04T08:07:54+00:00" + "last_validated_date": "2024-04-08T19:33:55+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays_NEG]": { - "last_validated_date": "2024-04-04T08:08:05+00:00" + "last_validated_date": "2024-04-08T19:34:05+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays_empty_EXC]": { - "last_validated_date": "2024-04-04T08:07:57+00:00" + "last_validated_date": "2024-04-08T19:33:58+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays_empty_null_NEG]": { - "last_validated_date": "2024-04-04T08:08:03+00:00" + "last_validated_date": "2024-04-08T19:34:04+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[boolean]": { - "last_validated_date": "2024-04-04T08:08:06+00:00" + "last_validated_date": "2024-04-08T19:34:06+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[boolean_NEG]": { - "last_validated_date": "2024-04-04T08:07:57+00:00" + "last_validated_date": "2024-04-08T19:33:58+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_many_rules]": { - "last_validated_date": "2024-04-04T08:11:59+00:00" + "last_validated_date": "2024-04-08T19:34:00+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_multi_match]": { - "last_validated_date": "2024-04-04T08:07:54+00:00" + "last_validated_date": "2024-04-08T19:33:55+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_multi_match_NEG]": { - "last_validated_date": "2024-04-04T08:07:55+00:00" + "last_validated_date": "2024-04-08T19:33:56+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_or]": { - "last_validated_date": "2024-04-04T08:08:00+00:00" + "last_validated_date": "2024-04-08T19:34:01+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_or_NEG]": { - "last_validated_date": "2024-04-04T08:08:00+00:00" + "last_validated_date": "2024-04-08T19:34:01+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase]": { - "last_validated_date": "2024-04-04T08:07:58+00:00" + "last_validated_date": "2024-04-08T19:33:59+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase_NEG]": { - "last_validated_date": "2024-04-04T08:08:04+00:00" + "last_validated_date": "2024-04-08T19:34:05+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase_list]": { - "last_validated_date": "2024-04-04T08:08:02+00:00" + "last_validated_date": "2024-04-08T19:34:03+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase_list_NEG]": { - "last_validated_date": "2024-04-04T08:07:55+00:00" + "last_validated_date": "2024-04-08T19:33:56+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number]": { - "last_validated_date": "2024-04-04T08:08:06+00:00" + "last_validated_date": "2024-04-08T19:34:06+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number_NEG]": { - "last_validated_date": "2024-04-04T08:07:59+00:00" + "last_validated_date": "2024-04-08T19:34:00+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number_list]": { - "last_validated_date": "2024-04-04T08:07:59+00:00" + "last_validated_date": "2024-04-08T19:34:00+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number_list_NEG]": { - "last_validated_date": "2024-04-04T08:07:56+00:00" + "last_validated_date": "2024-04-08T19:33:57+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string]": { - "last_validated_date": "2024-04-04T08:07:55+00:00" + "last_validated_date": "2024-04-08T19:33:56+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string_NEG]": { - "last_validated_date": "2024-04-04T08:07:57+00:00" + "last_validated_date": "2024-04-08T19:33:58+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string_list]": { - "last_validated_date": "2024-04-04T08:07:58+00:00" + "last_validated_date": "2024-04-08T19:33:59+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string_list_NEG]": { - "last_validated_date": "2024-04-04T08:08:04+00:00" + "last_validated_date": "2024-04-08T19:34:04+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_prefix]": { - "last_validated_date": "2024-04-04T08:08:01+00:00" + "last_validated_date": "2024-04-08T19:34:02+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_prefix_NEG]": { - "last_validated_date": "2024-04-04T08:07:56+00:00" + "last_validated_date": "2024-04-08T19:33:57+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_suffix]": { - "last_validated_date": "2024-04-04T08:08:02+00:00" + "last_validated_date": "2024-04-08T19:34:03+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_suffix_NEG]": { - "last_validated_date": "2024-04-04T08:08:01+00:00" + "last_validated_date": "2024-04-08T19:34:02+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists]": { - "last_validated_date": "2024-04-04T08:08:00+00:00" + "last_validated_date": "2024-04-08T19:34:01+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists_NEG]": { - "last_validated_date": "2024-04-04T08:08:05+00:00" + "last_validated_date": "2024-04-08T19:34:06+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists_false]": { - "last_validated_date": "2024-04-04T08:08:04+00:00" + "last_validated_date": "2024-04-08T19:34:05+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists_false_NEG]": { - "last_validated_date": "2024-04-04T08:08:05+00:00" + "last_validated_date": "2024-04-08T19:34:05+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ignorecase]": { - "last_validated_date": "2024-04-04T08:08:05+00:00" + "last_validated_date": "2024-04-08T19:34:05+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ignorecase_NEG]": { - "last_validated_date": "2024-04-04T08:08:04+00:00" + "last_validated_date": "2024-04-08T19:34:05+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ip_address]": { - "last_validated_date": "2024-04-04T08:07:56+00:00" + "last_validated_date": "2024-04-08T19:33:57+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ip_address_NEG]": { - "last_validated_date": "2024-04-04T08:08:00+00:00" + "last_validated_date": "2024-04-08T19:34:01+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_EXC]": { - "last_validated_date": "2024-04-04T08:08:00+00:00" + "last_validated_date": "2024-04-08T19:34:01+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_and]": { - "last_validated_date": "2024-04-04T08:08:06+00:00" + "last_validated_date": "2024-04-08T19:34:06+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_and_NEG]": { - "last_validated_date": "2024-04-04T08:08:01+00:00" + "last_validated_date": "2024-04-08T19:34:02+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_operatorcasing_EXC]": { - "last_validated_date": "2024-04-04T08:07:55+00:00" + "last_validated_date": "2024-04-08T19:33:56+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_syntax_EXC]": { - "last_validated_date": "2024-04-04T08:08:05+00:00" + "last_validated_date": "2024-04-08T19:34:06+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_prefix]": { - "last_validated_date": "2024-04-04T08:08:04+00:00" + "last_validated_date": "2024-04-08T19:34:04+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_prefix_NEG]": { - "last_validated_date": "2024-04-04T08:08:02+00:00" + "last_validated_date": "2024-04-08T19:34:03+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_prefix_ignorecase]": { - "last_validated_date": "2024-04-04T08:08:01+00:00" + "last_validated_date": "2024-04-08T19:34:02+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix]": { - "last_validated_date": "2024-04-04T08:08:04+00:00" + "last_validated_date": "2024-04-08T19:34:05+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix_NEG]": { - "last_validated_date": "2024-04-04T08:07:54+00:00" + "last_validated_date": "2024-04-08T19:33:54+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix_ignorecase]": { - "last_validated_date": "2024-04-04T08:08:01+00:00" + "last_validated_date": "2024-04-08T19:34:02+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix_ignorecase_NEG]": { - "last_validated_date": "2024-04-04T08:08:03+00:00" + "last_validated_date": "2024-04-08T19:34:04+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_complex_EXC]": { - "last_validated_date": "2024-04-04T08:08:03+00:00" + "last_validated_date": "2024-04-08T19:34:04+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_nonrepeating]": { - "last_validated_date": "2024-04-04T08:07:55+00:00" + "last_validated_date": "2024-04-08T19:33:56+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_nonrepeating_NEG]": { - "last_validated_date": "2024-04-04T08:07:59+00:00" + "last_validated_date": "2024-04-08T19:34:00+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_repeating]": { - "last_validated_date": "2024-04-04T08:07:53+00:00" + "last_validated_date": "2024-04-08T19:33:54+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_repeating_NEG]": { - "last_validated_date": "2024-04-04T08:07:54+00:00" + "last_validated_date": "2024-04-08T19:33:56+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_simplified]": { - "last_validated_date": "2024-04-04T08:07:55+00:00" + "last_validated_date": "2024-04-08T19:33:56+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_event]": { - "last_validated_date": "2024-04-04T08:08:01+00:00" + "last_validated_date": "2024-04-08T19:34:02+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_event_NEG]": { - "last_validated_date": "2024-04-04T08:08:02+00:00" + "last_validated_date": "2024-04-08T19:34:03+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_pattern]": { - "last_validated_date": "2024-04-04T08:07:58+00:00" + "last_validated_date": "2024-04-08T19:33:59+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_pattern_NEG]": { - "last_validated_date": "2024-04-04T08:08:01+00:00" + "last_validated_date": "2024-04-08T19:34:02+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dynamodb]": { - "last_validated_date": "2024-04-04T08:07:58+00:00" + "last_validated_date": "2024-04-08T19:33:59+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[int_nolist_EXC]": { - "last_validated_date": "2024-04-04T08:07:54+00:00" + "last_validated_date": "2024-04-08T19:33:55+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[key_case_sensitive_NEG]": { - "last_validated_date": "2024-04-04T08:08:02+00:00" + "last_validated_date": "2024-04-08T19:34:03+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[list_within_dict]": { + "last_validated_date": "2024-04-08T19:33:54+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[minimal]": { - "last_validated_date": "2024-04-04T08:08:04+00:00" + "last_validated_date": "2024-04-08T19:34:05+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[nested_json_NEG]": { - "last_validated_date": "2024-04-04T08:07:58+00:00" + "last_validated_date": "2024-04-08T19:33:59+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[null_value]": { - "last_validated_date": "2024-04-04T08:07:59+00:00" + "last_validated_date": "2024-04-08T19:34:00+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[null_value_NEG]": { - "last_validated_date": "2024-04-04T08:08:01+00:00" + "last_validated_date": "2024-04-08T19:34:02+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[number_comparison_float]": { - "last_validated_date": "2024-04-04T08:08:04+00:00" + "last_validated_date": "2024-04-08T19:34:05+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[operator_case_sensitive_EXC]": { - "last_validated_date": "2024-04-04T08:07:59+00:00" + "last_validated_date": "2024-04-08T19:34:00+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[operator_multiple_list]": { - "last_validated_date": "2024-04-04T08:07:58+00:00" + "last_validated_date": "2024-04-08T19:33:59+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[or-anything-but]": { - "last_validated_date": "2024-04-04T08:08:06+00:00" + "last_validated_date": "2024-04-08T19:34:07+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[or-exists-parent]": { - "last_validated_date": "2024-04-04T08:07:57+00:00" + "last_validated_date": "2024-04-08T19:33:58+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[or-exists]": { - "last_validated_date": "2024-04-04T08:07:55+00:00" + "last_validated_date": "2024-04-08T19:33:56+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[prefix]": { - "last_validated_date": "2024-04-04T08:08:02+00:00" + "last_validated_date": "2024-04-08T19:34:03+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[sample1]": { - "last_validated_date": "2024-04-04T08:08:02+00:00" + "last_validated_date": "2024-04-08T19:34:03+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[string]": { - "last_validated_date": "2024-04-04T08:08:02+00:00" + "last_validated_date": "2024-04-08T19:34:03+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[string_empty]": { - "last_validated_date": "2024-04-04T08:07:58+00:00" + "last_validated_date": "2024-04-08T19:33:59+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[string_nolist_EXC]": { - "last_validated_date": "2024-04-04T08:07:56+00:00" + "last_validated_date": "2024-04-08T19:33:57+00:00" }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern_with_escape_characters": { "last_validated_date": "2024-04-04T08:08:06+00:00" diff --git a/tests/aws/services/events/test_events_integrations.py b/tests/aws/services/events/test_events_integrations.py index 71da2d81af880..9cdf39bd3bb86 100644 --- a/tests/aws/services/events/test_events_integrations.py +++ b/tests/aws/services/events/test_events_integrations.py @@ -237,6 +237,202 @@ def test_put_events_with_target_lambda(create_lambda_function, cleanups, aws_cli assert actual_event["detail"] == EVENT_DETAIL +@markers.aws.validated +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") +def test_put_events_with_target_lambda_list_entry( + create_lambda_function, cleanups, aws_client, clean_up, snapshot +): + rule_name = f"rule-{short_uid()}" + function_name = f"lambda-func-{short_uid()}" + target_id = f"target-{short_uid()}" + bus_name = f"bus-{short_uid()}" + + # clean up + cleanups.append(lambda: clean_up(bus_name=bus_name, rule_name=rule_name, target_ids=target_id)) + + create_lambda_response = create_lambda_function( + handler_file=TEST_LAMBDA_PYTHON_ECHO, + func_name=function_name, + runtime=Runtime.python3_12, + ) + + func_arn = create_lambda_response["CreateFunctionResponse"]["FunctionArn"] + + event_pattern = {"detail": {"payload": {"automations": {"id": [{"exists": True}]}}}} + + aws_client.events.create_event_bus(Name=bus_name) + put_rule_response = aws_client.events.put_rule( + Name=rule_name, + EventBusName=bus_name, + EventPattern=json.dumps(event_pattern), + ) + aws_client.lambda_.add_permission( + FunctionName=function_name, + StatementId=f"{rule_name}-Event", + Action="lambda:InvokeFunction", + Principal="events.amazonaws.com", + SourceArn=put_rule_response["RuleArn"], + ) + put_target_response = aws_client.events.put_targets( + Rule=rule_name, + EventBusName=bus_name, + Targets=[{"Id": target_id, "Arn": func_arn}], + ) + + assert "FailedEntryCount" in put_target_response + assert "FailedEntries" in put_target_response + assert put_target_response["FailedEntryCount"] == 0 + assert put_target_response["FailedEntries"] == [] + + event_detail = { + "payload": { + "userId": 10, + "businessId": 3, + "channelId": 6, + "card": {"foo": "bar"}, + "targetEntity": True, + "entityAuditTrailEvent": {"foo": "bar"}, + "automations": [ + { + "id": "123", + "actions": [ + { + "id": "321", + "type": "SEND_NOTIFICATION", + "settings": { + "message": "", + "recipientEmails": [], + "subject": "", + "type": "SEND_NOTIFICATION", + }, + } + ], + } + ], + } + } + aws_client.events.put_events( + Entries=[ + { + "EventBusName": bus_name, + "Source": TEST_EVENT_PATTERN["source"][0], + "DetailType": TEST_EVENT_PATTERN["detail-type"][0], + "Detail": json.dumps(event_detail), + } + ] + ) + + # Get lambda's log events + events = retry( + check_expected_lambda_log_events_length, + retries=15, + sleep=1, + function_name=function_name, + expected_length=1, + logs_client=aws_client.logs, + ) + snapshot.match("events", events) + + +@markers.aws.validated +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") +def test_put_events_with_target_lambda_list_entries_partial_match( + create_lambda_function, cleanups, aws_client, clean_up, snapshot +): + rule_name = f"rule-{short_uid()}" + function_name = f"lambda-func-{short_uid()}" + target_id = f"target-{short_uid()}" + bus_name = f"bus-{short_uid()}" + + # clean up + cleanups.append(lambda: clean_up(bus_name=bus_name, rule_name=rule_name, target_ids=target_id)) + + rs = create_lambda_function( + handler_file=TEST_LAMBDA_PYTHON_ECHO, + func_name=function_name, + runtime=Runtime.python3_12, + ) + + func_arn = rs["CreateFunctionResponse"]["FunctionArn"] + + event_pattern = {"detail": {"payload": {"automations": {"id": [{"exists": True}]}}}} + + aws_client.events.create_event_bus(Name=bus_name) + rs = aws_client.events.put_rule( + Name=rule_name, + EventBusName=bus_name, + EventPattern=json.dumps(event_pattern), + ) + aws_client.lambda_.add_permission( + FunctionName=function_name, + StatementId=f"{rule_name}-Event", + Action="lambda:InvokeFunction", + Principal="events.amazonaws.com", + SourceArn=rs["RuleArn"], + ) + rs = aws_client.events.put_targets( + Rule=rule_name, + EventBusName=bus_name, + Targets=[{"Id": target_id, "Arn": func_arn}], + ) + + assert "FailedEntryCount" in rs + assert "FailedEntries" in rs + assert rs["FailedEntryCount"] == 0 + assert rs["FailedEntries"] == [] + + event_detail_partial_match = { + "payload": { + "userId": 10, + "businessId": 3, + "channelId": 6, + "card": {"foo": "bar"}, + "targetEntity": True, + "entityAuditTrailEvent": {"foo": "bar"}, + "automations": [ + {"foo": "bar"}, + { + "id": "123", + "actions": [ + { + "id": "321", + "type": "SEND_NOTIFICATION", + "settings": { + "message": "", + "recipientEmails": [], + "subject": "", + "type": "SEND_NOTIFICATION", + }, + } + ], + }, + {"bar": "foo"}, + ], + } + } + aws_client.events.put_events( + Entries=[ + { + "EventBusName": bus_name, + "Source": TEST_EVENT_PATTERN["source"][0], + "DetailType": TEST_EVENT_PATTERN["detail-type"][0], + "Detail": json.dumps(event_detail_partial_match), + }, + ] + ) + + # Get lambda's log events + events = retry( + check_expected_lambda_log_events_length, + retries=15, + sleep=1, + function_name=function_name, + expected_length=1, + logs_client=aws_client.logs, + ) + snapshot.match("events", events) + + @markers.aws.validated @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_should_ignore_schedules_for_put_event(create_lambda_function, cleanups, aws_client): diff --git a/tests/aws/services/events/test_events_integrations.snapshot.json b/tests/aws/services/events/test_events_integrations.snapshot.json new file mode 100644 index 0000000000000..a52a6f84d34b3 --- /dev/null +++ b/tests/aws/services/events/test_events_integrations.snapshot.json @@ -0,0 +1,104 @@ +{ + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_lambda_list_entry": { + "recorded-date": "08-04-2024, 17:32:58", + "recorded-content": { + "events": [ + { + "version": "0", + "id": "", + "detail-type": "core.update-account-command", + "source": "core.update-account-command", + "account": "111111111111", + "time": "date", + "region": "", + "resources": [], + "detail": { + "payload": { + "userId": 10, + "businessId": 3, + "channelId": 6, + "card": { + "foo": "bar" + }, + "targetEntity": true, + "entityAuditTrailEvent": { + "foo": "bar" + }, + "automations": [ + { + "id": "123", + "actions": [ + { + "id": "321", + "type": "SEND_NOTIFICATION", + "settings": { + "message": "", + "recipientEmails": [], + "subject": "", + "type": "SEND_NOTIFICATION" + } + } + ] + } + ] + } + } + } + ] + } + }, + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_lambda_list_entries_partial_match": { + "recorded-date": "03-04-2024, 20:00:13", + "recorded-content": { + "events": [ + { + "version": "0", + "id": "", + "detail-type": "core.update-account-command", + "source": "core.update-account-command", + "account": "111111111111", + "time": "date", + "region": "", + "resources": [], + "detail": { + "payload": { + "userId": 10, + "businessId": 3, + "channelId": 6, + "card": { + "foo": "bar" + }, + "targetEntity": true, + "entityAuditTrailEvent": { + "foo": "bar" + }, + "automations": [ + { + "foo": "bar" + }, + { + "id": "123", + "actions": [ + { + "id": "321", + "type": "SEND_NOTIFICATION", + "settings": { + "message": "", + "recipientEmails": [], + "subject": "", + "type": "SEND_NOTIFICATION" + } + } + ] + }, + { + "bar": "foo" + } + ] + } + } + } + ] + } + } +} diff --git a/tests/aws/services/events/test_events_integrations.validation.json b/tests/aws/services/events/test_events_integrations.validation.json index 4d99759c5d666..6d701cbe30a64 100644 --- a/tests/aws/services/events/test_events_integrations.validation.json +++ b/tests/aws/services/events/test_events_integrations.validation.json @@ -1,4 +1,10 @@ { + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_lambda_list_entries_partial_match": { + "last_validated_date": "2024-04-08T17:36:24+00:00" + }, + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_lambda_list_entry": { + "last_validated_date": "2024-04-08T17:33:44+00:00" + }, "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_sqs": { "last_validated_date": "2024-03-26T15:49:59+00:00" }, From 975a5f53885d6b964c93adc44711e27384320aef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Apr 2024 07:08:34 +0200 Subject: [PATCH 034/169] Bump python to 3.11.9-slim-bookworm (#10618) --- Dockerfile | 2 +- Dockerfile.s3 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9482fe9bc98e7..dc2d38796cd5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ jdk.localedata --include-locales en,th \ # base: Stage which installs necessary runtime dependencies (OS packages, java,...) -FROM python:3.11.8-slim-bookworm@sha256:90f8795536170fd08236d2ceb74fe7065dbf74f738d8b84bfbf263656654dc9b as base +FROM python:3.11.9-slim-bookworm@sha256:3800945e7ed50341ba8af48f449515c0a4e845277d56008c15bd84d52093e958 as base ARG TARGETARCH # Install runtime OS package dependencies diff --git a/Dockerfile.s3 b/Dockerfile.s3 index e062b46009c7f..77d5f7de22861 100644 --- a/Dockerfile.s3 +++ b/Dockerfile.s3 @@ -1,5 +1,5 @@ # base: Stage which installs necessary runtime dependencies (OS packages, filesystem...) -FROM python:3.11.8-slim-bookworm@sha256:90f8795536170fd08236d2ceb74fe7065dbf74f738d8b84bfbf263656654dc9b as base +FROM python:3.11.9-slim-bookworm@sha256:3800945e7ed50341ba8af48f449515c0a4e845277d56008c15bd84d52093e958 as base ARG TARGETARCH # set workdir From 4635726b714988f601d5db2e8fd06bde240371c0 Mon Sep 17 00:00:00 2001 From: Macwan Nevil Date: Tue, 9 Apr 2024 10:59:07 +0530 Subject: [PATCH 035/169] fixed tags operations in secretsmanager (#10579) --- .../services/secretsmanager/provider.py | 2 +- .../secretsmanager/test_secretsmanager.py | 62 ++++++++ .../test_secretsmanager.snapshot.json | 141 ++++++++++++++++++ .../test_secretsmanager.validation.json | 3 + 4 files changed, 207 insertions(+), 1 deletion(-) diff --git a/localstack/services/secretsmanager/provider.py b/localstack/services/secretsmanager/provider.py index a000f85d1a3a0..7a4b17375e280 100644 --- a/localstack/services/secretsmanager/provider.py +++ b/localstack/services/secretsmanager/provider.py @@ -528,7 +528,7 @@ def fake_secret_to_dict(fn, self): del res_dict["RotationEnabled"] if self.auto_rotate_after_days is None and "RotationRules" in res_dict: del res_dict["RotationRules"] - if not self.tags and "Tags" in res_dict: + if self.tags is None and "Tags" in res_dict: del res_dict["Tags"] for null_field in [key for key, value in res_dict.items() if value is None]: del res_dict[null_field] diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.py b/tests/aws/services/secretsmanager/test_secretsmanager.py index 30765250f45ca..c6bb84fb2065d 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.py +++ b/tests/aws/services/secretsmanager/test_secretsmanager.py @@ -2080,6 +2080,68 @@ def test_create_secret_version_from_empty_secret(self, aws_client, snapshot, cle ) snapshot.match("put-secret-value", response) + @markers.aws.validated + def test_secret_tags(self, aws_client, create_secret, sm_snapshot, cleanups): + secret_name = short_uid() + response = create_secret( + Name=secret_name, + ) + + sm_snapshot.add_transformers_list( + sm_snapshot.transform.secretsmanager_secret_id_arn(response, 0) + ) + sm_snapshot.match("create_secret", response) + + secret_arn = response["ARN"] + + describe_secret = aws_client.secretsmanager.describe_secret(SecretId=secret_arn) + sm_snapshot.match("describe_secret", describe_secret) + + tag_resource_1 = aws_client.secretsmanager.tag_resource( + SecretId=secret_arn, Tags=[{"Key": "tag1", "Value": "value1"}] + ) + sm_snapshot.match("tag_resource_1", tag_resource_1) + + describe_secret_1 = aws_client.secretsmanager.describe_secret(SecretId=secret_arn) + sm_snapshot.match("describe_secret_1", describe_secret_1) + + tag_resource_2 = aws_client.secretsmanager.tag_resource( + SecretId=secret_arn, Tags=[{"Key": "tag2", "Value": "value2"}] + ) + sm_snapshot.match("tag_resource_2", tag_resource_2) + + describe_secret_2 = aws_client.secretsmanager.describe_secret(SecretId=secret_arn) + sm_snapshot.match("describe_secret_2", describe_secret_2) + + untag_resource_1 = aws_client.secretsmanager.untag_resource( + SecretId=secret_arn, TagKeys=["tag1"] + ) + sm_snapshot.match("untag_resource_1", untag_resource_1) + + describe_secret_3 = aws_client.secretsmanager.describe_secret(SecretId=secret_arn) + sm_snapshot.match("describe_secret_3", describe_secret_3) + + untag_resource_2 = aws_client.secretsmanager.untag_resource( + SecretId=secret_arn, TagKeys=["tag2"] + ) + sm_snapshot.match("untag_resource_2", untag_resource_2) + + describe_secret_4 = aws_client.secretsmanager.describe_secret(SecretId=secret_arn) + sm_snapshot.match("describe_secret_4", describe_secret_4) + + aws_client.secretsmanager.tag_resource( + SecretId=secret_arn, + Tags=[{"Key": "tag3", "Value": "value3"}, {"Key": "tag4", "Value": "value4"}], + ) + + describe_secret_5 = aws_client.secretsmanager.describe_secret(SecretId=secret_arn) + sm_snapshot.match("describe_secret_5", describe_secret_5) + + aws_client.secretsmanager.untag_resource(SecretId=secret_arn, TagKeys=["tag3", "tag4"]) + + describe_secret_6 = aws_client.secretsmanager.describe_secret(SecretId=secret_arn) + sm_snapshot.match("describe_secret_6", describe_secret_6) + class TestSecretsManagerMultiAccounts: @markers.aws.validated diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json index 2061e5228ecaa..89776f6cbc396 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json @@ -4092,5 +4092,146 @@ } } } + }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_secret_tags": { + "recorded-date": "05-04-2024, 10:40:13", + "recorded-content": { + "create_secret": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "Name": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe_secret": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "LastChangedDate": "datetime", + "Name": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "tag_resource_1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe_secret_1": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "LastChangedDate": "datetime", + "Name": "", + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "tag_resource_2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe_secret_2": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "LastChangedDate": "datetime", + "Name": "", + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + }, + { + "Key": "tag2", + "Value": "value2" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "untag_resource_1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe_secret_3": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "LastChangedDate": "datetime", + "Name": "", + "Tags": [ + { + "Key": "tag2", + "Value": "value2" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "untag_resource_2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe_secret_4": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "LastChangedDate": "datetime", + "Name": "", + "Tags": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe_secret_5": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "LastChangedDate": "datetime", + "Name": "", + "Tags": [ + { + "Key": "tag3", + "Value": "value3" + }, + { + "Key": "tag4", + "Value": "value4" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe_secret_6": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "CreatedDate": "datetime", + "LastChangedDate": "datetime", + "Name": "", + "Tags": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json index 00c51c648d5d8..7ce2a7bfbd097 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json @@ -104,6 +104,9 @@ "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_secret_restore": { "last_validated_date": "2024-03-20T13:43:42+00:00" }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_secret_tags": { + "last_validated_date": "2024-04-01T13:21:01+00:00" + }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_update_secret_description": { "last_validated_date": "2024-03-15T08:12:49+00:00" }, From ef9e77bb3d02b8dfc76129cc060a5bd33c60fea8 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Tue, 9 Apr 2024 08:37:50 +0200 Subject: [PATCH 036/169] Upgrade pinned Python dependencies (#10619) --- .pre-commit-config.yaml | 2 +- requirements-base-runtime.txt | 4 ++-- requirements-dev.txt | 16 ++++++++-------- requirements-runtime.txt | 10 +++++----- requirements-test.txt | 16 ++++++++-------- requirements-typehint.txt | 26 +++++++++++++------------- 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3fad8b28a1d85..638e07407f84e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index a848b1d303b4a..0c655b14c3f0a 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -52,7 +52,7 @@ dnspython==2.6.1 # via localstack-core (pyproject.toml) docker==6.1.3 # via localstack-core (pyproject.toml) -flask==3.0.2 +flask==3.0.3 # via # localstack-core (pyproject.toml) # quart @@ -168,7 +168,7 @@ stevedore==5.2.0 # plux tailer==0.4.1 # via localstack-core (pyproject.toml) -typing-extensions==4.10.0 +typing-extensions==4.11.0 # via # localstack-twisted # readerwriterlock diff --git a/requirements-dev.txt b/requirements-dev.txt index c2d787a04ac1f..1976cfb3dce11 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -37,9 +37,9 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.135.0 +aws-cdk-lib==2.136.0 # via localstack-core -aws-sam-translator==1.86.0 +aws-sam-translator==1.87.0 # via # cfn-lint # localstack-core @@ -92,7 +92,7 @@ cffi==1.16.0 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==0.86.1 +cfn-lint==0.86.2 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -127,7 +127,7 @@ cython==3.0.10 # via localstack-core (pyproject.toml) decorator==5.1.1 # via jsonpath-rw -deepdiff==6.7.1 +deepdiff==7.0.1 # via # localstack-core # localstack-snapshot @@ -156,7 +156,7 @@ docutils==0.16 # via awscli filelock==3.13.3 # via virtualenv -flask==3.0.2 +flask==3.0.3 # via # localstack-core # quart @@ -218,7 +218,7 @@ joserfc==0.9.0 # via moto-ext jschema-to-python==1.2.3 # via cfn-lint -jsii==1.96.0 +jsii==1.97.0 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-kubectl-v20 @@ -279,7 +279,7 @@ mpmath==1.3.0 # via sympy multipart==0.2.4 # via moto-ext -networkx==3.2.1 +networkx==3.3 # via # cfn-lint # localstack-core (pyproject.toml) @@ -490,7 +490,7 @@ typeguard==2.13.3 # aws-cdk-lib # constructs # jsii -typing-extensions==4.10.0 +typing-extensions==4.11.0 # via # aws-sam-translator # jsii diff --git a/requirements-runtime.txt b/requirements-runtime.txt index d8efd434ec59b..8630e86553e8c 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -27,7 +27,7 @@ attrs==23.2.0 # localstack-twisted # referencing # sarif-om -aws-sam-translator==1.86.0 +aws-sam-translator==1.87.0 # via # cfn-lint # localstack-core (pyproject.toml) @@ -73,7 +73,7 @@ certifi==2024.2.2 # requests cffi==1.16.0 # via cryptography -cfn-lint==0.86.1 +cfn-lint==0.86.2 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -117,7 +117,7 @@ docker==6.1.3 # moto-ext docutils==0.16 # via awscli -flask==3.0.2 +flask==3.0.3 # via # localstack-core # quart @@ -216,7 +216,7 @@ mpmath==1.3.0 # via sympy multipart==0.2.4 # via moto-ext -networkx==3.2.1 +networkx==3.3 # via cfn-lint openapi-schema-validator==0.6.2 # via openapi-spec-validator @@ -363,7 +363,7 @@ tailer==0.4.1 # via # localstack-core # localstack-core (pyproject.toml) -typing-extensions==4.10.0 +typing-extensions==4.11.0 # via # aws-sam-translator # localstack-twisted diff --git a/requirements-test.txt b/requirements-test.txt index 3f605da692398..2048464648a87 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -37,9 +37,9 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.135.0 +aws-cdk-lib==2.136.0 # via localstack-core (pyproject.toml) -aws-sam-translator==1.86.0 +aws-sam-translator==1.87.0 # via # cfn-lint # localstack-core @@ -90,7 +90,7 @@ certifi==2024.2.2 # requests cffi==1.16.0 # via cryptography -cfn-lint==0.86.1 +cfn-lint==0.86.2 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -119,7 +119,7 @@ cryptography==42.0.5 # pyopenssl decorator==5.1.1 # via jsonpath-rw -deepdiff==6.7.1 +deepdiff==7.0.1 # via # localstack-core (pyproject.toml) # localstack-snapshot @@ -142,7 +142,7 @@ docker==6.1.3 # moto-ext docutils==0.16 # via awscli -flask==3.0.2 +flask==3.0.3 # via # localstack-core # quart @@ -202,7 +202,7 @@ joserfc==0.9.0 # via moto-ext jschema-to-python==1.2.3 # via cfn-lint -jsii==1.96.0 +jsii==1.97.0 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-kubectl-v20 @@ -263,7 +263,7 @@ mpmath==1.3.0 # via sympy multipart==0.2.4 # via moto-ext -networkx==3.2.1 +networkx==3.3 # via cfn-lint openapi-schema-validator==0.6.2 # via openapi-spec-validator @@ -453,7 +453,7 @@ typeguard==2.13.3 # aws-cdk-lib # constructs # jsii -typing-extensions==4.10.0 +typing-extensions==4.11.0 # via # aws-sam-translator # jsii diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 9469165dc781d..898a39e022056 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -37,9 +37,9 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.135.0 +aws-cdk-lib==2.136.0 # via localstack-core -aws-sam-translator==1.86.0 +aws-sam-translator==1.87.0 # via # cfn-lint # localstack-core @@ -96,7 +96,7 @@ cffi==1.16.0 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==0.86.1 +cfn-lint==0.86.2 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -131,7 +131,7 @@ cython==3.0.10 # via localstack-core decorator==5.1.1 # via jsonpath-rw -deepdiff==6.7.1 +deepdiff==7.0.1 # via # localstack-core # localstack-snapshot @@ -160,7 +160,7 @@ docutils==0.16 # via awscli filelock==3.13.3 # via virtualenv -flask==3.0.2 +flask==3.0.3 # via # localstack-core # quart @@ -222,7 +222,7 @@ joserfc==0.9.0 # via moto-ext jschema-to-python==1.2.3 # via cfn-lint -jsii==1.96.0 +jsii==1.97.0 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-kubectl-v20 @@ -313,7 +313,7 @@ mypy-boto3-ce==1.34.71 # via boto3-stubs mypy-boto3-cloudcontrol==1.34.0 # via boto3-stubs -mypy-boto3-cloudformation==1.34.66 +mypy-boto3-cloudformation==1.34.77 # via boto3-stubs mypy-boto3-cloudfront==1.34.0 # via boto3-stubs @@ -329,13 +329,13 @@ mypy-boto3-cognito-idp==1.34.59 # via boto3-stubs mypy-boto3-dms==1.34.0 # via boto3-stubs -mypy-boto3-docdb==1.34.13 +mypy-boto3-docdb==1.34.77 # via boto3-stubs mypy-boto3-dynamodb==1.34.67 # via boto3-stubs mypy-boto3-dynamodbstreams==1.34.0 # via boto3-stubs -mypy-boto3-ec2==1.34.73 +mypy-boto3-ec2==1.34.78 # via boto3-stubs mypy-boto3-ecr==1.34.0 # via boto3-stubs @@ -391,7 +391,7 @@ mypy-boto3-kms==1.34.65 # via boto3-stubs mypy-boto3-lakeformation==1.34.7 # via boto3-stubs -mypy-boto3-lambda==1.34.58 +mypy-boto3-lambda==1.34.77 # via boto3-stubs mypy-boto3-logs==1.34.66 # via boto3-stubs @@ -427,7 +427,7 @@ mypy-boto3-redshift==1.34.57 # via boto3-stubs mypy-boto3-redshift-data==1.34.0 # via boto3-stubs -mypy-boto3-resource-groups==1.34.0 +mypy-boto3-resource-groups==1.34.79 # via boto3-stubs mypy-boto3-resourcegroupstaggingapi==1.34.0 # via boto3-stubs @@ -475,7 +475,7 @@ mypy-boto3-wafv2==1.34.58 # via boto3-stubs mypy-boto3-xray==1.34.0 # via boto3-stubs -networkx==3.2.1 +networkx==3.3 # via # cfn-lint # localstack-core @@ -690,7 +690,7 @@ types-awscrt==0.20.5 # via botocore-stubs types-s3transfer==0.10.0 # via boto3-stubs -typing-extensions==4.10.0 +typing-extensions==4.11.0 # via # aws-sam-translator # boto3-stubs From 09c23deb7fef9b34c1a6fc9f3c3803d5cc1188eb Mon Sep 17 00:00:00 2001 From: Viren Nadkarni Date: Tue, 9 Apr 2024 14:22:35 +0530 Subject: [PATCH 037/169] Remove hardcoded credentials and region for unit tests (#10253) --- tests/unit/conftest.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index c1e6f402b972b..3655dfd940f0b 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,11 +1,13 @@ import pytest +from localstack import constants + @pytest.fixture(autouse=True) def set_boto_test_credentials_and_region(monkeypatch): """ Automatically sets the default credentials and region for all unit tests. """ - monkeypatch.setenv("AWS_ACCESS_KEY_ID", "test") - monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "test") - monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1") + monkeypatch.setenv("AWS_ACCESS_KEY_ID", constants.TEST_AWS_ACCESS_KEY_ID) + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", constants.TEST_AWS_SECRET_ACCESS_KEY) + monkeypatch.setenv("AWS_DEFAULT_REGION", constants.TEST_AWS_REGION_NAME) From 307b1901d6d9bab4219f4cb1e6bfec69051c2e38 Mon Sep 17 00:00:00 2001 From: Macwan Nevil Date: Tue, 9 Apr 2024 16:16:55 +0530 Subject: [PATCH 038/169] fix deprecated secret versions preservation in secretsmanager (#10572) --- .../services/secretsmanager/provider.py | 62 +++-- .../secretsmanager/test_secretsmanager.py | 80 +++++- .../test_secretsmanager.snapshot.json | 241 ++++++++++++++++++ .../test_secretsmanager.validation.json | 6 + 4 files changed, 372 insertions(+), 17 deletions(-) diff --git a/localstack/services/secretsmanager/provider.py b/localstack/services/secretsmanager/provider.py index 7a4b17375e280..8d6fe427bba43 100644 --- a/localstack/services/secretsmanager/provider.py +++ b/localstack/services/secretsmanager/provider.py @@ -84,6 +84,9 @@ AWSPREVIOUS: Final[str] = "AWSPREVIOUS" AWSPENDING: Final[str] = "AWSPENDING" AWSCURRENT: Final[str] = "AWSCURRENT" +# The maximum number of outdated versions that can be stored in the secret. +# see: https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_PutSecretValue.html +MAX_OUTDATED_SECRET_VERSIONS: Final[int] = 100 # # Error Messages. AWS_INVALID_REQUEST_MESSAGE_CREATE_WITH_SCHEDULED_DELETION: Final[str] = ( @@ -262,9 +265,10 @@ def list_secret_version_ids( self, context: RequestContext, request: ListSecretVersionIdsRequest ) -> ListSecretVersionIdsResponse: secret_id = request["SecretId"] + include_deprecated = request.get("IncludeDeprecated", False) self._raise_if_invalid_secret_id(secret_id) backend = SecretsmanagerProvider.get_moto_backend_for_resource(secret_id, context) - secrets = backend.list_secret_version_ids(secret_id) + secrets = backend.list_secret_version_ids(secret_id, include_deprecated=include_deprecated) return ListSecretVersionIdsResponse(**json.loads(secrets)) @handler("PutResourcePolicy", expand=False) @@ -480,7 +484,9 @@ def moto_smb_create_secret(fn, self, name, *args, **kwargs): @patch(SecretsManagerBackend.list_secret_version_ids) -def moto_smb_list_secret_version_ids(_, self, secret_id, *args, **kwargs): +def moto_smb_list_secret_version_ids( + _, self, secret_id: str, include_deprecated: bool, *args, **kwargs +): if secret_id not in self.secrets: raise SecretNotFoundException() @@ -496,18 +502,24 @@ def moto_smb_list_secret_version_ids(_, self, secret_id, *args, **kwargs): versions: list[SecretVersionsListEntry] = list() for version_id, version in secret.versions.items(): version_stages = version["version_stages"] - entry = SecretVersionsListEntry( - CreatedDate=version["createdate"], - VersionId=version_id, - VersionStages=version_stages, - ) + # Patch: include deprecated versions if include_deprecated is True. + # version_stages is empty if the version is deprecated. + # see: https://docs.aws.amazon.com/secretsmanager/latest/userguide/getting-started.html#term_version + if len(version_stages) > 0 or include_deprecated: + entry = SecretVersionsListEntry( + CreatedDate=version["createdate"], + VersionId=version_id, + ) - # Patch: bind LastAccessedDate if one exists for this version. - last_accessed_date = version.get("last_accessed_date") - if last_accessed_date: - entry["LastAccessedDate"] = last_accessed_date + if version_stages: + entry["VersionStages"] = version_stages - versions.append(entry) + # Patch: bind LastAccessedDate if one exists for this version. + last_accessed_date = version.get("last_accessed_date") + if last_accessed_date: + entry["LastAccessedDate"] = last_accessed_date + + versions.append(entry) # Patch: sort versions by date. versions.sort(key=lambda v: v["CreatedDate"], reverse=True) @@ -634,12 +646,20 @@ def backend_update_secret_version_stage( def fake_secret_reset_default_version(fn, self, secret_version, version_id): fn(self, secret_version, version_id) - # Remove versions with no version stages. - versions_no_stages = [ + # Remove versions with no version stages, if max limit of outdated versions is exceeded. + versions_no_stages: list[str] = [ version_id for version_id, version in self.versions.items() if not version["version_stages"] ] - for version_no_stages in versions_no_stages: - del self.versions[version_no_stages] + versions_to_delete: list[str] = [] + + # Patch: remove outdated versions if the max deprecated versions limit is exceeded. + if len(versions_no_stages) >= MAX_OUTDATED_SECRET_VERSIONS: + versions_to_delete = versions_no_stages[ + : len(versions_no_stages) - MAX_OUTDATED_SECRET_VERSIONS + ] + + for version_to_delete in versions_to_delete: + del self.versions[version_to_delete] @patch(FakeSecret.remove_version_stages_from_old_versions) @@ -802,6 +822,16 @@ def moto_secret_not_found_exception_init(fn, self): self.code = 400 +@patch(FakeSecret._form_version_ids_to_stages, pass_target=False) +def _form_version_ids_to_stages_modal(self): + version_id_to_stages: dict[str, list] = {} + for key, value in self.versions.items(): + # Patch: include version_stages in the response only if it is not empty. + if len(value["version_stages"]) > 0: + version_id_to_stages[key] = value["version_stages"] + return version_id_to_stages + + # patching resource policy in moto def get_resource_policy_model(self, secret_id): if self._is_valid_identifier(secret_id): diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.py b/tests/aws/services/secretsmanager/test_secretsmanager.py index c6bb84fb2065d..e2ce49523003b 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.py +++ b/tests/aws/services/secretsmanager/test_secretsmanager.py @@ -709,7 +709,6 @@ def test_update_secret_description(self, sm_snapshot, secret_name, aws_client): sm_snapshot.match("describe_secret_res_0", describe_secret_res_0) description_v1 = "MyDescription" - # update_secret_res_0 = aws_client.secretsmanager.update_secret( SecretId=secret_name, Description=description_v1 ) @@ -1037,6 +1036,85 @@ def test_update_secret_version_stages_current_pending_cycle_custom_stages_1( ) sm_snapshot.match("delete_secret_res_0", delete_secret_res_0) + @markers.snapshot.skip_snapshot_verify(paths=["$..Versions..KmsKeyIds"]) + @markers.aws.validated + def test_deprecated_secret_version_stage( + self, secret_name, create_secret, aws_client, sm_snapshot + ): + response = create_secret( + Name=secret_name, + SecretString="original", + Description="My secret", + ) + sm_snapshot.add_transformers_list( + sm_snapshot.transform.secretsmanager_secret_id_arn(response, 0) + ) + sm_snapshot.match("create_secret", response) + self._wait_created_is_listed(aws_client.secretsmanager, secret_name) + + response = aws_client.secretsmanager.list_secret_version_ids(SecretId=secret_name) + sm_snapshot.match("list_secret_version_ids", response) + + response = aws_client.secretsmanager.put_secret_value( + SecretId=secret_name, SecretString="update1" + ) + sm_snapshot.match("put_secret_value_1", response) + + response = aws_client.secretsmanager.list_secret_version_ids(SecretId=secret_name) + sm_snapshot.match("list_secret_version_ids_1", response) + + response = aws_client.secretsmanager.put_secret_value( + SecretId=secret_name, SecretString="update2" + ) + sm_snapshot.match("put_secret_value_2", response) + + response = aws_client.secretsmanager.list_secret_version_ids(SecretId=secret_name) + sm_snapshot.match("list_secret_version_ids_2", response) + + response = aws_client.secretsmanager.list_secret_version_ids( + SecretId=secret_name, IncludeDeprecated=True + ) + sm_snapshot.match("list_secret_version_ids_3", response) + + response = aws_client.secretsmanager.put_secret_value( + SecretId=secret_name, SecretString="update3" + ) + sm_snapshot.match("put_secret_value_3", response) + + response = aws_client.secretsmanager.list_secret_version_ids(SecretId=secret_name) + sm_snapshot.match("list_secret_version_ids_4", response) + + response = aws_client.secretsmanager.list_secret_version_ids( + SecretId=secret_name, IncludeDeprecated=True + ) + sm_snapshot.match("list_secret_version_ids_5", response) + + @markers.aws.only_localstack + def test_deprecated_secret_version(self, secret_name, create_secret, aws_client): + """ + This test ensures the version cleanup behavior in a simulated AWS environment. + Secrets Manager typically retains a maximum of 100 versions and does not + immediately delete versions created within the last 24 hours. + However, this test operates under the assumption that version timestamps are not evaluated, + and the cleanup process solely depends on reaching a version count threshold. + """ + create_secret(Name=secret_name, SecretString="original", Description="My secret") + self._wait_created_is_listed(aws_client.secretsmanager, secret_name) + + for i in range(130): + aws_client.secretsmanager.put_secret_value( + SecretId=secret_name, SecretString=f"update{i}" + ) + response = aws_client.secretsmanager.list_secret_version_ids( + SecretId=secret_name, IncludeDeprecated=True + ) + # In Secrets Manager, versions of secrets without labels are considered deprecated. + # There will be two labeled versions: + # - The current version, labeled AWSCURRENT + # - The previous version, labeled AWSPREVIOUS + # see: https://docs.aws.amazon.com/secretsmanager/latest/userguide/getting-started.html#term_version + assert len(response["Versions"]) == 102 + @markers.snapshot.skip_snapshot_verify(paths=["$..KmsKeyId", "$..KmsKeyIds"]) @markers.aws.validated def test_update_secret_version_stages_current_pending_cycle_custom_stages_2( diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json index 89776f6cbc396..0f83c9687015c 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json @@ -4233,5 +4233,246 @@ } } } + }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_deprecated_secret_version_stage": { + "recorded-date": "29-03-2024, 11:26:17", + "recorded-content": { + "create_secret": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "Name": "", + "VersionId": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_secret_version_ids": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "Name": "", + "Versions": [ + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSCURRENT" + ] + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "put_secret_value_1": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "Name": "", + "VersionId": "", + "VersionStages": [ + "AWSCURRENT" + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_secret_version_ids_1": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "Name": "", + "Versions": [ + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSPREVIOUS" + ] + }, + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSCURRENT" + ] + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "put_secret_value_2": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "Name": "", + "VersionId": "", + "VersionStages": [ + "AWSCURRENT" + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_secret_version_ids_2": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "Name": "", + "Versions": [ + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSPREVIOUS" + ] + }, + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSCURRENT" + ] + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_secret_version_ids_3": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "Name": "", + "Versions": [ + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "" + }, + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSPREVIOUS" + ] + }, + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSCURRENT" + ] + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "put_secret_value_3": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "Name": "", + "VersionId": "", + "VersionStages": [ + "AWSCURRENT" + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_secret_version_ids_4": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "Name": "", + "Versions": [ + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSPREVIOUS" + ] + }, + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSCURRENT" + ] + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_secret_version_ids_5": { + "ARN": "arn:aws:secretsmanager::111111111111:secret:", + "Name": "", + "Versions": [ + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "" + }, + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "" + }, + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSPREVIOUS" + ] + }, + { + "CreatedDate": "datetime", + "KmsKeyIds": [ + "DefaultEncryptionKey" + ], + "VersionId": "", + "VersionStages": [ + "AWSCURRENT" + ] + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json index 7ce2a7bfbd097..9c1df55652607 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json @@ -32,6 +32,12 @@ "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_delete_non_existent_secret_returns_as_if_secret_exists": { "last_validated_date": "2024-03-15T08:13:15+00:00" }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_deprecated_secret_version": { + "last_validated_date": "2024-03-29T09:36:11+00:00" + }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_deprecated_secret_version_stage": { + "last_validated_date": "2024-03-29T11:26:17+00:00" + }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_exp_raised_on_creation_of_secret_scheduled_for_deletion": { "last_validated_date": "2024-03-15T08:13:16+00:00" }, From 3da3d5cd004a2b0f32d58a6a9ff5f86b428c77c4 Mon Sep 17 00:00:00 2001 From: Macwan Nevil Date: Tue, 9 Apr 2024 19:17:27 +0530 Subject: [PATCH 039/169] added aws validate test case for ListSecrets filtering (#10608) --- .../secretsmanager/test_secretsmanager.py | 117 ++++++++++++++++++ .../test_secretsmanager.validation.json | 3 + 2 files changed, 120 insertions(+) diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.py b/tests/aws/services/secretsmanager/test_secretsmanager.py index e2ce49523003b..b06ca1d505d46 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.py +++ b/tests/aws/services/secretsmanager/test_secretsmanager.py @@ -261,6 +261,123 @@ def test_call_lists_secrets_multiple_times_snapshots( ) sm_snapshot.match("delete_secret_res_1", delete_secret_res_1) + @markers.aws.validated + def test_list_secrets_filtering(self, aws_client, create_secret): + unique_id = short_uid() + secret_name_1 = f"testing1/one-{unique_id}" + secret_name_2 = f"/testing2/two-{unique_id}" + secret_name_3 = f"testing3/three-{unique_id}" + secret_name_4 = f"/testing4/four-{unique_id}" + + create_secret(Name=secret_name_1, SecretString="secret", Description="a secret") + create_secret(Name=secret_name_2, SecretString="secret", Description="an secret") + create_secret(Name=secret_name_3, SecretString="secret", Description="asecret") + create_secret(Name=secret_name_4, SecretString="secret", Description="thesecret") + + self._wait_created_is_listed(aws_client.secretsmanager, secret_id=secret_name_1) + self._wait_created_is_listed(aws_client.secretsmanager, secret_id=secret_name_2) + self._wait_created_is_listed(aws_client.secretsmanager, secret_id=secret_name_3) + self._wait_created_is_listed(aws_client.secretsmanager, secret_id=secret_name_4) + + def assert_secret_names(res, include_secrets, exclude_secrets): + for secret in res["SecretList"]: + assert secret["Name"] in include_secrets + assert secret["Name"] not in exclude_secrets + + response = aws_client.secretsmanager.list_secrets( + Filters=[{"Key": "name", "Values": ["/"]}] + ) + assert_secret_names( + response, [secret_name_2, secret_name_4], [secret_name_1, secret_name_3] + ) + + response = aws_client.secretsmanager.list_secrets( + Filters=[{"Key": "name", "Values": ["!/"]}] + ) + assert_secret_names( + response, [secret_name_1, secret_name_3], [secret_name_2, secret_name_4] + ) + + response = aws_client.secretsmanager.list_secrets( + Filters=[{"Key": "name", "Values": ["testing1 one"]}] + ) + assert_secret_names( + response, [], [secret_name_1, secret_name_2, secret_name_3, secret_name_4] + ) + + response = aws_client.secretsmanager.list_secrets( + Filters=[{"Key": "description", "Values": ["a"]}] + ) + assert_secret_names( + response, [secret_name_1, secret_name_2, secret_name_3], [secret_name_4] + ) + + response = aws_client.secretsmanager.list_secrets( + Filters=[{"Key": "description", "Values": ["!a"]}] + ) + assert_secret_names( + response, [secret_name_4], [secret_name_1, secret_name_2, secret_name_3] + ) + + response = aws_client.secretsmanager.list_secrets( + Filters=[{"Key": "description", "Values": ["a secret"]}] + ) + assert_secret_names( + response, [secret_name_1, secret_name_2], [secret_name_3, secret_name_4] + ) + + response = aws_client.secretsmanager.list_secrets( + Filters=[ + {"Key": "description", "Values": ["a"]}, + {"Key": "name", "Values": ["secret"]}, + ] + ) + assert_secret_names( + response, [secret_name_1, secret_name_2, secret_name_3, secret_name_4], [] + ) + + response = aws_client.secretsmanager.list_secrets( + Filters=[ + {"Key": "description", "Values": ["a"]}, + {"Key": "name", "Values": ["an"]}, + ] + ) + assert_secret_names( + response, [secret_name_1, secret_name_2, secret_name_3, secret_name_4], [] + ) + + response = aws_client.secretsmanager.list_secrets( + Filters=[ + {"Key": "description", "Values": ["a secret"]}, + ] + ) + assert_secret_names( + response, [secret_name_1, secret_name_2], [secret_name_3, secret_name_4] + ) + + response = aws_client.secretsmanager.list_secrets( + Filters=[ + {"Key": "description", "Values": ["!a"]}, + ] + ) + assert_secret_names( + response, [secret_name_4], [secret_name_1, secret_name_2, secret_name_3] + ) + + response = aws_client.secretsmanager.list_secrets( + Filters=[{"Key": "description", "Values": ["!c"]}] + ) + assert_secret_names( + response, [secret_name_1, secret_name_2, secret_name_3, secret_name_4], [] + ) + + response = aws_client.secretsmanager.list_secrets( + Filters=[{"Key": "name", "Values": ["testing1 one"]}] + ) + assert_secret_names( + response, [], [secret_name_1, secret_name_2, secret_name_3, secret_name_4] + ) + @markers.aws.validated def test_create_multi_secrets(self, cleanups, aws_client): secret_names = [short_uid(), short_uid(), short_uid()] diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json index 9c1df55652607..13592210a1b6b 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json @@ -62,6 +62,9 @@ "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_last_updated_date": { "last_validated_date": "2024-03-15T08:12:47+00:00" }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_list_secrets_filtering": { + "last_validated_date": "2024-04-09T12:45:26+00:00" + }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_no_client_request_token[CreateSecret]": { "last_validated_date": "2024-03-15T08:14:56+00:00" }, From 349eab66f3c7909c74b746e2330ec825361370bf Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Tue, 9 Apr 2024 18:25:57 +0200 Subject: [PATCH 040/169] raise correct exceptions for simultaneous lambda function updates (#10614) --- .../invocation/docker_runtime_executor.py | 1 + .../lambda_/invocation/lambda_service.py | 19 +-- .../lambda_/invocation/version_manager.py | 11 +- localstack/testing/aws/lambda_utils.py | 4 + tests/aws/services/lambda_/test_lambda_api.py | 112 +++++++++++++++- .../lambda_/test_lambda_api.snapshot.json | 126 ++++++++++++++++++ .../lambda_/test_lambda_api.validation.json | 6 + 7 files changed, 259 insertions(+), 20 deletions(-) diff --git a/localstack/services/lambda_/invocation/docker_runtime_executor.py b/localstack/services/lambda_/invocation/docker_runtime_executor.py index c4501ca446d7f..a0d8dc41b397d 100644 --- a/localstack/services/lambda_/invocation/docker_runtime_executor.py +++ b/localstack/services/lambda_/invocation/docker_runtime_executor.py @@ -462,6 +462,7 @@ def prepare_version(cls, function_version: FunctionVersion) -> None: # Pull image for a given platform upon function creation such that invocations do not time out. if (image_name, platform) not in PULLED_IMAGES: try: + # FIXME multiple concurrent pulls could take place, which will slow them all down CONTAINER_CLIENT.pull_image(image_name, platform) PULLED_IMAGES.add((image_name, platform)) except NoSuchImage as e: diff --git a/localstack/services/lambda_/invocation/lambda_service.py b/localstack/services/lambda_/invocation/lambda_service.py index ea4ddb99682e5..d773508abf83b 100644 --- a/localstack/services/lambda_/invocation/lambda_service.py +++ b/localstack/services/lambda_/invocation/lambda_service.py @@ -149,6 +149,12 @@ def get_lambda_event_manager(self, function_arn: str) -> LambdaEventManager: return event_manager + def _start_lambda_version(self, version_manager: LambdaVersionManager) -> None: + new_state = version_manager.start() + self.update_version_state( + function_version=version_manager.function_version, new_state=new_state + ) + def create_function_version(self, function_version: FunctionVersion) -> Future[None]: """ Creates a new function version (manager), and puts it in the startup dict @@ -159,23 +165,21 @@ def create_function_version(self, function_version: FunctionVersion) -> Future[N qualified_arn = function_version.id.qualified_arn() version_manager = self.lambda_starting_versions.get(qualified_arn) if version_manager: - raise Exception( - "Version '%s' already starting up and in state %s", - qualified_arn, - version_manager.state, + raise ResourceConflictException( + f"The operation cannot be performed at this time. An update is in progress for resource: {function_version.id.unqualified_arn()}", + Type="User", ) state = lambda_stores[function_version.id.account][function_version.id.region] fn = state.functions.get(function_version.id.function_name) version_manager = LambdaVersionManager( function_arn=qualified_arn, function_version=function_version, - lambda_service=self, function=fn, counting_service=self.counting_service, assignment_service=self.assignment_service, ) self.lambda_starting_versions[qualified_arn] = version_manager - return self.task_executor.submit(version_manager.start) + return self.task_executor.submit(self._start_lambda_version, version_manager) def publish_version(self, function_version: FunctionVersion): """ @@ -201,13 +205,12 @@ def publish_version(self, function_version: FunctionVersion): version_manager = LambdaVersionManager( function_arn=qualified_arn, function_version=function_version, - lambda_service=self, function=fn, counting_service=self.counting_service, assignment_service=self.assignment_service, ) self.lambda_starting_versions[qualified_arn] = version_manager - version_manager.start() + self._start_lambda_version(version_manager) # Commands def invoke( diff --git a/localstack/services/lambda_/invocation/version_manager.py b/localstack/services/lambda_/invocation/version_manager.py index 23553984fbe91..2ea29001420bf 100644 --- a/localstack/services/lambda_/invocation/version_manager.py +++ b/localstack/services/lambda_/invocation/version_manager.py @@ -63,7 +63,6 @@ def __init__( function_arn: str, function_version: FunctionVersion, function: Function, - lambda_service: "LambdaService", counting_service: CountingService, assignment_service: AssignmentService, ): @@ -71,7 +70,6 @@ def __init__( self.function_arn = function_arn self.function_version = function_version self.function = function - self.lambda_service = lambda_service self.counting_service = counting_service self.assignment_service = assignment_service self.log_handler = LogHandler(function_version.config.role, function_version.id.region) @@ -85,8 +83,7 @@ def __init__( self.provisioned_state_lock = threading.RLock() self.state = None - def start(self) -> None: - new_state = None + def start(self) -> VersionState: try: self.log_handler.start_subscriber() time_before = time.perf_counter() @@ -115,11 +112,7 @@ def start(self) -> None: e, exc_info=True, ) - finally: - if new_state: - self.lambda_service.update_version_state( - function_version=self.function_version, new_state=new_state - ) + return new_state def stop(self) -> None: LOG.debug("Stopping lambda version '%s'", self.function_arn) diff --git a/localstack/testing/aws/lambda_utils.py b/localstack/testing/aws/lambda_utils.py index 5ea7851bf58bb..2108bbc46baab 100644 --- a/localstack/testing/aws/lambda_utils.py +++ b/localstack/testing/aws/lambda_utils.py @@ -333,3 +333,7 @@ def get_events(): return events return retry(get_events, retries=retries, sleep_before=5, sleep=5) + + +def is_docker_runtime_executor(): + return config.LAMBDA_RUNTIME_EXECUTOR in ["docker", ""] diff --git a/tests/aws/services/lambda_/test_lambda_api.py b/tests/aws/services/lambda_/test_lambda_api.py index e691eda1e3863..ed2b894c0ff35 100644 --- a/tests/aws/services/lambda_/test_lambda_api.py +++ b/tests/aws/services/lambda_/test_lambda_api.py @@ -14,6 +14,7 @@ import json import logging import re +import threading from hashlib import sha256 from io import BytesIO from typing import Callable @@ -34,6 +35,7 @@ from localstack.testing.aws.lambda_utils import ( _await_dynamodb_table_active, _await_event_source_mapping_enabled, + is_docker_runtime_executor, ) from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers @@ -51,6 +53,7 @@ TEST_LAMBDA_PYTHON_ECHO, TEST_LAMBDA_PYTHON_ECHO_ZIP, TEST_LAMBDA_PYTHON_VERSION, + TEST_LAMBDA_VERSION, check_concurrency_quota, ) @@ -823,6 +826,111 @@ def test_invalid_invoke(self, aws_client, snapshot): ) snapshot.match("invoke_function_name_pattern_exc", e.value.response) + @pytest.mark.skipif( + not is_docker_runtime_executor(), + reason="Test will fail against other executors as they are not patched to take longer for the update", + ) + @markers.snapshot.skip_snapshot_verify(paths=["$..LoggingConfig"]) + @markers.aws.validated + def test_lambda_concurrent_code_updates( + self, aws_client, create_lambda_function_aws, lambda_su_role, snapshot, monkeypatch + ): + # patch a function necessary for the lambda update to wait until we release it + # to be able to reliably capture the in-progress update state in LocalStack + from localstack.services.lambda_.invocation import docker_runtime_executor + from localstack.services.lambda_.invocation.docker_runtime_executor import ( + get_runtime_client_path, + ) + + update_finish_event = threading.Event() + update_finish_event.set() + + def _runtime_client_path(*args, **kwargs): + update_finish_event.wait() + return get_runtime_client_path(*args, **kwargs) + + monkeypatch.setattr( + docker_runtime_executor, "get_runtime_client_path", _runtime_client_path + ) + + function_name = f"test-lambda-{short_uid()}" + version_handler = load_file(TEST_LAMBDA_VERSION) + zip_file = create_lambda_archive(version_handler % "version0", get_content=True) + create_response = create_lambda_function_aws( + FunctionName=function_name, + Runtime=Runtime.python3_12, + Role=lambda_su_role, + Handler="handler.handler", + Code={"ZipFile": zip_file}, + ) + snapshot.match("create-function-response", create_response) + + # clear flag so the update operation takes as long as we want + update_finish_event.clear() + + zip_file_1 = create_lambda_archive(version_handler % "version1", get_content=True) + zip_file_2 = create_lambda_archive(version_handler % "version2", get_content=True) + aws_client.lambda_.update_function_code(FunctionName=function_name, ZipFile=zip_file_1) + with pytest.raises(ClientError) as e: + aws_client.lambda_.update_function_code(FunctionName=function_name, ZipFile=zip_file_2) + snapshot.match("update-during-in-progress-update-exc", e.value.response) + + # release hold on updates + update_finish_event.set() + aws_client.lambda_.get_waiter("function_updated_v2").wait(FunctionName=function_name) + + @pytest.mark.skipif( + not is_docker_runtime_executor(), + reason="Test will fail against other executors as they are not patched to take longer for the update", + ) + @markers.snapshot.skip_snapshot_verify(paths=["$..LoggingConfig"]) + @markers.aws.validated + def test_lambda_concurrent_config_updates( + self, aws_client, create_lambda_function, lambda_su_role, snapshot, monkeypatch + ): + # patch a function necessary for the lambda update to wait until we release it + # to be able to reliably capture the in-progress update state in LocalStack + from localstack.services.lambda_.invocation import docker_runtime_executor + from localstack.services.lambda_.invocation.docker_runtime_executor import ( + get_runtime_client_path, + ) + + update_finish_event = threading.Event() + update_finish_event.set() + + def _runtime_client_path(*args, **kwargs): + update_finish_event.wait() + return get_runtime_client_path(*args, **kwargs) + + monkeypatch.setattr( + docker_runtime_executor, "get_runtime_client_path", _runtime_client_path + ) + + function_name = f"test-lambda-{short_uid()}" + create_response = create_lambda_function( + handler_file=TEST_LAMBDA_PYTHON_ECHO, + func_name=function_name, + runtime=Runtime.python3_12, + role=lambda_su_role, + ) + snapshot.match("create-function-response", create_response) + + # clear flag so the update operation takes as long as we want + update_finish_event.clear() + + aws_client.lambda_.update_function_configuration( + FunctionName=function_name, Environment={"Variables": {"TEST": "TEST1"}} + ) + with pytest.raises(ClientError) as e: + aws_client.lambda_.update_function_configuration( + FunctionName=function_name, Environment={"Variables": {"TEST": "TEST2"}} + ) + snapshot.match("update-during-in-progress-update-exc", e.value.response) + + # release hold on updates + update_finish_event.set() + aws_client.lambda_.get_waiter("function_updated_v2").wait(FunctionName=function_name) + class TestLambdaImages: @pytest.fixture(scope="class") @@ -2511,9 +2619,7 @@ def test_lambda_eventinvokeconfig_exceptions( # New accounts in an organization have by default a quota of 10 or 50. class TestLambdaReservedConcurrency: @markers.aws.validated - def test_function_concurrency_exceptions( - self, create_lambda_function, snapshot, aws_client, monkeypatch - ): + def test_function_concurrency_exceptions(self, create_lambda_function, snapshot, aws_client): with pytest.raises(aws_client.lambda_.exceptions.ResourceNotFoundException) as e: aws_client.lambda_.put_function_concurrency( FunctionName="doesnotexist", ReservedConcurrentExecutions=1 diff --git a/tests/aws/services/lambda_/test_lambda_api.snapshot.json b/tests/aws/services/lambda_/test_lambda_api.snapshot.json index 6a45db3b88692..92ca832b7cb09 100644 --- a/tests/aws/services/lambda_/test_lambda_api.snapshot.json +++ b/tests/aws/services/lambda_/test_lambda_api.snapshot.json @@ -13786,5 +13786,131 @@ } } } + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_concurrent_config_updates": { + "recorded-date": "08-04-2024, 13:14:54", + "recorded-content": { + "create-function-response": { + "CreateEventSourceMappingResponse": null, + "CreateFunctionResponse": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 128, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Pending", + "StateReason": "The function is being created.", + "StateReasonCode": "Creating", + "Timeout": 30, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + } + }, + "update-during-in-progress-update-exc": { + "Error": { + "Code": "ResourceConflictException", + "Message": "The operation cannot be performed at this time. An update is in progress for resource: arn:aws:lambda::111111111111:function:" + }, + "Type": "User", + "message": "The operation cannot be performed at this time. An update is in progress for resource: arn:aws:lambda::111111111111:function:", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 409 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_concurrent_code_updates": { + "recorded-date": "09-04-2024, 11:23:28", + "recorded-content": { + "create-function-response": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 128, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Pending", + "StateReason": "The function is being created.", + "StateReasonCode": "Creating", + "Timeout": 3, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + }, + "update-during-in-progress-update-exc": { + "Error": { + "Code": "ResourceConflictException", + "Message": "The operation cannot be performed at this time. An update is in progress for resource: arn:aws:lambda::111111111111:function:" + }, + "Type": "User", + "message": "The operation cannot be performed at this time. An update is in progress for resource: arn:aws:lambda::111111111111:function:", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 409 + } + } + } } } diff --git a/tests/aws/services/lambda_/test_lambda_api.validation.json b/tests/aws/services/lambda_/test_lambda_api.validation.json index 543fc909d9f6e..e699d6a4c7bac 100644 --- a/tests/aws/services/lambda_/test_lambda_api.validation.json +++ b/tests/aws/services/lambda_/test_lambda_api.validation.json @@ -86,6 +86,12 @@ "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_code_location_zipfile": { "last_validated_date": "2023-11-20T15:46:52+00:00" }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_concurrent_code_updates": { + "last_validated_date": "2024-04-09T11:23:28+00:00" + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_concurrent_config_updates": { + "last_validated_date": "2024-04-08T13:14:54+00:00" + }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_list_functions": { "last_validated_date": "2023-11-20T15:47:16+00:00" }, From 380528109ce6b0762b062ce1acd2365533d8dd1b Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Wed, 10 Apr 2024 09:05:23 +0200 Subject: [PATCH 041/169] add container labels to container configuration and container run methods (#10624) --- .../utils/container_utils/container_client.py | 3 +++ .../utils/container_utils/docker_sdk_client.py | 2 ++ tests/integration/docker_utils/test_docker.py | 17 +++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/localstack/utils/container_utils/container_client.py b/localstack/utils/container_utils/container_client.py index fc718d4b2a5ee..d5dbbd20825fb 100644 --- a/localstack/utils/container_utils/container_client.py +++ b/localstack/utils/container_utils/container_client.py @@ -463,6 +463,7 @@ class ContainerConfiguration: workdir: Optional[str] = None platform: Optional[str] = None ulimits: Optional[List[Ulimit]] = None + labels: Optional[Dict[str, str]] = None class ContainerConfigurator(Protocol): @@ -838,6 +839,7 @@ def create_container_from_config(self, container_config: ContainerConfiguration) workdir=container_config.workdir, privileged=container_config.privileged, platform=container_config.platform, + labels=container_config.labels, ) @abstractmethod @@ -898,6 +900,7 @@ def run_container( dns: Optional[str] = None, additional_flags: Optional[str] = None, workdir: Optional[str] = None, + labels: Optional[Dict[str, str]] = None, platform: Optional[DockerPlatform] = None, privileged: Optional[bool] = None, ulimits: Optional[List[Ulimit]] = None, diff --git a/localstack/utils/container_utils/docker_sdk_client.py b/localstack/utils/container_utils/docker_sdk_client.py index 2e31db850afb4..8b42bb00f5515 100644 --- a/localstack/utils/container_utils/docker_sdk_client.py +++ b/localstack/utils/container_utils/docker_sdk_client.py @@ -736,6 +736,7 @@ def run_container( dns: Optional[str] = None, additional_flags: Optional[str] = None, workdir: Optional[str] = None, + labels: Optional[Dict[str, str]] = None, platform: Optional[DockerPlatform] = None, privileged: Optional[bool] = None, ulimits: Optional[List[Ulimit]] = None, @@ -776,6 +777,7 @@ def run_container( privileged=privileged, platform=platform, init=init, + labels=labels, **kwargs, ) result = self.start_container( diff --git a/tests/integration/docker_utils/test_docker.py b/tests/integration/docker_utils/test_docker.py index da9f61e5e4e79..5de94ab0dbeec 100644 --- a/tests/integration/docker_utils/test_docker.py +++ b/tests/integration/docker_utils/test_docker.py @@ -1799,6 +1799,23 @@ def test_create_container_with_labels(self, docker_client, create_container): result_labels = result.get("Config", {}).get("Labels") assert result_labels == labels + def test_run_container_with_labels(self, docker_client): + labels = {"foo": "bar", short_uid(): short_uid()} + container_name = _random_container_name() + try: + docker_client.run_container( + image_name="alpine", + command=["sh", "-c", "while true; do sleep 1; done"], + labels=labels, + name=container_name, + detach=True, + ) + result = docker_client.inspect_container(container_name_or_id=container_name) + result_labels = result.get("Config", {}).get("Labels") + assert result_labels == labels + finally: + docker_client.remove_container(container_name=container_name, force=True) + def _pull_image_if_not_exists(docker_client: ContainerClient, image_name: str): if image_name not in docker_client.get_docker_image_names(): From 48d58a64202d958f10bc9b7f73eedb054b0e089f Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:07:41 +0200 Subject: [PATCH 042/169] fix secretsmanager test (#10627) Co-authored-by: Nevil Macwan --- .../secretsmanager/test_secretsmanager.py | 47 ++++++++++--------- .../test_secretsmanager.validation.json | 2 +- .../stepfunctions/v2/test_stepfunctions_v2.py | 1 + 3 files changed, 28 insertions(+), 22 deletions(-) diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.py b/tests/aws/services/secretsmanager/test_secretsmanager.py index b06ca1d505d46..828aebe178f04 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.py +++ b/tests/aws/services/secretsmanager/test_secretsmanager.py @@ -1,6 +1,7 @@ import json import logging import os +import random import uuid from datetime import datetime from math import isclose @@ -263,11 +264,11 @@ def test_call_lists_secrets_multiple_times_snapshots( @markers.aws.validated def test_list_secrets_filtering(self, aws_client, create_secret): - unique_id = short_uid() - secret_name_1 = f"testing1/one-{unique_id}" - secret_name_2 = f"/testing2/two-{unique_id}" - secret_name_3 = f"testing3/three-{unique_id}" - secret_name_4 = f"/testing4/four-{unique_id}" + suffix = random.randint(10000, 99999) + secret_name_1 = f"testing1/one-{suffix}" + secret_name_2 = f"/testing2/two-{suffix}" + secret_name_3 = f"testing3/three-{suffix}" + secret_name_4 = f"/testing4/four-{suffix}" create_secret(Name=secret_name_1, SecretString="secret", Description="a secret") create_secret(Name=secret_name_2, SecretString="secret", Description="an secret") @@ -279,51 +280,55 @@ def test_list_secrets_filtering(self, aws_client, create_secret): self._wait_created_is_listed(aws_client.secretsmanager, secret_id=secret_name_3) self._wait_created_is_listed(aws_client.secretsmanager, secret_id=secret_name_4) - def assert_secret_names(res, include_secrets, exclude_secrets): - for secret in res["SecretList"]: - assert secret["Name"] in include_secrets - assert secret["Name"] not in exclude_secrets + def assert_secret_names(res: dict, include_secrets: set[str], exclude_secrets: set[str]): + secret_names = {secret["Name"] for secret in res["SecretList"]} + assert ( + include_secrets - secret_names + ) == set(), "At least one secret which should be included is not." + assert ( + exclude_secrets - secret_names + ) == exclude_secrets, "At least one secret which should not be included is." response = aws_client.secretsmanager.list_secrets( Filters=[{"Key": "name", "Values": ["/"]}] ) assert_secret_names( - response, [secret_name_2, secret_name_4], [secret_name_1, secret_name_3] + response, {secret_name_2, secret_name_4}, {secret_name_1, secret_name_3} ) response = aws_client.secretsmanager.list_secrets( Filters=[{"Key": "name", "Values": ["!/"]}] ) assert_secret_names( - response, [secret_name_1, secret_name_3], [secret_name_2, secret_name_4] + response, {secret_name_1, secret_name_3}, {secret_name_2, secret_name_4} ) response = aws_client.secretsmanager.list_secrets( Filters=[{"Key": "name", "Values": ["testing1 one"]}] ) assert_secret_names( - response, [], [secret_name_1, secret_name_2, secret_name_3, secret_name_4] + response, set(), {secret_name_1, secret_name_2, secret_name_3, secret_name_4} ) response = aws_client.secretsmanager.list_secrets( Filters=[{"Key": "description", "Values": ["a"]}] ) assert_secret_names( - response, [secret_name_1, secret_name_2, secret_name_3], [secret_name_4] + response, {secret_name_1, secret_name_2, secret_name_3}, {secret_name_4} ) response = aws_client.secretsmanager.list_secrets( Filters=[{"Key": "description", "Values": ["!a"]}] ) assert_secret_names( - response, [secret_name_4], [secret_name_1, secret_name_2, secret_name_3] + response, {secret_name_4}, {secret_name_1, secret_name_2, secret_name_3} ) response = aws_client.secretsmanager.list_secrets( Filters=[{"Key": "description", "Values": ["a secret"]}] ) assert_secret_names( - response, [secret_name_1, secret_name_2], [secret_name_3, secret_name_4] + response, {secret_name_1, secret_name_2}, {secret_name_3, secret_name_4} ) response = aws_client.secretsmanager.list_secrets( @@ -333,7 +338,7 @@ def assert_secret_names(res, include_secrets, exclude_secrets): ] ) assert_secret_names( - response, [secret_name_1, secret_name_2, secret_name_3, secret_name_4], [] + response, set(), {secret_name_1, secret_name_2, secret_name_3, secret_name_4} ) response = aws_client.secretsmanager.list_secrets( @@ -343,7 +348,7 @@ def assert_secret_names(res, include_secrets, exclude_secrets): ] ) assert_secret_names( - response, [secret_name_1, secret_name_2, secret_name_3, secret_name_4], [] + response, set(), {secret_name_1, secret_name_2, secret_name_3, secret_name_4} ) response = aws_client.secretsmanager.list_secrets( @@ -352,7 +357,7 @@ def assert_secret_names(res, include_secrets, exclude_secrets): ] ) assert_secret_names( - response, [secret_name_1, secret_name_2], [secret_name_3, secret_name_4] + response, {secret_name_1, secret_name_2}, {secret_name_3, secret_name_4} ) response = aws_client.secretsmanager.list_secrets( @@ -361,21 +366,21 @@ def assert_secret_names(res, include_secrets, exclude_secrets): ] ) assert_secret_names( - response, [secret_name_4], [secret_name_1, secret_name_2, secret_name_3] + response, {secret_name_4}, {secret_name_1, secret_name_2, secret_name_3} ) response = aws_client.secretsmanager.list_secrets( Filters=[{"Key": "description", "Values": ["!c"]}] ) assert_secret_names( - response, [secret_name_1, secret_name_2, secret_name_3, secret_name_4], [] + response, {secret_name_1, secret_name_2, secret_name_3, secret_name_4}, set() ) response = aws_client.secretsmanager.list_secrets( Filters=[{"Key": "name", "Values": ["testing1 one"]}] ) assert_secret_names( - response, [], [secret_name_1, secret_name_2, secret_name_3, secret_name_4] + response, set(), {secret_name_1, secret_name_2, secret_name_3, secret_name_4} ) @markers.aws.validated diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json index 13592210a1b6b..48f3a29b77ea3 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json @@ -63,7 +63,7 @@ "last_validated_date": "2024-03-15T08:12:47+00:00" }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_list_secrets_filtering": { - "last_validated_date": "2024-04-09T12:45:26+00:00" + "last_validated_date": "2024-04-10T08:25:18+00:00" }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_no_client_request_token[CreateSecret]": { "last_validated_date": "2024-03-15T08:14:56+00:00" diff --git a/tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py b/tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py index c3ebf4919cbfe..e1e264682df17 100644 --- a/tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py +++ b/tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py @@ -831,3 +831,4 @@ def check_invocations(): # clean up cleanup(sm_arn, state_machines_before, aws_client.stepfunctions) + # TODO also clean up other resources (like secrets) From ba8d36f18a91ae575cb60af653c2e470b473fcf5 Mon Sep 17 00:00:00 2001 From: Silvio Vasiljevic Date: Wed, 10 Apr 2024 14:21:31 +0200 Subject: [PATCH 043/169] Add missing library for transcribe on ARM (#10629) --- Dockerfile | 2 +- tests/aws/services/transcribe/test_transcribe.py | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index dc2d38796cd5d..65d21105df3a0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,7 @@ RUN --mount=type=cache,target=/var/cache/apt \ # Install dependencies to add additional repos apt-get install -y --no-install-recommends \ # Runtime packages (groff-base is necessary for AWS CLI help) - ca-certificates curl gnupg git make openssl tar pixz zip unzip groff-base iputils-ping nss-passwords procps iproute2 xz-utils + ca-certificates curl gnupg git make openssl tar pixz zip unzip groff-base iputils-ping nss-passwords procps iproute2 xz-utils libatomic1 # FIXME Node 18 actually shouldn't be necessary in Community, but we assume its presence in lots of tests # Install nodejs package from the dist release server. Note: we're installing from dist binaries, and not via diff --git a/tests/aws/services/transcribe/test_transcribe.py b/tests/aws/services/transcribe/test_transcribe.py index 77cf6b9ca0884..5235f7e61e1fe 100644 --- a/tests/aws/services/transcribe/test_transcribe.py +++ b/tests/aws/services/transcribe/test_transcribe.py @@ -9,7 +9,6 @@ from localstack.aws.connect import ServiceLevelClientFactory from localstack.testing.pytest import markers from localstack.utils.files import new_tmp_file -from localstack.utils.platform import get_arch from localstack.utils.strings import short_uid, to_str from localstack.utils.sync import poll_condition, retry @@ -23,10 +22,6 @@ def transcribe_snapshot_transformer(snapshot): snapshot.add_transformer(snapshot.transform.transcribe_api()) -@pytest.mark.skipif( - "arm" in get_arch(), - reason="Vosk transcription library has issues running on Circle CI arm64 executors.", -) class TestTranscribe: @staticmethod def _wait_transcription_job( From 2984327312a153f52eddaa4405372292bd102ff5 Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Wed, 10 Apr 2024 17:17:26 +0200 Subject: [PATCH 044/169] ensure container clients return the same format for container labels on list call (#10630) --- .../utils/container_utils/docker_cmd_client.py | 13 ++++++++++++- tests/integration/docker_utils/test_docker.py | 12 ++++++++++++ tests/unit/test_dockerclient.py | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/localstack/utils/container_utils/docker_cmd_client.py b/localstack/utils/container_utils/docker_cmd_client.py index 6072dfd7f1169..48088ca43d233 100644 --- a/localstack/utils/container_utils/docker_cmd_client.py +++ b/localstack/utils/container_utils/docker_cmd_client.py @@ -237,6 +237,7 @@ def list_containers(self, filter: Union[List[str], str, None] = None, all=True) container_list = [json.loads(line) for line in cmd_result.splitlines()] result = [] for container in container_list: + labels = self._transform_container_labels(container["Labels"]) result.append( { # support both, Docker and podman API response formats (`ID` vs `Id`) @@ -245,7 +246,7 @@ def list_containers(self, filter: Union[List[str], str, None] = None, all=True) # Docker returns a single string for `Names`, whereas podman returns a list of names "name": ensure_list(container["Names"])[0], "status": container["State"], - "labels": container["Labels"], + "labels": labels, } ) return result @@ -831,3 +832,13 @@ def _check_and_raise_no_such_container_error( process_stdout_lower = to_str(error.stdout).lower() if any(msg.lower() in process_stdout_lower for msg in error_messages): raise NoSuchContainer(container_name_or_id, stdout=error.stdout, stderr=error.stderr) + + def _transform_container_labels(self, labels: str) -> Dict[str, str]: + """ + Transforms the container labels returned by the docker command from the key-value pair format to a dict + :param labels: Input string, comma separated key value pairs. Example: key1=value1,key2=value2 + :return: Dict representation of the passed values, example: {"key1": "value1", "key2": "value2"} + """ + labels = labels.split(",") + labels = [label.partition("=") for label in labels] + return {label[0]: label[2] for label in labels} diff --git a/tests/integration/docker_utils/test_docker.py b/tests/integration/docker_utils/test_docker.py index 5de94ab0dbeec..7868dab246165 100644 --- a/tests/integration/docker_utils/test_docker.py +++ b/tests/integration/docker_utils/test_docker.py @@ -1816,6 +1816,18 @@ def test_run_container_with_labels(self, docker_client): finally: docker_client.remove_container(container_name=container_name, force=True) + def test_list_containers_with_labels(self, docker_client, create_container): + labels = {"foo": "bar", short_uid(): short_uid()} + container = create_container( + "alpine", command=["sh", "-c", "while true; do sleep 1; done"], labels=labels + ) + docker_client.start_container(container.container_id) + + containers = docker_client.list_containers(filter=f"id={container.container_id}") + assert len(containers) == 1 + container = containers[0] + assert container["labels"] == labels + def _pull_image_if_not_exists(docker_client: ContainerClient, image_name: str): if image_name not in docker_client.get_docker_image_names(): diff --git a/tests/unit/test_dockerclient.py b/tests/unit/test_dockerclient.py index 89afd824ac397..a1e23a697954f 100644 --- a/tests/unit/test_dockerclient.py +++ b/tests/unit/test_dockerclient.py @@ -37,7 +37,7 @@ def test_list_containers(self, run_mock): "id": mock_container["ID"], "image": mock_container["Image"], "name": mock_container["Names"], - "labels": mock_container["Labels"], + "labels": {"authors": "LocalStack Contributors"}, "status": mock_container["State"], } run_mock.return_value = json.dumps(mock_container) From 748a2488a2679186fd798e40aa051dd2c56f559d Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Wed, 10 Apr 2024 19:04:20 +0200 Subject: [PATCH 045/169] add s3 POST policy validation (#10616) --- localstack/aws/api/s3/__init__.py | 9 + localstack/aws/spec-patches.json | 20 + localstack/services/s3/presigned_url.py | 80 +++- localstack/services/s3/provider.py | 2 +- localstack/services/s3/v3/provider.py | 20 +- localstack/services/s3/v3/storage/core.py | 4 +- tests/aws/services/s3/test_s3.py | 351 ++++++++++++++++++ tests/aws/services/s3/test_s3.snapshot.json | 156 ++++++++ tests/aws/services/s3/test_s3.validation.json | 9 + 9 files changed, 643 insertions(+), 8 deletions(-) diff --git a/localstack/aws/api/s3/__init__.py b/localstack/aws/api/s3/__init__.py index 39ff9d944af3b..174e8a6cb38ed 100644 --- a/localstack/aws/api/s3/__init__.py +++ b/localstack/aws/api/s3/__init__.py @@ -932,6 +932,15 @@ class InvalidLocationConstraint(ServiceException): LocationConstraint: Optional[BucketRegion] +class EntityTooLarge(ServiceException): + code: str = "EntityTooLarge" + sender_fault: bool = False + status_code: int = 400 + MaxSizeAllowed: Optional[KeyLength] + HostId: Optional[HostId] + ProposedSize: Optional[ProposedSize] + + AbortDate = datetime diff --git a/localstack/aws/spec-patches.json b/localstack/aws/spec-patches.json index 8354cd50f541e..e47e706d3e6c7 100644 --- a/localstack/aws/spec-patches.json +++ b/localstack/aws/spec-patches.json @@ -1190,6 +1190,26 @@ "documentation": "

The specified location-constraint is not valid

", "exception": true } + }, + { + "op": "add", + "path": "/shapes/EntityTooLarge", + "value": { + "type": "structure", + "members": { + "MaxSizeAllowed": { + "shape": "KeyLength" + }, + "HostId": { + "shape": "HostId" + }, + "ProposedSize": { + "shape": "ProposedSize" + } + }, + "documentation": "

Your proposed upload exceeds the maximum allowed size

", + "exception": true + } } ] } diff --git a/localstack/services/s3/presigned_url.py b/localstack/services/s3/presigned_url.py index 2af7801c2ee98..621b243982f7a 100644 --- a/localstack/services/s3/presigned_url.py +++ b/localstack/services/s3/presigned_url.py @@ -21,10 +21,12 @@ from localstack import config from localstack.aws.accounts import get_account_id_from_access_key_id -from localstack.aws.api import RequestContext +from localstack.aws.api import CommonServiceException, RequestContext from localstack.aws.api.s3 import ( AccessDenied, AuthorizationQueryParametersError, + EntityTooLarge, + EntityTooSmall, InvalidArgument, InvalidBucketName, SignatureDoesNotMatch, @@ -707,7 +709,9 @@ def _validate_headers_for_moto(headers: Headers) -> None: raise SignatureDoesNotMatch('Wrong "X-Amz-Decoded-Content-Length" header') -def validate_post_policy(request_form: ImmutableMultiDict) -> None: +def validate_post_policy( + request_form: ImmutableMultiDict, additional_policy_metadata: dict +) -> None: """ Validate the pre-signed POST with its policy contained For now, only validates its expiration @@ -715,6 +719,7 @@ def validate_post_policy(request_form: ImmutableMultiDict) -> None: SigV4: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-authentication-HTTPPOST.html :param request_form: the form data contained in the pre-signed POST request + :param additional_policy_metadata: additional metadata needed to validate the policy (bucket name, object size) :raises AccessDenied, SignatureDoesNotMatch :return: None """ @@ -759,7 +764,76 @@ def validate_post_policy(request_form: ImmutableMultiDict) -> None: raise ex # TODO: validate the signature - # TODO: validate the request according to the policy + + # See https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html + # for the list of conditions and what matching they support + # TODO: + # 1. only support the kind of matching the field supports: `success_action_status` does not support `starts-with` + # matching + # 2. if there are fields that are not defined in the policy, we should reject it + + # Special case for LEGACY_V2: do not validate the conditions. Remove this check once we remove legacy_v2 + if not additional_policy_metadata: + return + + conditions = policy_decoded.get("conditions", []) + form_dict = {k.lower(): v for k, v in request_form.items()} + for condition in conditions: + if not _verify_condition(condition, form_dict, additional_policy_metadata): + raise AccessDenied( + f"Invalid according to Policy: Policy Condition failed: {condition}", + HostId=FAKE_HOST_ID, + ) + + +def _verify_condition(condition: list | dict, form: dict, additional_policy_metadata: dict) -> bool: + if isinstance(condition, dict) and len(condition) > 1: + raise CommonServiceException( + code="InvalidPolicyDocument", + message="Invalid Policy: Invalid Simple-Condition: Simple-Conditions must have exactly one property specified.", + ) + + match condition: + case {**kwargs}: + # this is the most performant to check for a dict with only one key + # alternative version is `key, val = next(iter(dict))` + for key, val in kwargs.items(): + k = key.lower() + if k == "bucket": + return additional_policy_metadata.get("bucket") == val + else: + return form.get(key) == val + + case ["eq", key, value]: + k = key.lower() + if k == "$bucket": + return additional_policy_metadata.get("bucket") == value + + return k.startswith("$") and form.get(k.lstrip("$")) == value + + case ["starts-with", key, value]: + # You can set the `starts-with` value to an empty string to accept anything + return key.startswith("$") and ( + not value or form.get(key.lstrip("$").lower(), "").startswith(value) + ) + + case ["content-length-range", start, end]: + size = additional_policy_metadata.get("content_length", 0) + if size < start: + raise EntityTooSmall( + "Your proposed upload is smaller than the minimum allowed size", + ProposedSize=size, + MinSizeAllowed=start, + ) + elif size > end: + raise EntityTooLarge( + "Your proposed upload exceeds the maximum allowed size", + ProposedSize=size, + MaxSizeAllowed=end, + HostId=FAKE_HOST_ID, + ) + else: + return True def _parse_policy_expiration_date(expiration_string: str) -> datetime.datetime: diff --git a/localstack/services/s3/provider.py b/localstack/services/s3/provider.py index ba8babc1ac33a..7d94a03afd981 100644 --- a/localstack/services/s3/provider.py +++ b/localstack/services/s3/provider.py @@ -1447,7 +1447,7 @@ def post_object( # see https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html # TODO: signature validation is not implemented for pre-signed POST # policy validation is not implemented either, except expiration and mandatory fields - validate_post_policy(context.request.form) + validate_post_policy(context.request.form, additional_policy_metadata={}) # Botocore has trouble parsing responses with status code in the 3XX range, it interprets them as exception # it then raises a nonsense one with a wrong code diff --git a/localstack/services/s3/v3/provider.py b/localstack/services/s3/v3/provider.py index d8e1b4afec7e1..458da9ceceed3 100644 --- a/localstack/services/s3/v3/provider.py +++ b/localstack/services/s3/v3/provider.py @@ -3661,16 +3661,24 @@ def post_object( store, s3_bucket = self._get_cross_account_bucket(context, bucket) form = context.request.form - validate_post_policy(form) object_key = context.request.form.get("key") if "file" in form: # in AWS, you can pass the file content as a string in the form field and not as a file object - stream = BytesIO(to_bytes(form["file"])) + file_data = to_bytes(form["file"]) + object_content_length = len(file_data) + stream = BytesIO(file_data) else: # this is the default behaviour fileobj = context.request.files["file"] stream = fileobj.stream + # stream is a SpooledTemporaryFile, so we can seek the stream to know its length, necessary for policy + # validation + original_pos = stream.tell() + object_content_length = stream.seek(0, 2) + # reset the stream and put it back at its original position + stream.seek(original_pos, 0) + if "${filename}" in object_key: # TODO: ${filename} is actually usable in all form fields # See https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/PresignedPost.html @@ -3678,6 +3686,14 @@ def post_object( # is recognized by all form fields. object_key = object_key.replace("${filename}", fileobj.filename) + # TODO: see if we need to pass additional metadata not contained in the policy from the table under + # https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html#sigv4-PolicyConditions + additional_policy_metadata = { + "bucket": bucket, + "content_length": object_content_length, + } + validate_post_policy(form, additional_policy_metadata) + if canned_acl := form.get("acl"): validate_canned_acl(canned_acl) acp = get_canned_acl(canned_acl, owner=s3_bucket.owner) diff --git a/localstack/services/s3/v3/storage/core.py b/localstack/services/s3/v3/storage/core.py index e00a5007142a4..b6b93b2c93dff 100644 --- a/localstack/services/s3/v3/storage/core.py +++ b/localstack/services/s3/v3/storage/core.py @@ -2,7 +2,7 @@ from io import RawIOBase from typing import IO, Iterable, Iterator, Literal, Optional -from localstack.aws.api.s3 import BucketName, MultipartUploadId, PartNumber +from localstack.aws.api.s3 import BucketName, PartNumber from localstack.services.s3.utils import ObjectRange from localstack.services.s3.v3.models import S3Multipart, S3Object, S3Part @@ -202,7 +202,7 @@ def copy( pass @abc.abstractmethod - def get_multipart(self, bucket: BucketName, upload_id: MultipartUploadId) -> S3StoredMultipart: + def get_multipart(self, bucket: BucketName, upload_id: S3Multipart) -> S3StoredMultipart: pass @abc.abstractmethod diff --git a/tests/aws/services/s3/test_s3.py b/tests/aws/services/s3/test_s3.py index 30c63e65695b9..67eb97e9c2da3 100644 --- a/tests/aws/services/s3/test_s3.py +++ b/tests/aws/services/s3/test_s3.py @@ -9969,6 +9969,26 @@ def test_replication_config( class TestS3PresignedPost: + DEFAULT_FILE_VALUE = "abcdef" + + def post_generated_presigned_post_with_default_file( + self, generated_request: dict + ) -> requests.Response: + return requests.post( + generated_request["url"], + data=generated_request["fields"], + files={"file": self.DEFAULT_FILE_VALUE}, + verify=False, + allow_redirects=False, + ) + + @staticmethod + def parse_response_xml(content: bytes) -> dict: + if not is_aws_cloud(): + # AWS use double quotes in error messages and LocalStack uses single. Try to unify before snapshotting + content = content.replace(b"'", b'"') + return xmltodict.parse(content) + @markers.aws.validated @pytest.mark.xfail( reason="failing sporadically with new HTTP gateway (only in CI)", @@ -10544,6 +10564,337 @@ def test_post_object_with_file_as_string(self, s3_bucket, aws_client, snapshot): response = aws_client.s3.list_objects_v2(Bucket=s3_bucket) snapshot.match("list-objects", response) + @pytest.mark.skipif( + condition=LEGACY_V2_S3_PROVIDER, + reason="not implemented in moto", + ) + @markers.snapshot.skip_snapshot_verify( + paths=[ + # TODO: wrong exception implement, still missing the extra input fields validation + "$.invalid-condition-missing-prefix.Error.Message", + # TODO: we should add HostId to every serialized exception for S3, and not have them as part as the spec + "$.invalid-condition-wrong-condition.Error.HostId", + ], + ) + @markers.aws.validated + def test_post_object_policy_conditions_validation_eq(self, s3_bucket, aws_client, snapshot): + snapshot.add_transformers_list( + [ + snapshot.transform.key_value( + "HostId", reference_replacement=False, value_replacement="" + ), + snapshot.transform.key_value("RequestId"), + ] + ) + object_key = "validate-policy-1" + + redirect_location = "http://localhost.test/random" + presigned_request = aws_client.s3.generate_presigned_post( + Bucket=s3_bucket, + Key=object_key, + Fields={"success_action_redirect": redirect_location}, + Conditions=[ + ["eq", "$success_action_redirect", redirect_location], + ], + ExpiresIn=60, + ) + + # PostObject with a wrong redirect location + presigned_request["fields"]["success_action_redirect"] = "http://wrong.location/test" + response = self.post_generated_presigned_post_with_default_file(presigned_request) + + # assert that it's rejected + assert response.status_code == 403 + resp_content = self.parse_response_xml(response.content) + snapshot.match("invalid-condition-eq", resp_content) + + # PostObject with a wrong condition (missing $ prefix) + presigned_request = aws_client.s3.generate_presigned_post( + Bucket=s3_bucket, + Key=object_key, + Fields={"success_action_redirect": redirect_location}, + Conditions=[ + ["eq", "success_action_redirect", redirect_location], + ], + ExpiresIn=60, + ) + + response = self.post_generated_presigned_post_with_default_file(presigned_request) + + # assert that it's rejected + assert response.status_code == 403 + resp_content = self.parse_response_xml(response.content) + snapshot.match("invalid-condition-missing-prefix", resp_content) + + # PostObject with a wrong condition (multiple condition in one dict) + presigned_request = aws_client.s3.generate_presigned_post( + Bucket=s3_bucket, + Key=object_key, + Fields={"success_action_redirect": redirect_location}, + Conditions=[ + {"bucket": s3_bucket, "success_action_redirect": redirect_location}, + ], + ExpiresIn=60, + ) + + response = self.post_generated_presigned_post_with_default_file(presigned_request) + + # assert that it's rejected + assert response.status_code == 400 + resp_content = self.parse_response_xml(response.content) + snapshot.match("invalid-condition-wrong-condition", resp_content) + + # PostObject with a wrong condition value casing + presigned_request = aws_client.s3.generate_presigned_post( + Bucket=s3_bucket, + Key=object_key, + Fields={"success_action_redirect": redirect_location}, + Conditions=[ + ["eq", "$success_action_redirect", redirect_location.replace("http://", "HTTP://")], + ], + ExpiresIn=60, + ) + response = self.post_generated_presigned_post_with_default_file(presigned_request) + # assert that it's rejected + assert response.status_code == 403 + resp_content = self.parse_response_xml(response.content) + snapshot.match("invalid-condition-wrong-value-casing", resp_content) + + object_expires = rfc_1123_datetime( + datetime.datetime.now(ZoneInfo("GMT")) + datetime.timedelta(minutes=10) + ) + + # test casing for x-amz-meta and specific Content-Type/Expires S3 headers + presigned_request = aws_client.s3.generate_presigned_post( + Bucket=s3_bucket, + Key=object_key, + ExpiresIn=60, + Fields={ + "x-amz-meta-test-1": "test-meta-1", + "x-amz-meta-TEST-2": "test-meta-2", + "Content-Type": "text/plain", + "Expires": object_expires, + }, + Conditions=[ + {"bucket": s3_bucket}, + ["eq", "$x-amz-meta-test-1", "test-meta-1"], + ["eq", "$x-amz-meta-test-2", "test-meta-2"], + ["eq", "$content-type", "text/plain"], + ["eq", "$Expires", object_expires], + ], + ) + # assert that it kept the casing + assert "x-amz-meta-TEST-2" in presigned_request["fields"] + response = self.post_generated_presigned_post_with_default_file(presigned_request) + # assert that it's accepted + assert response.status_code == 204 + + head_object = aws_client.s3.head_object(Bucket=s3_bucket, Key=object_key) + snapshot.match("head-object-metadata", head_object) + + # PostObject with a wrong condition key casing, should still work + presigned_request = aws_client.s3.generate_presigned_post( + Bucket=s3_bucket, + Key=object_key, + Fields={"success_action_redirect": redirect_location}, + Conditions=[ + ["eq", "$success_Action_REDIRECT", redirect_location], + ], + ExpiresIn=60, + ) + + # load the generated policy to assert that it kept the casing, and it is sent to AWS + generated_policy = json.loads( + base64.b64decode(presigned_request["fields"]["policy"]).decode("utf-8") + ) + eq_condition = [ + cond + for cond in generated_policy["conditions"] + if isinstance(cond, list) and cond[0] == "eq" + ][0] + assert eq_condition[1] == "$success_Action_REDIRECT" + + response = self.post_generated_presigned_post_with_default_file(presigned_request) + # assert that it's accepted + assert response.status_code == 303 + + final_object = aws_client.s3.get_object(Bucket=s3_bucket, Key=object_key) + snapshot.match("final-object", final_object) + + @pytest.mark.skipif( + condition=LEGACY_V2_S3_PROVIDER, + reason="not implemented in moto", + ) + @markers.snapshot.skip_snapshot_verify( + paths=[ + # TODO: wrong exception implement, still missing the extra input fields validation + "$.invalid-condition-missing-prefix.Error.Message", + # TODO: we should add HostId to every serialized exception for S3, and not have them as part as the spec + "$.invalid-condition-wrong-condition.Error.HostId", + ], + ) + @markers.aws.validated + def test_post_object_policy_conditions_validation_starts_with( + self, s3_bucket, aws_client, snapshot + ): + snapshot.add_transformers_list( + [ + snapshot.transform.key_value( + "HostId", reference_replacement=False, value_replacement="" + ), + snapshot.transform.key_value("RequestId"), + ] + ) + object_key = "validate-policy-2" + + redirect_location = "http://localhost.test/random" + presigned_request = aws_client.s3.generate_presigned_post( + Bucket=s3_bucket, + Key=object_key, + Fields={"success_action_redirect": redirect_location}, + Conditions=[ + ["starts-with", "$success_action_redirect", "http://localhost"], + ], + ExpiresIn=60, + ) + + # PostObject with a wrong redirect location start + presigned_request["fields"]["success_action_redirect"] = "http://wrong.location/test" + response = self.post_generated_presigned_post_with_default_file(presigned_request) + + # assert that it's rejected + assert response.status_code == 403 + resp_content = self.parse_response_xml(response.content) + snapshot.match("invalid-condition-starts-with", resp_content) + + # PostObject with a right redirect location start but wrong casing + presigned_request["fields"]["success_action_redirect"] = "HTTP://localhost.test/random" + response = self.post_generated_presigned_post_with_default_file(presigned_request) + + # assert that it's rejected + assert response.status_code == 403 + resp_content = self.parse_response_xml(response.content) + snapshot.match("invalid-condition-starts-with-casing", resp_content) + + # PostObject with a right redirect location start + presigned_request["fields"]["success_action_redirect"] = redirect_location + response = self.post_generated_presigned_post_with_default_file(presigned_request) + + # assert that it's accepted + assert response.status_code == 303 + assert response.headers["Location"].startswith(redirect_location) + + get_object = aws_client.s3.get_object(Bucket=s3_bucket, Key=object_key) + snapshot.match("get-object-1", get_object) + + presigned_request = aws_client.s3.generate_presigned_post( + Bucket=s3_bucket, + Key=object_key, + Fields={"success_action_redirect": redirect_location}, + Conditions=[ + [ + "starts-with", + "$success_action_redirect", + "", + ], # this allows to accept anything for it + ], + ExpiresIn=60, + ) + + # PostObject with a different redirect location, but should be accepted + # manually generate the pre-signed with a different file value to change ETag, to later validate that the file + # has properly been written in S3 + presigned_request["fields"]["success_action_redirect"] = "http://wrong.location/test" + response = requests.post( + presigned_request["url"], + data=presigned_request["fields"], + files={"file": "manual value to change ETag"}, + verify=False, + allow_redirects=False, + ) + + # assert that it's accepted + assert response.status_code == 303 + assert response.headers["Location"].startswith("http://wrong.location/test") + + get_object = aws_client.s3.get_object(Bucket=s3_bucket, Key=object_key) + snapshot.match("get-object-2", get_object) + + @pytest.mark.skipif( + condition=LEGACY_V2_S3_PROVIDER, + reason="not implemented in moto", + ) + @markers.snapshot.skip_snapshot_verify( + paths=[ + # TODO: we should add HostId to every serialized exception for S3, and not have them as part as the spec + "$.invalid-content-length-too-small.Error.HostId", + ], + ) + @markers.aws.validated + def test_post_object_policy_validation_size(self, s3_bucket, aws_client, snapshot): + snapshot.add_transformers_list( + [ + snapshot.transform.key_value( + "HostId", reference_replacement=False, value_replacement="" + ), + snapshot.transform.key_value("RequestId"), + ] + ) + object_key = "validate-policy-content-length" + presigned_request = aws_client.s3.generate_presigned_post( + Bucket=s3_bucket, + Key=object_key, + ExpiresIn=60, + Conditions=[ + {"bucket": s3_bucket}, + ["content-length-range", 5, 10], + ], + ) + # PostObject with a body length of 12 + response = requests.post( + presigned_request["url"], + data=presigned_request["fields"], + files={"file": "a" * 12}, + verify=False, + ) + + # assert that it's rejected + assert response.status_code == 400 + snapshot.match("invalid-content-length-too-big", xmltodict.parse(response.content)) + + # PostObject with a body length of 1 + response = requests.post( + presigned_request["url"], + data=presigned_request["fields"], + files={"file": "a" * 1}, + verify=False, + ) + + # assert that it's rejected + assert response.status_code == 400 + snapshot.match("invalid-content-length-too-small", xmltodict.parse(response.content)) + + # PostObject with a body length of 5 + response = requests.post( + presigned_request["url"], + data=presigned_request["fields"], + files={"file": "a" * 5}, + verify=False, + ) + assert response.status_code == 204 + + # PostObject with a body length of 10 + response = requests.post( + presigned_request["url"], + data=presigned_request["fields"], + files={"file": "a" * 10}, + verify=False, + ) + assert response.status_code == 204 + + final_object = aws_client.s3.get_object(Bucket=s3_bucket, Key=object_key) + snapshot.match("final-object", final_object) + def _s3_client_pre_signed_client(conf: Config, endpoint_url: str = None): if is_aws_cloud(): diff --git a/tests/aws/services/s3/test_s3.snapshot.json b/tests/aws/services/s3/test_s3.snapshot.json index fc69023424cbb..f2b123ad5d450 100644 --- a/tests/aws/services/s3/test_s3.snapshot.json +++ b/tests/aws/services/s3/test_s3.snapshot.json @@ -11454,5 +11454,161 @@ } } } + }, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_policy_validation_size": { + "recorded-date": "08-04-2024, 17:29:34", + "recorded-content": { + "invalid-content-length-too-big": { + "Error": { + "Code": "EntityTooLarge", + "HostId": "", + "MaxSizeAllowed": "10", + "Message": "Your proposed upload exceeds the maximum allowed size", + "ProposedSize": "12", + "RequestId": "" + } + }, + "invalid-content-length-too-small": { + "Error": { + "Code": "EntityTooSmall", + "HostId": "", + "Message": "Your proposed upload is smaller than the minimum allowed size", + "MinSizeAllowed": "5", + "ProposedSize": "1", + "RequestId": "" + } + }, + "final-object": { + "AcceptRanges": "bytes", + "Body": "aaaaaaaaaa", + "ContentLength": 10, + "ContentType": "binary/octet-stream", + "ETag": "\"e09c80c42fda55f9d992e59ca6b3307d\"", + "LastModified": "datetime", + "Metadata": {}, + "ServerSideEncryption": "AES256", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_policy_conditions_validation_eq": { + "recorded-date": "10-04-2024, 15:09:48", + "recorded-content": { + "invalid-condition-eq": { + "Error": { + "Code": "AccessDenied", + "HostId": "", + "Message": "Invalid according to Policy: Policy Condition failed: [\"eq\", \"$success_action_redirect\", \"http://localhost.test/random\"]", + "RequestId": "" + } + }, + "invalid-condition-missing-prefix": { + "Error": { + "Code": "AccessDenied", + "HostId": "", + "Message": "Invalid according to Policy: Policy Condition failed: [\"eq\", \"success_action_redirect\", \"http://localhost.test/random\"]", + "RequestId": "" + } + }, + "invalid-condition-wrong-condition": { + "Error": { + "Code": "InvalidPolicyDocument", + "HostId": "", + "Message": "Invalid Policy: Invalid Simple-Condition: Simple-Conditions must have exactly one property specified.", + "RequestId": "" + } + }, + "invalid-condition-wrong-value-casing": { + "Error": { + "Code": "AccessDenied", + "HostId": "", + "Message": "Invalid according to Policy: Policy Condition failed: [\"eq\", \"$success_action_redirect\", \"HTTP://localhost.test/random\"]", + "RequestId": "" + } + }, + "head-object-metadata": { + "AcceptRanges": "bytes", + "ContentLength": 6, + "ContentType": "text/plain", + "ETag": "\"e80b5017098950fc58aad83c8c14978e\"", + "Expires": "datetime", + "LastModified": "datetime", + "Metadata": { + "test-1": "test-meta-1", + "test-2": "test-meta-2" + }, + "ServerSideEncryption": "AES256", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "final-object": { + "AcceptRanges": "bytes", + "Body": "abcdef", + "ContentLength": 6, + "ContentType": "binary/octet-stream", + "ETag": "\"e80b5017098950fc58aad83c8c14978e\"", + "LastModified": "datetime", + "Metadata": {}, + "ServerSideEncryption": "AES256", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_policy_conditions_validation_starts_with": { + "recorded-date": "10-04-2024, 12:16:01", + "recorded-content": { + "invalid-condition-starts-with": { + "Error": { + "Code": "AccessDenied", + "HostId": "", + "Message": "Invalid according to Policy: Policy Condition failed: [\"starts-with\", \"$success_action_redirect\", \"http://localhost\"]", + "RequestId": "" + } + }, + "invalid-condition-starts-with-casing": { + "Error": { + "Code": "AccessDenied", + "HostId": "", + "Message": "Invalid according to Policy: Policy Condition failed: [\"starts-with\", \"$success_action_redirect\", \"http://localhost\"]", + "RequestId": "" + } + }, + "get-object-1": { + "AcceptRanges": "bytes", + "Body": "abcdef", + "ContentLength": 6, + "ContentType": "binary/octet-stream", + "ETag": "\"e80b5017098950fc58aad83c8c14978e\"", + "LastModified": "datetime", + "Metadata": {}, + "ServerSideEncryption": "AES256", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "get-object-2": { + "AcceptRanges": "bytes", + "Body": "manual value to change ETag", + "ContentLength": 27, + "ContentType": "binary/octet-stream", + "ETag": "\"365cb4550a52593ad95c6b31242d7418\"", + "LastModified": "datetime", + "Metadata": {}, + "ServerSideEncryption": "AES256", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/s3/test_s3.validation.json b/tests/aws/services/s3/test_s3.validation.json index e94b24584e521..0b8644df5d7c9 100644 --- a/tests/aws/services/s3/test_s3.validation.json +++ b/tests/aws/services/s3/test_s3.validation.json @@ -518,6 +518,15 @@ "tests/aws/services/s3/test_s3.py::TestS3ObjectLockRetention::test_s3_object_retention_exc": { "last_validated_date": "2023-08-09T15:58:37+00:00" }, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_policy_conditions_validation_eq": { + "last_validated_date": "2024-04-10T15:09:48+00:00" + }, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_policy_conditions_validation_starts_with": { + "last_validated_date": "2024-04-10T12:16:01+00:00" + }, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_policy_validation_size": { + "last_validated_date": "2024-04-08T17:29:34+00:00" + }, "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_with_file_as_string": { "last_validated_date": "2024-02-24T01:01:59+00:00" }, From c28d0fb75f11d38131d3fa2f6588bd70da745e15 Mon Sep 17 00:00:00 2001 From: Mathieu Cloutier <79954947+cloutierMat@users.noreply.github.com> Date: Wed, 10 Apr 2024 13:31:44 -0600 Subject: [PATCH 046/169] fix docker client ulimits (#10633) --- .../utils/container_utils/docker_cmd_client.py | 2 +- .../utils/container_utils/docker_sdk_client.py | 10 +--------- tests/integration/docker_utils/test_docker.py | 12 ++++++++++++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/localstack/utils/container_utils/docker_cmd_client.py b/localstack/utils/container_utils/docker_cmd_client.py index 48088ca43d233..10aec1e747990 100644 --- a/localstack/utils/container_utils/docker_cmd_client.py +++ b/localstack/utils/container_utils/docker_cmd_client.py @@ -790,7 +790,7 @@ def _build_run_create_cmd( cmd += ["--platform", platform] if ulimits: cmd += list( - itertools.chain.from_iterable(["--ulimits", str(ulimit)] for ulimit in ulimits) + itertools.chain.from_iterable(["--ulimit", str(ulimit)] for ulimit in ulimits) ) if init: cmd += ["--init"] diff --git a/localstack/utils/container_utils/docker_sdk_client.py b/localstack/utils/container_utils/docker_sdk_client.py index 8b42bb00f5515..572c0cc6dbca0 100644 --- a/localstack/utils/container_utils/docker_sdk_client.py +++ b/localstack/utils/container_utils/docker_sdk_client.py @@ -745,14 +745,6 @@ def run_container( LOG.debug("Running container with image: %s", image_name) container = None try: - kwargs = {} - if ulimits: - kwargs["ulimits"] = [ - docker.types.Ulimit( - name=ulimit.name, soft=ulimit.soft_limit, hard=ulimit.hard_limit - ) - for ulimit in ulimits - ] container = self.create_container( image_name, name=name, @@ -778,7 +770,7 @@ def run_container( platform=platform, init=init, labels=labels, - **kwargs, + ulimits=ulimits, ) result = self.start_container( container_name_or_id=container, diff --git a/tests/integration/docker_utils/test_docker.py b/tests/integration/docker_utils/test_docker.py index 7868dab246165..b7421dcde78d3 100644 --- a/tests/integration/docker_utils/test_docker.py +++ b/tests/integration/docker_utils/test_docker.py @@ -25,6 +25,7 @@ NoSuchNetwork, PortMappings, RegistryConnectionError, + Ulimit, Util, VolumeInfo, ) @@ -1432,6 +1433,17 @@ def test_run_with_additional_arguments_random_port( ) assert automatic_host_port > 0 + def test_run_with_ulimit(self, docker_client: ContainerClient): + container_name = f"c-{short_uid()}" + stdout, _ = docker_client.run_container( + "alpine", + name=container_name, + remove=True, + command=["sh", "-c", "ulimit -n"], + ulimits=[Ulimit(name="nofile", soft_limit=1024, hard_limit=1024)], + ) + assert stdout.decode(config.DEFAULT_ENCODING).strip() == "1024" + class TestDockerImages: def test_commit_creates_image_from_running_container(self, docker_client: ContainerClient): From 8fd2937f71515ced51fec412e1a09b394785c9bc Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Wed, 10 Apr 2024 22:25:07 +0200 Subject: [PATCH 047/169] StepFunctions: Fix Unknown Service errors (#10631) --- .../service/state_task_service_aws_sdk.py | 38 +++- localstack/services/stepfunctions/provider.py | 4 +- .../templates/services/services_templates.py | 6 + .../aws_sdk_sfn_send_task_failure.json5 | 14 ++ .../aws_sdk_sfn_send_task_success.json5 | 15 ++ .../v2/services/test_aws_sdk_task_service.py | 38 ++++ .../test_aws_sdk_task_service.snapshot.json | 179 ++++++++++++++++++ .../test_aws_sdk_task_service.validation.json | 8 +- 8 files changed, 292 insertions(+), 10 deletions(-) create mode 100644 tests/aws/services/stepfunctions/templates/services/statemachines/aws_sdk_sfn_send_task_failure.json5 create mode 100644 tests/aws/services/stepfunctions/templates/services/statemachines/aws_sdk_sfn_send_task_success.json5 diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_aws_sdk.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_aws_sdk.py index f3aae26ea9b6c..f532e6483b81a 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_aws_sdk.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_aws_sdk.py @@ -1,4 +1,6 @@ -from botocore.exceptions import ClientError +import logging + +from botocore.exceptions import ClientError, UnknownServiceError from localstack.aws.api.stepfunctions import HistoryEventType, TaskFailedEventDetails from localstack.aws.protocol.service_router import get_service_catalog @@ -22,9 +24,12 @@ from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails from localstack.services.stepfunctions.asl.utils.boto_client import boto_client_for +LOG = logging.getLogger(__name__) + class StateTaskServiceAwsSdk(StateTaskServiceCallback): - _NORMALISED_SERVICE_NAMES = {"dynamodb": "DynamoDb"} + # Defines bindings of lower-cased service names to the StepFunctions service name included in error messages. + _SERVICE_ERROR_NAMES = {"dynamodb": "DynamoDb", "sfn": "Sfn"} def from_state_props(self, state_props: StateProps) -> None: super().from_state_props(state_props=state_props) @@ -33,11 +38,30 @@ def _get_sfn_resource_type(self) -> str: return f"{self.resource.service_name}:{self.resource.api_name}" @staticmethod - def _normalise_service_name(service_name: str) -> str: + def _normalise_service_error_name(service_name: str) -> str: + # Computes the normalised service error name for the given service. + + # Return the explicit binding if one exists. service_name_lower = service_name.lower() - if service_name_lower in StateTaskServiceAwsSdk._NORMALISED_SERVICE_NAMES: - return StateTaskServiceAwsSdk._NORMALISED_SERVICE_NAMES[service_name_lower] - return get_service_catalog().get(service_name).service_id.replace(" ", "") + if service_name_lower in StateTaskServiceAwsSdk._SERVICE_ERROR_NAMES: + return StateTaskServiceAwsSdk._SERVICE_ERROR_NAMES[service_name_lower] + + # Attempt to retrieve the service name from the catalog. + try: + service_model = get_service_catalog().get(service_name) + if service_model is not None: + sfn_normalised_service_name = service_model.service_id.replace(" ", "") + return sfn_normalised_service_name + except UnknownServiceError: + LOG.warning( + f"No service for name '{service_name}' when building aws-sdk service error name." + ) + + # Revert to returning the resource's service name and log the missing binding. + LOG.error( + f"No normalised service error name for aws-sdk integration was found for service: '{service_name}'" + ) + return service_name @staticmethod def _normalise_exception_name(norm_service_name: str, ex: Exception) -> str: @@ -66,7 +90,7 @@ def _get_task_failure_event(self, env: Environment, error: str, cause: str) -> F def _from_error(self, env: Environment, ex: Exception) -> FailureEvent: if isinstance(ex, ClientError): - norm_service_name: str = self._normalise_service_name(self.resource.api_name) + norm_service_name: str = self._normalise_service_error_name(self.resource.api_name) error: str = self._normalise_exception_name(norm_service_name, ex) error_message: str = ex.response["Error"]["Message"] diff --git a/localstack/services/stepfunctions/provider.py b/localstack/services/stepfunctions/provider.py index 56d5750d3b593..d129ef6a16e34 100644 --- a/localstack/services/stepfunctions/provider.py +++ b/localstack/services/stepfunctions/provider.py @@ -373,7 +373,7 @@ def send_task_success( raise TaskTimedOut() else: raise TaskDoesNotExist() - raise InvalidToken() + raise InvalidToken("Invalid token") def send_task_failure( self, @@ -396,7 +396,7 @@ def send_task_failure( raise TaskTimedOut() else: raise TaskDoesNotExist() - raise InvalidToken() + raise InvalidToken("Invalid token") def start_execution( self, diff --git a/tests/aws/services/stepfunctions/templates/services/services_templates.py b/tests/aws/services/stepfunctions/templates/services/services_templates.py index 586c0e32d234d..479a0cc98c8d9 100644 --- a/tests/aws/services/stepfunctions/templates/services/services_templates.py +++ b/tests/aws/services/stepfunctions/templates/services/services_templates.py @@ -20,6 +20,12 @@ class ServicesTemplates(TemplateLoader): AWS_SDK_DYNAMODB_PUT_UPDATE_GET_ITEM: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/aws_sdk_dynamodb_put_update_get_item.json5" ) + AWS_SDK_SFN_SEND_TASK_FAILURE: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/aws_sdk_sfn_send_task_failure.json5" + ) + AWS_SDK_SFN_SEND_TASK_SUCCESS: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/aws_sdk_sfn_send_task_success.json5" + ) AWS_SDK_SFN_START_EXECUTION: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/aws_sdk_sfn_start_execution.json5" ) diff --git a/tests/aws/services/stepfunctions/templates/services/statemachines/aws_sdk_sfn_send_task_failure.json5 b/tests/aws/services/stepfunctions/templates/services/statemachines/aws_sdk_sfn_send_task_failure.json5 new file mode 100644 index 0000000000000..ac7477e0a1ac6 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/services/statemachines/aws_sdk_sfn_send_task_failure.json5 @@ -0,0 +1,14 @@ +{ + "Comment": "AWS_SDK_SFN_SEND_TASK_FAILURE", + "StartAt": "StartExecution", + "States": { + "StartExecution": { + "Type": "Task", + "Resource": "arn:aws:states:::aws-sdk:sfn:sendTaskFailure", + "Parameters": { + "TaskToken.$": "$$.Execution.Input.TaskToken" + }, + "End": true + } + } +} diff --git a/tests/aws/services/stepfunctions/templates/services/statemachines/aws_sdk_sfn_send_task_success.json5 b/tests/aws/services/stepfunctions/templates/services/statemachines/aws_sdk_sfn_send_task_success.json5 new file mode 100644 index 0000000000000..81ba4b82542f7 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/services/statemachines/aws_sdk_sfn_send_task_success.json5 @@ -0,0 +1,15 @@ +{ + "Comment": "AWS_SDK_SFN_SEND_TASK_SUCCESS", + "StartAt": "StartExecution", + "States": { + "StartExecution": { + "Type": "Task", + "Resource": "arn:aws:states:::aws-sdk:sfn:sendTaskSuccess", + "Parameters": { + "Output": "ParameterOutput", + "TaskToken.$": "$$.Execution.Input.TaskToken" + }, + "End": true + } + } +} diff --git a/tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py b/tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py index 54a44ce9c67e8..594e119acee23 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py +++ b/tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py @@ -1,5 +1,6 @@ import json +import pytest from localstack_snapshot.snapshots.transformer import JsonpathTransformer from localstack.testing.pytest import markers @@ -139,6 +140,43 @@ def test_dynamodb_put_update_get_item( exec_input, ) + @markers.snapshot.skip_snapshot_verify( + paths=[ + # TODO: aws-sdk SFN integration now appears to be inserting decorated error names into the cause messages. + # Upcoming work should collect more failure snapshot involving other aws-sdk integrations and trace a + # picture of generalisability of this behaviour. + # Hence insert this into the logic of the aws-sdk integration. + "$..cause" + ] + ) + @pytest.mark.parametrize( + "state_machine_template", + [ + ST.load_sfn_template(ST.AWS_SDK_SFN_SEND_TASK_SUCCESS), + ST.load_sfn_template(ST.AWS_SDK_SFN_SEND_TASK_FAILURE), + ], + ) + @markers.aws.validated + def test_sfn_send_task_outcome_with_no_such_token( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + state_machine_template, + ): + definition = json.dumps(state_machine_template) + + exec_input = json.dumps({"TaskToken": "NoSuchTaskToken"}) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + @markers.aws.validated def test_sfn_start_execution( self, diff --git a/tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.snapshot.json b/tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.snapshot.json index 2607c918d8b50..8e7dc484299a5 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.snapshot.json @@ -1487,5 +1487,184 @@ } } } + }, + "tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py::TestTaskServiceAwsSdk::test_sfn_send_task_outcome_with_no_such_token[state_machine_template0]": { + "recorded-date": "10-04-2024, 18:55:26", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "TaskToken": "NoSuchTaskToken" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "TaskToken": "NoSuchTaskToken" + }, + "inputDetails": { + "truncated": false + }, + "name": "StartExecution" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "Output": "ParameterOutput", + "TaskToken": "NoSuchTaskToken" + }, + "region": "", + "resource": "sendTaskSuccess", + "resourceType": "aws-sdk:sfn" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "sendTaskSuccess", + "resourceType": "aws-sdk:sfn" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskFailedEventDetails": { + "cause": "Invalid Token: 'Invalid token' (Service: Sfn, Status Code: 400, Request ID: )", + "error": "Sfn.InvalidTokenException", + "resource": "sendTaskSuccess", + "resourceType": "aws-sdk:sfn" + }, + "timestamp": "timestamp", + "type": "TaskFailed" + }, + { + "executionFailedEventDetails": { + "cause": "Invalid Token: 'Invalid token' (Service: Sfn, Status Code: 400, Request ID: )", + "error": "Sfn.InvalidTokenException" + }, + "id": 6, + "previousEventId": 5, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py::TestTaskServiceAwsSdk::test_sfn_send_task_outcome_with_no_such_token[state_machine_template1]": { + "recorded-date": "10-04-2024, 18:55:40", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "TaskToken": "NoSuchTaskToken" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "TaskToken": "NoSuchTaskToken" + }, + "inputDetails": { + "truncated": false + }, + "name": "StartExecution" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "TaskToken": "NoSuchTaskToken" + }, + "region": "", + "resource": "sendTaskFailure", + "resourceType": "aws-sdk:sfn" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "sendTaskFailure", + "resourceType": "aws-sdk:sfn" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskFailedEventDetails": { + "cause": "Invalid Token: 'Invalid token' (Service: Sfn, Status Code: 400, Request ID: )", + "error": "Sfn.InvalidTokenException", + "resource": "sendTaskFailure", + "resourceType": "aws-sdk:sfn" + }, + "timestamp": "timestamp", + "type": "TaskFailed" + }, + { + "executionFailedEventDetails": { + "cause": "Invalid Token: 'Invalid token' (Service: Sfn, Status Code: 400, Request ID: )", + "error": "Sfn.InvalidTokenException" + }, + "id": 6, + "previousEventId": 5, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.validation.json b/tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.validation.json index 8da5fdfdeb17d..9418db27ea537 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.validation.json +++ b/tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.validation.json @@ -11,7 +11,13 @@ "tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py::TestTaskServiceAwsSdk::test_list_secrets": { "last_validated_date": "2023-06-22T11:59:49+00:00" }, + "tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py::TestTaskServiceAwsSdk::test_sfn_send_task_outcome_with_no_such_token[state_machine_template0]": { + "last_validated_date": "2024-04-10T18:55:26+00:00" + }, + "tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py::TestTaskServiceAwsSdk::test_sfn_send_task_outcome_with_no_such_token[state_machine_template1]": { + "last_validated_date": "2024-04-10T18:55:40+00:00" + }, "tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py::TestTaskServiceAwsSdk::test_sfn_start_execution_implicit_json_serialisation": { "last_validated_date": "2024-02-05T11:29:16+00:00" } -} \ No newline at end of file +} From 0ed34c59f7d2514da614866a3ca108d88941dd0a Mon Sep 17 00:00:00 2001 From: Sannya Singal <32308435+sannya-singal@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:33:33 +0530 Subject: [PATCH 048/169] fix `connect` unit tests for cross-accounts (#10637) --- tests/unit/aws/test_connect.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/aws/test_connect.py b/tests/unit/aws/test_connect.py index 1c2cbb93aa3ef..02ff5c7c3baf0 100644 --- a/tests/unit/aws/test_connect.py +++ b/tests/unit/aws/test_connect.py @@ -68,7 +68,7 @@ def test_external_client_dto_is_not_registered(self): mock.meta.events.register.assert_not_called() @patch.object(ExternalClientFactory, "_get_client") - def test_external_client_credentials_origin(self, mock, monkeypatch): + def test_external_client_credentials_origin(self, mock, region_name, monkeypatch): connect_to = ExternalClientFactory(use_ssl=True) connect_to.get_client( "abc", region_name="xx-south-1", aws_access_key_id="foo", aws_secret_access_key="bar" @@ -92,7 +92,7 @@ def test_external_client_credentials_origin(self, mock, monkeypatch): ) mock.assert_called_once_with( service_name="def", - region_name="us-east-1", + region_name=region_name, use_ssl=True, verify=False, endpoint_url="http://localhost:4566", @@ -107,7 +107,7 @@ def test_external_client_credentials_origin(self, mock, monkeypatch): connect_to.get_client("def", region_name=None, aws_access_key_id=TEST_AWS_ACCESS_KEY_ID) mock.assert_called_once_with( service_name="def", - region_name="us-east-1", + region_name=region_name, use_ssl=True, verify=False, endpoint_url="http://localhost:4566", @@ -119,7 +119,7 @@ def test_external_client_credentials_origin(self, mock, monkeypatch): @patch.object(ExternalAwsClientFactory, "_get_client") def test_external_aws_client_credentials_loaded_from_env_if_set_to_none( - self, mock, monkeypatch + self, mock, region_name, monkeypatch ): session = boto3.Session() connect_to = ExternalAwsClientFactory(use_ssl=True, session=session) @@ -147,7 +147,7 @@ def test_external_aws_client_credentials_loaded_from_env_if_set_to_none( ) mock.assert_called_once_with( service_name="def", - region_name="us-east-1", + region_name=region_name, use_ssl=True, verify=True, endpoint_url=None, From 879045934c972478d039963f5ebb3fdbbd054091 Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Thu, 11 Apr 2024 10:17:37 +0200 Subject: [PATCH 049/169] refactor the TCP proxy into a clearer, server based construct (#10490) --- localstack/services/edge.py | 17 ++-- localstack/utils/server/proxy_server.py | 75 ---------------- localstack/utils/server/tcp_proxy.py | 108 ++++++++++++++++++++++++ tests/unit/test_edge.py | 4 +- tests/unit/utils/test_tcp_proxy.py | 78 +++++++++++++++++ 5 files changed, 198 insertions(+), 84 deletions(-) delete mode 100644 localstack/utils/server/proxy_server.py create mode 100644 localstack/utils/server/tcp_proxy.py create mode 100644 tests/unit/utils/test_tcp_proxy.py diff --git a/localstack/services/edge.py b/localstack/services/edge.py index 90cf3e964b653..495525867383d 100644 --- a/localstack/services/edge.py +++ b/localstack/services/edge.py @@ -15,8 +15,8 @@ from localstack.utils.collections import split_list_by from localstack.utils.net import get_free_tcp_port from localstack.utils.run import is_root, run -from localstack.utils.server.proxy_server import start_tcp_proxy -from localstack.utils.threads import FuncThread, start_thread +from localstack.utils.server.tcp_proxy import TCPProxy +from localstack.utils.threads import start_thread T = TypeVar("T") @@ -74,7 +74,7 @@ def start_component( def start_proxy( listen_str: str, target_address: HostAndPort, asynchronous: bool = False -) -> FuncThread: +) -> TCPProxy: """ Starts a TCP proxy to perform a low-level forwarding of incoming requests. @@ -94,15 +94,18 @@ def start_proxy( def do_start_tcp_proxy( listen: HostAndPort, target_address: HostAndPort, asynchronous: bool = False -) -> FuncThread: +) -> TCPProxy: src = str(listen) dst = str(target_address) LOG.debug("Starting Local TCP Proxy: %s -> %s", src, dst) - proxy = start_thread( - lambda *args, **kwargs: start_tcp_proxy(src=src, dst=dst, handler=None, **kwargs), - name="edge-tcp-proxy", + proxy = TCPProxy( + target_address=target_address.host, + target_port=target_address.port, + host=listen.host, + port=listen.port, ) + proxy.start() if not asynchronous: proxy.join() return proxy diff --git a/localstack/utils/server/proxy_server.py b/localstack/utils/server/proxy_server.py deleted file mode 100644 index fad25e321acdb..0000000000000 --- a/localstack/utils/server/proxy_server.py +++ /dev/null @@ -1,75 +0,0 @@ -import logging -import select -import socket -from typing import Union - -from localstack.constants import BIND_HOST, LOCALHOST_IP -from localstack.utils.functions import run_safe -from localstack.utils.numbers import is_number -from localstack.utils.threads import start_worker_thread - -LOG = logging.getLogger(__name__) - -BUFFER_SIZE = 2**10 # 1024 - -PortOrUrl = Union[str, int] - - -def start_tcp_proxy(src, dst, handler, **kwargs): - """Run a simple TCP proxy (tunneling raw connections from src to dst), using a message handler - that can be used to intercept messages and return predefined responses for certain requests. - - Arguments: - src -- Source IP address and port string. I.e.: '127.0.0.1:8000' - dst -- Destination IP address and port. I.e.: '127.0.0.1:8888' - handler -- a handler function to intercept requests (returns tuple (forward_value, response_value)) - """ - - src = "%s:%s" % (BIND_HOST, src) if is_number(src) else src - dst = "%s:%s" % (LOCALHOST_IP, dst) if is_number(dst) else dst - thread = kwargs.get("_thread") - - def ip_to_tuple(ip): - ip, port = ip.split(":") - return ip, int(port) - - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.bind(ip_to_tuple(src)) - s.listen(1) - s.settimeout(10) - - def handle_request(s_src, thread): - s_dst = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - s_dst.connect(ip_to_tuple(dst)) - - sockets = [s_src, s_dst] - while thread.running: - s_read, _, _ = select.select(sockets, [], []) - - for s in s_read: - data = s.recv(BUFFER_SIZE) - if data in [b"", "", None]: - return - - if s == s_src: - forward, response = data, None - if handler: - forward, response = handler(data) - if forward is not None: - s_dst.sendall(forward) - elif response is not None: - s_src.sendall(response) - return - elif s == s_dst: - s_src.sendall(data) - finally: - run_safe(s_src.close) - run_safe(s_dst.close) - - while thread.running: - try: - src_socket, _ = s.accept() - start_worker_thread(lambda *args, _thread: handle_request(src_socket, _thread)) - except socket.timeout: - pass diff --git a/localstack/utils/server/tcp_proxy.py b/localstack/utils/server/tcp_proxy.py new file mode 100644 index 0000000000000..943120307a056 --- /dev/null +++ b/localstack/utils/server/tcp_proxy.py @@ -0,0 +1,108 @@ +import logging +import select +import socket +from concurrent.futures import ThreadPoolExecutor +from typing import Callable + +from localstack.utils.serving import Server + +LOG = logging.getLogger(__name__) + + +class TCPProxy(Server): + """ + Server based TCP proxy abstraction. + This uses a ThreadPoolExecutor, so the maximum number of parallel connections is limited. + """ + + _target_address: str + _target_port: int + _handler: Callable[[bytes], tuple[bytes, bytes]] | None + _buffer_size: int + _thread_pool: ThreadPoolExecutor + _server_socket: socket.socket | None + + def __init__( + self, + target_address: str, + target_port: int, + port: int, + host: str, + handler: Callable[[bytes], tuple[bytes, bytes]] = None, + ) -> None: + super().__init__(port, host) + self._target_address = target_address + self._target_port = target_port + self._handler = handler + self._buffer_size = 1024 + # thread pool limited to 64 workers for now - can be increased or made configurable if this should not suffice + # for certain use cases + self._thread_pool = ThreadPoolExecutor(thread_name_prefix="tcp-proxy", max_workers=64) + self._server_socket = None + + def _handle_request(self, s_src: socket.socket): + try: + s_dst = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + with s_src as s_src, s_dst as s_dst: + s_dst.connect((self._target_address, self._target_port)) + + sockets = [s_src, s_dst] + while not self._stopped.is_set(): + s_read, _, _ = select.select(sockets, [], [], 1) + + for s in s_read: + data = s.recv(self._buffer_size) + if not data: + return + + if s == s_src: + forward, response = data, None + if self._handler: + forward, response = self._handler(data) + if forward is not None: + s_dst.sendall(forward) + elif response is not None: + s_src.sendall(response) + return + elif s == s_dst: + s_src.sendall(data) + except Exception as e: + LOG.error( + "Error while handling request from %s to %s:%s: %s", + s_src.getpeername(), + self._target_address, + self._target_port, + e, + ) + + def do_run(self): + self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._server_socket.bind((self.host, self.port)) + self._server_socket.listen(1) + self._server_socket.settimeout(10) + LOG.debug( + "Starting TCP proxy bound on %s:%s forwarding to %s:%s", + self.host, + self.port, + self._target_address, + self._target_port, + ) + + with self._server_socket: + while not self._stopped.is_set(): + try: + src_socket, _ = self._server_socket.accept() + self._thread_pool.submit(self._handle_request, src_socket) + except socket.timeout: + pass + except OSError as e: + # avoid creating an error message if OSError is thrown due to socket closing + if not self._stopped.is_set(): + LOG.warning("Error during during TCPProxy socket accept: %s", e) + + def do_shutdown(self): + if self._server_socket: + self._server_socket.shutdown(socket.SHUT_RDWR) + self._server_socket.close() + self._thread_pool.shutdown(cancel_futures=True) + LOG.debug("Shut down TCPProxy on %s:%s", self.host, self.port) diff --git a/tests/unit/test_edge.py b/tests/unit/test_edge.py index 516fb01225fb5..4047445a0cd36 100644 --- a/tests/unit/test_edge.py +++ b/tests/unit/test_edge.py @@ -36,7 +36,7 @@ def test_edge_tcp_proxy(httpserver): assert response.status_code == 200 assert response.text == "Target Server Response" finally: - proxy_server.stop() + proxy_server.shutdown() def test_edge_tcp_proxy_does_not_terminate_on_connection_error(): @@ -71,4 +71,4 @@ def test_edge_tcp_proxy_does_not_terminate_on_connection_error(): if httpserver.is_running(): httpserver.stop() finally: - proxy_server.stop() + proxy_server.shutdown() diff --git a/tests/unit/utils/test_tcp_proxy.py b/tests/unit/utils/test_tcp_proxy.py new file mode 100644 index 0000000000000..53105f203edec --- /dev/null +++ b/tests/unit/utils/test_tcp_proxy.py @@ -0,0 +1,78 @@ +import socket +import threading +from threading import Thread + +import pytest + +from localstack.utils.net import get_free_tcp_port, is_port_open +from localstack.utils.server.tcp_proxy import TCPProxy + + +class TestTCPProxy: + @pytest.fixture + def tcp_proxy(self): + proxies: list[TCPProxy] = [] + + def _create_proxy(target_address: str, target_port: int) -> TCPProxy: + port = get_free_tcp_port() + proxy = TCPProxy( + target_address=target_address, target_port=target_port, port=port, host="127.0.0.1" + ) + proxies.append(proxy) + return proxy + + yield _create_proxy + + for proxy in proxies: + proxy.shutdown() + + @pytest.fixture + def tcp_echo_server_port(self): + """Single threaded TCP echo server""" + stopped = threading.Event() + s_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s_sock.bind(("127.0.0.1", 0)) + port = s_sock.getsockname()[1] + + def _run_echo_server(): + with s_sock: + s_sock.listen(1) + while not stopped.is_set(): + try: + conn, _ = s_sock.accept() + except OSError: + # this happens when we shut down the server socket + pass + with conn: + while not stopped.is_set(): + data = conn.recv(1024) + if not data: + break + conn.sendall(data) + + echo_server_thread = Thread(target=_run_echo_server) + echo_server_thread.start() + + yield port + + stopped.set() + s_sock.shutdown(socket.SHUT_RDWR) + s_sock.close() + echo_server_thread.join(5) + assert not echo_server_thread.is_alive() + + def test_tcp_proxy_lifecycle(self, tcp_proxy, tcp_echo_server_port): + proxy = tcp_proxy(target_address="127.0.0.1", target_port=tcp_echo_server_port) + + proxy.start() + proxy.wait_is_up(timeout=5) + + with socket.create_connection(("127.0.0.1", proxy.port)) as c_sock: + data = b"test data" + c_sock.sendall(data) + received_data = c_sock.recv(1024) + assert received_data == data + + proxy.shutdown() + proxy.join(5) + assert not is_port_open(proxy.port) From 748825a7dce0470fe3707f35e86aaf0679ab6c89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20N=C3=A9meth?= Date: Thu, 11 Apr 2024 13:29:29 +0300 Subject: [PATCH 050/169] Add update template_body (#10556) Co-authored-by: Daniel Fangl Co-authored-by: Nevil Macwan --- .../engine/template_deployer.py | 7 ++- .../services/cloudformation/provider.py | 8 +++- .../cloudformation/api/test_update_stack.py | 45 +++++++++++++++++++ .../api/test_update_stack.snapshot.json | 27 +++++++++++ .../api/test_update_stack.validation.json | 3 ++ 5 files changed, 88 insertions(+), 2 deletions(-) diff --git a/localstack/services/cloudformation/engine/template_deployer.py b/localstack/services/cloudformation/engine/template_deployer.py index ecc910468c9c2..73c2f25928a12 100644 --- a/localstack/services/cloudformation/engine/template_deployer.py +++ b/localstack/services/cloudformation/engine/template_deployer.py @@ -1157,6 +1157,7 @@ def apply_changes( # TODO: ideally the entire template has to be replaced, but tricky at this point existing_stack.template["Metadata"] = new_stack.template.get("Metadata") + existing_stack.template_body = new_stack.template_body # start deployment loop return self.apply_changes_in_loop( @@ -1164,7 +1165,11 @@ def apply_changes( ) def apply_changes_in_loop( - self, changes: list[ChangeConfig], stack, action: Optional[str] = None, new_stack=None + self, + changes: list[ChangeConfig], + stack, + action: Optional[str] = None, + new_stack=None, ): def _run(*args): status_reason = None diff --git a/localstack/services/cloudformation/provider.py b/localstack/services/cloudformation/provider.py index 478d22c9f570b..c2341ddc04580 100644 --- a/localstack/services/cloudformation/provider.py +++ b/localstack/services/cloudformation/provider.py @@ -373,12 +373,17 @@ def update_stack( deployer = template_deployer.TemplateDeployer(context.account_id, context.region, stack) # TODO: there shouldn't be a "new" stack on update - new_stack = Stack(context.account_id, context.region, request, template) + new_stack = Stack( + context.account_id, context.region, request, template, request["TemplateBody"] + ) new_stack.set_resolved_parameters(resolved_parameters) stack.set_resolved_parameters(resolved_parameters) stack.set_resolved_stack_conditions(resolved_stack_conditions) try: deployer.update_stack(new_stack) + except NoStackUpdates as e: + stack.set_stack_status("UPDATE_COMPLETE") + raise ValidationError(str(e)) except Exception as e: stack.set_stack_status("UPDATE_FAILED") msg = f'Unable to update stack "{stack_name}": {e}' @@ -683,6 +688,7 @@ def create_change_set( ) # only set parameters for the changeset, then switch to stack on execute_change_set change_set.set_resolved_parameters(resolved_parameters) + change_set.template_body = template_body # TODO: evaluate conditions raw_conditions = transformed_template.get("Conditions", {}) diff --git a/tests/aws/services/cloudformation/api/test_update_stack.py b/tests/aws/services/cloudformation/api/test_update_stack.py index 495fdba5e9100..fedd7e30516c6 100644 --- a/tests/aws/services/cloudformation/api/test_update_stack.py +++ b/tests/aws/services/cloudformation/api/test_update_stack.py @@ -1,5 +1,6 @@ import json import os +import textwrap import botocore.errorfactory import botocore.exceptions @@ -407,3 +408,47 @@ def test_update_with_rollback_configuration(deploy_cfn_template, aws_client): # cleanup aws_client.cloudwatch.delete_alarms(AlarmNames=["HighResourceUsage"]) + + +@markers.aws.validated +@markers.snapshot.skip_snapshot_verify(["$..Stacks..ChangeSetId"]) +def test_diff_after_update(deploy_cfn_template, aws_client, snapshot): + template_1 = textwrap.dedent(""" + Resources: + SimpleParam: + Type: AWS::SSM::Parameter + Properties: + Value: before-stack-update + Type: String + """) + template_2 = textwrap.dedent(""" + Resources: + SimpleParam1: + Type: AWS::SSM::Parameter + Properties: + Value: after-stack-update + Type: String + """) + + stack = deploy_cfn_template( + template=template_1, + ) + + aws_client.cloudformation.get_waiter("stack_create_complete").wait(StackName=stack.stack_name) + aws_client.cloudformation.update_stack( + StackName=stack.stack_name, + TemplateBody=template_2, + ) + aws_client.cloudformation.get_waiter("stack_update_complete").wait(StackName=stack.stack_name) + get_template_response = aws_client.cloudformation.get_template(StackName=stack.stack_name) + snapshot.match("get-template-response", get_template_response) + + with pytest.raises(botocore.exceptions.ClientError) as exc_info: + aws_client.cloudformation.update_stack( + StackName=stack.stack_name, + TemplateBody=template_2, + ) + snapshot.match("update-error", exc_info.value.response) + + describe_stack_response = aws_client.cloudformation.describe_stacks(StackName=stack.stack_name) + assert describe_stack_response["Stacks"][0]["StackStatus"] == "UPDATE_COMPLETE" diff --git a/tests/aws/services/cloudformation/api/test_update_stack.snapshot.json b/tests/aws/services/cloudformation/api/test_update_stack.snapshot.json index 3a7d61f0ccaff..5021f4e042d1d 100644 --- a/tests/aws/services/cloudformation/api/test_update_stack.snapshot.json +++ b/tests/aws/services/cloudformation/api/test_update_stack.snapshot.json @@ -104,5 +104,32 @@ } } } + }, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_diff_after_update": { + "recorded-date": "09-04-2024, 06:19:23", + "recorded-content": { + "get-template-response": { + "StagesAvailable": [ + "Original", + "Processed" + ], + "TemplateBody": "\nResources:\n SimpleParam1:\n Type: AWS::SSM::Parameter\n Properties:\n Value: after-stack-update\n Type: String\n", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "update-error": { + "Error": { + "Code": "ValidationError", + "Message": "No updates are to be performed.", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } } } diff --git a/tests/aws/services/cloudformation/api/test_update_stack.validation.json b/tests/aws/services/cloudformation/api/test_update_stack.validation.json index f9af2c830a91a..3821105abaa2a 100644 --- a/tests/aws/services/cloudformation/api/test_update_stack.validation.json +++ b/tests/aws/services/cloudformation/api/test_update_stack.validation.json @@ -2,6 +2,9 @@ "tests/aws/services/cloudformation/api/test_update_stack.py::test_basic_update": { "last_validated_date": "2022-11-21T07:27:37+00:00" }, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_diff_after_update": { + "last_validated_date": "2024-04-09T06:19:23+00:00" + }, "tests/aws/services/cloudformation/api/test_update_stack.py::test_no_template_error": { "last_validated_date": "2022-11-21T07:57:45+00:00" }, From 5d9ec397c7fc43dd8202f76c12094cc9a78ff4f3 Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Fri, 12 Apr 2024 10:31:16 +0200 Subject: [PATCH 051/169] fix metrics upload failure on community PRs (#10643) --- .circleci/config.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 684138b3b7e1f..51c238d067410 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -506,12 +506,18 @@ jobs: - run: name: Upload test metrics and implemented coverage data to tinybird command: | - source .venv/bin/activate - mkdir parity_metrics && mv target/metric_reports/metric-report-raw-data-*amd64*.csv parity_metrics - METRIC_REPORT_DIR_PATH=parity_metrics \ - IMPLEMENTATION_COVERAGE_FILE=scripts/implementation_coverage_full.csv \ - SOURCE_TYPE=community \ - python -m scripts.tinybird.upload_raw_test_metrics_and_coverage + # check if a fork-only env var is set (https://circleci.com/docs/variables/) + if [ -z "$CIRCLE_PR_REPONAME" ]; then + source .venv/bin/activate + mkdir parity_metrics && mv target/metric_reports/metric-report-raw-data-*amd64*.csv parity_metrics + METRIC_REPORT_DIR_PATH=parity_metrics \ + IMPLEMENTATION_COVERAGE_FILE=scripts/implementation_coverage_full.csv \ + SOURCE_TYPE=community \ + python -m scripts.tinybird.upload_raw_test_metrics_and_coverage + else + echo "Skipping parity reporting to tinybird (no credentials, running on fork)..." + fi + - run: name: Create Coverage Diff (Code Coverage) # pycobertura diff will return with exit code 0-3 -> we currently expect 2 (2: the changes worsened the overall coverage), From 4cc862fb01733e39acea844be8da9077d2cfa145 Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Fri, 12 Apr 2024 11:14:10 +0100 Subject: [PATCH 052/169] Check for VPC existence for hostedzone (#10634) --- localstack/services/route53/provider.py | 21 +++++++++ tests/aws/services/route53/test_route53.py | 25 +++++++++- .../route53/test_route53.snapshot.json | 46 ++++++++++++++++++- .../route53/test_route53.validation.json | 5 +- 4 files changed, 94 insertions(+), 3 deletions(-) diff --git a/localstack/services/route53/provider.py b/localstack/services/route53/provider.py index bfe2a2d7be0b8..d858fe7914ae8 100644 --- a/localstack/services/route53/provider.py +++ b/localstack/services/route53/provider.py @@ -2,6 +2,7 @@ from typing import Optional import moto.route53.models as route53_models +from botocore.exceptions import ClientError from moto.route53.models import route53_backends from localstack.aws.api import RequestContext @@ -17,11 +18,13 @@ HealthCheck, HealthCheckId, HostedZoneConfig, + InvalidVPCId, Nonce, NoSuchHealthCheck, ResourceId, Route53Api, ) +from localstack.aws.connect import connect_to from localstack.services.moto import call_moto from localstack.services.plugins import ServiceLifecycleHook @@ -37,6 +40,24 @@ def create_hosted_zone( delegation_set_id: ResourceId = None, **kwargs, ) -> CreateHostedZoneResponse: + # private hosted zones cannot be created in a VPC that does not exist + # check that the VPC exists + if vpc: + vpc_id = vpc.get("VPCId") + vpc_region = vpc.get("VPCRegion") + if not vpc_id or not vpc_region: + raise Exception( + "VPCId and VPCRegion must be specified when creating a private hosted zone" + ) + try: + connect_to( + aws_access_key_id=context.account_id, region_name=vpc_region + ).ec2.describe_vpcs(VpcIds=[vpc_id]) + except ClientError as e: + if e.response.get("Error", {}).get("Code") == "InvalidVpcID.NotFound": + raise InvalidVPCId("The VPC ID is invalid.", sender_fault=True) from e + raise e + response = call_moto(context) # moto does not populate the VPC struct of the response if creating a private hosted zone diff --git a/tests/aws/services/route53/test_route53.py b/tests/aws/services/route53/test_route53.py index 160a772c6f5f7..d7a62b4eaa07a 100644 --- a/tests/aws/services/route53/test_route53.py +++ b/tests/aws/services/route53/test_route53.py @@ -49,7 +49,13 @@ def test_crud_health_check(self, aws_client): assert "NoSuchHealthCheck" in str(ctx.value) @markers.aws.validated - @markers.snapshot.skip_snapshot_verify(paths=["$..HostedZone.CallerReference"]) + @markers.snapshot.skip_snapshot_verify( + paths=[ + "$..HostedZone.CallerReference", + # moto does not return MaxItems for list_hosted_zones_by_vpc + "$..MaxItems", + ] + ) def test_create_private_hosted_zone( self, region_name, aws_client, cleanups, snapshot, hosted_zone ): @@ -76,6 +82,13 @@ def test_create_private_hosted_zone( response = aws_client.route53.get_hosted_zone(Id=zone_id) snapshot.match("get_hosted_zone", response) + response = aws_client.route53.list_hosted_zones_by_vpc(VPCId=vpc_id, VPCRegion=region_name) + snapshot.match("list_hosted_zones_by_vpc", response) + + response = aws_client.route53.list_hosted_zones() + zones = [zone for zone in response["HostedZones"] if name in zone["Name"]] + snapshot.match("list_hosted_zones", zones) + @markers.aws.unknown def test_associate_vpc_with_hosted_zone( self, cleanups, hosted_zone, aws_client, account_id, region_name @@ -150,6 +163,16 @@ def test_associate_vpc_with_hosted_zone( VPC={"VPCRegion": vpc_region, "VPCId": vpc2_id}, ) + @markers.aws.validated + def test_create_hosted_zone_in_non_existent_vpc( + self, aws_client, hosted_zone, snapshot, region_name + ): + vpc = {"VPCId": "non-existent", "VPCRegion": region_name} + with pytest.raises(aws_client.route53.exceptions.InvalidVPCId) as exc_info: + hosted_zone(Name=f"zone-{short_uid()}.com", VPC=vpc) + + snapshot.match("failure-response", exc_info.value.response) + @markers.aws.unknown def test_reusable_delegation_sets(self, aws_client): client = aws_client.route53 diff --git a/tests/aws/services/route53/test_route53.snapshot.json b/tests/aws/services/route53/test_route53.snapshot.json index 181a7ab22f6c5..e82bcf05108b1 100644 --- a/tests/aws/services/route53/test_route53.snapshot.json +++ b/tests/aws/services/route53/test_route53.snapshot.json @@ -47,7 +47,7 @@ } }, "tests/aws/services/route53/test_route53.py::TestRoute53::test_create_private_hosted_zone": { - "recorded-date": "15-12-2023, 15:20:07", + "recorded-date": "11-04-2024, 14:03:14", "recorded-content": { "create-hosted-zone-response": { "ChangeInfo": { @@ -96,6 +96,50 @@ "HTTPHeaders": {}, "HTTPStatusCode": 200 } + }, + "list_hosted_zones_by_vpc": { + "HostedZoneSummaries": [ + { + "HostedZoneId": "", + "Name": "", + "Owner": { + "OwningAccount": "111111111111" + } + } + ], + "MaxItems": "100", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_hosted_zones": [ + { + "Id": "/hostedzone/", + "Name": "", + "CallerReference": "", + "Config": { + "Comment": "test", + "PrivateZone": true + }, + "ResourceRecordSetCount": 2 + } + ] + } + }, + "tests/aws/services/route53/test_route53.py::TestRoute53::test_create_hosted_zone_in_non_existent_vpc": { + "recorded-date": "10-04-2024, 15:47:22", + "recorded-content": { + "failure-response": { + "Error": { + "Code": "InvalidVPCId", + "Message": "The VPC ID is invalid.", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } } } } diff --git a/tests/aws/services/route53/test_route53.validation.json b/tests/aws/services/route53/test_route53.validation.json index 60db1366275ea..dedc896d977d7 100644 --- a/tests/aws/services/route53/test_route53.validation.json +++ b/tests/aws/services/route53/test_route53.validation.json @@ -2,7 +2,10 @@ "tests/aws/services/route53/test_route53.py::TestRoute53::test_create_hosted_zone": { "last_validated_date": "2023-11-02T11:59:59+00:00" }, + "tests/aws/services/route53/test_route53.py::TestRoute53::test_create_hosted_zone_in_non_existent_vpc": { + "last_validated_date": "2024-04-10T15:47:22+00:00" + }, "tests/aws/services/route53/test_route53.py::TestRoute53::test_create_private_hosted_zone": { - "last_validated_date": "2023-12-15T14:20:07+00:00" + "last_validated_date": "2024-04-11T14:03:14+00:00" } } From d687094e455b50eb3f56650380a2064397efa316 Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Fri, 12 Apr 2024 12:17:35 +0100 Subject: [PATCH 053/169] More consistent CFn logging for individual resources (#10639) --- .../engine/template_deployer.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/localstack/services/cloudformation/engine/template_deployer.py b/localstack/services/cloudformation/engine/template_deployer.py index 73c2f25928a12..69e6b65fb1e81 100644 --- a/localstack/services/cloudformation/engine/template_deployer.py +++ b/localstack/services/cloudformation/engine/template_deployer.py @@ -895,7 +895,7 @@ def _safe_lookup_is_deleted(r_id): } if len(resources) == 0: break - for resource_id, resource in resources.items(): + for i, (resource_id, resource) in enumerate(resources.items()): try: # TODO: cache condition value in resource details on deployment and use cached value here if evaluate_resource_condition( @@ -907,6 +907,14 @@ def _safe_lookup_is_deleted(r_id): "Remove", logical_resource_id=resource_id ) # TODO: check actual return value + LOG.debug( + 'Handling "Remove" for resource "%s" (%s/%s) type "%s" in loop iteration %s', + resource_id, + i + 1, + len(resources), + resource["ResourceType"], + iteration_cycle, + ) executor.deploy_loop(resource, resource_provider_payload) self.stack.set_resource_status(resource_id, "DELETE_COMPLETE") except Exception as e: @@ -1248,6 +1256,15 @@ def do_apply_changes_in_loop(self, changes, stack): if not should_remove: del changes[j] continue + LOG.debug( + 'Handling "%s" for resource "%s" (%s/%s) type "%s" in loop iteration %s', + action, + resource_id, + j + 1, + len(changes), + res_change["ResourceType"], + i + 1, + ) self.apply_change(change, stack=stack) changes_done.append(change) del changes[j] From f4e4144a72f985873f504b5b7f88b496328fee77 Mon Sep 17 00:00:00 2001 From: Viren Nadkarni Date: Fri, 12 Apr 2024 17:16:08 +0530 Subject: [PATCH 054/169] Add libvirt as a build dependency (#10647) --- .github/workflows/tests-pro-integration.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests-pro-integration.yml b/.github/workflows/tests-pro-integration.yml index e5eb05afaafe9..0a7cdda40b3a1 100644 --- a/.github/workflows/tests-pro-integration.yml +++ b/.github/workflows/tests-pro-integration.yml @@ -217,8 +217,9 @@ jobs: - name: Install OS packages run: | sudo apt-get update + # libvirt-dev is required by the libvirt-python Python package # postgresql-14 pin is required to make explicit install of the version from the Ubuntu repos and not PGDG repos - sudo apt-get install -y --allow-downgrades libsnappy-dev jq postgresql-14=14.11-0ubuntu0* postgresql-client postgresql-plpython3 + sudo apt-get install -y --allow-downgrades libsnappy-dev jq postgresql-14=14.11-0ubuntu0* postgresql-client postgresql-plpython3 libvirt-dev - name: Cache Ext Dependencies (venv) if: inputs.disableCaching != true uses: actions/cache@v4 From e7b0badda832313ac4913ccddccf5a9f9a80b616 Mon Sep 17 00:00:00 2001 From: Bojan Miletic <4889922+Morijarti@users.noreply.github.com> Date: Fri, 12 Apr 2024 15:33:20 +0200 Subject: [PATCH 055/169] Implement Lambda Event Source Filter update in CFn Provider (#10632) --- .../aws_lambda_eventsourcemapping.py | 34 +++++++- .../cloudformation/resources/test_lambda.py | 44 ++++++++++ .../resources/test_lambda.snapshot.json | 85 +++++++++++++++++++ .../resources/test_lambda.validation.json | 3 + .../templates/lambda_dynamodb_filtering.yaml | 2 +- 5 files changed, 166 insertions(+), 2 deletions(-) diff --git a/localstack/services/lambda_/resource_providers/aws_lambda_eventsourcemapping.py b/localstack/services/lambda_/resource_providers/aws_lambda_eventsourcemapping.py index e67cf2b7d6062..c8340ca46e0d4 100644 --- a/localstack/services/lambda_/resource_providers/aws_lambda_eventsourcemapping.py +++ b/localstack/services/lambda_/resource_providers/aws_lambda_eventsourcemapping.py @@ -180,4 +180,36 @@ def update( - lambda:UpdateEventSourceMapping - lambda:GetEventSourceMapping """ - raise NotImplementedError + current_state = request.previous_state + model = request.desired_state + lambda_client = request.aws_client_factory.lambda_ + + params = util.select_attributes( + model=model, + params=[ + "FunctionName", + "Enabled", + "BatchSize", + "FilterCriteria", + "MaximumBatchingWindowInSeconds", + "DestinationConfig", + "MaximumRecordAgeInSeconds", + "BisectBatchOnFunctionError", + "MaximumRetryAttempts", + "ParallelizationFactor", + "SourceAccessConfigurations", + "TumblingWindowInSeconds", + "FunctionResponseTypes", + "ScalingConfig", + "DocumentDBEventSourceConfig", + ], + ) + lambda_client.update_event_source_mapping(UUID=current_state["Id"], **params) + + model["Id"] = current_state["Id"] + + return ProgressEvent( + status=OperationStatus.SUCCESS, + resource_model=model, + custom_context=request.custom_context, + ) diff --git a/tests/aws/services/cloudformation/resources/test_lambda.py b/tests/aws/services/cloudformation/resources/test_lambda.py index d1fd71508aeea..1cb361daa9bc0 100644 --- a/tests/aws/services/cloudformation/resources/test_lambda.py +++ b/tests/aws/services/cloudformation/resources/test_lambda.py @@ -50,6 +50,50 @@ def _assert_single_lambda_call(): retry(_assert_single_lambda_call, retries=30) +# TODO make a test simular to one above but for updated filtering + + +@markers.snapshot.skip_snapshot_verify( + ["$..EventSourceMappings..FunctionArn", "$..EventSourceMappings..LastProcessingResult"] +) +@markers.aws.validated +def test_lambda_w_dynamodb_event_filter_update(deploy_cfn_template, snapshot, aws_client): + snapshot.add_transformer(snapshot.transform.dynamodb_api()) + snapshot.add_transformer(snapshot.transform.lambda_api()) + function_name = f"test-fn-{short_uid()}" + table_name = f"ddb-tbl-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(table_name, "")) + + stack = deploy_cfn_template( + template_path=os.path.join( + os.path.dirname(__file__), "../../../templates/lambda_dynamodb_filtering.yaml" + ), + parameters={ + "FunctionName": function_name, + "TableName": table_name, + "Filter": '{"eventName": ["DELETE"]}', + }, + ) + source_mappings = aws_client.lambda_.list_event_source_mappings(FunctionName=function_name) + snapshot.match("source_mappings", source_mappings) + + deploy_cfn_template( + template_path=os.path.join( + os.path.dirname(__file__), "../../../templates/lambda_dynamodb_filtering.yaml" + ), + parameters={ + "FunctionName": function_name, + "TableName": table_name, + "Filter": '{"eventName": ["MODIFY"]}', + }, + stack_name=stack.stack_name, + is_update=True, + ) + + source_mappings = aws_client.lambda_.list_event_source_mappings(FunctionName=function_name) + snapshot.match("updated_source_mappings", source_mappings) + + @markers.snapshot.skip_snapshot_verify( paths=[ "$..Metadata", diff --git a/tests/aws/services/cloudformation/resources/test_lambda.snapshot.json b/tests/aws/services/cloudformation/resources/test_lambda.snapshot.json index 5aed054793ed0..224304b9e15bc 100644 --- a/tests/aws/services/cloudformation/resources/test_lambda.snapshot.json +++ b/tests/aws/services/cloudformation/resources/test_lambda.snapshot.json @@ -1411,5 +1411,90 @@ } } } + }, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_w_dynamodb_event_filter_update": { + "recorded-date": "11-04-2024, 18:29:12", + "recorded-content": { + "source_mappings": { + "EventSourceMappings": [ + { + "BatchSize": 1, + "BisectBatchOnFunctionError": false, + "DestinationConfig": { + "OnFailure": {} + }, + "EventSourceArn": "arn:aws:dynamodb::111111111111:table//stream/", + "FilterCriteria": { + "Filters": [ + { + "Pattern": { + "eventName": [ + "DELETE" + ] + } + } + ] + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionResponseTypes": [], + "LastModified": "datetime", + "LastProcessingResult": "No records processed", + "MaximumBatchingWindowInSeconds": 0, + "MaximumRecordAgeInSeconds": -1, + "MaximumRetryAttempts": -1, + "ParallelizationFactor": 1, + "StartingPosition": "TRIM_HORIZON", + "State": "Enabled", + "StateTransitionReason": "User action", + "TumblingWindowInSeconds": 0, + "UUID": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "updated_source_mappings": { + "EventSourceMappings": [ + { + "BatchSize": 1, + "BisectBatchOnFunctionError": false, + "DestinationConfig": { + "OnFailure": {} + }, + "EventSourceArn": "arn:aws:dynamodb::111111111111:table//stream/", + "FilterCriteria": { + "Filters": [ + { + "Pattern": { + "eventName": [ + "MODIFY" + ] + } + } + ] + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionResponseTypes": [], + "LastModified": "datetime", + "LastProcessingResult": "No records processed", + "MaximumBatchingWindowInSeconds": 0, + "MaximumRecordAgeInSeconds": -1, + "MaximumRetryAttempts": -1, + "ParallelizationFactor": 1, + "StartingPosition": "TRIM_HORIZON", + "State": "Enabled", + "StateTransitionReason": "User action", + "TumblingWindowInSeconds": 0, + "UUID": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/cloudformation/resources/test_lambda.validation.json b/tests/aws/services/cloudformation/resources/test_lambda.validation.json index 109fef1cae498..1a4ede1167750 100644 --- a/tests/aws/services/cloudformation/resources/test_lambda.validation.json +++ b/tests/aws/services/cloudformation/resources/test_lambda.validation.json @@ -32,6 +32,9 @@ "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_version": { "last_validated_date": "2023-02-27T16:31:32+00:00" }, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_w_dynamodb_event_filter_update": { + "last_validated_date": "2024-04-11T18:29:12+00:00" + }, "tests/aws/services/cloudformation/resources/test_lambda.py::test_multiple_lambda_permissions_for_singlefn": { "last_validated_date": "2023-03-09T21:07:56+00:00" } diff --git a/tests/aws/templates/lambda_dynamodb_filtering.yaml b/tests/aws/templates/lambda_dynamodb_filtering.yaml index 9a1f63742fbd5..be4792d824b0e 100644 --- a/tests/aws/templates/lambda_dynamodb_filtering.yaml +++ b/tests/aws/templates/lambda_dynamodb_filtering.yaml @@ -18,7 +18,7 @@ Resources: FunctionName: !Ref FunctionName InlineCode: exports.handler = async (event, context) => { console.log(JSON.stringify(event))} Handler: index.handler - Runtime: nodejs14.x + Runtime: nodejs20.x MemorySize: 128 Timeout: 100 Description: DynamoDB put event trigger. From 03c220a128bb541cfa2fe4a40338720d631bc7e1 Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Fri, 12 Apr 2024 19:59:41 +0100 Subject: [PATCH 056/169] Fix dev run container exiting from raw mode (#10646) --- localstack/utils/run.py | 37 +------------------------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/localstack/utils/run.py b/localstack/utils/run.py index 1312a4469ff03..dcc29d0dd8d7a 100644 --- a/localstack/utils/run.py +++ b/localstack/utils/run.py @@ -158,42 +158,7 @@ def run_interactive(command: List[str]): :param command: the command to pass to subprocess.Popen """ - # save original tty setting then set it to raw mode - import pty - import termios - import tty - - old_tty = termios.tcgetattr(sys.stdin) - tty.setraw(sys.stdin.fileno()) - - # open pseudo-terminal to interact with subprocess - master_fd, slave_fd = pty.openpty() - - try: - # use os.setsid() make it run in a new process group, or bash job control will not be enabled - p = subprocess.Popen( - command, - preexec_fn=os.setsid, - stdin=slave_fd, - stdout=slave_fd, - stderr=slave_fd, - universal_newlines=True, - ) - - while p.poll() is None: - r, w, e = select.select([sys.stdin, master_fd], [], []) - if sys.stdin in r: - d = os.read(sys.stdin.fileno(), 10240) - os.write(master_fd, d) - if d == b"\x04": - break - elif master_fd in r: - o = os.read(master_fd, 10240) - if o: - os.write(sys.stdout.fileno(), o) - finally: - # restore tty settings back - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty) + subprocess.check_call(command) def is_command_available(cmd: str) -> bool: From 0bb793695be5fd33cc5bcce72a705a5659eea64f Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Sat, 13 Apr 2024 09:47:38 +0200 Subject: [PATCH 057/169] adapt source distribution glob to new PEP 625 canonicalized name (#10652) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e573d6087019f..3fbbecc0c1297 100644 --- a/Makefile +++ b/Makefile @@ -79,7 +79,7 @@ dist: entrypoints ## Build source and built (wheel) distributions of the publish: clean-dist dist ## Publish the library to the central PyPi repository # make sure the dist archive contains a non-empty entry_points.txt file before uploading - tar --wildcards --to-stdout -xf dist/localstack-core*.tar.gz "localstack-core*/localstack_core.egg-info/entry_points.txt" | grep . > /dev/null 2>&1 || (echo "Refusing upload, localstack-core dist does not contain entrypoints." && exit 1) + tar --wildcards --to-stdout -xf dist/localstack_core*.tar.gz "localstack_core*/localstack_core.egg-info/entry_points.txt" | grep . > /dev/null 2>&1 || (echo "Refusing upload, localstack-core dist does not contain entrypoints." && exit 1) $(VENV_RUN); twine upload dist/* coveralls: ## Publish coveralls metrics From 5ca614ff9857b7b1d9c53407a64125642af73c69 Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Sat, 13 Apr 2024 12:25:56 +0200 Subject: [PATCH 058/169] use single character wildcard when matching the source distribution (#10656) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3fbbecc0c1297..97ecc0640b48c 100644 --- a/Makefile +++ b/Makefile @@ -79,7 +79,7 @@ dist: entrypoints ## Build source and built (wheel) distributions of the publish: clean-dist dist ## Publish the library to the central PyPi repository # make sure the dist archive contains a non-empty entry_points.txt file before uploading - tar --wildcards --to-stdout -xf dist/localstack_core*.tar.gz "localstack_core*/localstack_core.egg-info/entry_points.txt" | grep . > /dev/null 2>&1 || (echo "Refusing upload, localstack-core dist does not contain entrypoints." && exit 1) + tar --wildcards --to-stdout -xf dist/localstack?core*.tar.gz "localstack?core*/localstack_core.egg-info/entry_points.txt" | grep . > /dev/null 2>&1 || (echo "Refusing upload, localstack-core dist does not contain entrypoints." && exit 1) $(VENV_RUN); twine upload dist/* coveralls: ## Publish coveralls metrics From 7c7b9007232a4b0bf3794bfb1a06bc15fe8c22b8 Mon Sep 17 00:00:00 2001 From: Macwan Nevil Date: Mon, 15 Apr 2024 13:54:14 +0530 Subject: [PATCH 059/169] fixed exception handling in secretsmanager (#10626) --- .../services/secretsmanager/provider.py | 52 ++++++++++--------- .../secretsmanager/test_secretsmanager.py | 28 ++++++++++ .../test_secretsmanager.snapshot.json | 27 ++++++++++ .../test_secretsmanager.validation.json | 3 ++ 4 files changed, 85 insertions(+), 25 deletions(-) diff --git a/localstack/services/secretsmanager/provider.py b/localstack/services/secretsmanager/provider.py index 8d6fe427bba43..05eb784324135 100644 --- a/localstack/services/secretsmanager/provider.py +++ b/localstack/services/secretsmanager/provider.py @@ -6,22 +6,10 @@ import time from typing import Final, Optional, Union +import moto.secretsmanager.exceptions as moto_exception from botocore.utils import InvalidArnException from moto.iam.policy_validation import IAMPolicyDocumentValidator from moto.secretsmanager import secretsmanager_backends -from moto.secretsmanager.exceptions import ( - InvalidParameterException as MotoInvalidParameterException, -) -from moto.secretsmanager.exceptions import ( - InvalidRequestException as MotoInvalidRequestException, -) -from moto.secretsmanager.exceptions import ( - OperationNotPermittedOnReplica as MotoOperationNotPermittedOnReplica, -) -from moto.secretsmanager.exceptions import ( - SecretHasNoValueException as MotoSecretHasNoValueException, -) -from moto.secretsmanager.exceptions import SecretNotFoundException as MotoSecretNotFoundException from moto.secretsmanager.models import FakeSecret, SecretsManagerBackend from moto.secretsmanager.responses import SecretsManagerResponse @@ -133,7 +121,7 @@ def _raise_if_default_kms_key( ): try: secret = backend.describe_secret(secret_id) - except MotoSecretNotFoundException: + except moto_exception.SecretNotFoundException: raise ResourceNotFoundException("Secrets Manager can't find the specified secret.") if secret.kms_key_id is None and request.account_id != secret.account_id: raise InvalidRequestException( @@ -211,11 +199,13 @@ def delete_secret( recovery_window_in_days=recovery_window_in_days, force_delete_without_recovery=force_delete_without_recovery, ) - except MotoInvalidParameterException as e: + except moto_exception.InvalidParameterException as e: raise InvalidParameterException(str(e)) - except MotoInvalidRequestException as e: - raise InvalidRequestException(str(e)) - except MotoSecretNotFoundException: + except moto_exception.InvalidRequestException: + raise InvalidRequestException( + "You tried to perform the operation on a secret that's currently marked deleted." + ) + except moto_exception.SecretNotFoundException: raise SecretNotFoundException() return DeleteSecretResponse(ARN=arn, Name=name, DeletionDate=deletion_date) @@ -228,7 +218,7 @@ def describe_secret( backend = SecretsmanagerProvider.get_moto_backend_for_resource(secret_id, context) try: secret = backend.describe_secret(secret_id) - except MotoSecretNotFoundException: + except moto_exception.SecretNotFoundException: raise ResourceNotFoundException("Secrets Manager can't find the specified secret.") return DescribeSecretResponse(**secret.to_dict()) @@ -254,10 +244,22 @@ def get_secret_value( self._raise_if_default_kms_key(secret_id, context, backend) try: response = backend.get_secret_value(secret_id, version_id, version_stage) - except MotoSecretHasNoValueException: + except moto_exception.SecretNotFoundException: + raise ResourceNotFoundException( + f"Secrets Manager can't find the specified secret value for staging label: {version_stage}" + ) + except moto_exception.SecretStageVersionMismatchException: + raise InvalidRequestException( + "You provided a VersionStage that is not associated to the provided VersionId." + ) + except moto_exception.SecretHasNoValueException: raise ResourceNotFoundException( f"Secrets Manager can't find the specified secret value for staging label: {version_stage}" ) + except moto_exception.InvalidRequestException: + raise InvalidRequestException( + "You can't perform this operation on the secret because it was marked for deletion." + ) return GetSecretValueResponse(**response) @handler("ListSecretVersionIds", expand=False) @@ -333,7 +335,7 @@ def restore_secret( backend = SecretsmanagerProvider.get_moto_backend_for_resource(secret_id, context) try: arn, name = backend.restore_secret(secret_id) - except MotoSecretNotFoundException: + except moto_exception.SecretNotFoundException: raise ResourceNotFoundException("Secrets Manager can't find the specified secret.") return RestoreSecretResponse(ARN=arn, Name=name) @@ -392,13 +394,13 @@ def update_secret( client_request_token=client_req_token, kms_key_id=kms_key_id, ) - except MotoSecretNotFoundException: + except moto_exception.SecretNotFoundException: raise ResourceNotFoundException("Secrets Manager can't find the specified secret.") - except MotoOperationNotPermittedOnReplica: + except moto_exception.OperationNotPermittedOnReplica: raise InvalidRequestException( "Operation not permitted on a replica secret. Call must be made in primary secret's region." ) - except MotoInvalidRequestException: + except moto_exception.InvalidRequestException: raise InvalidRequestException( "An error occurred (InvalidRequestException) when calling the UpdateSecret operation: " "You can't perform this operation on the secret because it was marked for deletion." @@ -816,7 +818,7 @@ def backend_rotate_secret( return secret.to_short_dict(version_id=new_version_id) -@patch(MotoSecretNotFoundException.__init__) +@patch(moto_exception.SecretNotFoundException.__init__) def moto_secret_not_found_exception_init(fn, self): fn(self) self.code = 400 diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.py b/tests/aws/services/secretsmanager/test_secretsmanager.py index 828aebe178f04..94d66fa327883 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.py +++ b/tests/aws/services/secretsmanager/test_secretsmanager.py @@ -10,6 +10,7 @@ import pytest import requests from botocore.auth import SigV4Auth +from botocore.exceptions import ClientError from localstack.aws.api.lambda_ import Runtime from localstack.aws.api.secretsmanager import ( @@ -2342,6 +2343,33 @@ def test_secret_tags(self, aws_client, create_secret, sm_snapshot, cleanups): describe_secret_6 = aws_client.secretsmanager.describe_secret(SecretId=secret_arn) sm_snapshot.match("describe_secret_6", describe_secret_6) + @markers.aws.validated + def test_get_secret_value_errors(self, aws_client, create_secret, sm_snapshot): + secret_name = short_uid() + response = create_secret( + Name=secret_name, + SecretString="test", + ) + version_id = response["VersionId"] + + sm_snapshot.add_transformers_list( + sm_snapshot.transform.secretsmanager_secret_id_arn(response, 0) + ) + + secret_arn = response["ARN"] + + with pytest.raises(ClientError) as ex: + aws_client.secretsmanager.get_secret_value( + SecretId=secret_arn, VersionStage="AWSPREVIOUS" + ) + sm_snapshot.match("error_get_secret_value_non_existing", ex.value.response) + + with pytest.raises(ClientError) as exc: + aws_client.secretsmanager.get_secret_value( + SecretId=secret_name, VersionId=version_id, VersionStage="AWSPREVIOUS" + ) + sm_snapshot.match("mismatch_version_id_and_stage", exc.value.response) + class TestSecretsManagerMultiAccounts: @markers.aws.validated diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json index 0f83c9687015c..5971fd3634bfd 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.snapshot.json @@ -4474,5 +4474,32 @@ } } } + }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_get_secret_value_errors": { + "recorded-date": "11-04-2024, 05:37:47", + "recorded-content": { + "error_get_secret_value_non_existing": { + "Error": { + "Code": "ResourceNotFoundException", + "Message": "Secrets Manager can't find the specified secret value for staging label: AWSPREVIOUS" + }, + "Message": "Secrets Manager can't find the specified secret value for staging label: AWSPREVIOUS", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "mismatch_version_id_and_stage": { + "Error": { + "Code": "InvalidRequestException", + "Message": "You provided a VersionStage that is not associated to the provided VersionId." + }, + "Message": "You provided a VersionStage that is not associated to the provided VersionId.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } } } diff --git a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json index 48f3a29b77ea3..b670d09629fcf 100644 --- a/tests/aws/services/secretsmanager/test_secretsmanager.validation.json +++ b/tests/aws/services/secretsmanager/test_secretsmanager.validation.json @@ -44,6 +44,9 @@ "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_get_random_exclude_characters_and_symbols": { "last_validated_date": "2024-03-15T08:12:01+00:00" }, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_get_secret_value_errors": { + "last_validated_date": "2024-04-11T05:37:47+00:00" + }, "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_invalid_secret_name[ Inv *?!]Name\\\\-]": { "last_validated_date": "2024-03-15T08:12:44+00:00" }, From 86be5ef1e4ea1ef15fce475d8f7acb71cdf5ea22 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Mon, 15 Apr 2024 12:01:20 +0200 Subject: [PATCH 060/169] Update ASF APIs, update cfn signature (#10659) Co-authored-by: Alexander Rashed --- localstack/aws/api/cloudformation/__init__.py | 20 ++++++ localstack/aws/api/cloudwatch/__init__.py | 8 +++ localstack/aws/api/iam/__init__.py | 11 ++- localstack/aws/api/kms/__init__.py | 71 ++++++++++++++++++- localstack/aws/api/redshift/__init__.py | 1 + .../services/cloudformation/provider.py | 3 + pyproject.toml | 8 +-- requirements-base-runtime.txt | 4 +- requirements-dev.txt | 6 +- requirements-runtime.txt | 6 +- requirements-test.txt | 6 +- requirements-typehint.txt | 8 +-- 12 files changed, 130 insertions(+), 22 deletions(-) diff --git a/localstack/aws/api/cloudformation/__init__.py b/localstack/aws/api/cloudformation/__init__.py index 794124f2606b1..8d10648456f65 100644 --- a/localstack/aws/api/cloudformation/__init__.py +++ b/localstack/aws/api/cloudformation/__init__.py @@ -7,10 +7,14 @@ Account = str AccountGateStatusReason = str AccountsUrl = str +AfterContext = str +AfterValue = str AllowedValue = str Arn = str AutoDeploymentNullable = bool AutoUpdate = bool +BeforeContext = str +BeforeValue = str BoxedInteger = int BoxedMaxResults = int CapabilitiesReason = str @@ -49,6 +53,7 @@ InProgressStackInstancesCount = int InSyncStackInstancesCount = int IncludeNestedStacks = bool +IncludePropertyValues = bool IsActivated = bool IsDefaultConfiguration = bool IsDefaultVersion = bool @@ -102,6 +107,7 @@ ResourceIdentifierPropertyValue = str ResourceModel = str ResourceProperties = str +ResourcePropertyPath = str ResourceScanId = str ResourceScanStatusReason = str ResourceScannerMaxResults = int @@ -186,6 +192,12 @@ class AccountGateStatus(str): SKIPPED = "SKIPPED" +class AttributeChangeType(str): + Add = "Add" + Remove = "Remove" + Modify = "Modify" + + class CallAs(str): SELF = "SELF" DELEGATED_ADMIN = "DELEGATED_ADMIN" @@ -918,6 +930,10 @@ class ResourceTargetDefinition(TypedDict, total=False): Attribute: Optional[ResourceAttribute] Name: Optional[PropertyName] RequiresRecreation: Optional[RequiresRecreation] + Path: Optional[ResourcePropertyPath] + BeforeValue: Optional[BeforeValue] + AfterValue: Optional[AfterValue] + AttributeChangeType: Optional[AttributeChangeType] class ResourceChangeDetail(TypedDict, total=False): @@ -942,6 +958,8 @@ class ResourceChange(TypedDict, total=False): Details: Optional[ResourceChangeDetails] ChangeSetId: Optional[ChangeSetId] ModuleInfo: Optional[ModuleInfo] + BeforeContext: Optional[BeforeContext] + AfterContext: Optional[AfterContext] class Change(TypedDict, total=False): @@ -1303,6 +1321,7 @@ class DescribeChangeSetInput(ServiceRequest): ChangeSetName: ChangeSetNameOrId StackName: Optional[StackNameOrId] NextToken: Optional[NextToken] + IncludePropertyValues: Optional[IncludePropertyValues] class DescribeChangeSetOutput(TypedDict, total=False): @@ -2890,6 +2909,7 @@ def describe_change_set( change_set_name: ChangeSetNameOrId, stack_name: StackNameOrId = None, next_token: NextToken = None, + include_property_values: IncludePropertyValues = None, **kwargs, ) -> DescribeChangeSetOutput: raise NotImplementedError diff --git a/localstack/aws/api/cloudwatch/__init__.py b/localstack/aws/api/cloudwatch/__init__.py index 27d2d1b9d523d..2a6c35e42043e 100644 --- a/localstack/aws/api/cloudwatch/__init__.py +++ b/localstack/aws/api/cloudwatch/__init__.py @@ -72,6 +72,7 @@ NextToken = str OutputFormat = str Period = int +PeriodicSpikes = bool ResourceId = str ResourceName = str ResourceType = str @@ -348,6 +349,10 @@ class SingleMetricAnomalyDetector(TypedDict, total=False): Stat: Optional[AnomalyDetectorMetricStat] +class MetricCharacteristics(TypedDict, total=False): + PeriodicSpikes: Optional[PeriodicSpikes] + + class Range(TypedDict, total=False): StartTime: Timestamp EndTime: Timestamp @@ -368,6 +373,7 @@ class AnomalyDetector(TypedDict, total=False): Stat: Optional[AnomalyDetectorMetricStat] Configuration: Optional[AnomalyDetectorConfiguration] StateValue: Optional[AnomalyDetectorStateValue] + MetricCharacteristics: Optional[MetricCharacteristics] SingleMetricAnomalyDetector: Optional[SingleMetricAnomalyDetector] MetricMathAnomalyDetector: Optional[MetricMathAnomalyDetector] @@ -958,6 +964,7 @@ class PutAnomalyDetectorInput(ServiceRequest): Dimensions: Optional[Dimensions] Stat: Optional[AnomalyDetectorMetricStat] Configuration: Optional[AnomalyDetectorConfiguration] + MetricCharacteristics: Optional[MetricCharacteristics] SingleMetricAnomalyDetector: Optional[SingleMetricAnomalyDetector] MetricMathAnomalyDetector: Optional[MetricMathAnomalyDetector] @@ -1364,6 +1371,7 @@ def put_anomaly_detector( dimensions: Dimensions = None, stat: AnomalyDetectorMetricStat = None, configuration: AnomalyDetectorConfiguration = None, + metric_characteristics: MetricCharacteristics = None, single_metric_anomaly_detector: SingleMetricAnomalyDetector = None, metric_math_anomaly_detector: MetricMathAnomalyDetector = None, **kwargs, diff --git a/localstack/aws/api/iam/__init__.py b/localstack/aws/api/iam/__init__.py index 6b1cfa50febd6..26cf0ae7a07f6 100644 --- a/localstack/aws/api/iam/__init__.py +++ b/localstack/aws/api/iam/__init__.py @@ -64,6 +64,7 @@ maxPasswordAgeType = int minimumPasswordLengthType = int noSuchEntityMessage = str +openIdIdpCommunicationErrorExceptionMessage = str organizationsEntityPathType = str organizationsPolicyIdType = str passwordPolicyViolationMessage = str @@ -372,6 +373,12 @@ class NoSuchEntityException(ServiceException): status_code: int = 404 +class OpenIdIdpCommunicationErrorException(ServiceException): + code: str = "OpenIdIdpCommunicationError" + sender_fault: bool = True + status_code: int = 400 + + class PasswordPolicyViolationException(ServiceException): code: str = "PasswordPolicyViolation" sender_fault: bool = True @@ -626,7 +633,7 @@ class CreateLoginProfileResponse(TypedDict, total=False): class CreateOpenIDConnectProviderRequest(ServiceRequest): Url: OpenIDConnectProviderUrlType ClientIDList: Optional[clientIDListType] - ThumbprintList: thumbprintListType + ThumbprintList: Optional[thumbprintListType] Tags: Optional[tagListType] @@ -2385,8 +2392,8 @@ def create_open_id_connect_provider( self, context: RequestContext, url: OpenIDConnectProviderUrlType, - thumbprint_list: thumbprintListType, client_id_list: clientIDListType = None, + thumbprint_list: thumbprintListType = None, tags: tagListType = None, **kwargs, ) -> CreateOpenIDConnectProviderResponse: diff --git a/localstack/aws/api/kms/__init__.py b/localstack/aws/api/kms/__init__.py index 946fb55feb9b5..682c344e94cb2 100644 --- a/localstack/aws/api/kms/__init__.py +++ b/localstack/aws/api/kms/__init__.py @@ -28,6 +28,7 @@ PolicyType = str PrincipalIdType = str RegionType = str +RotationPeriodInDaysType = int TagKeyType = str TagValueType = str TrustAnchorCertificateType = str @@ -212,6 +213,11 @@ class OriginType(str): EXTERNAL_KEY_STORE = "EXTERNAL_KEY_STORE" +class RotationType(str): + AUTOMATIC = "AUTOMATIC" + ON_DEMAND = "ON_DEMAND" + + class SigningAlgorithmSpec(str): RSASSA_PSS_SHA_256 = "RSASSA_PSS_SHA_256" RSASSA_PSS_SHA_384 = "RSASSA_PSS_SHA_384" @@ -272,6 +278,12 @@ class CloudHsmClusterNotRelatedException(ServiceException): status_code: int = 400 +class ConflictException(ServiceException): + code: str = "ConflictException" + sender_fault: bool = False + status_code: int = 400 + + class CustomKeyStoreHasCMKsException(ServiceException): code: str = "CustomKeyStoreHasCMKsException" sender_fault: bool = False @@ -793,6 +805,7 @@ class EnableKeyRequest(ServiceRequest): class EnableKeyRotationRequest(ServiceRequest): KeyId: KeyIdType + RotationPeriodInDays: Optional[RotationPeriodInDaysType] class EncryptRequest(ServiceRequest): @@ -918,6 +931,10 @@ class GetKeyRotationStatusRequest(ServiceRequest): class GetKeyRotationStatusResponse(TypedDict, total=False): KeyRotationEnabled: Optional[BooleanType] + KeyId: Optional[KeyIdType] + RotationPeriodInDays: Optional[RotationPeriodInDaysType] + NextRotationDate: Optional[DateType] + OnDemandRotationStartDate: Optional[DateType] class GetParametersForImportRequest(ServiceRequest): @@ -1024,6 +1041,27 @@ class ListKeyPoliciesResponse(TypedDict, total=False): Truncated: Optional[BooleanType] +class ListKeyRotationsRequest(ServiceRequest): + KeyId: KeyIdType + Limit: Optional[LimitType] + Marker: Optional[MarkerType] + + +class RotationsListEntry(TypedDict, total=False): + KeyId: Optional[KeyIdType] + RotationDate: Optional[DateType] + RotationType: Optional[RotationType] + + +RotationsList = List[RotationsListEntry] + + +class ListKeyRotationsResponse(TypedDict, total=False): + Rotations: Optional[RotationsList] + NextMarker: Optional[MarkerType] + Truncated: Optional[BooleanType] + + class ListKeysRequest(ServiceRequest): Limit: Optional[LimitType] Marker: Optional[MarkerType] @@ -1108,6 +1146,14 @@ class RevokeGrantRequest(ServiceRequest): DryRun: Optional[NullableBooleanType] +class RotateKeyOnDemandRequest(ServiceRequest): + KeyId: KeyIdType + + +class RotateKeyOnDemandResponse(TypedDict, total=False): + KeyId: Optional[KeyIdType] + + class ScheduleKeyDeletionRequest(ServiceRequest): KeyId: KeyIdType PendingWindowInDays: Optional[PendingWindowInDaysType] @@ -1357,7 +1403,13 @@ def enable_key(self, context: RequestContext, key_id: KeyIdType, **kwargs) -> No raise NotImplementedError @handler("EnableKeyRotation") - def enable_key_rotation(self, context: RequestContext, key_id: KeyIdType, **kwargs) -> None: + def enable_key_rotation( + self, + context: RequestContext, + key_id: KeyIdType, + rotation_period_in_days: RotationPeriodInDaysType = None, + **kwargs, + ) -> None: raise NotImplementedError @handler("Encrypt") @@ -1539,6 +1591,17 @@ def list_key_policies( ) -> ListKeyPoliciesResponse: raise NotImplementedError + @handler("ListKeyRotations") + def list_key_rotations( + self, + context: RequestContext, + key_id: KeyIdType, + limit: LimitType = None, + marker: MarkerType = None, + **kwargs, + ) -> ListKeyRotationsResponse: + raise NotImplementedError + @handler("ListKeys") def list_keys( self, context: RequestContext, limit: LimitType = None, marker: MarkerType = None, **kwargs @@ -1633,6 +1696,12 @@ def revoke_grant( ) -> None: raise NotImplementedError + @handler("RotateKeyOnDemand") + def rotate_key_on_demand( + self, context: RequestContext, key_id: KeyIdType, **kwargs + ) -> RotateKeyOnDemandResponse: + raise NotImplementedError + @handler("ScheduleKeyDeletion") def schedule_key_deletion( self, diff --git a/localstack/aws/api/redshift/__init__.py b/localstack/aws/api/redshift/__init__.py index a3446f046b895..05f2963abad75 100644 --- a/localstack/aws/api/redshift/__init__.py +++ b/localstack/aws/api/redshift/__init__.py @@ -1258,6 +1258,7 @@ class Snapshot(TypedDict, total=False): SnapshotRetentionStartTime: Optional[TStamp] MasterPasswordSecretArn: Optional[String] MasterPasswordSecretKmsKeyId: Optional[String] + SnapshotArn: Optional[String] class AuthorizeSnapshotAccessResult(TypedDict, total=False): diff --git a/localstack/services/cloudformation/provider.py b/localstack/services/cloudformation/provider.py index c2341ddc04580..ca796a9f2d537 100644 --- a/localstack/services/cloudformation/provider.py +++ b/localstack/services/cloudformation/provider.py @@ -42,6 +42,7 @@ GetTemplateOutput, GetTemplateSummaryInput, GetTemplateSummaryOutput, + IncludePropertyValues, InsufficientCapabilitiesException, InvalidChangeSetStatusException, ListChangeSetsOutput, @@ -736,8 +737,10 @@ def describe_change_set( change_set_name: ChangeSetNameOrId, stack_name: StackNameOrId = None, next_token: NextToken = None, + include_property_values: IncludePropertyValues = None, **kwargs, ) -> DescribeChangeSetOutput: + # TODO add support for include_property_values # only relevant if change_set_name isn't an ARN if not ARN_CHANGESET_REGEX.match(change_set_name): if not stack_name: diff --git a/pyproject.toml b/pyproject.toml index 99b49bc5b1913..e85b91285972c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,9 +48,9 @@ Issues = "https://github.com/localstack/localstack/issues" # minimal required to actually run localstack on the host for services natively implemented in python base-runtime = [ # pinned / updated by ASF update action - "boto3==1.34.79", + "boto3==1.34.84", # pinned / updated by ASF update action - "botocore==1.34.79", + "botocore==1.34.84", "awscrt>=0.13.14", "cbor2>=5.2.0", "dnspython>=1.16.0", @@ -76,7 +76,7 @@ base-runtime = [ runtime = [ "localstack-core[base-runtime]", # pinned / updated by ASF update action - "awscli==1.32.79", + "awscli==1.32.84", "airspeed-ext>=0.6.3", "amazon_kclpy>=2.0.6,!=2.1.0", # antlr4-python3-runtime: exact pin because antlr4 runtime is tightly coupled to the generated parser code @@ -131,7 +131,7 @@ typehint = [ # typehint is an optional extension of the dev dependencies "localstack-core[dev]", # pinned / updated by ASF update action - "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.79", + "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.84", ] [tool.setuptools] diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index 0c655b14c3f0a..484b7becae332 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -14,9 +14,9 @@ blinker==1.7.0 # via # flask # quart -boto3==1.34.79 +boto3==1.34.84 # via localstack-core (pyproject.toml) -botocore==1.34.79 +botocore==1.34.84 # via # boto3 # localstack-core (pyproject.toml) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1976cfb3dce11..23e56b7dfcbc6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.87.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.79 +awscli==1.32.84 # via localstack-core awscrt==0.20.6 # via localstack-core @@ -55,12 +55,12 @@ blinker==1.7.0 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.79 +boto3==1.34.84 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.79 +botocore==1.34.84 # via # aws-xray-sdk # awscli diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 8630e86553e8c..6b04aab00832a 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -33,7 +33,7 @@ aws-sam-translator==1.87.0 # localstack-core (pyproject.toml) aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.79 +awscli==1.32.84 # via localstack-core (pyproject.toml) awscrt==0.20.6 # via localstack-core @@ -43,12 +43,12 @@ blinker==1.7.0 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.79 +boto3==1.34.84 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.79 +botocore==1.34.84 # via # aws-xray-sdk # awscli diff --git a/requirements-test.txt b/requirements-test.txt index 2048464648a87..74cba762bbb69 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.87.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.79 +awscli==1.32.84 # via localstack-core awscrt==0.20.6 # via localstack-core @@ -55,12 +55,12 @@ blinker==1.7.0 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.79 +boto3==1.34.84 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.79 +botocore==1.34.84 # via # aws-xray-sdk # awscli diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 898a39e022056..35a6e13f3b11a 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.87.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.79 +awscli==1.32.84 # via localstack-core awscrt==0.20.6 # via localstack-core @@ -55,14 +55,14 @@ blinker==1.7.0 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.79 +boto3==1.34.84 # via # aws-sam-translator # localstack-core # moto-ext -boto3-stubs==1.34.79 +boto3-stubs==1.34.84 # via localstack-core (pyproject.toml) -botocore==1.34.79 +botocore==1.34.84 # via # aws-xray-sdk # awscli From c17762987cb0ae0566fb262bd4c7d9df564b7738 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Mon, 15 Apr 2024 17:07:52 +0200 Subject: [PATCH 061/169] add S3 pre-signed POST test with credentials (#10641) --- tests/aws/services/s3/test_s3.py | 111 ++++++++++++++++++ tests/aws/services/s3/test_s3.snapshot.json | 19 +++ tests/aws/services/s3/test_s3.validation.json | 3 + 3 files changed, 133 insertions(+) diff --git a/tests/aws/services/s3/test_s3.py b/tests/aws/services/s3/test_s3.py index 67eb97e9c2da3..812fc0656ae18 100644 --- a/tests/aws/services/s3/test_s3.py +++ b/tests/aws/services/s3/test_s3.py @@ -10895,6 +10895,117 @@ def test_post_object_policy_validation_size(self, s3_bucket, aws_client, snapsho final_object = aws_client.s3.get_object(Bucket=s3_bucket, Key=object_key) snapshot.match("final-object", final_object) + @pytest.mark.skipif( + condition=TEST_S3_IMAGE or LEGACY_V2_S3_PROVIDER, + reason="STS not enabled in S3 image / moto does not implement this", + ) + @markers.aws.validated + def test_presigned_post_with_different_user_credentials( + self, + aws_client, + s3_create_bucket_with_client, + create_role_with_policy, + account_id, + wait_and_assume_role, + snapshot, + ): + snapshot.add_transformers_list( + [ + snapshot.transform.key_value( + "HostId", reference_replacement=False, value_replacement="" + ), + snapshot.transform.key_value("RequestId"), + ] + ) + bucket_name = f"bucket-{short_uid()}" + actions = [ + "s3:CreateBucket", + "s3:PutObject", + "s3:GetObject", + "s3:DeleteBucket", + "s3:DeleteObject", + ] + + assume_policy_doc = { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Principal": {"AWS": account_id}, + "Effect": "Allow", + } + ], + } + assume_policy_doc = json.dumps(assume_policy_doc) + role_name, role_arn = create_role_with_policy( + effect="Allow", + actions=actions, + assume_policy_doc=assume_policy_doc, + resource="*", + ) + + credentials = wait_and_assume_role(role_arn=role_arn) + + client = boto3.client( + "s3", + config=Config(signature_version="s3v4"), + endpoint_url=_endpoint_url(), + aws_access_key_id=credentials["AccessKeyId"], + aws_secret_access_key=credentials["SecretAccessKey"], + aws_session_token=credentials["SessionToken"], + ) + + retry( + lambda: s3_create_bucket_with_client(s3_client=client, Bucket=bucket_name), + sleep=3 if is_aws_cloud() else 0.5, + ) + + object_key = "validate-policy-full-credentials" + presigned_request = client.generate_presigned_post( + Bucket=bucket_name, + Key=object_key, + ExpiresIn=60, + Conditions=[ + {"bucket": bucket_name}, + ], + ) + # load the generated policy to assert that it kept the casing, and it is sent to AWS + generated_policy = json.loads( + base64.b64decode(presigned_request["fields"]["policy"]).decode("utf-8") + ) + policy_conditions_fields = set() + token_condition = None + for condition in generated_policy["conditions"]: + if isinstance(condition, dict): + for k, v in condition.items(): + policy_conditions_fields.add(k) + if k == "x-amz-security-token": + token_condition = v + else: + # format is [operator, key, value] + policy_conditions_fields.add(condition[1]) + + assert policy_conditions_fields == { + "bucket", + "key", + "x-amz-security-token", + "x-amz-credential", + "x-amz-date", + "x-amz-algorithm", + } + assert token_condition == credentials["SessionToken"] + + response = requests.post( + presigned_request["url"], + data=presigned_request["fields"], + files={"file": self.DEFAULT_FILE_VALUE}, + verify=False, + ) + assert response.status_code == 204 + + get_obj = aws_client.s3.get_object(Bucket=bucket_name, Key=object_key) + snapshot.match("get-obj", get_obj) + def _s3_client_pre_signed_client(conf: Config, endpoint_url: str = None): if is_aws_cloud(): diff --git a/tests/aws/services/s3/test_s3.snapshot.json b/tests/aws/services/s3/test_s3.snapshot.json index f2b123ad5d450..576e2951ec6f8 100644 --- a/tests/aws/services/s3/test_s3.snapshot.json +++ b/tests/aws/services/s3/test_s3.snapshot.json @@ -11610,5 +11610,24 @@ } } } + }, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_presigned_post_with_different_user_credentials": { + "recorded-date": "11-04-2024, 20:50:35", + "recorded-content": { + "get-obj": { + "AcceptRanges": "bytes", + "Body": "abcdef", + "ContentLength": 6, + "ContentType": "binary/octet-stream", + "ETag": "\"e80b5017098950fc58aad83c8c14978e\"", + "LastModified": "datetime", + "Metadata": {}, + "ServerSideEncryption": "AES256", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/s3/test_s3.validation.json b/tests/aws/services/s3/test_s3.validation.json index 0b8644df5d7c9..aba7e54031a6e 100644 --- a/tests/aws/services/s3/test_s3.validation.json +++ b/tests/aws/services/s3/test_s3.validation.json @@ -572,6 +572,9 @@ "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_request_missing_signature[s3v4]": { "last_validated_date": "2023-08-04T21:58:54+00:00" }, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_presigned_post_with_different_user_credentials": { + "last_validated_date": "2024-04-11T20:50:35+00:00" + }, "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_signature_authentication[s3-False]": { "last_validated_date": "2023-08-04T22:00:25+00:00" }, From f98dbf9130db1deb689af527521646d020be7930 Mon Sep 17 00:00:00 2001 From: Mathieu Cloutier <79954947+cloutierMat@users.noreply.github.com> Date: Mon, 15 Apr 2024 09:35:34 -0600 Subject: [PATCH 062/169] Validate existing test apigw (#10653) --- localstack/testing/pytest/fixtures.py | 10 +++- tests/aws/services/acm/test_acm.py | 20 ++++++-- tests/aws/services/acm/test_acm.snapshot.json | 41 ++++++++++++++++ .../aws/services/acm/test_acm.validation.json | 3 ++ .../apigateway/test_apigateway_basic.py | 49 ++++++++++++++----- .../test_apigateway_basic.snapshot.json | 44 +++++++++++++++++ .../test_apigateway_basic.validation.json | 8 ++- 7 files changed, 155 insertions(+), 20 deletions(-) diff --git a/localstack/testing/pytest/fixtures.py b/localstack/testing/pytest/fixtures.py index 163a130684551..1ef78774a7279 100644 --- a/localstack/testing/pytest/fixtures.py +++ b/localstack/testing/pytest/fixtures.py @@ -1940,7 +1940,15 @@ def create_rest_apigw(aws_client_factory): def _create_apigateway_function(**kwargs): region_name = kwargs.pop("region_name", None) - apigateway_client = aws_client_factory(region_name=region_name).apigateway + client_config = None + if is_aws_cloud(): + client_config = botocore.config.Config( + # Api gateway can throttle requests pretty heavily. Leading to potentially undeleted apis + retries={"max_attempts": 10, "mode": "adaptive"} + ) + apigateway_client = aws_client_factory( + region_name=region_name, config=client_config + ).apigateway kwargs.setdefault("name", f"api-{short_uid()}") response = apigateway_client.create_rest_api(**kwargs) diff --git a/tests/aws/services/acm/test_acm.py b/tests/aws/services/acm/test_acm.py index fba50b6f63e91..c670414034501 100644 --- a/tests/aws/services/acm/test_acm.py +++ b/tests/aws/services/acm/test_acm.py @@ -61,14 +61,24 @@ def _certificate_present(): snapshot.match("describe-certificate-response", describe_res) - @markers.aws.unknown - def test_domain_validation(self, acm_request_certificate, aws_client): + @markers.snapshot.skip_snapshot_verify( + paths=[ + "$..ResourceRecord", # Added by LS but not in aws response + "$..ValidationEmails", # Not in LS response + ] + ) + @markers.aws.validated + def test_domain_validation(self, acm_request_certificate, aws_client, snapshot): + snapshot.add_transformer(snapshot.transform.key_value("CertificateArn")) + snapshot.add_transformer(snapshot.transform.key_value("DomainName")) + snapshot.add_transformer(snapshot.transform.key_value("ValidationMethod")) + snapshot.add_transformer(snapshot.transform.key_value("SignatureAlgorithm")) + certificate_arn = acm_request_certificate()["CertificateArn"] result = aws_client.acm.describe_certificate(CertificateArn=certificate_arn) - options = result["Certificate"]["DomainValidationOptions"] - assert len(options) == 1 + snapshot.match("describe-certificate", result) - @markers.aws.unknown + @markers.aws.needs_fixing def test_boto_wait_for_certificate_validation( self, acm_request_certificate, aws_client, monkeypatch ): diff --git a/tests/aws/services/acm/test_acm.snapshot.json b/tests/aws/services/acm/test_acm.snapshot.json index d1aa096b73d47..ae7623aa0cb72 100644 --- a/tests/aws/services/acm/test_acm.snapshot.json +++ b/tests/aws/services/acm/test_acm.snapshot.json @@ -291,5 +291,46 @@ } } } + }, + "tests/aws/services/acm/test_acm.py::TestACM::test_domain_validation": { + "recorded-date": "12-04-2024, 15:36:37", + "recorded-content": { + "describe-certificate": { + "Certificate": { + "CertificateArn": "", + "CreatedAt": "datetime", + "DomainName": "", + "DomainValidationOptions": [ + { + "DomainName": "", + "ValidationDomain": "", + "ValidationEmails": [], + "ValidationMethod": "", + "ValidationStatus": "PENDING_VALIDATION" + } + ], + "ExtendedKeyUsages": [], + "InUseBy": [], + "Issuer": "Amazon", + "KeyAlgorithm": "RSA-2048", + "KeyUsages": [], + "Options": { + "CertificateTransparencyLoggingPreference": "ENABLED" + }, + "RenewalEligibility": "INELIGIBLE", + "SignatureAlgorithm": "", + "Status": "PENDING_VALIDATION", + "Subject": "CN=", + "SubjectAlternativeNames": [ + "" + ], + "Type": "AMAZON_ISSUED" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/acm/test_acm.validation.json b/tests/aws/services/acm/test_acm.validation.json index 5591990ba15d8..8201f405c3e71 100644 --- a/tests/aws/services/acm/test_acm.validation.json +++ b/tests/aws/services/acm/test_acm.validation.json @@ -5,6 +5,9 @@ "tests/aws/services/acm/test_acm.py::TestACM::test_create_certificate_for_multiple_alternative_domains": { "last_validated_date": "2024-01-09T14:58:14+00:00" }, + "tests/aws/services/acm/test_acm.py::TestACM::test_domain_validation": { + "last_validated_date": "2024-04-12T15:36:37+00:00" + }, "tests/aws/services/acm/test_acm.py::TestACM::test_import_certificate": { "last_validated_date": "2024-02-22T17:41:15+00:00" } diff --git a/tests/aws/services/apigateway/test_apigateway_basic.py b/tests/aws/services/apigateway/test_apigateway_basic.py index 947f8e4d90a9c..8c3734ad1068f 100644 --- a/tests/aws/services/apigateway/test_apigateway_basic.py +++ b/tests/aws/services/apigateway/test_apigateway_basic.py @@ -157,7 +157,8 @@ def test_delete_rest_api_with_invalid_id(self, aws_client): assert "foobar" in e.value.response["Error"]["Message"] @pytest.mark.parametrize("url_function", [path_based_url, host_based_url]) - @markers.aws.unknown + @markers.aws.only_localstack + # This is not a possible feature on aws. def test_create_rest_api_with_custom_id(self, create_rest_apigw, url_function, aws_client): apigw_name = f"gw-{short_uid()}" test_id = "testId123" @@ -177,8 +178,10 @@ def test_create_rest_api_with_custom_id(self, create_rest_apigw, url_function, a assert response.ok assert response._content == b'{"echo": "foobar", "response": "mocked"}' - @markers.aws.unknown - def test_update_rest_api_deployment(self, create_rest_apigw, aws_client): + @markers.aws.validated + def test_update_rest_api_deployment(self, create_rest_apigw, aws_client, snapshot): + snapshot.add_transformer(snapshot.transform.key_value("id")) + api_id, _, root = create_rest_apigw(name="test_gateway5") create_rest_resource_method( @@ -218,7 +221,7 @@ def test_update_rest_api_deployment(self, create_rest_apigw, aws_client): deploymentId=deployment_id, patchOperations=patch_operations, ) - assert deployment["description"] == "new-description" + snapshot.match("after-update", deployment) @markers.aws.validated def test_api_gateway_lambda_integration_aws_type( @@ -355,7 +358,9 @@ def test_invoke_endpoint_cors_headers( assert response.status_code == 200 assert "http://test.com" in response.headers["Access-Control-Allow-Origin"] - @markers.aws.unknown + # This test fails as it tries to create a lambda locally? + # It then leaves some resources behind, apigateway and policies + @markers.aws.needs_fixing @pytest.mark.parametrize( "api_path", [API_PATH_LAMBDA_PROXY_BACKEND, API_PATH_LAMBDA_PROXY_BACKEND_WITH_PATH_PARAM] ) @@ -376,7 +381,9 @@ def test_api_gateway_lambda_proxy_integration( aws_client.apigateway, ) - @markers.aws.unknown + # This test fails as it tries to create a lambda locally? + # It then leaves some resources behind, apigateway and policies + @markers.aws.needs_fixing def test_api_gateway_lambda_proxy_integration_with_is_base_64_encoded( self, integration_lambda, aws_client, create_iam_role_with_policy ): @@ -532,13 +539,17 @@ def _test_api_gateway_lambda_proxy_integration( assert "/yCqIBE=" == result_content["body"] assert ["isBase64Encoded"] - @markers.aws.unknown + # This test fails as it tries to create a lambda locally? + # It then leaves some resources behind, apigateway and policies + @markers.aws.needs_fixing def test_api_gateway_lambda_proxy_integration_any_method(self, integration_lambda): self._test_api_gateway_lambda_proxy_integration_any_method( integration_lambda, API_PATH_LAMBDA_PROXY_BACKEND_ANY_METHOD ) - @markers.aws.unknown + # This test fails as it tries to create a lambda locally? + # It then leaves some resources behind, apigateway and policies + @markers.aws.needs_fixing def test_api_gateway_lambda_proxy_integration_any_method_with_path_param( self, integration_lambda ): @@ -660,7 +671,9 @@ def test_malformed_response_apigw_invocation( assert result.headers.get("Content-Type") == "application/json" assert json.loads(result.content)["message"] == "Internal server error" - @markers.aws.unknown + # Missing certificate creation to create a domain + # this might end up being a bigger issue to fix until we have a validated certificate we can use + @markers.aws.needs_fixing def test_api_gateway_handle_domain_name(self, aws_client): domain_name = f"{short_uid()}.example.com" apigw_client = aws_client.apigateway @@ -699,15 +712,25 @@ def _test_api_gateway_lambda_proxy_integration_any_method(self, fn_name, path): else: assert 204 == result.status_code - @markers.aws.unknown + @markers.snapshot.skip_snapshot_verify( + paths=[ + "$..authType", # Not added by LS + "$..authorizerResultTtlInSeconds", # Exists in LS but not in AWS + ] + ) + @markers.aws.validated def test_apigateway_with_custom_authorization_method( - self, create_rest_apigw, aws_client, account_id, region_name, integration_lambda + self, create_rest_apigw, aws_client, account_id, region_name, integration_lambda, snapshot ): + snapshot.add_transformer(snapshot.transform.key_value("api_id")) + snapshot.add_transformer(snapshot.transform.key_value("authorizerUri")) + snapshot.add_transformer(snapshot.transform.key_value("id")) # create Lambda function lambda_uri = arns.lambda_function_arn(integration_lambda, account_id, region_name) # create REST API api_id, _, _ = create_rest_apigw(name="test-api") + snapshot.match("api-id", {"api_id": api_id}) root_res_id = aws_client.apigateway.get_resources(restApiId=api_id)["items"][0]["id"] # create authorizer at root resource @@ -719,6 +742,7 @@ def test_apigateway_with_custom_authorization_method( 2015-03-31/functions/{}/invocations".format(lambda_uri), identitySource="method.request.header.Auth", ) + snapshot.match("authorizer", authorizer) # create method with custom authorizer is_api_key_required = True @@ -730,8 +754,7 @@ def test_apigateway_with_custom_authorization_method( authorizerId=authorizer["id"], apiKeyRequired=is_api_key_required, ) - - assert authorizer["id"] == method_response["authorizerId"] + snapshot.match("put-method-response", method_response) @markers.aws.unknown def test_base_path_mapping(self, create_rest_apigw, aws_client): diff --git a/tests/aws/services/apigateway/test_apigateway_basic.snapshot.json b/tests/aws/services/apigateway/test_apigateway_basic.snapshot.json index 0102e87e8feb9..65bc69da07608 100644 --- a/tests/aws/services/apigateway/test_apigateway_basic.snapshot.json +++ b/tests/aws/services/apigateway/test_apigateway_basic.snapshot.json @@ -72,5 +72,49 @@ } } } + }, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_update_rest_api_deployment": { + "recorded-date": "12-04-2024, 21:24:49", + "recorded-content": { + "after-update": { + "createdDate": "datetime", + "description": "new-description", + "id": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_apigateway_with_custom_authorization_method": { + "recorded-date": "12-04-2024, 22:31:50", + "recorded-content": { + "api-id": { + "api_id": "" + }, + "authorizer": { + "authType": "custom", + "authorizerUri": "", + "id": "", + "identitySource": "method.request.header.Auth", + "name": "lambda_authorizer", + "type": "TOKEN", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + }, + "put-method-response": { + "apiKeyRequired": true, + "authorizationType": "CUSTOM", + "authorizerId": "", + "httpMethod": "GET", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + } + } } } diff --git a/tests/aws/services/apigateway/test_apigateway_basic.validation.json b/tests/aws/services/apigateway/test_apigateway_basic.validation.json index cfcf95656b62e..cb9d50ad20e38 100644 --- a/tests/aws/services/apigateway/test_apigateway_basic.validation.json +++ b/tests/aws/services/apigateway/test_apigateway_basic.validation.json @@ -1,4 +1,7 @@ { + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_apigateway_with_custom_authorization_method": { + "last_validated_date": "2024-04-12T22:31:50+00:00" + }, "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_apigateway_with_step_function_integration[DeleteStateMachine]": { "last_validated_date": "2023-09-07T20:40:38+00:00" }, @@ -7,5 +10,8 @@ }, "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_apigw_test_invoke_method_api": { "last_validated_date": "2024-02-04T18:48:24+00:00" + }, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_update_rest_api_deployment": { + "last_validated_date": "2024-04-12T21:24:49+00:00" } -} \ No newline at end of file +} From 921c76c9633c9de37fa27842440c18ba1272fe5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 07:17:59 +0200 Subject: [PATCH 063/169] Bump cla-assistant/github-action to 2.3.2 (#10667) --- .github/workflows/pr-cla.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-cla.yml b/.github/workflows/pr-cla.yml index 25342fd0fe9d6..f9c3d2a10a53f 100644 --- a/.github/workflows/pr-cla.yml +++ b/.github/workflows/pr-cla.yml @@ -16,7 +16,7 @@ jobs: steps: - name: "CLA Assistant" if: "(github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'" - uses: "cla-assistant/github-action@v2.1.3-beta" + uses: "cla-assistant/github-action@v2.3.2" env: GITHUB_TOKEN: "${{ secrets.PRO_ACCESS_TOKEN }}" PERSONAL_ACCESS_TOKEN: "${{ secrets.PRO_ACCESS_TOKEN }}" From 581dc54c38b7beba9be5c0949213c9f62d65dd79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 16 Apr 2024 07:18:59 +0200 Subject: [PATCH 064/169] Bump the docker-base-images group with 2 updates (#10668) --- Dockerfile | 4 ++-- Dockerfile.s3 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 65d21105df3a0..956f8b026f7b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # java-builder: Stage to build a custom JRE (with jlink) -FROM eclipse-temurin:11@sha256:ea119fd944947a21b3d617d402c8cb13671aa40cb28e3a87da5b02df3d95e935 as java-builder +FROM eclipse-temurin:11@sha256:8cf7bf0f13cc47e56c7461420e1a80585612afcbb0073602f846748d31cb1aba as java-builder # create a custom, minimized JRE via jlink RUN jlink --add-modules \ @@ -29,7 +29,7 @@ jdk.localedata --include-locales en,th \ # base: Stage which installs necessary runtime dependencies (OS packages, java,...) -FROM python:3.11.9-slim-bookworm@sha256:3800945e7ed50341ba8af48f449515c0a4e845277d56008c15bd84d52093e958 as base +FROM python:3.11.9-slim-bookworm@sha256:dad770592ab3582ab2dabcf0e18a863df9d86bd9d23efcfa614110ce49ac20e4 as base ARG TARGETARCH # Install runtime OS package dependencies diff --git a/Dockerfile.s3 b/Dockerfile.s3 index 77d5f7de22861..7fbfc00fa3ee9 100644 --- a/Dockerfile.s3 +++ b/Dockerfile.s3 @@ -1,5 +1,5 @@ # base: Stage which installs necessary runtime dependencies (OS packages, filesystem...) -FROM python:3.11.9-slim-bookworm@sha256:3800945e7ed50341ba8af48f449515c0a4e845277d56008c15bd84d52093e958 as base +FROM python:3.11.9-slim-bookworm@sha256:dad770592ab3582ab2dabcf0e18a863df9d86bd9d23efcfa614110ce49ac20e4 as base ARG TARGETARCH # set workdir From 4b72d6e14f01c579ce7c2a434eaf0af0a12916cc Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Tue, 16 Apr 2024 08:12:04 +0200 Subject: [PATCH 065/169] Upgrade pinned Python dependencies (#10669) --- .pre-commit-config.yaml | 2 +- requirements-base-runtime.txt | 8 +++--- requirements-basic.txt | 2 +- requirements-dev.txt | 24 ++++++++-------- requirements-runtime.txt | 18 ++++++------ requirements-test.txt | 20 ++++++------- requirements-typehint.txt | 54 +++++++++++++++++------------------ 7 files changed, 64 insertions(+), 64 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 638e07407f84e..1a3437cac63dc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.3.5 + rev: v0.3.7 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index 484b7becae332..5f65009c2b63a 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -8,7 +8,7 @@ aiofiles==23.2.1 # via quart attrs==23.2.0 # via localstack-twisted -awscrt==0.20.6 +awscrt==0.20.9 # via localstack-core (pyproject.toml) blinker==1.7.0 # via @@ -25,7 +25,7 @@ build==1.2.1 # via localstack-core (pyproject.toml) cachetools==5.3.3 # via localstack-core (pyproject.toml) -cbor2==5.6.2 +cbor2==5.6.3 # via localstack-core (pyproject.toml) certifi==2024.2.2 # via requests @@ -74,7 +74,7 @@ hyperframe==6.0.1 # via h2 hyperlink==21.0.0 # via localstack-twisted -idna==3.6 +idna==3.7 # via # hyperlink # localstack-twisted @@ -190,7 +190,7 @@ wsproto==1.2.0 # via hypercorn xmltodict==0.13.0 # via localstack-core (pyproject.toml) -zope-interface==6.2 +zope-interface==6.3 # via localstack-twisted # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements-basic.txt b/requirements-basic.txt index af61d47953689..151aae1bc6cac 100644 --- a/requirements-basic.txt +++ b/requirements-basic.txt @@ -24,7 +24,7 @@ dnslib==0.9.24 # via localstack-core (pyproject.toml) dnspython==2.6.1 # via localstack-core (pyproject.toml) -idna==3.6 +idna==3.7 # via requests markdown-it-py==3.0.0 # via rich diff --git a/requirements-dev.txt b/requirements-dev.txt index 23e56b7dfcbc6..d2ce5a533f1eb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -37,7 +37,7 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.136.0 +aws-cdk-lib==2.137.0 # via localstack-core aws-sam-translator==1.87.0 # via @@ -47,7 +47,7 @@ aws-xray-sdk==2.13.0 # via moto-ext awscli==1.32.84 # via localstack-core -awscrt==0.20.6 +awscrt==0.20.9 # via localstack-core blinker==1.7.0 # via @@ -80,7 +80,7 @@ cachetools==5.3.3 # localstack-core (pyproject.toml) cattrs==23.2.3 # via jsii -cbor2==5.6.2 +cbor2==5.6.3 # via localstack-core certifi==2024.2.2 # via @@ -92,7 +92,7 @@ cffi==1.16.0 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==0.86.2 +cfn-lint==0.86.3 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -154,7 +154,7 @@ docopt==0.6.2 # via coveralls docutils==0.16 # via awscli -filelock==3.13.3 +filelock==3.13.4 # via virtualenv flask==3.0.3 # via @@ -188,7 +188,7 @@ hyperlink==21.0.0 # via localstack-twisted identify==2.5.35 # via pre-commit -idna==3.6 +idna==3.7 # via # anyio # httpx @@ -225,7 +225,7 @@ jsii==1.97.0 # aws-cdk-asset-node-proxy-agent-v6 # aws-cdk-lib # constructs -json5==0.9.24 +json5==0.9.25 # via localstack-core jsondiff==2.0.0 # via moto-ext @@ -240,7 +240,7 @@ jsonpath-ng==1.6.1 # moto-ext jsonpath-rw==1.4.0 # via localstack-core -jsonpickle==3.0.3 +jsonpickle==3.0.4 # via jschema-to-python jsonpointer==2.4 # via jsonpatch @@ -350,9 +350,9 @@ pyasn1==0.6.0 # via rsa pycparser==2.22 # via cffi -pydantic==2.6.4 +pydantic==2.7.0 # via aws-sam-translator -pydantic-core==2.16.3 +pydantic-core==2.18.1 # via pydantic pygments==2.17.2 # via rich @@ -446,7 +446,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core (pyproject.toml) -ruff==0.3.5 +ruff==0.3.7 # via localstack-core (pyproject.toml) s3transfer==0.10.1 # via @@ -528,7 +528,7 @@ xmltodict==0.13.0 # via # localstack-core # moto-ext -zope-interface==6.2 +zope-interface==6.3 # via localstack-twisted # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 6b04aab00832a..b7b6cef4f764b 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -35,7 +35,7 @@ aws-xray-sdk==2.13.0 # via moto-ext awscli==1.32.84 # via localstack-core (pyproject.toml) -awscrt==0.20.6 +awscrt==0.20.9 # via localstack-core blinker==1.7.0 # via @@ -65,7 +65,7 @@ cachetools==5.3.3 # airspeed-ext # localstack-core # localstack-core (pyproject.toml) -cbor2==5.6.2 +cbor2==5.6.3 # via localstack-core certifi==2024.2.2 # via @@ -73,7 +73,7 @@ certifi==2024.2.2 # requests cffi==1.16.0 # via cryptography -cfn-lint==0.86.2 +cfn-lint==0.86.3 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -141,7 +141,7 @@ hyperframe==6.0.1 # via h2 hyperlink==21.0.0 # via localstack-twisted -idna==3.6 +idna==3.7 # via # hyperlink # localstack-twisted @@ -165,7 +165,7 @@ joserfc==0.9.0 # via moto-ext jschema-to-python==1.2.3 # via cfn-lint -json5==0.9.24 +json5==0.9.25 # via localstack-core (pyproject.toml) jsondiff==2.0.0 # via moto-ext @@ -179,7 +179,7 @@ jsonpath-ng==1.6.1 # moto-ext jsonpath-rw==1.4.0 # via localstack-core (pyproject.toml) -jsonpickle==3.0.3 +jsonpickle==3.0.4 # via jschema-to-python jsonpointer==2.4 # via jsonpatch @@ -258,9 +258,9 @@ pyasn1==0.6.0 # via rsa pycparser==2.22 # via cffi -pydantic==2.6.4 +pydantic==2.7.0 # via aws-sam-translator -pydantic-core==2.16.3 +pydantic-core==2.18.1 # via pydantic pygments==2.17.2 # via rich @@ -395,7 +395,7 @@ xmltodict==0.13.0 # via # localstack-core # moto-ext -zope-interface==6.2 +zope-interface==6.3 # via localstack-twisted # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements-test.txt b/requirements-test.txt index 74cba762bbb69..6e61778a4d361 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -37,7 +37,7 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.136.0 +aws-cdk-lib==2.137.0 # via localstack-core (pyproject.toml) aws-sam-translator==1.87.0 # via @@ -47,7 +47,7 @@ aws-xray-sdk==2.13.0 # via moto-ext awscli==1.32.84 # via localstack-core -awscrt==0.20.6 +awscrt==0.20.9 # via localstack-core blinker==1.7.0 # via @@ -80,7 +80,7 @@ cachetools==5.3.3 # localstack-core (pyproject.toml) cattrs==23.2.3 # via jsii -cbor2==5.6.2 +cbor2==5.6.3 # via localstack-core certifi==2024.2.2 # via @@ -90,7 +90,7 @@ certifi==2024.2.2 # requests cffi==1.16.0 # via cryptography -cfn-lint==0.86.2 +cfn-lint==0.86.3 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -172,7 +172,7 @@ hyperframe==6.0.1 # via h2 hyperlink==21.0.0 # via localstack-twisted -idna==3.6 +idna==3.7 # via # anyio # httpx @@ -209,7 +209,7 @@ jsii==1.97.0 # aws-cdk-asset-node-proxy-agent-v6 # aws-cdk-lib # constructs -json5==0.9.24 +json5==0.9.25 # via localstack-core jsondiff==2.0.0 # via moto-ext @@ -224,7 +224,7 @@ jsonpath-ng==1.6.1 # moto-ext jsonpath-rw==1.4.0 # via localstack-core -jsonpickle==3.0.3 +jsonpickle==3.0.4 # via jschema-to-python jsonpointer==2.4 # via jsonpatch @@ -321,9 +321,9 @@ pyasn1==0.6.0 # via rsa pycparser==2.22 # via cffi -pydantic==2.6.4 +pydantic==2.7.0 # via aws-sam-translator -pydantic-core==2.16.3 +pydantic-core==2.18.1 # via pydantic pygments==2.17.2 # via rich @@ -489,7 +489,7 @@ xmltodict==0.13.0 # via # localstack-core # moto-ext -zope-interface==6.2 +zope-interface==6.3 # via localstack-twisted # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 35a6e13f3b11a..397b2561c6459 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -37,7 +37,7 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.136.0 +aws-cdk-lib==2.137.0 # via localstack-core aws-sam-translator==1.87.0 # via @@ -47,7 +47,7 @@ aws-xray-sdk==2.13.0 # via moto-ext awscli==1.32.84 # via localstack-core -awscrt==0.20.6 +awscrt==0.20.9 # via localstack-core blinker==1.7.0 # via @@ -71,7 +71,7 @@ botocore==1.34.84 # localstack-snapshot # moto-ext # s3transfer -botocore-stubs==1.34.69 +botocore-stubs==1.34.84 # via boto3-stubs build==1.2.1 # via @@ -84,7 +84,7 @@ cachetools==5.3.3 # localstack-core (pyproject.toml) cattrs==23.2.3 # via jsii -cbor2==5.6.2 +cbor2==5.6.3 # via localstack-core certifi==2024.2.2 # via @@ -96,7 +96,7 @@ cffi==1.16.0 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==0.86.2 +cfn-lint==0.86.3 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -158,7 +158,7 @@ docopt==0.6.2 # via coveralls docutils==0.16 # via awscli -filelock==3.13.3 +filelock==3.13.4 # via virtualenv flask==3.0.3 # via @@ -192,7 +192,7 @@ hyperlink==21.0.0 # via localstack-twisted identify==2.5.35 # via pre-commit -idna==3.6 +idna==3.7 # via # anyio # httpx @@ -229,7 +229,7 @@ jsii==1.97.0 # aws-cdk-asset-node-proxy-agent-v6 # aws-cdk-lib # constructs -json5==0.9.24 +json5==0.9.25 # via localstack-core jsondiff==2.0.0 # via moto-ext @@ -244,7 +244,7 @@ jsonpath-ng==1.6.1 # moto-ext jsonpath-rw==1.4.0 # via localstack-core -jsonpickle==3.0.3 +jsonpickle==3.0.4 # via jschema-to-python jsonpointer==2.4 # via jsonpatch @@ -307,19 +307,19 @@ mypy-boto3-autoscaling==1.34.54 # via boto3-stubs mypy-boto3-backup==1.34.64 # via boto3-stubs -mypy-boto3-batch==1.34.72 +mypy-boto3-batch==1.34.83 # via boto3-stubs mypy-boto3-ce==1.34.71 # via boto3-stubs mypy-boto3-cloudcontrol==1.34.0 # via boto3-stubs -mypy-boto3-cloudformation==1.34.77 +mypy-boto3-cloudformation==1.34.84 # via boto3-stubs -mypy-boto3-cloudfront==1.34.0 +mypy-boto3-cloudfront==1.34.83 # via boto3-stubs mypy-boto3-cloudtrail==1.34.59 # via boto3-stubs -mypy-boto3-cloudwatch==1.34.75 +mypy-boto3-cloudwatch==1.34.83 # via boto3-stubs mypy-boto3-codecommit==1.34.6 # via boto3-stubs @@ -365,9 +365,9 @@ mypy-boto3-fis==1.34.63 # via boto3-stubs mypy-boto3-glacier==1.34.0 # via boto3-stubs -mypy-boto3-glue==1.34.76 +mypy-boto3-glue==1.34.84 # via boto3-stubs -mypy-boto3-iam==1.34.8 +mypy-boto3-iam==1.34.83 # via boto3-stubs mypy-boto3-identitystore==1.34.0 # via boto3-stubs @@ -387,7 +387,7 @@ mypy-boto3-kinesisanalytics==1.34.0 # via boto3-stubs mypy-boto3-kinesisanalyticsv2==1.34.64 # via boto3-stubs -mypy-boto3-kms==1.34.65 +mypy-boto3-kms==1.34.84 # via boto3-stubs mypy-boto3-lakeformation==1.34.7 # via boto3-stubs @@ -397,7 +397,7 @@ mypy-boto3-logs==1.34.66 # via boto3-stubs mypy-boto3-managedblockchain==1.34.0 # via boto3-stubs -mypy-boto3-mediaconvert==1.34.33 +mypy-boto3-mediaconvert==1.34.81 # via boto3-stubs mypy-boto3-mediastore==1.34.0 # via boto3-stubs @@ -413,17 +413,17 @@ mypy-boto3-organizations==1.34.56 # via boto3-stubs mypy-boto3-pi==1.34.0 # via boto3-stubs -mypy-boto3-pipes==1.34.0 +mypy-boto3-pipes==1.34.83 # via boto3-stubs mypy-boto3-qldb==1.34.49 # via boto3-stubs mypy-boto3-qldb-session==1.34.0 # via boto3-stubs -mypy-boto3-rds==1.34.65 +mypy-boto3-rds==1.34.83 # via boto3-stubs mypy-boto3-rds-data==1.34.6 # via boto3-stubs -mypy-boto3-redshift==1.34.57 +mypy-boto3-redshift==1.34.84 # via boto3-stubs mypy-boto3-redshift-data==1.34.0 # via boto3-stubs @@ -437,7 +437,7 @@ mypy-boto3-route53resolver==1.34.15 # via boto3-stubs mypy-boto3-s3==1.34.65 # via boto3-stubs -mypy-boto3-s3control==1.34.18 +mypy-boto3-s3control==1.34.83 # via boto3-stubs mypy-boto3-sagemaker==1.34.74 # via boto3-stubs @@ -546,9 +546,9 @@ pyasn1==0.6.0 # via rsa pycparser==2.22 # via cffi -pydantic==2.6.4 +pydantic==2.7.0 # via aws-sam-translator -pydantic-core==2.16.3 +pydantic-core==2.18.1 # via pydantic pygments==2.17.2 # via rich @@ -642,7 +642,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core -ruff==0.3.5 +ruff==0.3.7 # via localstack-core s3transfer==0.10.1 # via @@ -686,9 +686,9 @@ typeguard==2.13.3 # aws-cdk-lib # constructs # jsii -types-awscrt==0.20.5 +types-awscrt==0.20.9 # via botocore-stubs -types-s3transfer==0.10.0 +types-s3transfer==0.10.1 # via boto3-stubs typing-extensions==4.11.0 # via @@ -825,7 +825,7 @@ xmltodict==0.13.0 # via # localstack-core # moto-ext -zope-interface==6.2 +zope-interface==6.3 # via localstack-twisted # The following packages are considered to be unsafe in a requirements file: From ef73f6f91977cf6ab099b4c82998d3ec62816da0 Mon Sep 17 00:00:00 2001 From: Sannya Singal <32308435+sannya-singal@users.noreply.github.com> Date: Tue, 16 Apr 2024 18:14:45 +0530 Subject: [PATCH 066/169] add `AWS::Events::ApiDestination` cloudformation resource provider (#10670) --- .../aws_events_apidestination.py | 115 ++++++++++++++++++ .../aws_events_apidestination.schema.json | 92 ++++++++++++++ .../aws_events_apidestination_plugin.py | 20 +++ .../cloudformation/resources/test_events.py | 29 +++++ .../resources/test_events.validation.json | 3 + tests/aws/templates/events_apidestination.yml | 91 ++++++++++++++ 6 files changed, 350 insertions(+) create mode 100644 localstack/services/events/resource_providers/aws_events_apidestination.py create mode 100644 localstack/services/events/resource_providers/aws_events_apidestination.schema.json create mode 100644 localstack/services/events/resource_providers/aws_events_apidestination_plugin.py create mode 100644 tests/aws/templates/events_apidestination.yml diff --git a/localstack/services/events/resource_providers/aws_events_apidestination.py b/localstack/services/events/resource_providers/aws_events_apidestination.py new file mode 100644 index 0000000000000..372d45de40dce --- /dev/null +++ b/localstack/services/events/resource_providers/aws_events_apidestination.py @@ -0,0 +1,115 @@ +# LocalStack Resource Provider Scaffolding v2 +from __future__ import annotations + +from pathlib import Path +from typing import Optional, TypedDict + +import localstack.services.cloudformation.provider_utils as util +from localstack.services.cloudformation.resource_provider import ( + OperationStatus, + ProgressEvent, + ResourceProvider, + ResourceRequest, +) + + +class EventsApiDestinationProperties(TypedDict): + ConnectionArn: Optional[str] + HttpMethod: Optional[str] + InvocationEndpoint: Optional[str] + Arn: Optional[str] + Description: Optional[str] + InvocationRateLimitPerSecond: Optional[int] + Name: Optional[str] + + +REPEATED_INVOCATION = "repeated_invocation" + + +class EventsApiDestinationProvider(ResourceProvider[EventsApiDestinationProperties]): + TYPE = "AWS::Events::ApiDestination" # Autogenerated. Don't change + SCHEMA = util.get_schema_path(Path(__file__)) # Autogenerated. Don't change + + def create( + self, + request: ResourceRequest[EventsApiDestinationProperties], + ) -> ProgressEvent[EventsApiDestinationProperties]: + """ + Create a new resource. + + Primary identifier fields: + - /properties/Name + + Required properties: + - ConnectionArn + - InvocationEndpoint + - HttpMethod + + Create-only properties: + - /properties/Name + + Read-only properties: + - /properties/Arn + + IAM permissions required: + - events:CreateApiDestination + - events:DescribeApiDestination + + """ + model = request.desired_state + events = request.aws_client_factory.events + + response = events.create_api_destination(**model) + model["Arn"] = response["ApiDestinationArn"] + + return ProgressEvent( + status=OperationStatus.SUCCESS, + resource_model=model, + custom_context=request.custom_context, + ) + + def read( + self, + request: ResourceRequest[EventsApiDestinationProperties], + ) -> ProgressEvent[EventsApiDestinationProperties]: + """ + Fetch resource information + + IAM permissions required: + - events:DescribeApiDestination + """ + raise NotImplementedError + + def delete( + self, + request: ResourceRequest[EventsApiDestinationProperties], + ) -> ProgressEvent[EventsApiDestinationProperties]: + """ + Delete a resource + + IAM permissions required: + - events:DeleteApiDestination + - events:DescribeApiDestination + """ + model = request.desired_state + events = request.aws_client_factory.events + + events.delete_api_destination(Name=model["Name"]) + return ProgressEvent( + status=OperationStatus.SUCCESS, + resource_model=model, + custom_context=request.custom_context, + ) + + def update( + self, + request: ResourceRequest[EventsApiDestinationProperties], + ) -> ProgressEvent[EventsApiDestinationProperties]: + """ + Update a resource + + IAM permissions required: + - events:UpdateApiDestination + - events:DescribeApiDestination + """ + raise NotImplementedError diff --git a/localstack/services/events/resource_providers/aws_events_apidestination.schema.json b/localstack/services/events/resource_providers/aws_events_apidestination.schema.json new file mode 100644 index 0000000000000..f50460b1aea17 --- /dev/null +++ b/localstack/services/events/resource_providers/aws_events_apidestination.schema.json @@ -0,0 +1,92 @@ +{ + "typeName": "AWS::Events::ApiDestination", + "description": "Resource Type definition for AWS::Events::ApiDestination.", + "properties": { + "Name": { + "description": "Name of the apiDestination.", + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "Description": { + "type": "string", + "maxLength": 512 + }, + "ConnectionArn": { + "description": "The arn of the connection.", + "type": "string" + }, + "Arn": { + "description": "The arn of the api destination.", + "type": "string" + }, + "InvocationRateLimitPerSecond": { + "type": "integer", + "minimum": 1 + }, + "InvocationEndpoint": { + "description": "Url endpoint to invoke.", + "type": "string" + }, + "HttpMethod": { + "type": "string", + "enum": [ + "GET", + "HEAD", + "POST", + "OPTIONS", + "PUT", + "DELETE", + "PATCH" + ] + } + }, + "additionalProperties": false, + "createOnlyProperties": [ + "/properties/Name" + ], + "readOnlyProperties": [ + "/properties/Arn" + ], + "required": [ + "ConnectionArn", + "InvocationEndpoint", + "HttpMethod" + ], + "primaryIdentifier": [ + "/properties/Name" + ], + "tagging": { + "taggable": false + }, + "handlers": { + "create": { + "permissions": [ + "events:CreateApiDestination", + "events:DescribeApiDestination" + ] + }, + "read": { + "permissions": [ + "events:DescribeApiDestination" + ] + }, + "update": { + "permissions": [ + "events:UpdateApiDestination", + "events:DescribeApiDestination" + ] + }, + "delete": { + "permissions": [ + "events:DeleteApiDestination", + "events:DescribeApiDestination" + ] + }, + "list": { + "permissions": [ + "events:ListApiDestinations" + ] + } + } +} diff --git a/localstack/services/events/resource_providers/aws_events_apidestination_plugin.py b/localstack/services/events/resource_providers/aws_events_apidestination_plugin.py new file mode 100644 index 0000000000000..0aa7ada08cc50 --- /dev/null +++ b/localstack/services/events/resource_providers/aws_events_apidestination_plugin.py @@ -0,0 +1,20 @@ +from typing import Optional, Type + +from localstack.services.cloudformation.resource_provider import ( + CloudFormationResourceProviderPlugin, + ResourceProvider, +) + + +class EventsApiDestinationProviderPlugin(CloudFormationResourceProviderPlugin): + name = "AWS::Events::ApiDestination" + + def __init__(self): + self.factory: Optional[Type[ResourceProvider]] = None + + def load(self): + from localstack.services.events.resource_providers.aws_events_apidestination import ( + EventsApiDestinationProvider, + ) + + self.factory = EventsApiDestinationProvider diff --git a/tests/aws/services/cloudformation/resources/test_events.py b/tests/aws/services/cloudformation/resources/test_events.py index 3b28501c69ac5..a93f424ba31d7 100644 --- a/tests/aws/services/cloudformation/resources/test_events.py +++ b/tests/aws/services/cloudformation/resources/test_events.py @@ -10,6 +10,35 @@ LOG = logging.getLogger(__name__) +@markers.aws.validated +def test_cfn_event_api_destination_resource(deploy_cfn_template, region_name, aws_client): + def _assert(expected_len): + rs = aws_client.events.list_event_buses() + event_buses = [eb for eb in rs["EventBuses"] if eb["Name"] == "my-test-bus"] + assert len(event_buses) == expected_len + rs = aws_client.events.list_connections() + connections = [con for con in rs["Connections"] if con["Name"] == "my-test-conn"] + assert len(connections) == expected_len + rs = aws_client.events.list_api_destinations() + api_destinations = [ + ad for ad in rs["ApiDestinations"] if ad["Name"] == "my-test-destination" + ] + assert len(api_destinations) == expected_len + + stack = deploy_cfn_template( + template_path=os.path.join( + os.path.dirname(__file__), "../../../templates/events_apidestination.yml" + ), + parameters={ + "Region": region_name, + }, + ) + _assert(1) + + stack.destroy() + _assert(0) + + @markers.aws.validated def test_eventbus_policies(deploy_cfn_template, aws_client): event_bus_name = f"event-bus-{short_uid()}" diff --git a/tests/aws/services/cloudformation/resources/test_events.validation.json b/tests/aws/services/cloudformation/resources/test_events.validation.json index e73ef54c8273e..178ef3817fc37 100644 --- a/tests/aws/services/cloudformation/resources/test_events.validation.json +++ b/tests/aws/services/cloudformation/resources/test_events.validation.json @@ -1,4 +1,7 @@ { + "tests/aws/services/cloudformation/resources/test_events.py::test_cfn_event_api_destination_resource": { + "last_validated_date": "2024-04-16T06:36:56+00:00" + }, "tests/aws/services/cloudformation/resources/test_events.py::test_rule_properties": { "last_validated_date": "2023-12-01T14:03:52+00:00" } diff --git a/tests/aws/templates/events_apidestination.yml b/tests/aws/templates/events_apidestination.yml new file mode 100644 index 0000000000000..444cb9bbf4dd5 --- /dev/null +++ b/tests/aws/templates/events_apidestination.yml @@ -0,0 +1,91 @@ +AWSTemplateFormatVersion: '2010-09-09' + +Parameters: + Region: + Type: String + Default: 'us-east-1' + +Resources: + LocalEventBus: + Type: AWS::Events::EventBus + Properties: + Name: my-test-bus + + LocalEventConnection: + Type: AWS::Events::Connection + Properties: + Name: my-test-conn + AuthorizationType: API_KEY + AuthParameters: + ApiKeyAuthParameters: + ApiKeyName: apikey123 + ApiKeyValue: secretapikey123 + Description: test events connection + + MyLambdaFunctionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: LambdaBasicExecution + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: arn:aws:logs:*:*:* + + MyLambdaFunction: + Type: AWS::Lambda::Function + Properties: + Code: + ZipFile: | + def handler(event, ctx): + return {"statusCode": 200, "body": "hello"} + Role: !GetAtt MyLambdaFunctionRole.Arn + Handler: index.handler + Runtime: python3.8 + FunctionName: my-test-function + + MyApiGatewayResource: + Type: AWS::ApiGateway::Resource + Properties: + RestApiId: !Ref MyApiGateway + ParentId: !GetAtt MyApiGateway.RootResourceId + PathPart: 'myendpoint' + + MyApiGatewayMethod: + Type: AWS::ApiGateway::Method + Properties: + RestApiId: !Ref MyApiGateway + ResourceId: !Ref MyApiGatewayResource + HttpMethod: 'POST' + AuthorizationType: 'NONE' + Integration: + IntegrationHttpMethod: 'POST' + Type: 'AWS_PROXY' + Uri: !Sub 'arn:aws:apigateway:${Region}:lambda:path/2015-03-31/functions/${MyLambdaFunction.Arn}/invocations' + + MyApiGateway: + Type: AWS::ApiGateway::RestApi + Properties: + Name: MyApiGateway + + LocalEventApiDestination: + Type: AWS::Events::ApiDestination + Properties: + Name: my-test-destination + ConnectionArn: !GetAtt LocalEventConnection.Arn + Description: test events api destination + HttpMethod: POST + InvocationEndpoint: + Fn::Sub: 'https://${MyApiGateway}.execute-api.${Region}.amazonaws.com/myendpoint' From caf4e8c48f5280d4a0f559e5953fdd3f3a211fa8 Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Tue, 16 Apr 2024 15:02:09 +0100 Subject: [PATCH 067/169] CFn: handle `OperationStatus.PENDING` events from resource providers (#10673) Co-authored-by: Bojan Miletic <4889922+Morijarti@users.noreply.github.com> --- .../engine/template_deployer.py | 42 ++++++++++++++++--- .../cloudformation/resource_provider.py | 3 ++ 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/localstack/services/cloudformation/engine/template_deployer.py b/localstack/services/cloudformation/engine/template_deployer.py index 69e6b65fb1e81..573817bdfb0a6 100644 --- a/localstack/services/cloudformation/engine/template_deployer.py +++ b/localstack/services/cloudformation/engine/template_deployer.py @@ -415,7 +415,8 @@ def _resolve_refs_recursively( selected_map = mappings.get(mapping_id) if not selected_map: raise Exception( - f"Cannot find Mapping with ID {mapping_id} for Fn::FindInMap: {value[keys_list[0]]} {list(resources.keys())}" # TODO: verify + f"Cannot find Mapping with ID {mapping_id} for Fn::FindInMap: {value[keys_list[0]]} {list(resources.keys())}" + # TODO: verify ) first_level_attribute = value[keys_list[0]][1] @@ -915,8 +916,37 @@ def _safe_lookup_is_deleted(r_id): resource["ResourceType"], iteration_cycle, ) - executor.deploy_loop(resource, resource_provider_payload) - self.stack.set_resource_status(resource_id, "DELETE_COMPLETE") + event = executor.deploy_loop(resource, resource_provider_payload) + match event.status: + case OperationStatus.SUCCESS: + self.stack.set_resource_status(resource_id, "DELETE_COMPLETE") + case OperationStatus.PENDING: + # the resource is still being deleted, specifically the provider has + # signalled that the deployment loop should skip this resource this + # time and come back to it later, likely due to unmet child + # resources still existing because we don't delete things in the + # correct order yet. + continue + case OperationStatus.FAILED: + if iteration_cycle == max_cycle: + LOG.exception( + "Last cycle failed to delete resource with id %s. Reason: %s", + resource_id, + event.message or "unknown", + ) + else: + # the resource failed to delete this time, but we have more + # iterations left to complete the process + continue + case OperationStatus.IN_PROGRESS: + # the resource provider executor should not return this state, so + # this state is a programming error + raise Exception( + "Programming error: ResourceProviderExecutor cannot return IN_PROGRESS" + ) + case other_status: + raise Exception(f"Use of unsupported status found: {other_status}") + except Exception as e: if iteration_cycle == max_cycle: LOG.exception( @@ -1399,9 +1429,9 @@ def apply_change(self, change: ChangeConfig, stack: Stack) -> None: case OperationStatus.SUCCESS: stack.set_resource_status(resource_id, f"{stack_action}_COMPLETE") case OperationStatus.PENDING: - # this isn't really a state we use at the moment - raise Exception( - f"Usage of currently unsupported operation status detected: {OperationStatus.PENDING}" + # signal to the main loop that we should come back to this resource in the future + raise DependencyNotYetSatisfied( + resource_ids=[], message="Resource dependencies not yet satisfied" ) case OperationStatus.IN_PROGRESS: raise Exception("Resource deployment loop should not finish in this state") diff --git a/localstack/services/cloudformation/resource_provider.py b/localstack/services/cloudformation/resource_provider.py index 812c04b30c578..23c26e7f56663 100644 --- a/localstack/services/cloudformation/resource_provider.py +++ b/localstack/services/cloudformation/resource_provider.py @@ -444,6 +444,9 @@ def deploy_loop( time.sleep(0) else: time.sleep(sleep_time) + case OperationStatus.PENDING: + # come back to this resource in another iteration + return event case invalid_status: raise ValueError( f"Invalid OperationStatus ({invalid_status}) returned for resource {raw_payload['requestData']['logicalResourceId']} (type {raw_payload['resourceType']})" From 3241303c511b6904ffc5c6f3609deaa152d4d67f Mon Sep 17 00:00:00 2001 From: Marccio Silva Date: Tue, 16 Apr 2024 12:32:55 -0300 Subject: [PATCH 068/169] Fix multi region KMS key creation configuration issue (#10625) --- localstack/services/kms/models.py | 39 ++++ localstack/services/kms/provider.py | 24 ++- tests/aws/services/kms/test_kms.py | 18 +- tests/aws/services/kms/test_kms.snapshot.json | 56 +++++- .../aws/services/kms/test_kms.validation.json | 182 +++++++++++++----- 5 files changed, 252 insertions(+), 67 deletions(-) diff --git a/localstack/services/kms/models.py b/localstack/services/kms/models.py index fb2262bd20db7..fa3e98511cfc9 100644 --- a/localstack/services/kms/models.py +++ b/localstack/services/kms/models.py @@ -33,7 +33,11 @@ KMSInvalidSignatureException, MacAlgorithmSpec, MessageType, + MultiRegionConfiguration, + MultiRegionKey, + MultiRegionKeyType, OriginType, + ReplicateKeyRequest, SigningAlgorithmSpec, UnsupportedOperationException, ) @@ -342,6 +346,34 @@ def verify( # AWS itself raises this exception without any additional message. raise KMSInvalidSignatureException() + # This method gets called when a key is replicated to another region. It's meant to populate the required metadata + # fields in a new replica key. + def replicate_metadata( + self, replicate_key_request: ReplicateKeyRequest, account_id: str, replica_region: str + ) -> None: + self.metadata["Description"] = replicate_key_request.get("Description") or "" + primary_key_arn = self.metadata["Arn"] + # Multi region keys have the same key ID for all replicas, but ARNs differ, as they include actual regions of + # replicas. + self.calculate_and_set_arn(account_id, replica_region) + + current_replica_keys = self.metadata.get("MultiRegionConfiguration", {}).get( + "ReplicaKeys", [] + ) + current_replica_keys.append(MultiRegionKey(Arn=self.metadata["Arn"], Region=replica_region)) + primary_key_region = ( + self.metadata.get("MultiRegionConfiguration", {}).get("PrimaryKey", {}).get("Region") + ) + + self.metadata["MultiRegionConfiguration"] = MultiRegionConfiguration( + MultiRegionKeyType=MultiRegionKeyType.REPLICA, + PrimaryKey=MultiRegionKey( + Arn=primary_key_arn, + Region=primary_key_region, + ), + ReplicaKeys=current_replica_keys, + ) + def _get_hmac_context(self, mac_algorithm: MacAlgorithmSpec) -> hmac.HMAC: if mac_algorithm == "HMAC_SHA_224": h = hmac.HMAC(self.crypto_key.key_material, hashes.SHA224()) @@ -468,6 +500,13 @@ def _populate_metadata( ) self._populate_mac_algorithms(self.metadata.get("KeyUsage"), self.metadata.get("KeySpec")) + if self.metadata["MultiRegion"]: + self.metadata["MultiRegionConfiguration"] = MultiRegionConfiguration( + MultiRegionKeyType=MultiRegionKeyType.PRIMARY, + PrimaryKey=MultiRegionKey(Arn=self.metadata["Arn"], Region=region), + ReplicaKeys=[], + ) + def add_tags(self, tags: List) -> None: # Just in case we get None from somewhere. if not tags: diff --git a/localstack/services/kms/provider.py b/localstack/services/kms/provider.py index 5b9a7767738b4..95d60198de5f1 100644 --- a/localstack/services/kms/provider.py +++ b/localstack/services/kms/provider.py @@ -76,6 +76,7 @@ ListResourceTagsResponse, MacAlgorithmSpec, MarkerType, + MultiRegionKey, NotFoundException, NullableBooleanType, PlaintextType, @@ -465,27 +466,38 @@ def describe_key( def replicate_key( self, context: RequestContext, request: ReplicateKeyRequest ) -> ReplicateKeyResponse: - key = self._get_kms_key(context.account_id, context.region, request.get("KeyId")) + account_id = context.account_id + key = self._get_kms_key(account_id, context.region, request.get("KeyId")) key_id = key.metadata.get("KeyId") if not key.metadata.get("MultiRegion"): raise UnsupportedOperationException( f"Unable to replicate a non-MultiRegion key {key_id}" ) replica_region = request.get("ReplicaRegion") - replicate_to_store = kms_stores[context.account_id][replica_region] + replicate_to_store = kms_stores[account_id][replica_region] if key_id in replicate_to_store.keys: raise AlreadyExistsException( f"Unable to replicate key {key_id} to region {replica_region}, as the key " f"already exist there" ) replica_key = copy.deepcopy(key) - replica_key.metadata["Description"] = request.get("Description", "") - # Multiregion keys have the same key ID for all replicas, but ARNs differ, as they include actual regions of - # replicas. - replica_key.calculate_and_set_arn(context.account_id, replica_region) + replica_key.replicate_metadata(request, account_id, replica_region) replicate_to_store.keys[key_id] = replica_key + + self.update_primary_key_with_replica_keys(key, replica_key, replica_region) + return ReplicateKeyResponse(ReplicaKeyMetadata=replica_key.metadata) + @staticmethod + # Adds new multi region replica key to the primary key's metadata. + def update_primary_key_with_replica_keys(key: KmsKey, replica_key: KmsKey, region: str): + key.metadata["MultiRegionConfiguration"]["ReplicaKeys"].append( + MultiRegionKey( + Arn=replica_key.metadata["Arn"], + Region=region, + ) + ) + @handler("UpdateKeyDescription", expand=False) def update_key_description( self, context: RequestContext, request: UpdateKeyDescriptionRequest diff --git a/tests/aws/services/kms/test_kms.py b/tests/aws/services/kms/test_kms.py index e3c20177f00f1..31b49a7d9aff0 100644 --- a/tests/aws/services/kms/test_kms.py +++ b/tests/aws/services/kms/test_kms.py @@ -788,26 +788,28 @@ def get_alias_arn_by_alias_name(kms_client, alias_name): aws_client.kms.encrypt(KeyId=alias_name, Plaintext="encrypt-me") @markers.aws.validated - def test_create_multi_region_key(self, kms_create_key): - key = kms_create_key(MultiRegion=True) + def test_create_multi_region_key(self, kms_create_key, snapshot): + snapshot.add_transformer(snapshot.transform.kms_api()) + key = kms_create_key(MultiRegion=True, Description="test multi region key") assert key["KeyId"].startswith("mrk-") - assert key["MultiRegion"] + snapshot.match("create_multi_region_key", key) @markers.aws.validated - def test_non_multi_region_keys_should_not_have_multi_region_properties(self, kms_create_key): - key = kms_create_key(MultiRegion=False) + def test_non_multi_region_keys_should_not_have_multi_region_properties( + self, kms_create_key, snapshot + ): + snapshot.add_transformer(snapshot.transform.kms_api()) + key = kms_create_key(MultiRegion=False, Description="test non multi region key") assert not key["KeyId"].startswith("mrk-") - assert not key["MultiRegion"] + snapshot.match("non_multi_region_keys_should_not_have_multi_region_properties", key) @markers.aws.validated @markers.snapshot.skip_snapshot_verify( paths=[ "$..KeyMetadata.Enabled", "$..KeyMetadata.KeyState", - "$..KeyMetadata.MultiRegionConfiguration", # not implemented "$..ReplicaKeyMetadata.Enabled", "$..ReplicaKeyMetadata.KeyState", - "$..ReplicaKeyMetadata.MultiRegionConfiguration", # not implemented "$..ReplicaPolicy", # not implemented ], ) diff --git a/tests/aws/services/kms/test_kms.snapshot.json b/tests/aws/services/kms/test_kms.snapshot.json index 3dbcbb51da413..2a1ba8a74c29c 100644 --- a/tests/aws/services/kms/test_kms.snapshot.json +++ b/tests/aws/services/kms/test_kms.snapshot.json @@ -339,7 +339,7 @@ } }, "tests/aws/services/kms/test_kms.py::TestKMS::test_replicate_key": { - "recorded-date": "13-07-2023, 11:59:29", + "recorded-date": "11-04-2024, 15:51:52", "recorded-content": { "describe-key-from-different-region": { "Error": { @@ -1672,5 +1672,59 @@ } } } + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_create_multi_region_key": { + "recorded-date": "11-04-2024, 14:46:26", + "recorded-content": { + "create_multi_region_key": { + "AWSAccountId": "111111111111", + "Arn": "arn:aws:kms::111111111111:key/", + "CreationDate": "datetime", + "CustomerMasterKeySpec": "SYMMETRIC_DEFAULT", + "Description": "test multi region key", + "Enabled": true, + "EncryptionAlgorithms": [ + "SYMMETRIC_DEFAULT" + ], + "KeyId": "", + "KeyManager": "CUSTOMER", + "KeySpec": "SYMMETRIC_DEFAULT", + "KeyState": "Enabled", + "KeyUsage": "ENCRYPT_DECRYPT", + "MultiRegion": true, + "MultiRegionConfiguration": { + "MultiRegionKeyType": "PRIMARY", + "PrimaryKey": { + "Arn": "arn:aws:kms::111111111111:key/", + "Region": "" + }, + "ReplicaKeys": [] + }, + "Origin": "AWS_KMS" + } + } + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_non_multi_region_keys_should_not_have_multi_region_properties": { + "recorded-date": "11-04-2024, 15:47:59", + "recorded-content": { + "non_multi_region_keys_should_not_have_multi_region_properties": { + "AWSAccountId": "111111111111", + "Arn": "arn:aws:kms::111111111111:key/", + "CreationDate": "datetime", + "CustomerMasterKeySpec": "SYMMETRIC_DEFAULT", + "Description": "test non multi region key", + "Enabled": true, + "EncryptionAlgorithms": [ + "SYMMETRIC_DEFAULT" + ], + "KeyId": "", + "KeyManager": "CUSTOMER", + "KeySpec": "SYMMETRIC_DEFAULT", + "KeyState": "Enabled", + "KeyUsage": "ENCRYPT_DECRYPT", + "MultiRegion": false, + "Origin": "AWS_KMS" + } + } } } diff --git a/tests/aws/services/kms/test_kms.validation.json b/tests/aws/services/kms/test_kms.validation.json index 022ebab386276..24062e3481726 100644 --- a/tests/aws/services/kms/test_kms.validation.json +++ b/tests/aws/services/kms/test_kms.validation.json @@ -1,161 +1,239 @@ { + "tests/aws/services/kms/test_kms.py::TestKMS::test_all_types_of_key_id_can_be_used_for_encryption": { + "last_validated_date": "2024-04-11T15:53:39+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_cant_delete_deleted_key": { + "last_validated_date": "2024-04-11T15:54:00+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_cant_use_disabled_or_deleted_keys": { + "last_validated_date": "2024-04-11T15:53:59+00:00" + }, "tests/aws/services/kms/test_kms.py::TestKMS::test_create_alias": { - "last_validated_date": "2023-04-13T09:29:27+00:00" + "last_validated_date": "2024-04-11T15:52:25+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_create_grant_with_invalid_key": { + "last_validated_date": "2024-04-11T15:52:39+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_create_grant_with_same_name_two_keys": { + "last_validated_date": "2024-04-11T15:52:42+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_create_grant_with_valid_key": { + "last_validated_date": "2024-04-11T15:52:40+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_create_key": { - "last_validated_date": "2023-04-13T09:29:30+00:00" + "last_validated_date": "2024-04-11T15:26:14+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_create_list_delete_alias": { + "last_validated_date": "2024-04-11T15:53:50+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_create_multi_region_key": { + "last_validated_date": "2024-04-11T15:53:40+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_describe_and_list_sign_key": { + "last_validated_date": "2024-04-11T15:53:27+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_disable_and_enable_key": { + "last_validated_date": "2024-04-11T15:52:38+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_decrypt[RSA_2048-RSAES_OAEP_SHA_256]": { + "last_validated_date": "2024-04-11T15:53:19+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_decrypt[SYMMETRIC_DEFAULT-SYMMETRIC_DEFAULT]": { + "last_validated_date": "2024-04-11T15:53:18+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_decrypt_encryption_context": { - "last_validated_date": "2023-05-11T20:46:49+00:00" + "last_validated_date": "2024-04-11T15:54:22+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_validate_plaintext_size_per_key_type[RSA_2048-RSAES_OAEP_SHA_1]": { - "last_validated_date": "2023-04-13T09:30:27+00:00" + "last_validated_date": "2024-04-11T15:53:20+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_validate_plaintext_size_per_key_type[RSA_2048-RSAES_OAEP_SHA_256]": { - "last_validated_date": "2023-04-13T09:30:28+00:00" + "last_validated_date": "2024-04-11T15:53:21+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_validate_plaintext_size_per_key_type[RSA_3072-RSAES_OAEP_SHA_1]": { - "last_validated_date": "2023-04-13T09:30:29+00:00" + "last_validated_date": "2024-04-11T15:53:22+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_validate_plaintext_size_per_key_type[RSA_3072-RSAES_OAEP_SHA_256]": { - "last_validated_date": "2023-04-13T09:30:30+00:00" + "last_validated_date": "2024-04-11T15:53:22+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_validate_plaintext_size_per_key_type[RSA_4096-RSAES_OAEP_SHA_1]": { - "last_validated_date": "2023-04-13T09:30:31+00:00" + "last_validated_date": "2024-04-11T15:53:23+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_validate_plaintext_size_per_key_type[RSA_4096-RSAES_OAEP_SHA_256]": { - "last_validated_date": "2023-04-13T09:30:32+00:00" + "last_validated_date": "2024-04-11T15:53:24+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_error_messaging_for_invalid_keys": { - "last_validated_date": "2023-04-13T09:31:23+00:00" + "last_validated_date": "2024-04-11T15:54:19+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_and_verify_mac[HMAC_224-HMAC_SHA_224]": { - "last_validated_date": "2023-11-07T13:06:46+00:00" + "last_validated_date": "2024-04-11T15:54:05+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_and_verify_mac[HMAC_256-HMAC_SHA_256]": { - "last_validated_date": "2023-11-07T13:06:48+00:00" + "last_validated_date": "2024-04-11T15:54:07+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_and_verify_mac[HMAC_384-HMAC_SHA_384]": { - "last_validated_date": "2023-11-07T13:06:51+00:00" + "last_validated_date": "2024-04-11T15:54:09+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_and_verify_mac[HMAC_512-HMAC_SHA_512]": { - "last_validated_date": "2023-11-07T13:06:52+00:00" + "last_validated_date": "2024-04-11T15:54:11+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_random[1024]": { - "last_validated_date": "2023-04-13T09:29:51+00:00" + "last_validated_date": "2024-04-11T15:52:49+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_random[12]": { - "last_validated_date": "2023-04-13T09:29:50+00:00" + "last_validated_date": "2024-04-11T15:52:48+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_random[1]": { - "last_validated_date": "2023-04-13T09:29:51+00:00" + "last_validated_date": "2024-04-11T15:52:49+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_random[44]": { - "last_validated_date": "2023-04-13T09:29:50+00:00" + "last_validated_date": "2024-04-11T15:52:48+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_random[91]": { - "last_validated_date": "2023-04-13T09:29:51+00:00" + "last_validated_date": "2024-04-11T15:52:49+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_random_invalid_number_of_bytes[0]": { - "last_validated_date": "2023-04-13T09:29:53+00:00" + "last_validated_date": "2024-04-11T15:52:50+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_random_invalid_number_of_bytes[1025]": { - "last_validated_date": "2023-04-13T09:29:54+00:00" + "last_validated_date": "2024-04-11T15:52:51+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_random_invalid_number_of_bytes[None]": { - "last_validated_date": "2023-04-13T09:29:52+00:00" + "last_validated_date": "2024-04-11T15:52:50+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_get_key_does_not_exist": { - "last_validated_date": "2023-04-13T09:29:34+00:00" + "last_validated_date": "2024-04-11T15:52:32+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_get_key_in_different_region": { - "last_validated_date": "2023-07-13T09:58:37+00:00" + "last_validated_date": "2024-04-11T15:52:31+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_get_key_invalid_uuid": { - "last_validated_date": "2023-11-07T13:05:57+00:00" + "last_validated_date": "2024-04-11T15:52:33+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_get_parameters_for_import": { - "last_validated_date": "2023-10-25T10:29:27+00:00" + "last_validated_date": "2024-04-11T15:54:23+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_get_public_key": { + "last_validated_date": "2024-04-11T15:53:25+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_get_put_list_key_policies": { + "last_validated_date": "2024-04-11T15:53:55+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_hmac_create_key": { - "last_validated_date": "2023-04-13T09:34:18+00:00" + "last_validated_date": "2024-04-10T20:40:13+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_hmac_create_key_invalid_operations": { "last_validated_date": "2023-04-13T09:31:06+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_import_key_asymmetric": { - "last_validated_date": "2024-01-24T10:44:14+00:00" + "last_validated_date": "2024-04-11T15:53:35+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_import_key_symmetric": { - "last_validated_date": "2024-01-24T10:44:12+00:00" + "last_validated_date": "2024-04-11T15:53:31+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_invalid_generate_mac[HMAC_224-HMAC_SHA_256]": { - "last_validated_date": "2023-04-13T09:31:14+00:00" + "last_validated_date": "2024-04-11T15:54:11+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_invalid_generate_mac[HMAC_256-INVALID]": { - "last_validated_date": "2023-04-13T09:31:15+00:00" + "last_validated_date": "2024-04-11T15:54:12+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_invalid_key_usage": { + "last_validated_date": "2024-04-11T15:53:16+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_invalid_verify_mac[HMAC_256-HMAC_SHA_256-some different important message]": { - "last_validated_date": "2023-04-13T09:31:17+00:00" + "last_validated_date": "2024-04-11T15:54:14+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_invalid_verify_mac[HMAC_256-HMAC_SHA_512-some important message]": { - "last_validated_date": "2023-04-13T09:31:18+00:00" + "last_validated_date": "2024-04-11T15:54:15+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_invalid_verify_mac[HMAC_256-INVALID-some important message]": { - "last_validated_date": "2023-04-13T09:31:19+00:00" + "last_validated_date": "2024-04-11T15:54:16+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_key_rotation_status": { + "last_validated_date": "2024-04-11T15:53:48+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_list_aliases_of_key": { + "last_validated_date": "2024-04-11T15:53:36+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_list_grants_with_invalid_key": { + "last_validated_date": "2024-04-11T15:52:39+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_list_keys": { + "last_validated_date": "2024-04-11T15:52:34+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_non_multi_region_keys_should_not_have_multi_region_properties": { + "last_validated_date": "2024-04-11T15:53:41+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_plaintext_size_for_encrypt": { - "last_validated_date": "2023-04-13T09:31:24+00:00" + "last_validated_date": "2024-04-11T15:54:20+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_replicate_key": { - "last_validated_date": "2023-07-13T09:59:29+00:00" + "last_validated_date": "2024-04-11T15:53:44+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_retire_grant_with_grant_token": { + "last_validated_date": "2024-04-11T15:52:46+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_revoke_grant": { + "last_validated_date": "2024-04-11T15:52:44+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_schedule_and_cancel_key_deletion": { + "last_validated_date": "2024-04-11T15:52:36+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_sign_verify[ECC_NIST_P256-ECDSA_SHA_256]": { - "last_validated_date": "2023-04-13T09:30:15+00:00" + "last_validated_date": "2024-04-11T15:53:09+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_sign_verify[ECC_NIST_P384-ECDSA_SHA_384]": { - "last_validated_date": "2023-04-13T09:30:19+00:00" + "last_validated_date": "2024-04-11T15:53:12+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_sign_verify[ECC_SECG_P256K1-ECDSA_SHA_256]": { - "last_validated_date": "2023-04-13T09:30:22+00:00" + "last_validated_date": "2024-04-11T15:53:15+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_sign_verify[RSA_2048-RSASSA_PSS_SHA_256]": { - "last_validated_date": "2023-04-13T09:29:59+00:00" + "last_validated_date": "2024-04-11T15:52:53+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_sign_verify[RSA_2048-RSASSA_PSS_SHA_384]": { - "last_validated_date": "2023-04-13T09:30:02+00:00" + "last_validated_date": "2024-04-11T15:52:57+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_sign_verify[RSA_2048-RSASSA_PSS_SHA_512]": { - "last_validated_date": "2023-04-13T09:30:05+00:00" + "last_validated_date": "2024-04-11T15:53:00+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_sign_verify[RSA_4096-RSASSA_PKCS1_V1_5_SHA_256]": { - "last_validated_date": "2023-04-13T09:30:09+00:00" + "last_validated_date": "2024-04-11T15:53:03+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMS::test_sign_verify[RSA_4096-RSASSA_PKCS1_V1_5_SHA_512]": { - "last_validated_date": "2023-04-13T09:30:12+00:00" + "last_validated_date": "2024-04-11T15:53:06+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_tag_untag_list_tags": { + "last_validated_date": "2024-04-11T15:53:57+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_update_alias": { + "last_validated_date": "2024-04-11T15:53:53+00:00" + }, + "tests/aws/services/kms/test_kms.py::TestKMS::test_update_key_description": { + "last_validated_date": "2024-04-11T15:53:46+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_encryption_context_generate_data_key": { - "last_validated_date": "2023-06-16T10:47:28+00:00" + "last_validated_date": "2024-04-11T15:54:32+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_encryption_context_generate_data_key_pair": { - "last_validated_date": "2023-06-16T15:51:27+00:00" + "last_validated_date": "2024-04-11T15:54:35+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_encryption_context_generate_data_key_pair_without_plaintext": { - "last_validated_date": "2023-06-16T15:51:43+00:00" + "last_validated_date": "2024-04-11T15:54:36+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_encryption_context_generate_data_key_without_plaintext": { - "last_validated_date": "2023-06-16T10:47:45+00:00" + "last_validated_date": "2024-04-11T15:54:34+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_generate_data_key": { - "last_validated_date": "2023-05-11T12:40:24+00:00" + "last_validated_date": "2024-04-11T15:54:30+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_generate_data_key_pair": { - "last_validated_date": "2023-05-11T12:40:23+00:00" + "last_validated_date": "2024-04-11T15:54:29+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_generate_data_key_pair_without_plaintext": { - "last_validated_date": "2023-05-11T12:40:23+00:00" + "last_validated_date": "2024-04-11T15:54:28+00:00" }, "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_generate_data_key_without_plaintext": { - "last_validated_date": "2023-05-11T12:40:24+00:00" + "last_validated_date": "2024-04-11T15:54:31+00:00" } } From 9af4246ef854ab27a2f420799ce45caedf8d2f26 Mon Sep 17 00:00:00 2001 From: Thomas Rausch Date: Wed, 17 Apr 2024 00:00:51 +0200 Subject: [PATCH 069/169] fix lambda function URLs when they don't exist or the service is not loaded (#10640) --- localstack/aws/app.py | 4 ++- localstack/aws/handlers/service_plugin.py | 34 ++++++++++++++----- localstack/aws/protocol/service_router.py | 20 ++++++++++- localstack/services/lambda_/urlrouter.py | 29 +++++++++------- tests/aws/services/lambda_/test_lambda.py | 15 ++++++++ .../lambda_/test_lambda.validation.json | 3 ++ 6 files changed, 83 insertions(+), 22 deletions(-) diff --git a/localstack/aws/app.py b/localstack/aws/app.py index b702d5f9a64cb..d6f67e3d27875 100644 --- a/localstack/aws/app.py +++ b/localstack/aws/app.py @@ -3,7 +3,7 @@ from localstack.aws.api import RequestContext from localstack.aws.chain import HandlerChain from localstack.aws.handlers.metric_handler import MetricHandler -from localstack.aws.handlers.service_plugin import ServiceLoader +from localstack.aws.handlers.service_plugin import ServiceLoader, ServiceLoaderForDataPlane from localstack.aws.trace import TracingHandlerChain from localstack.services.plugins import SERVICE_PLUGINS, ServiceManager, ServicePluginManager from localstack.utils.ssl import create_ssl_cert, install_predefined_cert_if_available @@ -22,6 +22,7 @@ def __init__(self, service_manager: ServiceManager = None) -> None: self.service_request_router = ServiceRequestRouter() # lazy-loads services into the router load_service = ServiceLoader(self.service_manager, self.service_request_router) + load_service_for_data_plane = ServiceLoaderForDataPlane(load_service) metric_collector = MetricHandler() # the main request handler chain @@ -31,6 +32,7 @@ def __init__(self, service_manager: ServiceManager = None) -> None: handlers.add_internal_request_params, handlers.handle_runtime_shutdown, metric_collector.create_metric_handler_item, + load_service_for_data_plane, handlers.preprocess_request, handlers.parse_service_name, # enforce_cors and content_decoder depend on the service name handlers.enforce_cors, diff --git a/localstack/aws/handlers/service_plugin.py b/localstack/aws/handlers/service_plugin.py index 0af5ed4f6ad6b..6faddde0a7e07 100644 --- a/localstack/aws/handlers/service_plugin.py +++ b/localstack/aws/handlers/service_plugin.py @@ -2,7 +2,6 @@ import logging import threading -from typing import Optional from localstack.http import Response from localstack.services.plugins import Service, ServiceManager @@ -10,8 +9,8 @@ from ...utils.bootstrap import is_api_enabled from ..api import RequestContext -from ..api.core import ServiceOperation from ..chain import Handler, HandlerChain +from ..protocol.service_router import determine_aws_service_model_for_data_plane from .service import ServiceRequestRouter LOG = logging.getLogger(__name__) @@ -31,6 +30,7 @@ def __init__( self.service_manager = service_manager self.service_request_router = service_request_router self.service_locks = SynchronizedDefaultDict(threading.RLock) + self.loaded_services = set() def __call__(self, chain: HandlerChain, context: RequestContext, response: Response): return self.require_service(chain, context, response) @@ -40,6 +40,9 @@ def require_service(self, _: HandlerChain, context: RequestContext, response: Re return service_name: str = context.service.service_name + if service_name in self.loaded_services: + return + if not self.service_manager.exists(service_name): raise NotImplementedError elif not is_api_enabled(service_name): @@ -47,20 +50,16 @@ def require_service(self, _: HandlerChain, context: RequestContext, response: Re f"Service '{service_name}' is not enabled. Please check your 'SERVICES' configuration variable." ) - service_operation: Optional[ServiceOperation] = context.service_operation request_router = self.service_request_router # Ensure the Service is loaded and set to ServiceState.RUNNING if not in an erroneous state. service_plugin: Service = self.service_manager.require(service_name) - # Continue adding service skelethon and handlers to the router if these are missing. - if service_operation in request_router.handlers: - return - with self.service_locks[context.service.service_name]: # try again to avoid race conditions - if service_operation in request_router.handlers: + if service_name in self.loaded_services: return + self.loaded_services.add(service_name) if isinstance(service_plugin, Service): request_router.add_skeleton(service_plugin.skeleton) else: @@ -68,3 +67,22 @@ def require_service(self, _: HandlerChain, context: RequestContext, response: Re f"found plugin for '{service_name}', " f"but cannot attach service plugin of type '{type(service_plugin)}'", ) + + +class ServiceLoaderForDataPlane(Handler): + """ + Specific lightweight service loader that loads services based only on hostname indicators. This allows + us to correctly load services when things like lambda function URLs or APIGW REST APIs are called + before the services were actually loaded. + """ + + def __init__(self, service_loader: ServiceLoader): + self.service_loader = service_loader + + def __call__(self, chain: HandlerChain, context: RequestContext, response: Response): + if context.service: + return + + if service := determine_aws_service_model_for_data_plane(context.request): + context.service = service + self.service_loader.require_service(chain, context, response) diff --git a/localstack/aws/protocol/service_router.py b/localstack/aws/protocol/service_router.py index 479686248b4f5..f8080c04f8a22 100644 --- a/localstack/aws/protocol/service_router.py +++ b/localstack/aws/protocol/service_router.py @@ -138,7 +138,9 @@ def custom_signing_name_rules(signing_name: str, path: str) -> Optional[ServiceM def custom_host_addressing_rules(host: str) -> Optional[ServiceModelIdentifier]: """ - Rules based on the host header of the request. + Rules based on the host header of the request, which is typically the data plane of a service. + + # TODO: ELB, AppSync, CloudFront, ... """ if ".execute-api." in host: return ServiceModelIdentifier("apigateway") @@ -146,6 +148,9 @@ def custom_host_addressing_rules(host: str) -> Optional[ServiceModelIdentifier]: if ".lambda-url." in host: return ServiceModelIdentifier("lambda") + if ".s3-website." in host: + return ServiceModelIdentifier("s3") + def custom_path_addressing_rules(path: str) -> Optional[ServiceModelIdentifier]: """ @@ -305,6 +310,19 @@ def resolve_conflicts( ) +def determine_aws_service_model_for_data_plane( + request: Request, services: ServiceCatalog = None +) -> Optional[ServiceModel]: + """ + A stripped down version of ``determine_aws_service_model`` which only checks hostname indicators for + the AWS data plane, such as s3 websites, lambda function URLs, or API gateway routes. + """ + custom_host_match = custom_host_addressing_rules(request.host) + if custom_host_match: + services = services or get_service_catalog() + return services.get(*custom_host_match) + + def determine_aws_service_model( request: Request, services: ServiceCatalog = None ) -> Optional[ServiceModel]: diff --git a/localstack/services/lambda_/urlrouter.py b/localstack/services/lambda_/urlrouter.py index fb166d22088ac..716a315879721 100644 --- a/localstack/services/lambda_/urlrouter.py +++ b/localstack/services/lambda_/urlrouter.py @@ -56,20 +56,25 @@ def register_routes(self) -> None: def handle_lambda_url_invocation( self, request: Request, api_id: str, region: str, **url_params: dict[str, str] ) -> HttpResponse: - response = HttpResponse(headers={"Content-type": "application/json"}) + response = HttpResponse() + response.mimetype = "application/json" lambda_url_config = None - try: - for account_id in lambda_stores.keys(): - store = lambda_stores[account_id][region] - for fn in store.functions.values(): - for url_config in fn.function_url_configs.values(): - if url_config.url_id == api_id: - lambda_url_config = url_config - except IndexError as e: - LOG.warning(f"Lambda URL ({api_id}) not found: {e}") - response.set_json({"Message": None}) - response.status = "404" + + for account_id in lambda_stores.keys(): + store = lambda_stores[account_id][region] + for fn in store.functions.values(): + for url_config in fn.function_url_configs.values(): + if url_config.url_id == api_id: + lambda_url_config = url_config + + # TODO: check if errors are different when the URL has existed previously + if lambda_url_config is None: + LOG.info("Lambda URL %s does not exist", request.url) + response.data = '{"Message":null}' + response.status = 403 + response.headers["x-amzn-ErrorType"] = "AccessDeniedException" + # TODO: x-amzn-requestid return response event = event_for_lambda_url( diff --git a/tests/aws/services/lambda_/test_lambda.py b/tests/aws/services/lambda_/test_lambda.py index a20ab46ca7555..fb060c89b84bb 100644 --- a/tests/aws/services/lambda_/test_lambda.py +++ b/tests/aws/services/lambda_/test_lambda.py @@ -1128,6 +1128,21 @@ def test_lambda_url_invalid_invoke_mode(self, create_lambda_function, snapshot, ) snapshot.match("invoke_function_invalid_invoke_type", e.value.response) + @markers.aws.validated + def test_lambda_url_non_existing_url(self): + lambda_url_subdomain = "0123456789abcdefghijklmnopqrstuv.lambda-url.us-east-1" + + if is_aws_cloud(): + url = f"https://{lambda_url_subdomain}.on.aws" + else: + url = config.external_service_url(subdomains=lambda_url_subdomain) + + response = requests.get(url) + assert response.text == '{"Message":null}' + assert response.status_code == 403 + assert response.headers["Content-Type"] == "application/json" + assert response.headers["x-amzn-ErrorType"] == "AccessDeniedException" + @markers.snapshot.skip_snapshot_verify( paths=[ "$..headers.domain", # TODO: LS Lambda should populate this value for AWS parity diff --git a/tests/aws/services/lambda_/test_lambda.validation.json b/tests/aws/services/lambda_/test_lambda.validation.json index 0c9244b8f0c46..fa6b800f08675 100644 --- a/tests/aws/services/lambda_/test_lambda.validation.json +++ b/tests/aws/services/lambda_/test_lambda.validation.json @@ -200,6 +200,9 @@ "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation_exception": { "last_validated_date": "2023-11-20T21:05:44+00:00" }, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_non_existing_url": { + "last_validated_date": "2024-04-11T17:16:39+00:00" + }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaVersions::test_lambda_handler_update": { "last_validated_date": "2024-02-14T14:32:31+00:00" }, From b6b827cae6926175e0f73efa24ca43bfd4ec89ad Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Wed, 17 Apr 2024 11:21:31 +0100 Subject: [PATCH 070/169] StepFunctions: Support for TestState Api Action (#10168) --- .../asl/component/common/parameters.py | 4 +- .../asl/component/common/result_selector.py | 6 +- .../state/state_choice/state_choice.py | 1 - .../asl/component/state/state_pass/result.py | 12 +- .../component/state/state_pass/state_pass.py | 2 +- .../asl/component/test_state/__init__.py | 0 .../component/test_state/program/__init__.py | 0 .../test_state/program/test_state_program.py | 61 + .../component/test_state/state/__init__.py | 0 .../state/test_state_state_props.py | 21 + .../stepfunctions/asl/eval/environment.py | 2 +- .../asl/eval/test_state/__init__.py | 0 .../asl/eval/test_state/environment.py | 52 + .../asl/eval/test_state/program_state.py | 11 + .../stepfunctions/asl/parse/asl_parser.py | 10 +- .../asl/parse/test_state/__init__.py | 0 .../asl/parse/test_state/asl_parser.py | 39 + .../asl/parse/test_state/preprocessor.py | 121 ++ .../asl/static_analyser/__init__.py | 0 .../asl/static_analyser/static_analyser.py | 10 + .../static_analyser/test_state/__init__.py | 0 .../test_state/test_state_analyser.py | 49 + .../stepfunctions/backend/execution.py | 91 +- .../stepfunctions/backend/execution_worker.py | 22 +- .../backend/execution_worker_comm.py | 2 +- .../stepfunctions/backend/state_machine.py | 25 + .../backend/test_state/__init__.py | 0 .../backend/test_state/execution.py | 131 ++ .../backend/test_state/execution_worker.py | 29 + localstack/services/stepfunctions/provider.py | 90 +- localstack/utils/aws/arns.py | 17 + tests/aws/services/stepfunctions/conftest.py | 9 + .../templates/test_state/__init__.py | 0 .../statemachines/base_choice_state.json5 | 31 + .../statemachines/base_fail_state.json5 | 5 + .../base_lambda_service_task_state.json5 | 9 + .../base_lambda_task_state.json5 | 5 + .../statemachines/base_pass_state.json5 | 4 + .../base_result_pass_state.json5 | 7 + .../statemachines/base_succeed_state.json5 | 3 + .../statemachines/base_wait_state.json5 | 5 + .../io_lambda_service_task_state.json5 | 17 + .../statemachines/io_pass_state.json5 | 10 + .../statemachines/io_result_pass_state.json5 | 13 + .../test_state/test_state_templates.py | 35 + .../stepfunctions/v2/test_state/__init__.py | 0 .../test_state/test_test_state_scenarios.py | 249 ++++ .../test_test_state_scenarios.snapshot.json | 1139 +++++++++++++++++ .../test_test_state_scenarios.validation.json | 83 ++ 49 files changed, 2352 insertions(+), 80 deletions(-) create mode 100644 localstack/services/stepfunctions/asl/component/test_state/__init__.py create mode 100644 localstack/services/stepfunctions/asl/component/test_state/program/__init__.py create mode 100644 localstack/services/stepfunctions/asl/component/test_state/program/test_state_program.py create mode 100644 localstack/services/stepfunctions/asl/component/test_state/state/__init__.py create mode 100644 localstack/services/stepfunctions/asl/component/test_state/state/test_state_state_props.py create mode 100644 localstack/services/stepfunctions/asl/eval/test_state/__init__.py create mode 100644 localstack/services/stepfunctions/asl/eval/test_state/environment.py create mode 100644 localstack/services/stepfunctions/asl/eval/test_state/program_state.py create mode 100644 localstack/services/stepfunctions/asl/parse/test_state/__init__.py create mode 100644 localstack/services/stepfunctions/asl/parse/test_state/asl_parser.py create mode 100644 localstack/services/stepfunctions/asl/parse/test_state/preprocessor.py create mode 100644 localstack/services/stepfunctions/asl/static_analyser/__init__.py create mode 100644 localstack/services/stepfunctions/asl/static_analyser/static_analyser.py create mode 100644 localstack/services/stepfunctions/asl/static_analyser/test_state/__init__.py create mode 100644 localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py create mode 100644 localstack/services/stepfunctions/backend/test_state/__init__.py create mode 100644 localstack/services/stepfunctions/backend/test_state/execution.py create mode 100644 localstack/services/stepfunctions/backend/test_state/execution_worker.py create mode 100644 tests/aws/services/stepfunctions/templates/test_state/__init__.py create mode 100644 tests/aws/services/stepfunctions/templates/test_state/statemachines/base_choice_state.json5 create mode 100644 tests/aws/services/stepfunctions/templates/test_state/statemachines/base_fail_state.json5 create mode 100644 tests/aws/services/stepfunctions/templates/test_state/statemachines/base_lambda_service_task_state.json5 create mode 100644 tests/aws/services/stepfunctions/templates/test_state/statemachines/base_lambda_task_state.json5 create mode 100644 tests/aws/services/stepfunctions/templates/test_state/statemachines/base_pass_state.json5 create mode 100644 tests/aws/services/stepfunctions/templates/test_state/statemachines/base_result_pass_state.json5 create mode 100644 tests/aws/services/stepfunctions/templates/test_state/statemachines/base_succeed_state.json5 create mode 100644 tests/aws/services/stepfunctions/templates/test_state/statemachines/base_wait_state.json5 create mode 100644 tests/aws/services/stepfunctions/templates/test_state/statemachines/io_lambda_service_task_state.json5 create mode 100644 tests/aws/services/stepfunctions/templates/test_state/statemachines/io_pass_state.json5 create mode 100644 tests/aws/services/stepfunctions/templates/test_state/statemachines/io_result_pass_state.json5 create mode 100644 tests/aws/services/stepfunctions/templates/test_state/test_state_templates.py create mode 100644 tests/aws/services/stepfunctions/v2/test_state/__init__.py create mode 100644 tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py create mode 100644 tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.snapshot.json create mode 100644 tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.validation.json diff --git a/localstack/services/stepfunctions/asl/component/common/parameters.py b/localstack/services/stepfunctions/asl/component/common/parameters.py index 532c0ee581bb6..5621884e415a7 100644 --- a/localstack/services/stepfunctions/asl/component/common/parameters.py +++ b/localstack/services/stepfunctions/asl/component/common/parameters.py @@ -8,8 +8,10 @@ class Parameters(EvalComponent): + payload_tmpl: Final[PayloadTmpl] + def __init__(self, payload_tmpl: PayloadTmpl): - self.payload_tmpl: Final[PayloadTmpl] = payload_tmpl + self.payload_tmpl = payload_tmpl def _eval_body(self, env: Environment) -> None: self.payload_tmpl.eval(env=env) diff --git a/localstack/services/stepfunctions/asl/component/common/result_selector.py b/localstack/services/stepfunctions/asl/component/common/result_selector.py index c51f6aa6fdebb..b194c514d8fb9 100644 --- a/localstack/services/stepfunctions/asl/component/common/result_selector.py +++ b/localstack/services/stepfunctions/asl/component/common/result_selector.py @@ -1,3 +1,5 @@ +from typing import Final + from localstack.services.stepfunctions.asl.component.common.payload.payloadvalue.payloadtmpl.payload_tmpl import ( PayloadTmpl, ) @@ -6,8 +8,10 @@ class ResultSelector(EvalComponent): + payload_tmpl: Final[PayloadTmpl] + def __init__(self, payload_tmpl: PayloadTmpl): - self.payload_tmpl: PayloadTmpl = payload_tmpl + self.payload_tmpl = payload_tmpl def _eval_body(self, env: Environment) -> None: self.payload_tmpl.eval(env=env) diff --git a/localstack/services/stepfunctions/asl/component/state/state_choice/state_choice.py b/localstack/services/stepfunctions/asl/component/state/state_choice/state_choice.py index 8d3d88e35afc5..5811729971094 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_choice/state_choice.py +++ b/localstack/services/stepfunctions/asl/component/state/state_choice/state_choice.py @@ -46,7 +46,6 @@ def _eval_state(self, env: Environment) -> None: if self.default_state: self._next_state_name = self.default_state.state_name - # TODO: Lazy evaluation? for rule in self.choices_decl.rules: rule.eval(env) res = env.stack.pop() diff --git a/localstack/services/stepfunctions/asl/component/state/state_pass/result.py b/localstack/services/stepfunctions/asl/component/state/state_pass/result.py index af39910ecf5a4..11e86ed536654 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_pass/result.py +++ b/localstack/services/stepfunctions/asl/component/state/state_pass/result.py @@ -1,8 +1,14 @@ import json -from localstack.services.stepfunctions.asl.component.component import Component +from localstack.services.stepfunctions.asl.component.eval_component import EvalComponent +from localstack.services.stepfunctions.asl.eval.environment import Environment -class Result(Component): +class Result(EvalComponent): + result_obj: json + def __init__(self, result_obj: json): - self.result_obj: json = result_obj + self.result_obj = result_obj + + def _eval_body(self, env: Environment) -> None: + env.stack.append(self.result_obj) diff --git a/localstack/services/stepfunctions/asl/component/state/state_pass/state_pass.py b/localstack/services/stepfunctions/asl/component/state/state_pass/state_pass.py index 3c5f2ac055bf8..d98d800a90650 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_pass/state_pass.py +++ b/localstack/services/stepfunctions/asl/component/state/state_pass/state_pass.py @@ -68,7 +68,7 @@ def _eval_state(self, env: Environment) -> None: self.parameters.eval(env=env) if self.result: - env.stack.append(self.result.result_obj) + self.result.eval(env=env) if self.result_path: self.result_path.eval(env) diff --git a/localstack/services/stepfunctions/asl/component/test_state/__init__.py b/localstack/services/stepfunctions/asl/component/test_state/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/localstack/services/stepfunctions/asl/component/test_state/program/__init__.py b/localstack/services/stepfunctions/asl/component/test_state/program/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/localstack/services/stepfunctions/asl/component/test_state/program/test_state_program.py b/localstack/services/stepfunctions/asl/component/test_state/program/test_state_program.py new file mode 100644 index 0000000000000..79c17b38121b6 --- /dev/null +++ b/localstack/services/stepfunctions/asl/component/test_state/program/test_state_program.py @@ -0,0 +1,61 @@ +import logging +import threading +from typing import Final + +from localstack.aws.api.stepfunctions import ( + ExecutionFailedEventDetails, +) +from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( + FailureEventException, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name import ( + StatesErrorName, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name_type import ( + StatesErrorNameType, +) +from localstack.services.stepfunctions.asl.component.eval_component import EvalComponent +from localstack.services.stepfunctions.asl.component.state.state import CommonStateField +from localstack.services.stepfunctions.asl.eval.test_state.environment import TestStateEnvironment +from localstack.services.stepfunctions.asl.utils.encoding import to_json_str +from localstack.utils.threads import TMP_THREADS + +LOG = logging.getLogger(__name__) + +TEST_CASE_EXECUTION_TIMEOUT_SECONDS: Final[int] = 300 # 5 minutes. + + +class TestStateProgram(EvalComponent): + test_state: Final[CommonStateField] + + def __init__( + self, + test_state: CommonStateField, + ): + self.test_state = test_state + + def eval(self, env: TestStateEnvironment) -> None: + env.next_state_name = self.test_state.name + worker_thread = threading.Thread(target=super().eval, args=(env,)) + TMP_THREADS.append(worker_thread) + worker_thread.start() + worker_thread.join(timeout=TEST_CASE_EXECUTION_TIMEOUT_SECONDS) + is_timeout = worker_thread.is_alive() + if is_timeout: + env.set_timed_out() + + def _eval_body(self, env: TestStateEnvironment) -> None: + try: + env.inspection_data["input"] = to_json_str(env.inp) + self.test_state.eval(env=env) + except FailureEventException as ex: + env.set_error(error=ex.get_execution_failed_event_details()) + except Exception as ex: + cause = f"{type(ex).__name__}({str(ex)})" + LOG.error(f"Stepfunctions computation ended with exception '{cause}'.") + env.set_error( + ExecutionFailedEventDetails( + error=StatesErrorName(typ=StatesErrorNameType.StatesRuntime).error_name, + cause=cause, + ) + ) diff --git a/localstack/services/stepfunctions/asl/component/test_state/state/__init__.py b/localstack/services/stepfunctions/asl/component/test_state/state/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/localstack/services/stepfunctions/asl/component/test_state/state/test_state_state_props.py b/localstack/services/stepfunctions/asl/component/test_state/state/test_state_state_props.py new file mode 100644 index 0000000000000..a249898c1b406 --- /dev/null +++ b/localstack/services/stepfunctions/asl/component/test_state/state/test_state_state_props.py @@ -0,0 +1,21 @@ +from typing import Any, Final + +from localstack.services.stepfunctions.asl.component.common.parameters import Parameters +from localstack.services.stepfunctions.asl.component.common.path.input_path import InputPath +from localstack.services.stepfunctions.asl.component.common.path.result_path import ResultPath +from localstack.services.stepfunctions.asl.component.common.result_selector import ResultSelector +from localstack.services.stepfunctions.asl.component.state.state_pass.result import Result +from localstack.services.stepfunctions.asl.component.state.state_props import StateProps + +EQUAL_SUBTYPES: Final[list[type]] = [InputPath, Parameters, ResultSelector, ResultPath, Result] + + +class TestStateStateProps(StateProps): + def add(self, instance: Any) -> None: + inst_type = type(instance) + # Subclasses + for typ in EQUAL_SUBTYPES: + if issubclass(inst_type, typ): + self._add(typ, instance) + return + super().add(instance=instance) diff --git a/localstack/services/stepfunctions/asl/eval/environment.py b/localstack/services/stepfunctions/asl/eval/environment.py index 01eb6beaaa1fe..afea27dc6c00b 100644 --- a/localstack/services/stepfunctions/asl/eval/environment.py +++ b/localstack/services/stepfunctions/asl/eval/environment.py @@ -178,7 +178,7 @@ def open_frame( previous_event_id=self.event_history_context.source_event_id ) - frame = Environment.as_frame_of(self, event_history_context) + frame = self.as_frame_of(self, event_history_context) self._frames.append(frame) return frame diff --git a/localstack/services/stepfunctions/asl/eval/test_state/__init__.py b/localstack/services/stepfunctions/asl/eval/test_state/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/localstack/services/stepfunctions/asl/eval/test_state/environment.py b/localstack/services/stepfunctions/asl/eval/test_state/environment.py new file mode 100644 index 0000000000000..069cb467704ef --- /dev/null +++ b/localstack/services/stepfunctions/asl/eval/test_state/environment.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from typing import Final + +from localstack.aws.api.stepfunctions import Arn, InspectionData +from localstack.services.stepfunctions.asl.eval.aws_execution_details import AWSExecutionDetails +from localstack.services.stepfunctions.asl.eval.contextobject.contex_object import ( + ContextObjectInitData, +) +from localstack.services.stepfunctions.asl.eval.environment import Environment +from localstack.services.stepfunctions.asl.eval.event.event_history import EventHistoryContext +from localstack.services.stepfunctions.asl.eval.program_state import ProgramRunning +from localstack.services.stepfunctions.asl.eval.test_state.program_state import ( + ProgramChoiceSelected, +) +from localstack.services.stepfunctions.backend.activity import Activity + + +class TestStateEnvironment(Environment): + inspection_data: Final[InspectionData] + + def __init__( + self, + aws_execution_details: AWSExecutionDetails, + context_object_init: ContextObjectInitData, + event_history_context: EventHistoryContext, + activity_store: dict[Arn, Activity], + ): + super().__init__( + aws_execution_details=aws_execution_details, + context_object_init=context_object_init, + event_history_context=event_history_context, + activity_store=activity_store, + ) + self.inspection_data = InspectionData() + + @classmethod + def as_frame_of( + cls, env: TestStateEnvironment, event_history_frame_cache: EventHistoryContext + ) -> TestStateEnvironment: + frame = super().as_frame_of(env=env, event_history_frame_cache=event_history_frame_cache) + frame.inspection_data = env.inspection_data + return frame + + def set_choice_selected(self, next_state_name: str) -> None: + with self._state_mutex: + if isinstance(self._program_state, ProgramRunning): + self._program_state = ProgramChoiceSelected(next_state_name=next_state_name) + self.program_state_event.set() + self.program_state_event.clear() + else: + raise RuntimeError("Cannot set choice selected for non running ProgramState.") diff --git a/localstack/services/stepfunctions/asl/eval/test_state/program_state.py b/localstack/services/stepfunctions/asl/eval/test_state/program_state.py new file mode 100644 index 0000000000000..d9576ceda285b --- /dev/null +++ b/localstack/services/stepfunctions/asl/eval/test_state/program_state.py @@ -0,0 +1,11 @@ +from typing import Final + +from localstack.services.stepfunctions.asl.eval.program_state import ProgramState + + +class ProgramChoiceSelected(ProgramState): + next_state_name: Final[str] + + def __init__(self, next_state_name: str): + super().__init__() + self.next_state_name = next_state_name diff --git a/localstack/services/stepfunctions/asl/parse/asl_parser.py b/localstack/services/stepfunctions/asl/parse/asl_parser.py index 6a8378207538a..29c9c93f53bf5 100644 --- a/localstack/services/stepfunctions/asl/parse/asl_parser.py +++ b/localstack/services/stepfunctions/asl/parse/asl_parser.py @@ -1,12 +1,12 @@ import abc from typing import Final -from antlr4 import CommonTokenStream, InputStream +from antlr4 import CommonTokenStream, InputStream, ParserRuleContext from antlr4.error.ErrorListener import ErrorListener from localstack.services.stepfunctions.asl.antlr.runtime.ASLLexer import ASLLexer from localstack.services.stepfunctions.asl.antlr.runtime.ASLParser import ASLParser -from localstack.services.stepfunctions.asl.component.program.program import Program +from localstack.services.stepfunctions.asl.component.eval_component import EvalComponent from localstack.services.stepfunctions.asl.parse.preprocessor import Preprocessor @@ -48,11 +48,11 @@ def __repr__(self): class AmazonStateLanguageParser(abc.ABC): @staticmethod - def parse(src: str) -> Program: + def parse(definition: str) -> tuple[EvalComponent, ParserRuleContext]: # Attempt to build the AST and look out for syntax errors. syntax_error_listener = SyntaxErrorListener() - input_stream = InputStream(src) + input_stream = InputStream(definition) lexer = ASLLexer(input_stream) stream = CommonTokenStream(lexer) parser = ASLParser(stream) @@ -68,4 +68,4 @@ def parse(src: str) -> Program: preprocessor = Preprocessor() program = preprocessor.visit(tree) - return program + return program, tree diff --git a/localstack/services/stepfunctions/asl/parse/test_state/__init__.py b/localstack/services/stepfunctions/asl/parse/test_state/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/localstack/services/stepfunctions/asl/parse/test_state/asl_parser.py b/localstack/services/stepfunctions/asl/parse/test_state/asl_parser.py new file mode 100644 index 0000000000000..d4c4b8b3ef582 --- /dev/null +++ b/localstack/services/stepfunctions/asl/parse/test_state/asl_parser.py @@ -0,0 +1,39 @@ +from antlr4 import CommonTokenStream, InputStream, ParserRuleContext + +from localstack.services.stepfunctions.asl.antlr.runtime.ASLLexer import ASLLexer +from localstack.services.stepfunctions.asl.antlr.runtime.ASLParser import ASLParser +from localstack.services.stepfunctions.asl.component.eval_component import EvalComponent +from localstack.services.stepfunctions.asl.parse.asl_parser import ( + AmazonStateLanguageParser, + ASLParserException, + SyntaxErrorListener, +) +from localstack.services.stepfunctions.asl.parse.test_state.preprocessor import ( + TestStatePreprocessor, +) + + +class TestStateAmazonStateLanguageParser(AmazonStateLanguageParser): + @staticmethod + def parse(definition: str) -> tuple[EvalComponent, ParserRuleContext]: + # Attempt to build the AST and look out for syntax errors. + syntax_error_listener = SyntaxErrorListener() + + input_stream = InputStream(definition) + lexer = ASLLexer(input_stream) + stream = CommonTokenStream(lexer) + parser = ASLParser(stream) + parser.removeErrorListeners() + parser.addErrorListener(syntax_error_listener) + # Unlike the main Program parser, TestState parsing occurs at a state declaration level. + tree = parser.state_decl_body() + + errors = syntax_error_listener.errors + if errors: + raise ASLParserException(errors=errors) + + # Attempt to preprocess the AST into evaluation components. + preprocessor = TestStatePreprocessor() + test_state_program = preprocessor.visit(tree) + + return test_state_program, tree diff --git a/localstack/services/stepfunctions/asl/parse/test_state/preprocessor.py b/localstack/services/stepfunctions/asl/parse/test_state/preprocessor.py new file mode 100644 index 0000000000000..c08f7b32a9a20 --- /dev/null +++ b/localstack/services/stepfunctions/asl/parse/test_state/preprocessor.py @@ -0,0 +1,121 @@ +import enum +from typing import Final + +from localstack.services.stepfunctions.asl.antlr.runtime.ASLParser import ASLParser +from localstack.services.stepfunctions.asl.component.common.parameters import Parameters +from localstack.services.stepfunctions.asl.component.common.path.input_path import InputPath +from localstack.services.stepfunctions.asl.component.common.path.result_path import ResultPath +from localstack.services.stepfunctions.asl.component.common.result_selector import ResultSelector +from localstack.services.stepfunctions.asl.component.state.state import CommonStateField +from localstack.services.stepfunctions.asl.component.state.state_choice.state_choice import ( + StateChoice, +) +from localstack.services.stepfunctions.asl.component.state.state_execution.execute_state import ( + ExecutionState, +) +from localstack.services.stepfunctions.asl.component.state.state_pass.result import Result +from localstack.services.stepfunctions.asl.component.test_state.program.test_state_program import ( + TestStateProgram, +) +from localstack.services.stepfunctions.asl.component.test_state.state.test_state_state_props import ( + TestStateStateProps, +) +from localstack.services.stepfunctions.asl.eval.test_state.environment import TestStateEnvironment +from localstack.services.stepfunctions.asl.parse.preprocessor import Preprocessor +from localstack.services.stepfunctions.asl.utils.encoding import to_json_str + + +class InspectionDataKey(enum.Enum): + INPUT = "input" + AFTER_INPUT_PATH = "afterInputPath" + AFTER_PARAMETERS = "afterParameters" + RESULT = "result" + AFTER_RESULT_SELECTOR = "afterResultSelector" + AFTER_RESULT_PATH = "afterResultPath" + REQUEST = "request" + RESPONSE = "response" + + +def _decorated_updated_choice_inspection_data(method): + def wrapper(env: TestStateEnvironment, *args, **kwargs): + method(env, *args, **kwargs) + env.set_choice_selected(env.next_state_name) + + return wrapper + + +def _decorated_updates_inspection_data(method, inspection_data_key: InspectionDataKey): + def wrapper(env: TestStateEnvironment, *args, **kwargs): + method(env, *args, **kwargs) + result = to_json_str(env.stack[-1]) + env.inspection_data[inspection_data_key.value] = result # noqa: we know that the here value is a supported inspection data field by design. + + return wrapper + + +def _decorate_state_field(state_field: CommonStateField) -> None: + if isinstance(state_field, ExecutionState): + state_field._eval_execution = _decorated_updates_inspection_data( + method=state_field._eval_execution, # noqa: as part of the decoration we access this protected member. + inspection_data_key=InspectionDataKey.RESULT, + ) + elif isinstance(state_field, StateChoice): + state_field._eval_body = _decorated_updated_choice_inspection_data( + method=state_field._eval_body # noqa: as part of the decoration we access this protected member. + ) + + +class TestStatePreprocessor(Preprocessor): + STATE_NAME: Final[str] = "TestState" + + def visitState_decl_body(self, ctx: ASLParser.State_decl_bodyContext) -> TestStateProgram: + state_props = TestStateStateProps() + state_props.name = self.STATE_NAME + for child in ctx.children: + cmp = self.visit(child) + state_props.add(cmp) + state_field = self._common_state_field_of(state_props=state_props) + _decorate_state_field(state_field) + return TestStateProgram(state_field) + + def visitInput_path_decl(self, ctx: ASLParser.Input_path_declContext) -> InputPath: + input_path: InputPath = super().visitInput_path_decl(ctx=ctx) + input_path._eval_body = _decorated_updates_inspection_data( + method=input_path._eval_body, # noqa + inspection_data_key=InspectionDataKey.AFTER_INPUT_PATH, + ) + return input_path + + def visitParameters_decl(self, ctx: ASLParser.Parameters_declContext) -> Parameters: + parameters: Parameters = super().visitParameters_decl(ctx=ctx) + parameters._eval_body = _decorated_updates_inspection_data( + method=parameters._eval_body, # noqa + inspection_data_key=InspectionDataKey.AFTER_PARAMETERS, + ) + return parameters + + def visitResult_selector_decl( + self, ctx: ASLParser.Result_selector_declContext + ) -> ResultSelector: + result_selector: ResultSelector = super().visitResult_selector_decl(ctx=ctx) + result_selector._eval_body = _decorated_updates_inspection_data( + method=result_selector._eval_body, # noqa + inspection_data_key=InspectionDataKey.AFTER_RESULT_SELECTOR, + ) + return result_selector + + def visitResult_path_decl(self, ctx: ASLParser.Result_path_declContext) -> ResultPath: + result_path: ResultPath = super().visitResult_path_decl(ctx=ctx) + result_path._eval_body = _decorated_updates_inspection_data( + method=result_path._eval_body, # noqa + inspection_data_key=InspectionDataKey.AFTER_RESULT_PATH, + ) + return result_path + + def visitResult_decl(self, ctx: ASLParser.Result_declContext) -> Result: + result: Result = super().visitResult_decl(ctx=ctx) + result._eval_body = _decorated_updates_inspection_data( + method=result._eval_body, + inspection_data_key=InspectionDataKey.RESULT, # noqa + ) + return result diff --git a/localstack/services/stepfunctions/asl/static_analyser/__init__.py b/localstack/services/stepfunctions/asl/static_analyser/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/localstack/services/stepfunctions/asl/static_analyser/static_analyser.py b/localstack/services/stepfunctions/asl/static_analyser/static_analyser.py new file mode 100644 index 0000000000000..81b8c576953fe --- /dev/null +++ b/localstack/services/stepfunctions/asl/static_analyser/static_analyser.py @@ -0,0 +1,10 @@ +import abc + +from localstack.services.stepfunctions.asl.antlr.runtime.ASLParserVisitor import ASLParserVisitor +from localstack.services.stepfunctions.asl.parse.asl_parser import AmazonStateLanguageParser + + +class StaticAnalyser(ASLParserVisitor, abc.ABC): + def analyse(self, definition: str) -> None: + _, parser_rule_context = AmazonStateLanguageParser.parse(definition) + self.visit(parser_rule_context) diff --git a/localstack/services/stepfunctions/asl/static_analyser/test_state/__init__.py b/localstack/services/stepfunctions/asl/static_analyser/test_state/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py b/localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py new file mode 100644 index 0000000000000..08ef6d9460f4d --- /dev/null +++ b/localstack/services/stepfunctions/asl/static_analyser/test_state/test_state_analyser.py @@ -0,0 +1,49 @@ +from typing import Final + +from localstack.services.stepfunctions.asl.antlr.runtime.ASLParser import ASLParser +from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.service.resource import ( + ActivityResource, + Resource, + ServiceResource, +) +from localstack.services.stepfunctions.asl.component.state.state_type import StateType +from localstack.services.stepfunctions.asl.parse.test_state.asl_parser import ( + TestStateAmazonStateLanguageParser, +) +from localstack.services.stepfunctions.asl.static_analyser.static_analyser import StaticAnalyser + + +class TestStateStaticAnalyser(StaticAnalyser): + _SUPPORTED_STATE_TYPES: Final[set[StateType]] = { + StateType.Task, + StateType.Pass, + StateType.Wait, + StateType.Choice, + StateType.Succeed, + StateType.Fail, + } + + def analyse(self, definition) -> None: + _, parser_rule_context = TestStateAmazonStateLanguageParser.parse(definition) + self.visit(parser_rule_context) + + def visitState_type(self, ctx: ASLParser.State_typeContext) -> None: + state_type_value: int = ctx.children[0].symbol.type + state_type = StateType(state_type_value) + if state_type not in self._SUPPORTED_STATE_TYPES: + raise ValueError(f"Unsupported state type for TestState runs '{state_type}'.") + + def visitResource_decl(self, ctx: ASLParser.Resource_declContext) -> None: + resource_str: str = ctx.keyword_or_string().getText()[1:-1] + resource = Resource.from_resource_arn(resource_str) + + if isinstance(resource, ActivityResource): + raise ValueError( + f"ActivityResources are not supported for TestState runs {resource_str}." + ) + + if isinstance(resource, ServiceResource): + if resource.condition is not None: + raise ValueError( + f"Service integration patterns are not supported for TestState runs {resource_str}." + ) diff --git a/localstack/services/stepfunctions/backend/execution.py b/localstack/services/stepfunctions/backend/execution.py index 88e6f87e1d4a0..3abd0df2bbebf 100644 --- a/localstack/services/stepfunctions/backend/execution.py +++ b/localstack/services/stepfunctions/backend/execution.py @@ -1,6 +1,7 @@ from __future__ import annotations import datetime +import json import logging from typing import Final, Optional @@ -16,7 +17,6 @@ HistoryEventList, InvalidName, SensitiveCause, - SensitiveData, SensitiveError, StartExecutionOutput, Timestamp, @@ -43,7 +43,9 @@ from localstack.services.stepfunctions.asl.utils.encoding import to_json_str from localstack.services.stepfunctions.backend.activity import Activity from localstack.services.stepfunctions.backend.execution_worker import ExecutionWorker -from localstack.services.stepfunctions.backend.execution_worker_comm import ExecutionWorkerComm +from localstack.services.stepfunctions.backend.execution_worker_comm import ( + ExecutionWorkerCommunication, +) from localstack.services.stepfunctions.backend.state_machine import ( StateMachineInstance, StateMachineVersion, @@ -52,18 +54,16 @@ LOG = logging.getLogger(__name__) -class BaseExecutionWorkerComm(ExecutionWorkerComm): +class BaseExecutionWorkerCommunication(ExecutionWorkerCommunication): def __init__(self, execution: Execution): self.execution: Execution = execution - def terminated(self) -> None: + def _reflect_execution_status(self): exit_program_state: ProgramState = self.execution.exec_worker.env.program_state() self.execution.stop_date = datetime.datetime.now(tz=datetime.timezone.utc) if isinstance(exit_program_state, ProgramEnded): self.execution.exec_status = ExecutionStatus.SUCCEEDED - self.execution.output = to_json_str( - self.execution.exec_worker.env.inp, separators=(",", ":") - ) + self.execution.output = self.execution.exec_worker.env.inp elif isinstance(exit_program_state, ProgramStopped): self.execution.exec_status = ExecutionStatus.ABORTED elif isinstance(exit_program_state, ProgramError): @@ -76,7 +76,10 @@ def terminated(self) -> None: raise RuntimeWarning( f"Execution ended with unsupported ProgramState type '{type(exit_program_state)}'." ) - self.execution._publish_execution_status_change_event() + + def terminated(self) -> None: + self._reflect_execution_status() + self.execution.publish_execution_status_change_event() class Execution: @@ -89,14 +92,14 @@ class Execution: state_machine: Final[StateMachineInstance] start_date: Final[Timestamp] - input_data: Final[Optional[dict]] + input_data: Final[Optional[json]] input_details: Final[Optional[CloudWatchEventsExecutionDataDetails]] trace_header: Final[Optional[TraceHeader]] exec_status: Optional[ExecutionStatus] stop_date: Optional[Timestamp] - output: Optional[SensitiveData] + output: Optional[json] output_details: Optional[CloudWatchEventsExecutionDataDetails] error: Optional[SensitiveError] @@ -116,7 +119,7 @@ def __init__( state_machine: StateMachineInstance, start_date: Timestamp, activity_store: dict[Arn, Activity], - input_data: Optional[dict] = None, + input_data: Optional[json] = None, trace_header: Optional[TraceHeader] = None, ): self.name = name @@ -157,7 +160,7 @@ def to_describe_output(self) -> DescribeExecutionOutput: traceHeader=self.trace_header, ) if describe_output["status"] == ExecutionStatus.SUCCEEDED: - describe_output["output"] = self.output + describe_output["output"] = to_json_str(self.output, separators=(",", ":")) describe_output["outputDetails"] = self.output_details if self.error is not None: describe_output["error"] = self.error @@ -218,34 +221,46 @@ def _to_serialized_date(timestamp: datetime.datetime) -> str: f'{timestamp.astimezone(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]}Z' ) - def start(self) -> None: - # TODO: checks exec_worker does not exists already? - if self.exec_worker: - raise InvalidName() # TODO. - self.exec_worker = ExecutionWorker( - definition=self.state_machine.definition, - input_data=self.input_data, - exec_comm=BaseExecutionWorkerComm(self), - context_object_init=ContextObjectInitData( - Execution=ContextObjectExecution( - Id=self.exec_arn, - Input=self.input_data, - Name=self.name, - RoleArn=self.role_arn, - StartTime=self._to_serialized_date(self.start_date), - ), - StateMachine=ContextObjectStateMachine( - Id=self.state_machine.arn, - Name=self.state_machine.name, - ), + def _get_start_execution_worker_comm(self) -> BaseExecutionWorkerCommunication: + return BaseExecutionWorkerCommunication(self) + + def _get_start_context_object_init_data(self) -> ContextObjectInitData: + return ContextObjectInitData( + Execution=ContextObjectExecution( + Id=self.exec_arn, + Input=self.input_data, + Name=self.name, + RoleArn=self.role_arn, + StartTime=self._to_serialized_date(self.start_date), ), - aws_execution_details=AWSExecutionDetails( - account=self.account_id, region=self.region_name, role_arn=self.role_arn + StateMachine=ContextObjectStateMachine( + Id=self.state_machine.arn, + Name=self.state_machine.name, ), + ) + + def _get_start_aws_execution_details(self) -> AWSExecutionDetails: + return AWSExecutionDetails( + account=self.account_id, region=self.region_name, role_arn=self.role_arn + ) + + def _get_start_execution_worker(self) -> ExecutionWorker: + return ExecutionWorker( + definition=self.state_machine.definition, + input_data=self.input_data, + exec_comm=self._get_start_execution_worker_comm(), + context_object_init=self._get_start_context_object_init_data(), + aws_execution_details=self._get_start_aws_execution_details(), activity_store=self._activity_store, ) + + def start(self) -> None: + # TODO: checks exec_worker does not exists already? + if self.exec_worker: + raise InvalidName() # TODO. + self.exec_worker = self._get_start_execution_worker() self.exec_status = ExecutionStatus.RUNNING - self._publish_execution_status_change_event() + self.publish_execution_status_change_event() self.exec_worker.start() def stop(self, stop_date: datetime.datetime, error: Optional[str], cause: Optional[str]): @@ -253,11 +268,13 @@ def stop(self, stop_date: datetime.datetime, error: Optional[str], cause: Option if exec_worker: exec_worker.stop(stop_date=stop_date, cause=cause, error=error) - def _publish_execution_status_change_event(self): + def publish_execution_status_change_event(self): input_value = ( dict() if not self.input_data else to_json_str(self.input_data, separators=(",", ":")) ) - output_value = self.output + output_value = ( + None if self.output is None else to_json_str(self.output, separators=(",", ":")) + ) output_details = None if output_value is None else self.output_details entry = PutEventsRequestEntry( Source="aws.states", diff --git a/localstack/services/stepfunctions/backend/execution_worker.py b/localstack/services/stepfunctions/backend/execution_worker.py index c073eab3cd1b0..bd447d220809a 100644 --- a/localstack/services/stepfunctions/backend/execution_worker.py +++ b/localstack/services/stepfunctions/backend/execution_worker.py @@ -10,7 +10,7 @@ HistoryEventExecutionDataDetails, HistoryEventType, ) -from localstack.services.stepfunctions.asl.component.program.program import Program +from localstack.services.stepfunctions.asl.component.eval_component import EvalComponent from localstack.services.stepfunctions.asl.eval.aws_execution_details import AWSExecutionDetails from localstack.services.stepfunctions.asl.eval.contextobject.contex_object import ( ContextObjectInitData, @@ -21,14 +21,16 @@ from localstack.services.stepfunctions.asl.parse.asl_parser import AmazonStateLanguageParser from localstack.services.stepfunctions.asl.utils.encoding import to_json_str from localstack.services.stepfunctions.backend.activity import Activity -from localstack.services.stepfunctions.backend.execution_worker_comm import ExecutionWorkerComm +from localstack.services.stepfunctions.backend.execution_worker_comm import ( + ExecutionWorkerCommunication, +) class ExecutionWorker: env: Optional[Environment] _definition: Definition _input_data: Optional[dict] - _exec_comm: Final[ExecutionWorkerComm] + _exec_comm: Final[ExecutionWorkerCommunication] _context_object_init: Final[ContextObjectInitData] _aws_execution_details: Final[AWSExecutionDetails] _activity_store: dict[Arn, Activity] @@ -39,7 +41,7 @@ def __init__( input_data: Optional[dict], context_object_init: ContextObjectInitData, aws_execution_details: AWSExecutionDetails, - exec_comm: ExecutionWorkerComm, + exec_comm: ExecutionWorkerCommunication, activity_store: dict[Arn, Activity], ): self._definition = definition @@ -50,14 +52,20 @@ def __init__( self._activity_store = activity_store self.env = None - def _execution_logic(self): - program: Program = AmazonStateLanguageParser.parse(self._definition) - self.env = Environment( + def _get_evaluation_entrypoint(self) -> EvalComponent: + return AmazonStateLanguageParser.parse(self._definition)[0] + + def _get_evaluation_environment(self) -> Environment: + return Environment( aws_execution_details=self._aws_execution_details, context_object_init=self._context_object_init, event_history_context=EventHistoryContext.of_program_start(), activity_store=self._activity_store, ) + + def _execution_logic(self): + program = self._get_evaluation_entrypoint() + self.env = self._get_evaluation_environment() self.env.inp = copy.deepcopy( self._input_data ) # The program will mutate the input_data, which is otherwise constant in regard to the execution value. diff --git a/localstack/services/stepfunctions/backend/execution_worker_comm.py b/localstack/services/stepfunctions/backend/execution_worker_comm.py index cb82687d7bd48..c2e1d74849bbe 100644 --- a/localstack/services/stepfunctions/backend/execution_worker_comm.py +++ b/localstack/services/stepfunctions/backend/execution_worker_comm.py @@ -1,7 +1,7 @@ import abc -class ExecutionWorkerComm(abc.ABC): +class ExecutionWorkerCommunication(abc.ABC): """ Defines abstract callbacks for Execution's workers to report their progress, such as termination. Execution instances define custom callbacks routines to update their state according to the latest diff --git a/localstack/services/stepfunctions/backend/state_machine.py b/localstack/services/stepfunctions/backend/state_machine.py index f691c0a55e4c4..0940526e548e6 100644 --- a/localstack/services/stepfunctions/backend/state_machine.py +++ b/localstack/services/stepfunctions/backend/state_machine.py @@ -80,6 +80,31 @@ def describe(self) -> DescribeStateMachineOutput: def itemise(self): ... +class TestStateMachine(StateMachineInstance): + def __init__( + self, + name: Name, + arn: Arn, + definition: Definition, + role_arn: Arn, + create_date: Optional[datetime.datetime] = None, + ): + super().__init__( + name, + arn, + definition, + role_arn, + create_date, + StateMachineType.STANDARD, + None, + None, + None, + ) + + def itemise(self): + raise NotImplementedError("TestStateMachine does not support itemise.") + + class TagManager: _tags: Final[dict[str, Optional[str]]] diff --git a/localstack/services/stepfunctions/backend/test_state/__init__.py b/localstack/services/stepfunctions/backend/test_state/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/localstack/services/stepfunctions/backend/test_state/execution.py b/localstack/services/stepfunctions/backend/test_state/execution.py new file mode 100644 index 0000000000000..9b96425ea9fc3 --- /dev/null +++ b/localstack/services/stepfunctions/backend/test_state/execution.py @@ -0,0 +1,131 @@ +from __future__ import annotations + +import logging +import threading +from typing import Optional + +from localstack.aws.api.stepfunctions import ( + Arn, + ExecutionStatus, + InspectionLevel, + TestExecutionStatus, + TestStateOutput, + Timestamp, +) +from localstack.services.stepfunctions.asl.eval.program_state import ( + ProgramEnded, + ProgramError, + ProgramState, +) +from localstack.services.stepfunctions.asl.eval.test_state.program_state import ( + ProgramChoiceSelected, +) +from localstack.services.stepfunctions.asl.utils.encoding import to_json_str +from localstack.services.stepfunctions.backend.activity import Activity +from localstack.services.stepfunctions.backend.execution import ( + BaseExecutionWorkerCommunication, + Execution, +) +from localstack.services.stepfunctions.backend.state_machine import StateMachineInstance +from localstack.services.stepfunctions.backend.test_state.execution_worker import ( + TestStateExecutionWorker, +) + +LOG = logging.getLogger(__name__) + + +class TestStateExecution(Execution): + exec_worker: Optional[TestStateExecutionWorker] + next_state: Optional[str] + + class TestCaseExecutionWorkerCommunication(BaseExecutionWorkerCommunication): + _execution: TestStateExecution + + def terminated(self) -> None: + exit_program_state: ProgramState = self.execution.exec_worker.env.program_state() + if isinstance(exit_program_state, ProgramChoiceSelected): + self.execution.exec_status = ExecutionStatus.SUCCEEDED + self.execution.output = self.execution.exec_worker.env.inp + self.execution.next_state = exit_program_state.next_state_name + else: + self._reflect_execution_status() + + def __init__( + self, + name: str, + role_arn: Arn, + exec_arn: Arn, + account_id: str, + region_name: str, + state_machine: StateMachineInstance, + start_date: Timestamp, + activity_store: dict[Arn, Activity], + input_data: Optional[dict] = None, + ): + super().__init__( + name=name, + role_arn=role_arn, + exec_arn=exec_arn, + account_id=account_id, + region_name=region_name, + state_machine=state_machine, + start_date=start_date, + activity_store=activity_store, + input_data=input_data, + trace_header=None, + ) + self._execution_terminated_event = threading.Event() + self.next_state = None + + def _get_start_execution_worker_comm(self) -> BaseExecutionWorkerCommunication: + return self.TestCaseExecutionWorkerCommunication(self) + + def _get_start_execution_worker(self) -> TestStateExecutionWorker: + return TestStateExecutionWorker( + definition=self.state_machine.definition, + input_data=self.input_data, + exec_comm=self._get_start_execution_worker_comm(), + context_object_init=self._get_start_context_object_init_data(), + aws_execution_details=self._get_start_aws_execution_details(), + activity_store=self._activity_store, + ) + + def publish_execution_status_change_event(self): + # Do not publish execution status change events during test state execution. + pass + + def to_test_state_output(self, inspection_level: InspectionLevel) -> TestStateOutput: + exit_program_state: ProgramState = self.exec_worker.env.program_state() + if isinstance(exit_program_state, ProgramEnded): + output_str = to_json_str(self.output) + test_state_output = TestStateOutput( + status=TestExecutionStatus.SUCCEEDED, output=output_str + ) + elif isinstance(exit_program_state, ProgramError): + test_state_output = TestStateOutput( + status=TestExecutionStatus.FAILED, + error=exit_program_state.error["error"], + cause=exit_program_state.error["cause"], + ) + elif isinstance(exit_program_state, ProgramChoiceSelected): + output_str = to_json_str(self.output) + test_state_output = TestStateOutput( + status=TestExecutionStatus.SUCCEEDED, nextState=self.next_state, output=output_str + ) + else: + # TODO: handle other statuses + LOG.warning( + f"Unsupported StateMachine exit type for TestState '{type(exit_program_state)}'" + ) + output_str = to_json_str(self.output) + test_state_output = TestStateOutput( + status=TestExecutionStatus.FAILED, output=output_str + ) + + match inspection_level: + case InspectionLevel.TRACE: + test_state_output["inspectionData"] = self.exec_worker.env.inspection_data + case InspectionLevel.DEBUG: + test_state_output["inspectionData"] = self.exec_worker.env.inspection_data + + return test_state_output diff --git a/localstack/services/stepfunctions/backend/test_state/execution_worker.py b/localstack/services/stepfunctions/backend/test_state/execution_worker.py new file mode 100644 index 0000000000000..717e8f4716f0a --- /dev/null +++ b/localstack/services/stepfunctions/backend/test_state/execution_worker.py @@ -0,0 +1,29 @@ +from typing import Optional + +from localstack.services.stepfunctions.asl.component.eval_component import EvalComponent +from localstack.services.stepfunctions.asl.eval.environment import Environment +from localstack.services.stepfunctions.asl.eval.event.event_history import EventHistoryContext +from localstack.services.stepfunctions.asl.eval.test_state.environment import TestStateEnvironment +from localstack.services.stepfunctions.asl.parse.test_state.asl_parser import ( + TestStateAmazonStateLanguageParser, +) +from localstack.services.stepfunctions.backend.execution_worker import ExecutionWorker + + +class TestStateExecutionWorker(ExecutionWorker): + env: Optional[TestStateEnvironment] + + def _get_evaluation_entrypoint(self) -> EvalComponent: + return TestStateAmazonStateLanguageParser.parse(self._definition)[0] + + def _get_evaluation_environment(self) -> Environment: + return TestStateEnvironment( + aws_execution_details=self._aws_execution_details, + context_object_init=self._context_object_init, + event_history_context=EventHistoryContext.of_program_start(), + activity_store=self._activity_store, + ) + + def start(self): + # bypass the native async execution of ASL programs. + self._execution_logic() diff --git a/localstack/services/stepfunctions/provider.py b/localstack/services/stepfunctions/provider.py index d129ef6a16e34..258401789c1d6 100644 --- a/localstack/services/stepfunctions/provider.py +++ b/localstack/services/stepfunctions/provider.py @@ -30,6 +30,7 @@ GetActivityTaskOutput, GetExecutionHistoryOutput, IncludeExecutionDataGetExecutionHistory, + InspectionLevel, InvalidArn, InvalidDefinition, InvalidExecutionInput, @@ -52,6 +53,7 @@ Publish, PublishStateMachineVersionOutput, ResourceNotFound, + RevealSecrets, ReverseOrder, RevisionId, SendTaskFailureOutput, @@ -73,6 +75,7 @@ TaskDoesNotExist, TaskTimedOut, TaskToken, + TestStateOutput, ToleratedFailureCount, ToleratedFailurePercentage, TraceHeader, @@ -95,30 +98,36 @@ CallbackOutcomeSuccess, ) from localstack.services.stepfunctions.asl.parse.asl_parser import ( - AmazonStateLanguageParser, ASLParserException, ) +from localstack.services.stepfunctions.asl.static_analyser.static_analyser import StaticAnalyser +from localstack.services.stepfunctions.asl.static_analyser.test_state.test_state_analyser import ( + TestStateStaticAnalyser, +) from localstack.services.stepfunctions.backend.activity import Activity, ActivityTask from localstack.services.stepfunctions.backend.execution import Execution from localstack.services.stepfunctions.backend.state_machine import ( StateMachineInstance, StateMachineRevision, StateMachineVersion, + TestStateMachine, ) from localstack.services.stepfunctions.backend.store import SFNStore, sfn_stores +from localstack.services.stepfunctions.backend.test_state.execution import TestStateExecution from localstack.state import StateVisitor from localstack.utils.aws.arns import ( - ArnData, - parse_arn, stepfunctions_activity_arn, + stepfunctions_execution_state_machine_arn, stepfunctions_state_machine_arn, ) -from localstack.utils.strings import long_uid +from localstack.utils.strings import long_uid, short_uid LOG = logging.getLogger(__name__) class StepFunctionsProvider(StepfunctionsApi, ServiceLifecycleHook): + _TEST_STATE_MAX_TIMEOUT_SECONDS: Final[int] = 300 # 5 minutes. + @staticmethod def get_store(context: RequestContext) -> SFNStore: return sfn_stores[context.account_id][context.region] @@ -238,11 +247,10 @@ def _revision_by_name( return None @staticmethod - def _validate_definition(definition: str): - # Validate - # TODO: pass through static analyser. + def _validate_definition(definition: str, static_analysers: list[StaticAnalyser]) -> None: try: - AmazonStateLanguageParser.parse(definition) + for static_analyser in static_analysers: + static_analyser.analyse(definition) except ASLParserException as asl_parser_exception: invalid_definition = InvalidDefinition() invalid_definition.message = repr(asl_parser_exception) @@ -281,7 +289,9 @@ def create_state_machine( ) state_machine_definition: str = request["definition"] - StepFunctionsProvider._validate_definition(definition=state_machine_definition) + StepFunctionsProvider._validate_definition( + definition=state_machine_definition, static_analysers=[StaticAnalyser()] + ) name: Optional[Name] = request["name"] arn = stepfunctions_state_machine_arn( @@ -431,18 +441,8 @@ def start_execution( else state_machine.arn ) exec_name = name or long_uid() # TODO: validate name format - arn_data: ArnData = parse_arn(normalised_state_machine_arn) - exec_arn = ":".join( - [ - "arn", - arn_data["partition"], - arn_data["service"], - arn_data["region"], - arn_data["account"], - "execution", - "".join(arn_data["resource"].split(":")[1:]), - exec_name, - ] + exec_arn = stepfunctions_execution_state_machine_arn( + normalised_state_machine_arn, exec_name ) if exec_arn in self.get_store(context).executions: raise InvalidName() # TODO @@ -663,7 +663,7 @@ def update_state_machine( ) if definition is not None: - self._validate_definition(definition=definition) + self._validate_definition(definition=definition, static_analysers=[StaticAnalyser()]) revision_id = state_machine.create_revision(definition=definition, role_arn=role_arn) @@ -816,6 +816,52 @@ def update_map_run( return UpdateMapRunOutput() raise ResourceNotFound() + def test_state( + self, + context: RequestContext, + definition: Definition, + role_arn: Arn, + input: SensitiveData = None, + inspection_level: InspectionLevel = None, + reveal_secrets: RevealSecrets = None, + **kwargs, + ) -> TestStateOutput: + StepFunctionsProvider._validate_definition( + definition=definition, static_analysers=[TestStateStaticAnalyser()] + ) + + name: Optional[Name] = f"TestState-{short_uid()}" + arn = stepfunctions_state_machine_arn( + name=name, account_id=context.account_id, region_name=context.region + ) + state_machine = TestStateMachine( + name=name, + arn=arn, + role_arn=role_arn, + definition=definition, + ) + exec_arn = stepfunctions_execution_state_machine_arn(state_machine.arn, name) + + input_json = json.loads(input) + execution = TestStateExecution( + name=name, + role_arn=role_arn, + exec_arn=exec_arn, + account_id=context.account_id, + region_name=context.region, + state_machine=state_machine, + start_date=datetime.datetime.now(tz=datetime.timezone.utc), + input_data=input_json, + activity_store=self.get_store(context).activities, + ) + execution.start() + + test_state_output = execution.to_test_state_output( + inspection_level=inspection_level or InspectionLevel.INFO + ) + + return test_state_output + def create_activity( self, context: RequestContext, name: Name, tags: TagList = None, **kwargs ) -> CreateActivityOutput: diff --git a/localstack/utils/aws/arns.py b/localstack/utils/aws/arns.py index 9e53bf61bf6c8..86b80095f372f 100644 --- a/localstack/utils/aws/arns.py +++ b/localstack/utils/aws/arns.py @@ -266,6 +266,23 @@ def stepfunctions_state_machine_arn(name: str, account_id: str, region_name: str return _resource_arn(name, pattern, account_id=account_id, region_name=region_name) +def stepfunctions_execution_state_machine_arn(state_machine_arn: str, execution_name: str) -> str: + arn_data: ArnData = parse_arn(state_machine_arn) + execution_arn = ":".join( + [ + "arn", + arn_data["partition"], + arn_data["service"], + arn_data["region"], + arn_data["account"], + "execution", + "".join(arn_data["resource"].split(":")[1:]), + execution_name, + ] + ) + return execution_arn + + def stepfunctions_activity_arn(name: str, account_id: str, region_name: str) -> str: pattern = "arn:aws:states:%s:%s:activity:%s" return _resource_arn(name, pattern, account_id=account_id, region_name=region_name) diff --git a/tests/aws/services/stepfunctions/conftest.py b/tests/aws/services/stepfunctions/conftest.py index 74afdad166969..3d896beb93c18 100644 --- a/tests/aws/services/stepfunctions/conftest.py +++ b/tests/aws/services/stepfunctions/conftest.py @@ -3,6 +3,7 @@ from typing import Final import pytest +from botocore.config import Config from jsonpath_ng.ext import parse from localstack_snapshot.snapshots.transformer import ( JsonpathTransformer, @@ -146,6 +147,14 @@ def transform(self, input_data: dict, *, ctx: TransformContext) -> dict: return input_data +@pytest.fixture +def stepfunctions_client_test_state(aws_client_factory): + # For TestState calls, boto will prepend "sync-" to the endpoint string. As we operate on localhost, + # this function creates a new stepfunctions client with that functionality disabled. + # Using this client only for test_state calls forces future occurrences to handle this issue explicitly. + return aws_client_factory(config=Config(inject_host_prefix=is_aws_cloud())).stepfunctions + + @pytest.fixture def create_iam_role_for_sfn(aws_client, cleanups, create_state_machine): iam_client = aws_client.iam diff --git a/tests/aws/services/stepfunctions/templates/test_state/__init__.py b/tests/aws/services/stepfunctions/templates/test_state/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_choice_state.json5 b/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_choice_state.json5 new file mode 100644 index 0000000000000..831c39d41158b --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_choice_state.json5 @@ -0,0 +1,31 @@ +{ + "Type": "Choice", + "Choices": [ + { + "Not": { + "Variable": "$.type", + "StringEquals": "Private" + }, + "Next": "Public" + }, + { + "Variable": "$.value", + "NumericEquals": 0, + "Next": "ValueIsZero" + }, + { + "And": [ + { + "Variable": "$.value", + "NumericGreaterThanEquals": 20 + }, + { + "Variable": "$.value", + "NumericLessThan": 30 + } + ], + "Next": "ValueInTwenties" + } + ], + "Default": "DefaultState" +} diff --git a/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_fail_state.json5 b/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_fail_state.json5 new file mode 100644 index 0000000000000..0ffdf1f6c40df --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_fail_state.json5 @@ -0,0 +1,5 @@ +{ + "Type": "Fail", + "Error": "SomeFailure", + "Cause": "This state machines raises a 'SomeFailure' failure." +} diff --git a/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_lambda_service_task_state.json5 b/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_lambda_service_task_state.json5 new file mode 100644 index 0000000000000..ca37fca1a5f41 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_lambda_service_task_state.json5 @@ -0,0 +1,9 @@ +{ + "Type": "Task", + "Resource": "arn:aws:states:::lambda:invoke", + "Parameters": { + "FunctionName.$": "$.FunctionName", + "Payload.$": "$.Payload" + }, + "End": true, +} diff --git a/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_lambda_task_state.json5 b/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_lambda_task_state.json5 new file mode 100644 index 0000000000000..a3494c8c354bf --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_lambda_task_state.json5 @@ -0,0 +1,5 @@ +{ + "Type": "Task", + "Resource": "__tbd__", + "End": true, +} diff --git a/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_pass_state.json5 b/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_pass_state.json5 new file mode 100644 index 0000000000000..ba1658ee703de --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_pass_state.json5 @@ -0,0 +1,4 @@ +{ + "Type": "Pass", + "End": true, +} diff --git a/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_result_pass_state.json5 b/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_result_pass_state.json5 new file mode 100644 index 0000000000000..0fb258fc8e9bc --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_result_pass_state.json5 @@ -0,0 +1,7 @@ +{ + "Type": "Pass", + "Result": { + "resultKey": "result value" + }, + "End": true, +} diff --git a/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_succeed_state.json5 b/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_succeed_state.json5 new file mode 100644 index 0000000000000..a0f3411efdd35 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_succeed_state.json5 @@ -0,0 +1,3 @@ +{ + "Type": "Succeed", +} diff --git a/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_wait_state.json5 b/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_wait_state.json5 new file mode 100644 index 0000000000000..a38b9e41e2fb1 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/test_state/statemachines/base_wait_state.json5 @@ -0,0 +1,5 @@ +{ + "Type": "Wait", + "Seconds": 1, + "End": true, +} diff --git a/tests/aws/services/stepfunctions/templates/test_state/statemachines/io_lambda_service_task_state.json5 b/tests/aws/services/stepfunctions/templates/test_state/statemachines/io_lambda_service_task_state.json5 new file mode 100644 index 0000000000000..91c2777a1dc5a --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/test_state/statemachines/io_lambda_service_task_state.json5 @@ -0,0 +1,17 @@ +{ + "Type": "Task", + "Resource": "arn:aws:states:::lambda:invoke", + "InputPath": "$.inputPathField", + "Parameters": { + "FunctionName.$": "$.FunctionName", + "Payload": { + "Input.$": "$", + "AdditionalParam": "Value" + } + }, + "ResultSelector": { + "LambdaOutput.$": "$" + }, + "ResultPath": "$.resultPathField", + "End": true, +} diff --git a/tests/aws/services/stepfunctions/templates/test_state/statemachines/io_pass_state.json5 b/tests/aws/services/stepfunctions/templates/test_state/statemachines/io_pass_state.json5 new file mode 100644 index 0000000000000..bfe343493868b --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/test_state/statemachines/io_pass_state.json5 @@ -0,0 +1,10 @@ +{ + "Type": "Pass", + "InputPath": "$.initialData", + "Parameters": { + "staticValue": "some value", + "inputValue.$": "$.fieldFromInput" + }, + "ResultPath": "$.modifiedData", + "End": true +} diff --git a/tests/aws/services/stepfunctions/templates/test_state/statemachines/io_result_pass_state.json5 b/tests/aws/services/stepfunctions/templates/test_state/statemachines/io_result_pass_state.json5 new file mode 100644 index 0000000000000..128d861017955 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/test_state/statemachines/io_result_pass_state.json5 @@ -0,0 +1,13 @@ +{ + "Type": "Pass", + "InputPath": "$.initialData", + "Parameters": { + "staticValue": "some value", + "inputValue.$": "$.fieldFromInput" + }, + "Result": { + "resultKey": "result value" + }, + "ResultPath": "$.modifiedData", + "End": true +} diff --git a/tests/aws/services/stepfunctions/templates/test_state/test_state_templates.py b/tests/aws/services/stepfunctions/templates/test_state/test_state_templates.py new file mode 100644 index 0000000000000..18b5aa888ffd2 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/test_state/test_state_templates.py @@ -0,0 +1,35 @@ +import os +from typing import Final + +from tests.aws.services.stepfunctions.templates.template_loader import TemplateLoader + +_THIS_FOLDER: Final[str] = os.path.dirname(os.path.realpath(__file__)) + + +class TestStateTemplate(TemplateLoader): + BASE_FAIL_STATE: Final[str] = os.path.join(_THIS_FOLDER, "statemachines/base_fail_state.json5") + BASE_SUCCEED_STATE: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/base_succeed_state.json5" + ) + BASE_WAIT_STATE: Final[str] = os.path.join(_THIS_FOLDER, "statemachines/base_wait_state.json5") + BASE_PASS_STATE: Final[str] = os.path.join(_THIS_FOLDER, "statemachines/base_pass_state.json5") + BASE_CHOICE_STATE: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/base_choice_state.json5" + ) + BASE_RESULT_PASS_STATE: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/base_result_pass_state.json5" + ) + IO_PASS_STATE: Final[str] = os.path.join(_THIS_FOLDER, "statemachines/io_pass_state.json5") + IO_RESULT_PASS_STATE: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/io_result_pass_state.json5" + ) + + BASE_LAMBDA_TASK_STATE: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/base_lambda_task_state.json5" + ) + BASE_LAMBDA_SERVICE_TASK_STATE: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/base_lambda_service_task_state.json5" + ) + IO_LAMBDA_SERVICE_TASK_STATE: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/io_lambda_service_task_state.json5" + ) diff --git a/tests/aws/services/stepfunctions/v2/test_state/__init__.py b/tests/aws/services/stepfunctions/v2/test_state/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py b/tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py new file mode 100644 index 0000000000000..f8fd777002c5b --- /dev/null +++ b/tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py @@ -0,0 +1,249 @@ +import json + +import pytest +from localstack_snapshot.snapshots.transformer import RegexTransformer + +from localstack.aws.api.lambda_ import Runtime +from localstack.aws.api.stepfunctions import InspectionLevel +from localstack.testing.pytest import markers +from localstack.utils.strings import short_uid +from tests.aws.services.stepfunctions.templates.services.services_templates import ( + ServicesTemplates as ST, +) +from tests.aws.services.stepfunctions.templates.test_state.test_state_templates import ( + TestStateTemplate as TST, +) + +HELLO_WORLD_INPUT = json.dumps({"Value": "HelloWorld"}) +NESTED_DICT_INPUT = json.dumps( + { + "initialData": {"fieldFromInput": "value from input", "otherField": "other"}, + "unrelatedData": {"someOtherField": 1234}, + } +) +BASE_CHOICE_STATE_INPUT = json.dumps({"type": "Private", "value": 22}) + +BASE_TEMPLATE_INPUT_BINDINGS: list[tuple[str, str]] = [ + (TST.BASE_PASS_STATE, HELLO_WORLD_INPUT), + (TST.BASE_RESULT_PASS_STATE, HELLO_WORLD_INPUT), + (TST.IO_PASS_STATE, NESTED_DICT_INPUT), + (TST.IO_RESULT_PASS_STATE, NESTED_DICT_INPUT), + (TST.BASE_FAIL_STATE, HELLO_WORLD_INPUT), + (TST.BASE_SUCCEED_STATE, HELLO_WORLD_INPUT), + (TST.BASE_CHOICE_STATE, BASE_CHOICE_STATE_INPUT), +] +IDS_BASE_TEMPLATE_INPUT_BINDINGS: list[str] = [ + "BASE_PASS_STATE", + "BASE_RESULT_PASS_STATE", + "IO_PASS_STATE", + "IO_RESULT_PASS_STATE", + "BASE_FAIL_STATE", + "BASE_SUCCEED_STATE", + "BASE_CHOICE_STATE", +] + + +@markers.snapshot.skip_snapshot_verify( + paths=[ + "$..loggingConfiguration", + "$..tracingConfiguration", + "$..SdkHttpMetadata", + "$..SdkResponseMetadata", + ] +) +class TestStateCaseScenarios: + # TODO: consider aggregating all `test_base_inspection_level_*` into a single parametrised function, and evaluate + # solutions for snapshot skips and parametrisation complexity. + + @markers.aws.validated + @pytest.mark.parametrize( + "tct_template,execution_input", + BASE_TEMPLATE_INPUT_BINDINGS, + ids=IDS_BASE_TEMPLATE_INPUT_BINDINGS, + ) + def test_base_inspection_level_info( + self, + stepfunctions_client_test_state, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + tct_template, + execution_input, + ): + sfn_role_arn = create_iam_role_for_sfn() + + template = TST.load_sfn_template(tct_template) + definition = json.dumps(template) + + test_case_response = stepfunctions_client_test_state.test_state( + definition=definition, + roleArn=sfn_role_arn, + input=execution_input, + inspectionLevel=InspectionLevel.INFO, + ) + sfn_snapshot.match("test_case_response", test_case_response) + + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + paths=[ + # Unknown generalisable behaviour by AWS leads to the outputting of undeclared and + # unsupported state modifiers. Such as ResultSelector, which is neither defined in + # this Pass state, nor supported by Pass states. + "$..inspectionData.afterInputPath", + "$..inspectionData.afterParameters", + "$..inspectionData.afterResultPath", + "$..inspectionData.afterResultSelector", + ] + ) + @pytest.mark.parametrize( + "tct_template,execution_input", + BASE_TEMPLATE_INPUT_BINDINGS, + ids=IDS_BASE_TEMPLATE_INPUT_BINDINGS, + ) + def test_base_inspection_level_debug( + self, + stepfunctions_client_test_state, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + tct_template, + execution_input, + ): + sfn_role_arn = create_iam_role_for_sfn() + + template = TST.load_sfn_template(tct_template) + definition = json.dumps(template) + + test_case_response = stepfunctions_client_test_state.test_state( + definition=definition, + roleArn=sfn_role_arn, + input=execution_input, + inspectionLevel=InspectionLevel.DEBUG, + ) + sfn_snapshot.match("test_case_response", test_case_response) + + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + paths=[ + # Unknown generalisable behaviour by AWS leads to the outputting of undeclared and + # unsupported state modifiers. Such as ResultSelector, which is neither defined in + # this Pass state, nor supported by Pass states. + "$..inspectionData.afterInputPath", + "$..inspectionData.afterParameters", + "$..inspectionData.afterResultPath", + "$..inspectionData.afterResultSelector", + ] + ) + @pytest.mark.parametrize( + "tct_template,execution_input", + BASE_TEMPLATE_INPUT_BINDINGS, + ids=IDS_BASE_TEMPLATE_INPUT_BINDINGS, + ) + def test_base_inspection_level_trace( + self, + stepfunctions_client_test_state, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + tct_template, + execution_input, + ): + sfn_role_arn = create_iam_role_for_sfn() + + template = TST.load_sfn_template(tct_template) + definition = json.dumps(template) + + test_case_response = stepfunctions_client_test_state.test_state( + definition=definition, + roleArn=sfn_role_arn, + input=execution_input, + inspectionLevel=InspectionLevel.TRACE, + ) + sfn_snapshot.match("test_case_response", test_case_response) + + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + paths=[ + # Unknown generalisable behaviour by AWS leads to the outputting of undeclared and + # unsupported state modifiers. + "$..inspectionData.afterInputPath", + "$..inspectionData.afterParameters", + "$..inspectionData.afterResultPath", + "$..inspectionData.afterResultSelector", + ] + ) + @pytest.mark.parametrize( + "inspection_level", [InspectionLevel.INFO, InspectionLevel.DEBUG, InspectionLevel.TRACE] + ) + def test_base_lambda_task_state( + self, + stepfunctions_client_test_state, + create_iam_role_for_sfn, + create_state_machine, + create_lambda_function, + sfn_snapshot, + inspection_level, + ): + function_name = f"lambda_func_{short_uid()}" + create_1_res = create_lambda_function( + func_name=function_name, + handler_file=ST.LAMBDA_RETURN_BYTES_STR, + runtime=Runtime.python3_12, + ) + sfn_snapshot.add_transformer(RegexTransformer(function_name, "")) + + template = TST.load_sfn_template(TST.BASE_LAMBDA_TASK_STATE) + template["Resource"] = create_1_res["CreateFunctionResponse"]["FunctionArn"] + definition = json.dumps(template) + exec_input = json.dumps({"inputData": "HelloWorld"}) + + sfn_role_arn = create_iam_role_for_sfn() + test_case_response = stepfunctions_client_test_state.test_state( + definition=definition, + roleArn=sfn_role_arn, + input=exec_input, + inspectionLevel=inspection_level, + ) + sfn_snapshot.match("test_case_response", test_case_response) + + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + paths=[ + # Unknown generalisable behaviour by AWS leads to the outputting of undeclared state modifiers. + "$..inspectionData.afterInputPath", + "$..inspectionData.afterResultPath", + "$..inspectionData.afterResultSelector", + ] + ) + @pytest.mark.parametrize( + "inspection_level", [InspectionLevel.INFO, InspectionLevel.DEBUG, InspectionLevel.TRACE] + ) + def test_base_lambda_service_task_state( + self, + stepfunctions_client_test_state, + create_iam_role_for_sfn, + create_state_machine, + create_lambda_function, + sfn_snapshot, + inspection_level, + ): + function_name = f"lambda_func_{short_uid()}" + create_lambda_function( + func_name=function_name, + handler_file=ST.LAMBDA_ID_FUNCTION, + runtime=Runtime.python3_12, + ) + sfn_snapshot.add_transformer(RegexTransformer(function_name, "")) + + template = TST.load_sfn_template(TST.BASE_LAMBDA_SERVICE_TASK_STATE) + definition = json.dumps(template) + exec_input = json.dumps({"FunctionName": function_name, "Payload": None}) + + sfn_role_arn = create_iam_role_for_sfn() + test_case_response = stepfunctions_client_test_state.test_state( + definition=definition, + roleArn=sfn_role_arn, + input=exec_input, + inspectionLevel=inspection_level, + ) + sfn_snapshot.match("test_case_response", test_case_response) diff --git a/tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.snapshot.json b/tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.snapshot.json new file mode 100644 index 0000000000000..1e11cdcc339f2 --- /dev/null +++ b/tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.snapshot.json @@ -0,0 +1,1139 @@ +{ + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[BASE_PASS_STATE]": { + "recorded-date": "12-04-2024, 20:45:35", + "recorded-content": { + "test_case_response": { + "output": { + "Value": "HelloWorld" + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[BASE_RESULT_PASS_STATE]": { + "recorded-date": "12-04-2024, 20:45:49", + "recorded-content": { + "test_case_response": { + "output": { + "resultKey": "result value" + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[IO_PASS_STATE]": { + "recorded-date": "12-04-2024, 20:46:03", + "recorded-content": { + "test_case_response": { + "output": { + "initialData": { + "fieldFromInput": "value from input", + "otherField": "other" + }, + "unrelatedData": { + "someOtherField": 1234 + }, + "modifiedData": { + "staticValue": "some value", + "inputValue": "value from input" + } + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[IO_RESULT_PASS_STATE]": { + "recorded-date": "12-04-2024, 20:46:15", + "recorded-content": { + "test_case_response": { + "output": { + "initialData": { + "fieldFromInput": "value from input", + "otherField": "other" + }, + "unrelatedData": { + "someOtherField": 1234 + }, + "modifiedData": { + "resultKey": "result value" + } + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[BASE_FAIL_STATE]": { + "recorded-date": "12-04-2024, 20:46:28", + "recorded-content": { + "test_case_response": { + "cause": "This state machines raises a 'SomeFailure' failure.", + "error": "SomeFailure", + "status": "FAILED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[BASE_SUCCEED_STATE]": { + "recorded-date": "12-04-2024, 20:46:41", + "recorded-content": { + "test_case_response": { + "output": { + "Value": "HelloWorld" + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[BASE_CHOICE_STATE]": { + "recorded-date": "12-04-2024, 20:46:53", + "recorded-content": { + "test_case_response": { + "nextState": "ValueInTwenties", + "output": { + "type": "Private", + "value": 22 + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[BASE_PASS_STATE]": { + "recorded-date": "12-04-2024, 20:47:06", + "recorded-content": { + "test_case_response": { + "inspectionData": { + "afterInputPath": { + "Value": "HelloWorld" + }, + "afterParameters": { + "Value": "HelloWorld" + }, + "afterResultPath": { + "Value": "HelloWorld" + }, + "afterResultSelector": { + "Value": "HelloWorld" + }, + "input": { + "Value": "HelloWorld" + } + }, + "output": { + "Value": "HelloWorld" + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[BASE_RESULT_PASS_STATE]": { + "recorded-date": "12-04-2024, 20:47:19", + "recorded-content": { + "test_case_response": { + "inspectionData": { + "afterResultPath": { + "resultKey": "result value" + }, + "afterResultSelector": { + "resultKey": "result value" + }, + "input": { + "Value": "HelloWorld" + }, + "result": { + "resultKey": "result value" + } + }, + "output": { + "resultKey": "result value" + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[IO_PASS_STATE]": { + "recorded-date": "12-04-2024, 20:47:31", + "recorded-content": { + "test_case_response": { + "inspectionData": { + "afterInputPath": { + "fieldFromInput": "value from input", + "otherField": "other" + }, + "afterParameters": { + "staticValue": "some value", + "inputValue": "value from input" + }, + "afterResultPath": { + "initialData": { + "fieldFromInput": "value from input", + "otherField": "other" + }, + "unrelatedData": { + "someOtherField": 1234 + }, + "modifiedData": { + "staticValue": "some value", + "inputValue": "value from input" + } + }, + "afterResultSelector": { + "staticValue": "some value", + "inputValue": "value from input" + }, + "input": { + "initialData": { + "fieldFromInput": "value from input", + "otherField": "other" + }, + "unrelatedData": { + "someOtherField": 1234 + } + } + }, + "output": { + "initialData": { + "fieldFromInput": "value from input", + "otherField": "other" + }, + "unrelatedData": { + "someOtherField": 1234 + }, + "modifiedData": { + "staticValue": "some value", + "inputValue": "value from input" + } + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[IO_RESULT_PASS_STATE]": { + "recorded-date": "12-04-2024, 20:47:44", + "recorded-content": { + "test_case_response": { + "inspectionData": { + "afterResultPath": { + "initialData": { + "fieldFromInput": "value from input", + "otherField": "other" + }, + "unrelatedData": { + "someOtherField": 1234 + }, + "modifiedData": { + "resultKey": "result value" + } + }, + "afterResultSelector": { + "resultKey": "result value" + }, + "input": { + "initialData": { + "fieldFromInput": "value from input", + "otherField": "other" + }, + "unrelatedData": { + "someOtherField": 1234 + } + }, + "result": { + "resultKey": "result value" + } + }, + "output": { + "initialData": { + "fieldFromInput": "value from input", + "otherField": "other" + }, + "unrelatedData": { + "someOtherField": 1234 + }, + "modifiedData": { + "resultKey": "result value" + } + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[BASE_FAIL_STATE]": { + "recorded-date": "12-04-2024, 20:47:56", + "recorded-content": { + "test_case_response": { + "cause": "This state machines raises a 'SomeFailure' failure.", + "error": "SomeFailure", + "inspectionData": { + "afterInputPath": { + "Value": "HelloWorld" + }, + "input": { + "Value": "HelloWorld" + } + }, + "status": "FAILED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[BASE_SUCCEED_STATE]": { + "recorded-date": "12-04-2024, 20:48:10", + "recorded-content": { + "test_case_response": { + "inspectionData": { + "afterInputPath": { + "Value": "HelloWorld" + }, + "input": { + "Value": "HelloWorld" + } + }, + "output": { + "Value": "HelloWorld" + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[BASE_CHOICE_STATE]": { + "recorded-date": "12-04-2024, 20:48:24", + "recorded-content": { + "test_case_response": { + "inspectionData": { + "afterInputPath": { + "type": "Private", + "value": 22 + }, + "input": { + "type": "Private", + "value": 22 + } + }, + "nextState": "ValueInTwenties", + "output": { + "type": "Private", + "value": 22 + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[BASE_PASS_STATE]": { + "recorded-date": "12-04-2024, 20:48:37", + "recorded-content": { + "test_case_response": { + "inspectionData": { + "afterInputPath": { + "Value": "HelloWorld" + }, + "afterParameters": { + "Value": "HelloWorld" + }, + "afterResultPath": { + "Value": "HelloWorld" + }, + "afterResultSelector": { + "Value": "HelloWorld" + }, + "input": { + "Value": "HelloWorld" + } + }, + "output": { + "Value": "HelloWorld" + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[BASE_RESULT_PASS_STATE]": { + "recorded-date": "12-04-2024, 20:48:50", + "recorded-content": { + "test_case_response": { + "inspectionData": { + "afterResultPath": { + "resultKey": "result value" + }, + "afterResultSelector": { + "resultKey": "result value" + }, + "input": { + "Value": "HelloWorld" + }, + "result": { + "resultKey": "result value" + } + }, + "output": { + "resultKey": "result value" + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[IO_PASS_STATE]": { + "recorded-date": "12-04-2024, 20:49:03", + "recorded-content": { + "test_case_response": { + "inspectionData": { + "afterInputPath": { + "fieldFromInput": "value from input", + "otherField": "other" + }, + "afterParameters": { + "staticValue": "some value", + "inputValue": "value from input" + }, + "afterResultPath": { + "initialData": { + "fieldFromInput": "value from input", + "otherField": "other" + }, + "unrelatedData": { + "someOtherField": 1234 + }, + "modifiedData": { + "staticValue": "some value", + "inputValue": "value from input" + } + }, + "afterResultSelector": { + "staticValue": "some value", + "inputValue": "value from input" + }, + "input": { + "initialData": { + "fieldFromInput": "value from input", + "otherField": "other" + }, + "unrelatedData": { + "someOtherField": 1234 + } + } + }, + "output": { + "initialData": { + "fieldFromInput": "value from input", + "otherField": "other" + }, + "unrelatedData": { + "someOtherField": 1234 + }, + "modifiedData": { + "staticValue": "some value", + "inputValue": "value from input" + } + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[IO_RESULT_PASS_STATE]": { + "recorded-date": "12-04-2024, 20:49:22", + "recorded-content": { + "test_case_response": { + "inspectionData": { + "afterResultPath": { + "initialData": { + "fieldFromInput": "value from input", + "otherField": "other" + }, + "unrelatedData": { + "someOtherField": 1234 + }, + "modifiedData": { + "resultKey": "result value" + } + }, + "afterResultSelector": { + "resultKey": "result value" + }, + "input": { + "initialData": { + "fieldFromInput": "value from input", + "otherField": "other" + }, + "unrelatedData": { + "someOtherField": 1234 + } + }, + "result": { + "resultKey": "result value" + } + }, + "output": { + "initialData": { + "fieldFromInput": "value from input", + "otherField": "other" + }, + "unrelatedData": { + "someOtherField": 1234 + }, + "modifiedData": { + "resultKey": "result value" + } + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[BASE_FAIL_STATE]": { + "recorded-date": "12-04-2024, 20:49:31", + "recorded-content": { + "test_case_response": { + "cause": "This state machines raises a 'SomeFailure' failure.", + "error": "SomeFailure", + "inspectionData": { + "afterInputPath": { + "Value": "HelloWorld" + }, + "input": { + "Value": "HelloWorld" + } + }, + "status": "FAILED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[BASE_SUCCEED_STATE]": { + "recorded-date": "12-04-2024, 20:49:44", + "recorded-content": { + "test_case_response": { + "inspectionData": { + "afterInputPath": { + "Value": "HelloWorld" + }, + "input": { + "Value": "HelloWorld" + } + }, + "output": { + "Value": "HelloWorld" + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[BASE_CHOICE_STATE]": { + "recorded-date": "12-04-2024, 20:49:57", + "recorded-content": { + "test_case_response": { + "inspectionData": { + "afterInputPath": { + "type": "Private", + "value": 22 + }, + "input": { + "type": "Private", + "value": 22 + } + }, + "nextState": "ValueInTwenties", + "output": { + "type": "Private", + "value": 22 + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_task_state[INFO]": { + "recorded-date": "12-04-2024, 20:50:22", + "recorded-content": { + "test_case_response": { + "output": "\"HelloWorld!\"", + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_task_state[DEBUG]": { + "recorded-date": "12-04-2024, 20:50:37", + "recorded-content": { + "test_case_response": { + "inspectionData": { + "afterInputPath": { + "inputData": "HelloWorld" + }, + "afterParameters": { + "inputData": "HelloWorld" + }, + "afterResultPath": "\"HelloWorld!\"", + "afterResultSelector": "\"HelloWorld!\"", + "input": { + "inputData": "HelloWorld" + }, + "result": "\"HelloWorld!\"" + }, + "output": "\"HelloWorld!\"", + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_task_state[TRACE]": { + "recorded-date": "12-04-2024, 20:50:52", + "recorded-content": { + "test_case_response": { + "inspectionData": { + "afterInputPath": { + "inputData": "HelloWorld" + }, + "afterParameters": { + "inputData": "HelloWorld" + }, + "afterResultPath": "\"HelloWorld!\"", + "afterResultSelector": "\"HelloWorld!\"", + "input": { + "inputData": "HelloWorld" + }, + "result": "\"HelloWorld!\"" + }, + "output": "\"HelloWorld!\"", + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_service_task_state[INFO]": { + "recorded-date": "12-04-2024, 20:51:07", + "recorded-content": { + "test_case_response": { + "output": { + "ExecutedVersion": "$LATEST", + "Payload": {}, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": [ + "" + ], + "Content-Length": [ + "2" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "2", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StatusCode": 200 + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_service_task_state[DEBUG]": { + "recorded-date": "12-04-2024, 20:51:22", + "recorded-content": { + "test_case_response": { + "inspectionData": { + "afterInputPath": { + "FunctionName": "", + "Payload": null + }, + "afterParameters": { + "FunctionName": "", + "Payload": null + }, + "afterResultPath": { + "ExecutedVersion": "$LATEST", + "Payload": {}, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": [ + "" + ], + "Content-Length": [ + "2" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "2", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StatusCode": 200 + }, + "afterResultSelector": { + "ExecutedVersion": "$LATEST", + "Payload": {}, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": [ + "" + ], + "Content-Length": [ + "2" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "2", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StatusCode": 200 + }, + "input": { + "FunctionName": "", + "Payload": null + }, + "result": { + "ExecutedVersion": "$LATEST", + "Payload": {}, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": [ + "" + ], + "Content-Length": [ + "2" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "2", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StatusCode": 200 + } + }, + "output": { + "ExecutedVersion": "$LATEST", + "Payload": {}, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": [ + "" + ], + "Content-Length": [ + "2" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "2", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StatusCode": 200 + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_service_task_state[TRACE]": { + "recorded-date": "12-04-2024, 20:51:37", + "recorded-content": { + "test_case_response": { + "inspectionData": { + "afterInputPath": { + "FunctionName": "", + "Payload": null + }, + "afterParameters": { + "FunctionName": "", + "Payload": null + }, + "afterResultPath": { + "ExecutedVersion": "$LATEST", + "Payload": {}, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": [ + "" + ], + "Content-Length": [ + "2" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "2", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StatusCode": 200 + }, + "afterResultSelector": { + "ExecutedVersion": "$LATEST", + "Payload": {}, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": [ + "" + ], + "Content-Length": [ + "2" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "2", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StatusCode": 200 + }, + "input": { + "FunctionName": "", + "Payload": null + }, + "result": { + "ExecutedVersion": "$LATEST", + "Payload": {}, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": [ + "" + ], + "Content-Length": [ + "2" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "2", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StatusCode": 200 + } + }, + "output": { + "ExecutedVersion": "$LATEST", + "Payload": {}, + "SdkHttpMetadata": { + "AllHttpHeaders": { + "X-Amz-Executed-Version": [ + "$LATEST" + ], + "x-amzn-Remapped-Content-Length": [ + "0" + ], + "Connection": [ + "keep-alive" + ], + "x-amzn-RequestId": [ + "" + ], + "Content-Length": [ + "2" + ], + "Date": "date", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id", + "Content-Type": [ + "application/json" + ] + }, + "HttpHeaders": { + "Connection": "keep-alive", + "Content-Length": "2", + "Content-Type": "application/json", + "Date": "date", + "X-Amz-Executed-Version": "$LATEST", + "x-amzn-Remapped-Content-Length": "0", + "x-amzn-RequestId": "", + "X-Amzn-Trace-Id": "X-Amzn-Trace-Id" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + }, + "StatusCode": 200 + }, + "status": "SUCCEEDED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + } +} diff --git a/tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.validation.json b/tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.validation.json new file mode 100644 index 0000000000000..6ee1aeac5b542 --- /dev/null +++ b/tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.validation.json @@ -0,0 +1,83 @@ +{ + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[BASE_CHOICE_STATE]": { + "last_validated_date": "2024-04-12T20:48:24+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[BASE_FAIL_STATE]": { + "last_validated_date": "2024-04-12T20:47:56+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[BASE_PASS_STATE]": { + "last_validated_date": "2024-04-12T20:47:06+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[BASE_RESULT_PASS_STATE]": { + "last_validated_date": "2024-04-12T20:47:19+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[BASE_SUCCEED_STATE]": { + "last_validated_date": "2024-04-12T20:48:10+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[IO_PASS_STATE]": { + "last_validated_date": "2024-04-12T20:47:31+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[IO_RESULT_PASS_STATE]": { + "last_validated_date": "2024-04-12T20:47:44+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[BASE_CHOICE_STATE]": { + "last_validated_date": "2024-04-12T20:46:53+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[BASE_FAIL_STATE]": { + "last_validated_date": "2024-04-12T20:46:28+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[BASE_PASS_STATE]": { + "last_validated_date": "2024-04-12T20:45:35+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[BASE_RESULT_PASS_STATE]": { + "last_validated_date": "2024-04-12T20:45:49+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[BASE_SUCCEED_STATE]": { + "last_validated_date": "2024-04-12T20:46:41+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[IO_PASS_STATE]": { + "last_validated_date": "2024-04-12T20:46:03+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[IO_RESULT_PASS_STATE]": { + "last_validated_date": "2024-04-12T20:46:15+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[BASE_CHOICE_STATE]": { + "last_validated_date": "2024-04-12T20:49:57+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[BASE_FAIL_STATE]": { + "last_validated_date": "2024-04-12T20:49:31+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[BASE_PASS_STATE]": { + "last_validated_date": "2024-04-12T20:48:37+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[BASE_RESULT_PASS_STATE]": { + "last_validated_date": "2024-04-12T20:48:50+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[BASE_SUCCEED_STATE]": { + "last_validated_date": "2024-04-12T20:49:44+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[IO_PASS_STATE]": { + "last_validated_date": "2024-04-12T20:49:03+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[IO_RESULT_PASS_STATE]": { + "last_validated_date": "2024-04-12T20:49:22+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_service_task_state[DEBUG]": { + "last_validated_date": "2024-04-12T20:51:22+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_service_task_state[INFO]": { + "last_validated_date": "2024-04-12T20:51:07+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_service_task_state[TRACE]": { + "last_validated_date": "2024-04-12T20:51:37+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_task_state[DEBUG]": { + "last_validated_date": "2024-04-12T20:50:37+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_task_state[INFO]": { + "last_validated_date": "2024-04-12T20:50:22+00:00" + }, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_task_state[TRACE]": { + "last_validated_date": "2024-04-12T20:50:52+00:00" + } +} From 451968fabb223e189323df02a8d08fac110fddec Mon Sep 17 00:00:00 2001 From: Viren Nadkarni Date: Wed, 17 Apr 2024 16:58:30 +0530 Subject: [PATCH 071/169] Bump moto-ext to 5.0.5.post1 (#10638) --- localstack/services/cloudwatch/provider.py | 59 ++++++++++++---------- pyproject.toml | 2 +- requirements-base-runtime.txt | 26 ++++++++-- requirements-basic.txt | 50 ++++++++++++++++-- requirements-dev.txt | 4 +- requirements-runtime.txt | 4 +- requirements-test.txt | 4 +- requirements-typehint.txt | 4 +- 8 files changed, 108 insertions(+), 45 deletions(-) diff --git a/localstack/services/cloudwatch/provider.py b/localstack/services/cloudwatch/provider.py index e8a2953a65ec5..985fd878e78e9 100644 --- a/localstack/services/cloudwatch/provider.py +++ b/localstack/services/cloudwatch/provider.py @@ -1,6 +1,7 @@ import json import logging import uuid +from typing import Any, Optional from xml.sax.saxutils import escape from moto.cloudwatch import cloudwatch_backends @@ -103,48 +104,48 @@ def update_state(target, self, reason, reason_data, state_value): def put_metric_alarm( target, self, - name, - namespace, - metric_name, - metric_data_queries, - comparison_operator, - evaluation_periods, - datapoints_to_alarm, - period, - threshold, - statistic, - extended_statistic, - description, - dimensions, - alarm_actions, - ok_actions, - insufficient_data_actions, - unit, - actions_enabled, - treat_missing_data, - evaluate_low_sample_count_percentile, - threshold_metric_id, - rule=None, - tags=None, -): + name: str, + namespace: str, + metric_name: str, + comparison_operator: str, + evaluation_periods: int, + period: int, + threshold: float, + statistic: str, + description: str, + dimensions: list[dict[str, str]], + alarm_actions: list[str], + metric_data_queries: Optional[list[Any]] = None, + datapoints_to_alarm: Optional[int] = None, + extended_statistic: Optional[str] = None, + ok_actions: Optional[list[str]] = None, + insufficient_data_actions: Optional[list[str]] = None, + unit: Optional[str] = None, + actions_enabled: bool = True, + treat_missing_data: Optional[str] = None, + evaluate_low_sample_count_percentile: Optional[str] = None, + threshold_metric_id: Optional[str] = None, + rule: Optional[str] = None, + tags: Optional[list[dict[str, str]]] = None, +) -> FakeAlarm: if description: description = escape(description) - target( + return target( self, name, namespace, metric_name, - metric_data_queries, comparison_operator, evaluation_periods, - datapoints_to_alarm, period, threshold, statistic, - extended_statistic, description, dimensions, alarm_actions, + metric_data_queries, + datapoints_to_alarm, + extended_statistic, ok_actions, insufficient_data_actions, unit, @@ -276,6 +277,8 @@ def _set_alarm_actions(context, alarm_names, enabled): def _cleanup_describe_output(alarm): + if "Metrics" in alarm and len(alarm["Metrics"]) == 0: + alarm.pop("Metrics") reason_data = alarm.get("StateReasonData") if reason_data is not None and reason_data in ("{}", ""): alarm.pop("StateReasonData") diff --git a/pyproject.toml b/pyproject.toml index e85b91285972c..a769e4aed271e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,7 @@ runtime = [ "json5>=0.9.11", "jsonpath-ng>=1.6.1", "jsonpath-rw>=1.4.0", - "moto-ext[all]==5.0.4.post1", + "moto-ext[all]==5.0.5.post1", "opensearch-py>=2.4.1", "pymongo>=4.2.0", "pyopenssl>=23.0.0", diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index 5f65009c2b63a..fd840bc64d927 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -15,11 +15,14 @@ blinker==1.7.0 # flask # quart boto3==1.34.84 - # via localstack-core (pyproject.toml) + # via + # localstack-core (pyproject.toml) + # moto-ext botocore==1.34.84 # via # boto3 # localstack-core (pyproject.toml) + # moto-ext # s3transfer build==1.2.1 # via localstack-core (pyproject.toml) @@ -43,6 +46,7 @@ constantly==23.10.4 cryptography==42.0.5 # via # localstack-core (pyproject.toml) + # moto-ext # pyopenssl dill==0.3.6 # via localstack-core (pyproject.toml) @@ -88,6 +92,7 @@ itsdangerous==2.1.2 jinja2==3.1.3 # via # flask + # moto-ext # quart jmespath==1.0.1 # via @@ -108,6 +113,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py +moto-ext==5.0.5.post1 packaging==24.0 # via # build @@ -133,11 +139,15 @@ pyopenssl==24.1.0 pyproject-hooks==1.0.0 # via build python-dateutil==2.9.0.post0 - # via botocore + # via + # botocore + # moto-ext python-dotenv==1.0.1 # via localstack-core (pyproject.toml) pyyaml==6.0.1 - # via localstack-core (pyproject.toml) + # via + # localstack-core (pyproject.toml) + # responses quart==0.19.5 # via localstack-core (pyproject.toml) readerwriterlock==1.0.9 @@ -146,10 +156,14 @@ requests==2.31.0 # via # docker # localstack-core (pyproject.toml) + # moto-ext # requests-aws4auth + # responses # rolo requests-aws4auth==1.2.3 # via localstack-core (pyproject.toml) +responses==0.25.0 + # via moto-ext rich==13.7.1 # via localstack-core (pyproject.toml) rolo==0.4.0 @@ -178,18 +192,22 @@ urllib3==2.2.1 # docker # localstack-core (pyproject.toml) # requests + # responses websocket-client==1.7.0 # via docker werkzeug==3.0.2 # via # flask # localstack-core (pyproject.toml) + # moto-ext # quart # rolo wsproto==1.2.0 # via hypercorn xmltodict==0.13.0 - # via localstack-core (pyproject.toml) + # via + # localstack-core (pyproject.toml) + # moto-ext zope-interface==6.3 # via localstack-twisted diff --git a/requirements-basic.txt b/requirements-basic.txt index 151aae1bc6cac..8f130af859c26 100644 --- a/requirements-basic.txt +++ b/requirements-basic.txt @@ -4,6 +4,13 @@ # # pip-compile --output-file=requirements-basic.txt --strip-extras --unsafe-package=distribute --unsafe-package=localstack-core --unsafe-package=pip --unsafe-package=setuptools pyproject.toml # +boto3==1.34.84 + # via moto-ext +botocore==1.34.84 + # via + # boto3 + # moto-ext + # s3transfer build==1.2.1 # via localstack-core (pyproject.toml) cachetools==5.3.3 @@ -17,7 +24,9 @@ charset-normalizer==3.3.2 click==8.1.7 # via localstack-core (pyproject.toml) cryptography==42.0.5 - # via localstack-core (pyproject.toml) + # via + # localstack-core (pyproject.toml) + # moto-ext dill==0.3.6 # via localstack-core (pyproject.toml) dnslib==0.9.24 @@ -26,10 +35,21 @@ dnspython==2.6.1 # via localstack-core (pyproject.toml) idna==3.7 # via requests +jinja2==3.1.3 + # via moto-ext +jmespath==1.0.1 + # via + # boto3 + # botocore markdown-it-py==3.0.0 # via rich +markupsafe==2.1.5 + # via + # jinja2 + # werkzeug mdurl==0.1.2 # via markdown-it-py +moto-ext==5.0.5.post1 packaging==24.0 # via build pbr==6.0.0 @@ -44,16 +64,31 @@ pygments==2.17.2 # via rich pyproject-hooks==1.0.0 # via build +python-dateutil==2.9.0.post0 + # via + # botocore + # moto-ext python-dotenv==1.0.1 # via localstack-core (pyproject.toml) pyyaml==6.0.1 - # via localstack-core (pyproject.toml) + # via + # localstack-core (pyproject.toml) + # responses requests==2.31.0 - # via localstack-core (pyproject.toml) + # via + # localstack-core (pyproject.toml) + # moto-ext + # responses +responses==0.25.0 + # via moto-ext rich==13.7.1 # via localstack-core (pyproject.toml) +s3transfer==0.10.1 + # via boto3 semver==3.0.2 # via localstack-core (pyproject.toml) +six==1.16.0 + # via python-dateutil stevedore==5.2.0 # via # localstack-core (pyproject.toml) @@ -61,4 +96,11 @@ stevedore==5.2.0 tailer==0.4.1 # via localstack-core (pyproject.toml) urllib3==2.2.1 - # via requests + # via + # botocore + # requests + # responses +werkzeug==3.0.2 + # via moto-ext +xmltodict==0.13.0 + # via moto-ext diff --git a/requirements-dev.txt b/requirements-dev.txt index d2ce5a533f1eb..4a3e5545519fe 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -273,7 +273,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext==5.0.4.post1 +moto-ext==5.0.5.post1 # via localstack-core mpmath==1.3.0 # via sympy @@ -344,7 +344,7 @@ publication==0.0.3 # aws-cdk-lib # constructs # jsii -py-partiql-parser==0.5.2 +py-partiql-parser==0.5.4 # via moto-ext pyasn1==0.6.0 # via rsa diff --git a/requirements-runtime.txt b/requirements-runtime.txt index b7b6cef4f764b..f893976de212f 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -210,7 +210,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext==5.0.4.post1 +moto-ext==5.0.5.post1 # via localstack-core (pyproject.toml) mpmath==1.3.0 # via sympy @@ -252,7 +252,7 @@ psutil==5.9.8 # via # localstack-core # localstack-core (pyproject.toml) -py-partiql-parser==0.5.2 +py-partiql-parser==0.5.4 # via moto-ext pyasn1==0.6.0 # via rsa diff --git a/requirements-test.txt b/requirements-test.txt index 6e61778a4d361..efb8d297492c2 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -257,7 +257,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext==5.0.4.post1 +moto-ext==5.0.5.post1 # via localstack-core mpmath==1.3.0 # via sympy @@ -315,7 +315,7 @@ publication==0.0.3 # aws-cdk-lib # constructs # jsii -py-partiql-parser==0.5.2 +py-partiql-parser==0.5.4 # via moto-ext pyasn1==0.6.0 # via rsa diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 397b2561c6459..0c628ea289950 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -277,7 +277,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext==5.0.4.post1 +moto-ext==5.0.5.post1 # via localstack-core mpmath==1.3.0 # via sympy @@ -540,7 +540,7 @@ publication==0.0.3 # aws-cdk-lib # constructs # jsii -py-partiql-parser==0.5.2 +py-partiql-parser==0.5.4 # via moto-ext pyasn1==0.6.0 # via rsa From 01b9d170a526e6c353217acecd8c7b9809560c10 Mon Sep 17 00:00:00 2001 From: Thomas Rausch Date: Wed, 17 Apr 2024 19:55:22 +0200 Subject: [PATCH 072/169] fix apigw data plane service matching (#10682) --- localstack/aws/handlers/cors.py | 22 +++++++++++----------- localstack/aws/protocol/service_router.py | 9 ++++++--- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/localstack/aws/handlers/cors.py b/localstack/aws/handlers/cors.py index 25740cf68dfbf..ed5c0b5a4c374 100644 --- a/localstack/aws/handlers/cors.py +++ b/localstack/aws/handlers/cors.py @@ -145,19 +145,19 @@ def should_enforce_self_managed_service(context: RequestContext) -> bool: the targeting service :return: True if the CORS rules should be enforced in here. """ - if config.DISABLE_CUSTOM_CORS_S3 and config.DISABLE_CUSTOM_CORS_APIGATEWAY: - return True # allow only certain api calls without checking origin - if context.service: - service_name = context.service.service_name - if not config.DISABLE_CUSTOM_CORS_S3 and service_name == "s3": + if not config.DISABLE_CUSTOM_CORS_S3: + if context.service and context.service.service_name == "s3": return False - if not config.DISABLE_CUSTOM_CORS_APIGATEWAY and service_name == "apigateway": - is_user_request = ( - PATH_USER_REQUEST in context.request.path or ".execute-api." in context.request.host - ) - if is_user_request: - return False + + if not config.DISABLE_CUSTOM_CORS_APIGATEWAY: + # we don't check for service_name == "apigw" here because ``.execute-api.`` can be either apigw v1 or v2 + is_user_request = ( + ".execute-api." in context.request.host or PATH_USER_REQUEST in context.request.path + ) + if is_user_request: + return False + return True diff --git a/localstack/aws/protocol/service_router.py b/localstack/aws/protocol/service_router.py index f8080c04f8a22..2640253ee3747 100644 --- a/localstack/aws/protocol/service_router.py +++ b/localstack/aws/protocol/service_router.py @@ -140,10 +140,13 @@ def custom_host_addressing_rules(host: str) -> Optional[ServiceModelIdentifier]: """ Rules based on the host header of the request, which is typically the data plane of a service. - # TODO: ELB, AppSync, CloudFront, ... + Some services are added through a patch in ext. """ - if ".execute-api." in host: - return ServiceModelIdentifier("apigateway") + + # a note on ``.execute-api.`` and why it shouldn't be added as a check here: ``.execute-api.`` was previously + # mapped distinctly to ``apigateway``, but this assumption is too strong, since the URL can be apigw v1, v2, + # or apigw management api. so in short, simply based on the host header, it's not possible to unambiguously + # associate a specific apigw service to the request. if ".lambda-url." in host: return ServiceModelIdentifier("lambda") From 2dd047587498998d41312ca52c5365da21e8679b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristopher=20Pinz=C3=B3n?= Date: Wed, 17 Apr 2024 14:28:16 -0500 Subject: [PATCH 073/169] add cdk infra setup for ecr images (#10678) --- .../testing/scenario/cdk_lambda_helper.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/localstack/testing/scenario/cdk_lambda_helper.py b/localstack/testing/scenario/cdk_lambda_helper.py index 6381cba532356..7b464e9aa526e 100644 --- a/localstack/testing/scenario/cdk_lambda_helper.py +++ b/localstack/testing/scenario/cdk_lambda_helper.py @@ -1,3 +1,4 @@ +import base64 import os import shutil import tempfile @@ -8,9 +9,11 @@ from botocore.exceptions import ClientError from localstack.utils.aws.resources import create_s3_bucket +from localstack.utils.docker_utils import DOCKER_CLIENT from localstack.utils.run import LOG, run if TYPE_CHECKING: + from mypy_boto3_ecr import ECRClient from mypy_boto3_s3 import S3Client @@ -144,6 +147,34 @@ def _zip_lambda_resources( temp_zip.write(file_path, archive_name) +def generate_ecr_image_from_dockerfile( + ecr_client: "ECRClient", repository_name: str, file_path: str +): + """ + Helper function to generate an ECR image from a dockerfile. + + :param ecr_client: client for ECR + :param repository_name: name for the repository to be created + :param file_path: path of the file to be used + :return: None + """ + repository_uri = ecr_client.create_repository( + repositoryName=repository_name, + )["repository"]["repositoryUri"] + + auth_response = ecr_client.get_authorization_token() + auth_token = auth_response["authorizationData"][0]["authorizationToken"].encode() + username, password = base64.b64decode(auth_token).decode().split(":") + registry = auth_response["authorizationData"][0]["proxyEndpoint"] + DOCKER_CLIENT.login(username, password, registry=registry) + + temp_dir = tempfile.mkdtemp() + destination_file = os.path.join(temp_dir, "Dockerfile") + shutil.copy2(file_path, destination_file) + DOCKER_CLIENT.build_image(dockerfile_path=destination_file, image_name=repository_uri) + DOCKER_CLIENT.push_image(repository_uri) + + def _upload_to_s3(s3_client: "S3Client", bucket_name: str, key_name: str, file: str): try: create_s3_bucket(bucket_name, s3_client) From 0c8f87b3948ceda19031ded63487037b198eefcc Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Wed, 17 Apr 2024 21:55:07 +0200 Subject: [PATCH 074/169] fix APIGW: add rootResourceId to REST API creation (#10665) --- localstack/services/apigateway/helpers.py | 12 +- localstack/services/apigateway/provider.py | 44 +- localstack/testing/pytest/fixtures.py | 4 +- .../testing/snapshots/transformer_utility.py | 1 + tests/aws/services/apigateway/conftest.py | 22 +- .../apigateway/test_apigateway_api.py | 497 ++-- .../test_apigateway_api.snapshot.json | 2038 +++++++++-------- .../test_apigateway_api.validation.json | 170 +- .../apigateway/test_apigateway_extended.py | 19 +- .../test_apigateway_extended.snapshot.json | 36 +- .../test_apigateway_extended.validation.json | 8 +- .../apigateway/test_apigateway_import.py | 3 +- .../test_apigateway_import.snapshot.json | 54 +- .../test_apigateway_import.validation.json | 31 +- .../test_apigateway_integrations.py | 21 +- ...test_apigateway_integrations.snapshot.json | 3 +- ...st_apigateway_integrations.validation.json | 2 +- .../cloudformation/api/test_transformers.py | 14 +- .../api/test_transformers.snapshot.json | 15 +- .../api/test_transformers.validation.json | 2 +- .../resources/test_apigateway.snapshot.json | 6 +- .../resources/test_apigateway.validation.json | 4 +- 22 files changed, 1583 insertions(+), 1423 deletions(-) diff --git a/localstack/services/apigateway/helpers.py b/localstack/services/apigateway/helpers.py index 909f8ce1ee811..3cadfec89870a 100644 --- a/localstack/services/apigateway/helpers.py +++ b/localstack/services/apigateway/helpers.py @@ -937,9 +937,6 @@ def import_api_from_openapi_spec( # rest_api.name = resolved_schema.get("info", {}).get("title") rest_api.description = resolved_schema.get("info", {}).get("description") - # Remove default root, then add paths from API spec - # TODO: the default mode is now `merge`, not `overwrite` if using `PutRestApi` - rest_api.resources = {} # authorizers map to avoid duplication authorizers = {} @@ -1355,7 +1352,14 @@ def create_method_resource(child, method, method_schema): base_path = base_path.strip("/").partition("/")[-1] base_path = f"/{base_path}" if base_path else "" - for path in resolved_schema.get("paths", {}): + api_paths = resolved_schema.get("paths", {}) + if api_paths: + # Remove default root, then add paths from API spec + # TODO: the default mode is now `merge`, not `overwrite` if using `PutRestApi` + # TODO: quick hack for now, but do not remove the rootResource if the OpenAPI file is empty + rest_api.resources = {} + + for path in api_paths: get_or_create_path(base_path + path, base_path=base_path) # binary types diff --git a/localstack/services/apigateway/provider.py b/localstack/services/apigateway/provider.py index a992296d46a28..565e1d1d31dba 100644 --- a/localstack/services/apigateway/provider.py +++ b/localstack/services/apigateway/provider.py @@ -246,6 +246,9 @@ def create_rest_api(self, context: RequestContext, request: CreateRestApiRequest rest_api = get_moto_rest_api(context, rest_api_id=result["id"]) rest_api.version = request.get("version") response: RestApi = rest_api.to_dict() + # TODO: remove once this is fixed upstream + if "rootResourceId" not in response: + response["rootResourceId"] = get_moto_rest_api_root_resource(rest_api) remove_empty_attributes_from_rest_api(response) store = get_apigateway_store(context=context) rest_api_container = RestApiContainer(rest_api=response) @@ -281,6 +284,10 @@ def create_api_key( def get_rest_api(self, context: RequestContext, rest_api_id: String, **kwargs) -> RestApi: rest_api: RestApi = call_moto(context) remove_empty_attributes_from_rest_api(rest_api) + # TODO: remove once this is fixed upstream + if "rootResourceId" not in rest_api: + moto_rest_api = get_moto_rest_api(context, rest_api_id=rest_api_id) + rest_api["rootResourceId"] = get_moto_rest_api_root_resource(moto_rest_api) return rest_api def update_rest_api( @@ -326,7 +333,7 @@ def update_rest_api( elif patch_op_path == "/minimumCompressionSize": if patch_op["op"] != "replace": raise BadRequestException( - "Invalid patch operation specified. Must be 'add'|'remove'|'replace'" + "Invalid patch operation specified. Must be one of: [replace]" ) try: @@ -358,6 +365,9 @@ def update_rest_api( rest_api.minimum_compression_size = None response = rest_api.to_dict() + if "rootResourceId" not in response: + response["rootResourceId"] = get_moto_rest_api_root_resource(rest_api) + remove_empty_attributes_from_rest_api(response, remove_tags=False) store = get_apigateway_store(context=context) store.rest_apis[rest_api_id].rest_api = response @@ -376,6 +386,9 @@ def put_rest_api(self, context: RequestContext, request: PutRestApiRequest) -> R remove_empty_attributes_from_rest_api(response) store = get_apigateway_store(context=context) store.rest_apis[request["restApiId"]].rest_api = response + # TODO: remove once this is fixed upstream + if "rootResourceId" not in response: + response["rootResourceId"] = get_moto_rest_api_root_resource(rest_api) # TODO: verify this response = to_rest_api_response_json(response) response.setdefault("tags", {}) @@ -475,6 +488,9 @@ def get_rest_apis( response: RestApis = call_moto(context) for rest_api in response["items"]: remove_empty_attributes_from_rest_api(rest_api) + if "rootResourceId" not in rest_api: + moto_rest_api = get_moto_rest_api(context, rest_api_id=rest_api["id"]) + rest_api["rootResourceId"] = get_moto_rest_api_root_resource(moto_rest_api) return response # resources @@ -807,7 +823,24 @@ def update_method( # if the path is not supported by the operation, ignore it and skip op_supported_path = UPDATE_METHOD_PATCH_PATHS.get(op, []) if not any(path.startswith(s_path) for s_path in op_supported_path): - continue + available_ops = [ + available_op + for available_op in ("add", "replace", "delete") + if available_op != op + ] + supported_ops = ", ".join( + [ + supported_op + for supported_op in available_ops + if any( + path.startswith(s_path) + for s_path in UPDATE_METHOD_PATCH_PATHS.get(supported_op, []) + ) + ] + ) + raise BadRequestException( + f"Invalid patch operation specified. Must be one of: [{supported_ops}]" + ) value = patch_operation.get("value") if op not in ("add", "replace"): @@ -2512,6 +2545,13 @@ def validate_model_in_use(moto_rest_api: MotoRestAPI, model_name: str) -> None: ) +def get_moto_rest_api_root_resource(moto_rest_api: MotoRestAPI) -> str: + for res_id, res_obj in moto_rest_api.resources.items(): + if res_obj.path_part == "/" and not res_obj.parent_id: + return res_id + raise Exception(f"Unable to find root resource for API {moto_rest_api.id}") + + def create_custom_context( context: RequestContext, action: str, parameters: ServiceRequest ) -> RequestContext: diff --git a/localstack/testing/pytest/fixtures.py b/localstack/testing/pytest/fixtures.py index 1ef78774a7279..749624bc946b9 100644 --- a/localstack/testing/pytest/fixtures.py +++ b/localstack/testing/pytest/fixtures.py @@ -1954,10 +1954,8 @@ def _create_apigateway_function(**kwargs): response = apigateway_client.create_rest_api(**kwargs) api_id = response.get("id") rest_apis.append((api_id, region_name)) - resources = apigateway_client.get_resources(restApiId=api_id) - root_id = next(item for item in resources["items"] if item["path"] == "/")["id"] - return api_id, response.get("name"), root_id + return api_id, response.get("name"), response.get("rootResourceId") yield _create_apigateway_function diff --git a/localstack/testing/snapshots/transformer_utility.py b/localstack/testing/snapshots/transformer_utility.py index ebec9054116e8..411107d60fbc3 100644 --- a/localstack/testing/snapshots/transformer_utility.py +++ b/localstack/testing/snapshots/transformer_utility.py @@ -156,6 +156,7 @@ def apigateway_api(): TransformerUtility.key_value("id"), TransformerUtility.key_value("name"), TransformerUtility.key_value("parentId"), + TransformerUtility.key_value("rootResourceId"), ] @staticmethod diff --git a/tests/aws/services/apigateway/conftest.py b/tests/aws/services/apigateway/conftest.py index e6a4caf8dc0be..9dae4ecd0c26d 100644 --- a/tests/aws/services/apigateway/conftest.py +++ b/tests/aws/services/apigateway/conftest.py @@ -1,6 +1,8 @@ import pytest +from botocore.config import Config from localstack.constants import APPLICATION_JSON +from localstack.testing.aws.util import is_aws_cloud from localstack.utils.strings import short_uid from tests.aws.services.apigateway.apigateway_fixtures import ( create_rest_api_deployment, @@ -198,18 +200,32 @@ def _factory(rest_api_id: str, stage_name: str): @pytest.fixture -def import_apigw(aws_client): +def import_apigw(aws_client, aws_client_factory): rest_api_ids = [] + if is_aws_cloud(): + client_config = ( + Config( + # Api gateway can throttle requests pretty heavily. Leading to potentially undeleted apis + retries={"max_attempts": 10, "mode": "adaptive"} + ) + if is_aws_cloud() + else None + ) + + apigateway_client = aws_client_factory(config=client_config).apigateway + else: + apigateway_client = aws_client.apigateway + def _import_apigateway_function(*args, **kwargs): - response, root_id = import_rest_api(aws_client.apigateway, **kwargs) + response, root_id = import_rest_api(apigateway_client, **kwargs) rest_api_ids.append(response.get("id")) return response, root_id yield _import_apigateway_function for rest_api_id in rest_api_ids: - delete_rest_api(aws_client.apigateway, restApiId=rest_api_id) + delete_rest_api(apigateway_client, restApiId=rest_api_id) @pytest.fixture diff --git a/tests/aws/services/apigateway/test_apigateway_api.py b/tests/aws/services/apigateway/test_apigateway_api.py index 5131d396e448f..b8b38ba092f3a 100644 --- a/tests/aws/services/apigateway/test_apigateway_api.py +++ b/tests/aws/services/apigateway/test_apigateway_api.py @@ -68,197 +68,37 @@ def delete_rest_api_retry(client, rest_api_id: str): @pytest.fixture -def apigw_create_rest_api(aws_client): +def apigw_create_rest_api(aws_client, aws_client_factory): + if is_aws_cloud(): + client_config = ( + Config( + # Api gateway can throttle requests pretty heavily. Leading to potentially undeleted apis + retries={"max_attempts": 10, "mode": "adaptive"} + ) + if is_aws_cloud() + else None + ) + + apigateway_client = aws_client_factory(config=client_config).apigateway + else: + apigateway_client = aws_client.apigateway + rest_apis = [] def _factory(*args, **kwargs): if "name" not in kwargs: kwargs["name"] = f"test-api-{short_uid()}" - response = aws_client.apigateway.create_rest_api(*args, **kwargs) + response = apigateway_client.create_rest_api(*args, **kwargs) rest_apis.append(response["id"]) return response yield _factory for rest_api_id in rest_apis: - delete_rest_api_retry(aws_client.apigateway, rest_api_id) - - -class TestApiGatewayApi: - @markers.aws.validated - def test_invoke_test_method(self, create_rest_apigw, snapshot, aws_client): - snapshot.add_transformer( - KeyValueBasedTransformer( - lambda k, v: str(v) if k == "latency" else None, "latency", replace_reference=False - ) - ) - snapshot.add_transformer( - snapshot.transform.key_value("log", "log", reference_replacement=False) - ) - - api_id, _, root = create_rest_apigw(name="aws lambda api") - - # Create the /pets resource - root_resource_id, _ = create_rest_resource( - aws_client.apigateway, restApiId=api_id, parentId=root, pathPart="pets" - ) - # Create the /pets/{petId} resource - resource_id, _ = create_rest_resource( - aws_client.apigateway, restApiId=api_id, parentId=root_resource_id, pathPart="{petId}" - ) - # Create the GET method for /pets/{petId} - create_rest_resource_method( - aws_client.apigateway, - restApiId=api_id, - resourceId=resource_id, - httpMethod="GET", - authorizationType="NONE", - requestParameters={ - "method.request.path.petId": True, - }, - ) - # Create the POST method for /pets/{petId} - create_rest_resource_method( - aws_client.apigateway, - restApiId=api_id, - resourceId=resource_id, - httpMethod="POST", - authorizationType="NONE", - requestParameters={ - "method.request.path.petId": True, - }, - ) - # Create the response for method GET /pets/{petId} - create_rest_api_method_response( - aws_client.apigateway, - restApiId=api_id, - resourceId=resource_id, - httpMethod="GET", - statusCode="200", - ) - # Create the response for method POST /pets/{petId} - create_rest_api_method_response( - aws_client.apigateway, - restApiId=api_id, - resourceId=resource_id, - httpMethod="POST", - statusCode="200", - ) - # Create the integration to connect GET /pets/{petId} to a backend - create_rest_api_integration( - aws_client.apigateway, - restApiId=api_id, - resourceId=resource_id, - httpMethod="GET", - type="MOCK", - integrationHttpMethod="GET", - requestParameters={ - "integration.request.path.id": "method.request.path.petId", - }, - requestTemplates={"application/json": json.dumps({"statusCode": 200})}, - ) - # Create the integration to connect POST /pets/{petId} to a backend - create_rest_api_integration( - aws_client.apigateway, - restApiId=api_id, - resourceId=resource_id, - httpMethod="POST", - type="MOCK", - integrationHttpMethod="POST", - requestParameters={ - "integration.request.path.id": "method.request.path.petId", - }, - requestTemplates={"application/json": json.dumps({"statusCode": 200})}, - ) - # Create the 200 integration response for GET /pets/{petId} - create_rest_api_integration_response( - aws_client.apigateway, - restApiId=api_id, - resourceId=resource_id, - httpMethod="GET", - statusCode="200", - responseTemplates={"application/json": json.dumps({"petId": "$input.params('petId')"})}, - ) - # Create the 200 integration response for POST /pets/{petId} - create_rest_api_integration_response( - aws_client.apigateway, - restApiId=api_id, - resourceId=resource_id, - httpMethod="POST", - statusCode="200", - responseTemplates={"application/json": json.dumps({"petId": "$input.params('petId')"})}, - ) - - def invoke_method(api_id, resource_id, path_with_query_string, method, body=""): - res = aws_client.apigateway.test_invoke_method( - restApiId=api_id, - resourceId=resource_id, - httpMethod=method, - pathWithQueryString=path_with_query_string, - body=body, - ) - assert 200 == res.get("status") - return res + delete_rest_api_retry(apigateway_client, rest_api_id) - response = retry( - invoke_method, - retries=10, - sleep=5, - api_id=api_id, - resource_id=resource_id, - path_with_query_string="/pets/123", - method="GET", - ) - assert "HTTP Method: GET, Resource Path: /pets/123" in response["log"] - snapshot.match("test-invoke-method-get", response) - - response = retry( - invoke_method, - retries=10, - sleep=5, - api_id=api_id, - resource_id=resource_id, - path_with_query_string="/pets/123?foo=bar", - method="GET", - ) - snapshot.match("test-invoke-method-get-with-qs", response) - - response = retry( - invoke_method, - retries=10, - sleep=5, - api_id=api_id, - resource_id=resource_id, - path_with_query_string="/pets/123", - method="POST", - body=json.dumps({"foo": "bar"}), - ) - assert "HTTP Method: POST, Resource Path: /pets/123" in response["log"] - snapshot.match("test-invoke-method-post-with-body", response) - - # assert resource and rest api doesn't exist - with pytest.raises(ClientError) as ex: - aws_client.apigateway.test_invoke_method( - restApiId=api_id, - resourceId="invalid_res", - httpMethod="POST", - pathWithQueryString="/pets/123", - body=json.dumps({"foo": "bar"}), - ) - snapshot.match("resource-id-not-found", ex.value.response) - assert ex.value.response["Error"]["Code"] == "NotFoundException" - - with pytest.raises(ClientError) as ex: - aws_client.apigateway.test_invoke_method( - restApiId=api_id, - resourceId="invalid_res", - httpMethod="POST", - pathWithQueryString="/pets/123", - body=json.dumps({"foo": "bar"}), - ) - snapshot.match("rest-api-not-found", ex.value.response) - assert ex.value.response["Error"]["Code"] == "NotFoundException" +class TestApiGatewayApiRestApi: @markers.aws.validated def test_list_and_delete_apis(self, apigw_create_rest_api, snapshot, aws_client): api_name1 = f"test-list-and-delete-apis-{short_uid()}" @@ -529,6 +369,8 @@ def test_update_rest_api_invalid_api_id(self, snapshot, aws_client): snapshot.match("not-found-update-rest-api", ex.value.response) assert ex.value.response["Error"]["Code"] == "NotFoundException" + +class TestApiGatewayApiResource: @markers.aws.validated def test_resource_lifecycle(self, apigw_create_rest_api, snapshot, aws_client): snapshot.add_transformer(SortingTransformer("items", lambda x: x["path"])) @@ -888,6 +730,8 @@ def test_create_proxy_resource_validation(self, apigw_create_rest_api, snapshot, ) snapshot.match("create-greedy-child-resource", greedy_child_response) + +class TestApiGatewayApiAuthorizer: @markers.aws.validated def test_authorizer_crud_no_api(self, snapshot, aws_client): # maybe move this test to a full lifecycle one @@ -906,45 +750,15 @@ def test_authorizer_crud_no_api(self, snapshot, aws_client): aws_client.apigateway.get_authorizers(restApiId="test-fake-rest-id") snapshot.match("wrong-rest-api-id-get-authorizers", e.value.response) - @markers.aws.validated - def test_doc_arts_crud_no_api(self, snapshot, aws_client): - # maybe move this test to a full lifecycle one - with pytest.raises(ClientError) as e: - aws_client.apigateway.create_documentation_part( - restApiId="test-fake-rest-id", - location={"type": "API"}, - properties='{\n\t"info": {\n\t\t"description" : "Your first API with Amazon API Gateway."\n\t}\n}', - ) - snapshot.match("wrong-rest-api-id-create-doc-part", e.value.response) - - with pytest.raises(ClientError) as e: - aws_client.apigateway.get_documentation_parts(restApiId="test-fake-rest-id") - snapshot.match("wrong-rest-api-id-get-doc-parts", e.value.response) - - @markers.aws.validated - def test_validators_crud_no_api(self, snapshot, aws_client): - # maybe move this test to a full lifecycle one - with pytest.raises(ClientError) as e: - aws_client.apigateway.create_request_validator( - restApiId="test-fake-rest-id", - name="test-validator", - validateRequestBody=True, - validateRequestParameters=False, - ) - snapshot.match("wrong-rest-api-id-create-validator", e.value.response) - - with pytest.raises(ClientError) as e: - aws_client.apigateway.get_request_validators(restApiId="test-fake-rest-id") - snapshot.match("wrong-rest-api-id-get-validators", e.value.response) +class TestApiGatewayApiMethod: @markers.aws.validated def test_method_lifecycle(self, apigw_create_rest_api, snapshot, aws_client): response = apigw_create_rest_api( name=f"test-api-{short_uid()}", description="testing resource method lifecycle" ) api_id = response["id"] - root_rest_api_resource = aws_client.apigateway.get_resources(restApiId=api_id) - root_id = root_rest_api_resource["items"][0]["id"] + root_id = response["rootResourceId"] put_base_method_response = aws_client.apigateway.put_method( restApiId=api_id, @@ -980,8 +794,7 @@ def test_method_request_parameters(self, apigw_create_rest_api, snapshot, aws_cl name=f"test-api-{short_uid()}", description="testing resource method request params" ) api_id = response["id"] - root_rest_api_resource = aws_client.apigateway.get_resources(restApiId=api_id) - root_id = root_rest_api_resource["items"][0]["id"] + root_id = response["rootResourceId"] put_method_response = aws_client.apigateway.put_method( restApiId=api_id, @@ -1028,8 +841,7 @@ def test_put_method_model(self, apigw_create_rest_api, snapshot, aws_client): name=f"test-api-{short_uid()}", description="testing resource method model" ) api_id = response["id"] - root_rest_api_resource = aws_client.apigateway.get_resources(restApiId=api_id) - root_id = root_rest_api_resource["items"][0]["id"] + root_id = response["rootResourceId"] create_model = aws_client.apigateway.create_model( name="MySchema", @@ -1137,8 +949,7 @@ def test_put_method_validation(self, apigw_create_rest_api, snapshot, aws_client name=f"test-api-{short_uid()}", description="testing resource method request params" ) api_id = response["id"] - root_rest_api_resource = aws_client.apigateway.get_resources(restApiId=api_id) - root_id = root_rest_api_resource["items"][0]["id"] + root_id = response["rootResourceId"] # wrong RestApiId with pytest.raises(ClientError) as e: @@ -1215,8 +1026,7 @@ def test_update_method(self, apigw_create_rest_api, snapshot, aws_client): name=f"test-api-{short_uid()}", description="testing update method" ) api_id = response["id"] - root_rest_api_resource = aws_client.apigateway.get_resources(restApiId=api_id) - root_id = root_rest_api_resource["items"][0]["id"] + root_id = response["rootResourceId"] put_method_response = aws_client.apigateway.put_method( restApiId=api_id, @@ -1306,8 +1116,7 @@ def test_update_method_validation(self, apigw_create_rest_api, snapshot, aws_cli name=f"test-api-{short_uid()}", description="testing resource method request params" ) api_id = response["id"] - root_rest_api_resource = aws_client.apigateway.get_resources(restApiId=api_id) - root_id = root_rest_api_resource["items"][0]["id"] + root_id = response["rootResourceId"] with pytest.raises(ClientError) as e: aws_client.apigateway.update_method( @@ -1349,17 +1158,31 @@ def test_update_method_validation(self, apigw_create_rest_api, snapshot, aws_cli ) snapshot.match("put-method-response", put_method_response) - # unsupported operation ? + # unsupported operation patch_operations_add = [ {"op": "add", "path": "/operationName", "value": "operationName"}, ] - unsupported_operation_resp = aws_client.apigateway.update_method( - restApiId=api_id, - resourceId=root_id, - httpMethod="ANY", - patchOperations=patch_operations_add, - ) - snapshot.match("unsupported-operation", unsupported_operation_resp) + with pytest.raises(ClientError) as e: + aws_client.apigateway.update_method( + restApiId=api_id, + resourceId=root_id, + httpMethod="ANY", + patchOperations=patch_operations_add, + ) + snapshot.match("unsupported-operation", e.value.response) + + # unsupported operation + patch_operations_add_2 = [ + {"op": "add", "path": "/requestValidatorId", "value": "wrong-id"}, + ] + with pytest.raises(ClientError) as e: + aws_client.apigateway.update_method( + restApiId=api_id, + resourceId=root_id, + httpMethod="ANY", + patchOperations=patch_operations_add_2, + ) + snapshot.match("unsupported-operation-2", e.value.response) # unsupported path with pytest.raises(ClientError) as e: @@ -1468,9 +1291,10 @@ def test_update_method_validation(self, apigw_create_rest_api, snapshot, aws_cli ) snapshot.match("wrong-req-validator-id", e.value.response) + +class TestApiGatewayApiModels: @markers.aws.validated def test_model_lifecycle(self, apigw_create_rest_api, snapshot, aws_client): - snapshot.add_transformer(SortingTransformer("items", lambda x: x["name"])) # taken from https://docs.aws.amazon.com/apigateway/latest/api/API_CreateModel.html#API_CreateModel_Examples response = apigw_create_rest_api( name=f"test-api-{short_uid()}", description="testing resource model lifecycle" @@ -1487,6 +1311,7 @@ def test_model_lifecycle(self, apigw_create_rest_api, snapshot, aws_client): snapshot.match("create-model", create_model_response) get_models_response = aws_client.apigateway.get_models(restApiId=api_id) + get_models_response["items"].sort(key=lambda x: x["name"]) snapshot.match("get-models", get_models_response) # manually assert the presence of 2 default models, Error and Empty, as snapshots will replace names @@ -1671,6 +1496,22 @@ def test_update_model(self, apigw_create_rest_api, snapshot, aws_client): class TestApiGatewayApiRequestValidator: + @markers.aws.validated + def test_validators_crud_no_api(self, snapshot, aws_client): + # maybe move this test to a full lifecycle one + with pytest.raises(ClientError) as e: + aws_client.apigateway.create_request_validator( + restApiId="test-fake-rest-id", + name="test-validator", + validateRequestBody=True, + validateRequestParameters=False, + ) + snapshot.match("wrong-rest-api-id-create-validator", e.value.response) + + with pytest.raises(ClientError) as e: + aws_client.apigateway.get_request_validators(restApiId="test-fake-rest-id") + snapshot.match("wrong-rest-api-id-get-validators", e.value.response) + @markers.aws.validated def test_request_validator_lifecycle(self, apigw_create_rest_api, snapshot, aws_client): response = apigw_create_rest_api( @@ -1858,6 +1699,21 @@ def test_invalid_update_request_validator_operations( class TestApiGatewayApiDocumentationPart: + @markers.aws.validated + def test_doc_parts_crud_no_api(self, snapshot, aws_client): + # maybe move this test to a full lifecycle one + with pytest.raises(ClientError) as e: + aws_client.apigateway.create_documentation_part( + restApiId="test-fake-rest-id", + location={"type": "API"}, + properties='{\n\t"info": {\n\t\t"description" : "Your first API with Amazon API Gateway."\n\t}\n}', + ) + snapshot.match("wrong-rest-api-id-create-doc-part", e.value.response) + + with pytest.raises(ClientError) as e: + aws_client.apigateway.get_documentation_parts(restApiId="test-fake-rest-id") + snapshot.match("wrong-rest-api-id-get-doc-parts", e.value.response) + @markers.aws.validated def test_documentation_part_lifecycle(self, apigw_create_rest_api, snapshot, aws_client): response = apigw_create_rest_api( @@ -2362,6 +2218,182 @@ def test_update_gateway_response( ) +class TestApigatewayTestInvoke: + @markers.aws.validated + def test_invoke_test_method(self, create_rest_apigw, snapshot, aws_client): + snapshot.add_transformer( + KeyValueBasedTransformer( + lambda k, v: str(v) if k == "latency" else None, "latency", replace_reference=False + ) + ) + snapshot.add_transformer( + snapshot.transform.key_value("log", "log", reference_replacement=False) + ) + + api_id, _, root = create_rest_apigw(name="aws lambda api") + + # Create the /pets resource + root_resource_id, _ = create_rest_resource( + aws_client.apigateway, restApiId=api_id, parentId=root, pathPart="pets" + ) + # Create the /pets/{petId} resource + resource_id, _ = create_rest_resource( + aws_client.apigateway, restApiId=api_id, parentId=root_resource_id, pathPart="{petId}" + ) + # Create the GET method for /pets/{petId} + create_rest_resource_method( + aws_client.apigateway, + restApiId=api_id, + resourceId=resource_id, + httpMethod="GET", + authorizationType="NONE", + requestParameters={ + "method.request.path.petId": True, + }, + ) + # Create the POST method for /pets/{petId} + create_rest_resource_method( + aws_client.apigateway, + restApiId=api_id, + resourceId=resource_id, + httpMethod="POST", + authorizationType="NONE", + requestParameters={ + "method.request.path.petId": True, + }, + ) + # Create the response for method GET /pets/{petId} + create_rest_api_method_response( + aws_client.apigateway, + restApiId=api_id, + resourceId=resource_id, + httpMethod="GET", + statusCode="200", + ) + # Create the response for method POST /pets/{petId} + create_rest_api_method_response( + aws_client.apigateway, + restApiId=api_id, + resourceId=resource_id, + httpMethod="POST", + statusCode="200", + ) + # Create the integration to connect GET /pets/{petId} to a backend + create_rest_api_integration( + aws_client.apigateway, + restApiId=api_id, + resourceId=resource_id, + httpMethod="GET", + type="MOCK", + integrationHttpMethod="GET", + requestParameters={ + "integration.request.path.id": "method.request.path.petId", + }, + requestTemplates={"application/json": json.dumps({"statusCode": 200})}, + ) + # Create the integration to connect POST /pets/{petId} to a backend + create_rest_api_integration( + aws_client.apigateway, + restApiId=api_id, + resourceId=resource_id, + httpMethod="POST", + type="MOCK", + integrationHttpMethod="POST", + requestParameters={ + "integration.request.path.id": "method.request.path.petId", + }, + requestTemplates={"application/json": json.dumps({"statusCode": 200})}, + ) + # Create the 200 integration response for GET /pets/{petId} + create_rest_api_integration_response( + aws_client.apigateway, + restApiId=api_id, + resourceId=resource_id, + httpMethod="GET", + statusCode="200", + responseTemplates={"application/json": json.dumps({"petId": "$input.params('petId')"})}, + ) + # Create the 200 integration response for POST /pets/{petId} + create_rest_api_integration_response( + aws_client.apigateway, + restApiId=api_id, + resourceId=resource_id, + httpMethod="POST", + statusCode="200", + responseTemplates={"application/json": json.dumps({"petId": "$input.params('petId')"})}, + ) + + def invoke_method(api_id, resource_id, path_with_query_string, method, body=""): + res = aws_client.apigateway.test_invoke_method( + restApiId=api_id, + resourceId=resource_id, + httpMethod=method, + pathWithQueryString=path_with_query_string, + body=body, + ) + assert 200 == res.get("status") + return res + + response = retry( + invoke_method, + retries=10, + sleep=5, + api_id=api_id, + resource_id=resource_id, + path_with_query_string="/pets/123", + method="GET", + ) + assert "HTTP Method: GET, Resource Path: /pets/123" in response["log"] + snapshot.match("test-invoke-method-get", response) + + response = retry( + invoke_method, + retries=10, + sleep=5, + api_id=api_id, + resource_id=resource_id, + path_with_query_string="/pets/123?foo=bar", + method="GET", + ) + snapshot.match("test-invoke-method-get-with-qs", response) + + response = retry( + invoke_method, + retries=10, + sleep=5, + api_id=api_id, + resource_id=resource_id, + path_with_query_string="/pets/123", + method="POST", + body=json.dumps({"foo": "bar"}), + ) + assert "HTTP Method: POST, Resource Path: /pets/123" in response["log"] + snapshot.match("test-invoke-method-post-with-body", response) + + # assert resource and rest api doesn't exist + with pytest.raises(ClientError) as ex: + aws_client.apigateway.test_invoke_method( + restApiId=api_id, + resourceId="invalid_res", + httpMethod="POST", + pathWithQueryString="/pets/123", + body=json.dumps({"foo": "bar"}), + ) + snapshot.match("resource-id-not-found", ex.value.response) + assert ex.value.response["Error"]["Code"] == "NotFoundException" + + with pytest.raises(ClientError) as ex: + aws_client.apigateway.test_invoke_method( + restApiId=api_id, + resourceId="invalid_res", + httpMethod="POST", + pathWithQueryString="/pets/123", + body=json.dumps({"foo": "bar"}), + ) + snapshot.match("rest-api-not-found", ex.value.response) + assert ex.value.response["Error"]["Code"] == "NotFoundException" + + class TestApigatewayIntegration: @markers.aws.validated def test_put_integration_wrong_type( @@ -2373,13 +2405,10 @@ def test_put_integration_wrong_type( description="APIGW test PutIntegration Types", ) api_id = response["id"] - - root_rest_api_resource = aws_client.apigateway.get_resources(restApiId=api_id) - - root_id = root_rest_api_resource["items"][0]["id"] + root_resource_id = response["rootResourceId"] with pytest.raises(ClientError) as e: apigw_client.put_integration( - restApiId=api_id, resourceId=root_id, httpMethod="GET", type="HTTPS_PROXY" + restApiId=api_id, resourceId=root_resource_id, httpMethod="GET", type="HTTPS_PROXY" ) snapshot.match("put-integration-wrong-type", e.value.response) diff --git a/tests/aws/services/apigateway/test_apigateway_api.snapshot.json b/tests/aws/services/apigateway/test_apigateway_api.snapshot.json index 60f04077be3d3..ffc1d24af931f 100644 --- a/tests/aws/services/apigateway/test_apigateway_api.snapshot.json +++ b/tests/aws/services/apigateway/test_apigateway_api.snapshot.json @@ -1,6 +1,6 @@ { - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_list_and_delete_apis": { - "recorded-date": "01-02-2023, 20:16:52", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_list_and_delete_apis": { + "recorded-date": "15-04-2024, 15:09:48", "recorded-content": { "create-rest-api-1": { "apiKeySource": "HEADER", @@ -14,6 +14,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 201 @@ -31,6 +32,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 201 @@ -49,7 +51,8 @@ ] }, "id": "", - "name": "" + "name": "", + "rootResourceId": "" }, { "apiKeySource": "HEADER", @@ -62,7 +65,8 @@ ] }, "id": "", - "name": "" + "name": "", + "rootResourceId": "" } ], "ResponseMetadata": { @@ -89,7 +93,8 @@ ] }, "id": "", - "name": "" + "name": "", + "rootResourceId": "" } ], "ResponseMetadata": { @@ -99,8 +104,121 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_create_rest_api_with_tags": { - "recorded-date": "01-02-2023, 20:11:19", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_get_api_case_insensitive": { + "recorded-date": "15-04-2024, 15:10:18", + "recorded-content": { + "create-rest-api": { + "apiKeySource": "HEADER", + "createdDate": "datetime", + "description": "lower case api", + "disableExecuteApiEndpoint": false, + "endpointConfiguration": { + "types": [ + "EDGE" + ] + }, + "id": "", + "name": "", + "rootResourceId": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + }, + "get-api-upper-case": { + "Error": { + "Code": "NotFoundException", + "Message": "Invalid API identifier specified 111111111111:" + }, + "message": "Invalid API identifier specified 111111111111:", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 404 + } + } + } + }, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_create_rest_api_with_optional_params": { + "recorded-date": "15-04-2024, 15:11:50", + "recorded-content": { + "create-only-name": { + "apiKeySource": "HEADER", + "createdDate": "datetime", + "disableExecuteApiEndpoint": false, + "endpointConfiguration": { + "types": [ + "EDGE" + ] + }, + "id": "", + "name": "", + "rootResourceId": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + }, + "create-empty-desc": { + "Error": { + "Code": "BadRequestException", + "Message": "Description cannot be an empty string" + }, + "message": "Description cannot be an empty string", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "create-with-version": { + "apiKeySource": "HEADER", + "createdDate": "datetime", + "disableExecuteApiEndpoint": false, + "endpointConfiguration": { + "types": [ + "EDGE" + ] + }, + "id": "", + "name": "", + "rootResourceId": "", + "version": "v1", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + }, + "create-with-empty-binary-media": { + "apiKeySource": "HEADER", + "createdDate": "datetime", + "disableExecuteApiEndpoint": false, + "endpointConfiguration": { + "types": [ + "EDGE" + ] + }, + "id": "", + "name": "", + "rootResourceId": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + }, + "string-compression-size": { + "Error": { + "Code": "BadRequestException", + "Message": "Invalid minimum compression size, must be between 0 and 10485760" + }, + "message": "Invalid minimum compression size, must be between 0 and 10485760", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_create_rest_api_with_tags": { + "recorded-date": "15-04-2024, 15:12:32", "recorded-content": { "create-rest-api-w-tags": { "apiKeySource": "HEADER", @@ -114,6 +232,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": { "MY_TAG1": "MY_VALUE1" }, @@ -134,6 +253,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": { "MY_TAG1": "MY_VALUE1" }, @@ -156,6 +276,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": { "MY_TAG1": "MY_VALUE1" } @@ -168,8 +289,8 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_rest_api_operation_add_remove": { - "recorded-date": "01-02-2023, 20:11:40", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_update_rest_api_operation_add_remove": { + "recorded-date": "15-04-2024, 15:12:34", "recorded-content": { "update-rest-api-add": { "apiKeySource": "HEADER", @@ -187,6 +308,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": {}, "ResponseMetadata": { "HTTPHeaders": {}, @@ -209,6 +331,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": {}, "ResponseMetadata": { "HTTPHeaders": {}, @@ -229,6 +352,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": {}, "ResponseMetadata": { "HTTPHeaders": {}, @@ -237,14 +361,11 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_rest_api_behaviour": { - "recorded-date": "01-02-2023, 20:27:17", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_update_rest_api_compression": { + "recorded-date": "15-04-2024, 15:13:28", "recorded-content": { - "update-rest-api-array": { + "enable-compression": { "apiKeySource": "HEADER", - "binaryMediaTypes": [ - "-" - ], "createdDate": "datetime", "description": "this is my api", "disableExecuteApiEndpoint": false, @@ -254,70 +375,99 @@ ] }, "id": "", + "minimumCompressionSize": 10, "name": "", + "rootResourceId": "", "tags": {}, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "update-rest-api-add-base-path": { - "Error": { - "Code": "BadRequestException", - "Message": "Invalid patch path /binaryMediaTypes" + "disable-compression": { + "apiKeySource": "HEADER", + "createdDate": "datetime", + "description": "this is my api", + "disableExecuteApiEndpoint": false, + "endpointConfiguration": { + "types": [ + "EDGE" + ] }, - "message": "Invalid patch path /binaryMediaTypes", + "id": "", + "name": "", + "rootResourceId": "", + "tags": {}, "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 400 + "HTTPStatusCode": 200 } }, - "update-rest-api-replace-base-path": { + "set-compression-zero": { + "apiKeySource": "HEADER", + "createdDate": "datetime", + "description": "this is my api", + "disableExecuteApiEndpoint": false, + "endpointConfiguration": { + "types": [ + "EDGE" + ] + }, + "id": "", + "minimumCompressionSize": 0, + "name": "", + "rootResourceId": "", + "tags": {}, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "set-negative-compression": { "Error": { "Code": "BadRequestException", - "Message": "Invalid patch path /binaryMediaTypes" + "Message": "Invalid minimum compression size, must be between 0 and 10485760" }, - "message": "Invalid patch path /binaryMediaTypes", + "message": "Invalid minimum compression size, must be between 0 and 10485760", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 } }, - "update-rest-api-remove-base-path": { + "set-string-compression": { "Error": { "Code": "BadRequestException", - "Message": "Invalid patch path /binaryMediaTypes" + "Message": "Invalid minimum compression size, must be between 0 and 10485760" }, - "message": "Invalid patch path /binaryMediaTypes", + "message": "Invalid minimum compression size, must be between 0 and 10485760", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 } - } - } - }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_rest_api_invalid_api_id": { - "recorded-date": "01-02-2023, 22:26:45", - "recorded-content": { - "not-found-update-rest-api": { + }, + "unsupported-operation": { "Error": { - "Code": "NotFoundException", - "Message": "Invalid API identifier specified 111111111111:api_id" + "Code": "BadRequestException", + "Message": "Invalid patch operation specified. Must be one of: [replace]" }, - "message": "Invalid API identifier specified 111111111111:api_id", + "message": "Invalid patch operation specified. Must be one of: [replace]", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 404 + "HTTPStatusCode": 400 } } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_create_rest_api_with_optional_params": { - "recorded-date": "04-04-2023, 17:58:40", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_update_rest_api_behaviour": { + "recorded-date": "15-04-2024, 15:14:15", "recorded-content": { - "create-only-name": { + "update-rest-api-array": { "apiKeySource": "HEADER", + "binaryMediaTypes": [ + "-" + ], "createdDate": "datetime", + "description": "this is my api", "disableExecuteApiEndpoint": false, "endpointConfiguration": { "types": [ @@ -326,70 +476,66 @@ }, "id": "", "name": "", + "rootResourceId": "", + "tags": {}, "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 201 + "HTTPStatusCode": 200 } }, - "create-empty-desc": { + "update-rest-api-add-base-path": { "Error": { "Code": "BadRequestException", - "Message": "Description cannot be an empty string" + "Message": "Invalid patch path /binaryMediaTypes" }, - "message": "Description cannot be an empty string", + "message": "Invalid patch path /binaryMediaTypes", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 } }, - "create-with-version": { - "apiKeySource": "HEADER", - "createdDate": "datetime", - "disableExecuteApiEndpoint": false, - "endpointConfiguration": { - "types": [ - "EDGE" - ] + "update-rest-api-replace-base-path": { + "Error": { + "Code": "BadRequestException", + "Message": "Invalid patch path /binaryMediaTypes" }, - "id": "", - "name": "", - "version": "v1", + "message": "Invalid patch path /binaryMediaTypes", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 201 + "HTTPStatusCode": 400 } }, - "create-with-empty-binary-media": { - "apiKeySource": "HEADER", - "createdDate": "datetime", - "disableExecuteApiEndpoint": false, - "endpointConfiguration": { - "types": [ - "EDGE" - ] + "update-rest-api-remove-base-path": { + "Error": { + "Code": "BadRequestException", + "Message": "Invalid patch path /binaryMediaTypes" }, - "id": "", - "name": "", + "message": "Invalid patch path /binaryMediaTypes", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 201 + "HTTPStatusCode": 400 } - }, - "string-compression-size": { + } + } + }, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_update_rest_api_invalid_api_id": { + "recorded-date": "15-04-2024, 15:14:15", + "recorded-content": { + "not-found-update-rest-api": { "Error": { - "Code": "BadRequestException", - "Message": "Invalid minimum compression size, must be between 0 and 10485760" + "Code": "NotFoundException", + "Message": "Invalid API identifier specified 111111111111:api_id" }, - "message": "Invalid minimum compression size, must be between 0 and 10485760", + "message": "Invalid API identifier specified 111111111111:api_id", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 400 + "HTTPStatusCode": 404 } } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_resource_lifecycle": { - "recorded-date": "23-02-2023, 22:08:31", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_resource_lifecycle": { + "recorded-date": "15-04-2024, 17:29:03", "recorded-content": { "rest-api-root-resource": { "items": [ @@ -505,8 +651,8 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_resource_behaviour": { - "recorded-date": "24-02-2023, 17:56:07", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_update_resource_behaviour": { + "recorded-date": "15-04-2024, 17:29:39", "recorded-content": { "nonexistent-resource": { "Error": { @@ -646,8 +792,8 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_delete_resource": { - "recorded-date": "23-02-2023, 20:41:28", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_delete_resource": { + "recorded-date": "15-04-2024, 17:30:24", "recorded-content": { "delete-resource": { "ResponseMetadata": { @@ -680,17 +826,33 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_create_proxy_resource": { - "recorded-date": "24-02-2023, 15:56:43", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_create_resource_parent_invalid": { + "recorded-date": "15-04-2024, 17:31:16", "recorded-content": { - "create-base-proxy-resource": { - "id": "", - "parentId": "", - "path": "/{proxy+}", - "pathPart": "{proxy+}", + "wrong-resource-parent-id": { + "Error": { + "Code": "NotFoundException", + "Message": "Invalid Resource identifier specified" + }, + "message": "Invalid Resource identifier specified", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 201 + "HTTPStatusCode": 404 + } + } + } + }, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_create_proxy_resource": { + "recorded-date": "15-04-2024, 17:31:31", + "recorded-content": { + "create-base-proxy-resource": { + "id": "", + "parentId": "", + "path": "/{proxy+}", + "pathPart": "{proxy+}", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 } }, "create-proxy-sibling-resource": "", @@ -836,8 +998,8 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_create_proxy_resource_validation": { - "recorded-date": "24-02-2023, 16:17:00", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_create_proxy_resource_validation": { + "recorded-date": "15-04-2024, 17:32:33", "recorded-content": { "create-base-proxy-resource": { "id": "", @@ -894,24 +1056,8 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_create_resource_parent_invalid": { - "recorded-date": "24-02-2023, 16:39:00", - "recorded-content": { - "wrong-resource-parent-id": { - "Error": { - "Code": "NotFoundException", - "Message": "Invalid Resource identifier specified" - }, - "message": "Invalid Resource identifier specified", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 404 - } - } - } - }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_authorizer_crud_no_api": { - "recorded-date": "28-02-2023, 20:06:16", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiAuthorizer::test_authorizer_crud_no_api": { + "recorded-date": "15-04-2024, 18:43:45", "recorded-content": { "wrong-rest-api-id-create-authorizer": { "Error": { @@ -937,26 +1083,50 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_doc_arts_crud_no_api": { - "recorded-date": "28-02-2023, 20:11:32", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_method_lifecycle": { + "recorded-date": "15-04-2024, 21:22:46", "recorded-content": { - "wrong-rest-api-id-create-doc-part": { + "put-base-method-response": { + "apiKeyRequired": false, + "authorizationType": "NONE", + "httpMethod": "ANY", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + }, + "get-base-method-response": { + "apiKeyRequired": false, + "authorizationType": "NONE", + "httpMethod": "ANY", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "del-base-method-response": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 204 + } + }, + "get-deleted-method-response": { "Error": { "Code": "NotFoundException", - "Message": "Invalid API identifier specified 111111111111:test-fake-rest-id" + "Message": "Invalid Method identifier specified" }, - "message": "Invalid API identifier specified 111111111111:test-fake-rest-id", + "message": "Invalid Method identifier specified", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 404 } }, - "wrong-rest-api-id-get-doc-parts": { + "delete-deleted-method-response": { "Error": { "Code": "NotFoundException", - "Message": "Invalid API identifier specified 111111111111:test-fake-rest-id" + "Message": "Invalid Method identifier specified" }, - "message": "Invalid API identifier specified 111111111111:test-fake-rest-id", + "message": "Invalid Method identifier specified", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 404 @@ -964,165 +1134,192 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_validators_crud_no_api": { - "recorded-date": "28-02-2023, 20:18:59", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_method_request_parameters": { + "recorded-date": "15-04-2024, 21:23:23", "recorded-content": { - "wrong-rest-api-id-create-validator": { - "Error": { - "Code": "BadRequestException", - "Message": "Invalid REST API identifier specified" + "put-method-request-params-response": { + "apiKeyRequired": false, + "authorizationType": "NONE", + "httpMethod": "ANY", + "requestParameters": { + "method.request.header.h_optional": false, + "method.request.header.h_required": true, + "method.request.querystring.q_optional": false, + "method.request.querystring.q_required": true }, - "message": "Invalid REST API identifier specified", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 400 + "HTTPStatusCode": 201 } }, - "wrong-rest-api-id-get-validators": { + "get-method-request-params-response": { + "apiKeyRequired": false, + "authorizationType": "NONE", + "httpMethod": "ANY", + "requestParameters": { + "method.request.header.h_optional": false, + "method.request.header.h_required": true, + "method.request.querystring.q_optional": false, + "method.request.querystring.q_required": true + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "req-params-same-name": { "Error": { - "Code": "NotFoundException", - "Message": "Invalid API identifier specified 111111111111:test-fake-rest-id" + "Code": "BadRequestException", + "Message": "Parameter names must be unique across querystring, header and path" }, - "message": "Invalid API identifier specified 111111111111:test-fake-rest-id", + "message": "Parameter names must be unique across querystring, header and path", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 404 + "HTTPStatusCode": 400 } } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_get_api_case_insensitive": { - "recorded-date": "01-03-2023, 16:26:45", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_put_method_model": { + "recorded-date": "15-04-2024, 21:23:48", "recorded-content": { - "create-rest-api": { - "apiKeySource": "HEADER", - "createdDate": "datetime", - "description": "lower case api", - "disableExecuteApiEndpoint": false, - "endpointConfiguration": { - "types": [ - "EDGE" - ] - }, + "create-model": { + "contentType": "application/json", "id": "", "name": "", + "schema": { + "title": "", + "type": "object" + }, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 201 } }, - "get-api-upper-case": { - "Error": { - "Code": "NotFoundException", - "Message": "Invalid API identifier specified 111111111111:" + "create-model-2": { + "contentType": "application/json", + "id": "", + "name": "Two", + "schema": { + "title": "Two", + "type": "object" }, - "message": "Invalid API identifier specified 111111111111:", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 404 + "HTTPStatusCode": 201 } - } - } - }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_method_lifecycle": { - "recorded-date": "15-03-2023, 12:06:26", - "recorded-content": { - "put-base-method-response": { + }, + "put-method-request-models": { "apiKeyRequired": false, "authorizationType": "NONE", "httpMethod": "ANY", + "requestModels": { + "application/json": "" + }, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 201 } }, - "get-base-method-response": { + "delete-model-used": { + "Error": { + "Code": "ConflictException", + "Message": "Cannot delete model '', is referenced in method request: //ANY" + }, + "message": "Cannot delete model '', is referenced in method request: //ANY", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 409 + } + }, + "update-method-model": { "apiKeyRequired": false, "authorizationType": "NONE", "httpMethod": "ANY", + "requestModels": { + "application/json": "Two" + }, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "del-base-method-response": { + "delete-model-unused": { "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 204 + "HTTPStatusCode": 202 } }, - "get-deleted-method-response": { + "delete-model-used-2": { "Error": { - "Code": "NotFoundException", - "Message": "Invalid Method identifier specified" + "Code": "ConflictException", + "Message": "Cannot delete model 'Two', is referenced in method request: //ANY" }, - "message": "Invalid Method identifier specified", + "message": "Cannot delete model 'Two', is referenced in method request: //ANY", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 404 + "HTTPStatusCode": 409 } }, - "delete-deleted-method-response": { - "Error": { - "Code": "NotFoundException", - "Message": "Invalid Method identifier specified" - }, - "message": "Invalid Method identifier specified", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 404 - } - } - } - }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_method_request_parameters": { - "recorded-date": "24-02-2023, 19:25:22", - "recorded-content": { - "put-method-request-params-response": { + "put-method-2-request-models": { "apiKeyRequired": false, "authorizationType": "NONE", "httpMethod": "ANY", - "requestParameters": { - "method.request.header.h_optional": false, - "method.request.header.h_required": true, - "method.request.querystring.q_optional": false, - "method.request.querystring.q_required": true + "requestModels": { + "application/json": "Two" }, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 201 } }, - "get-method-request-params-response": { + "delete-model-used-by-2-method": { + "Error": { + "Code": "ConflictException", + "Message": "Cannot delete model 'Two', is referenced in method request: /test/ANY" + }, + "message": "Cannot delete model 'Two', is referenced in method request: /test/ANY", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 409 + } + }, + "update-method-model-2": { "apiKeyRequired": false, "authorizationType": "NONE", "httpMethod": "ANY", - "requestParameters": { - "method.request.header.h_optional": false, - "method.request.header.h_required": true, - "method.request.querystring.q_optional": false, - "method.request.querystring.q_required": true - }, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "req-params-same-name": { + "delete-model-used-by-method-1": { "Error": { - "Code": "BadRequestException", - "Message": "Parameter names must be unique across querystring, header and path" + "Code": "ConflictException", + "Message": "Cannot delete model 'Two', is referenced in method request: //ANY" }, - "message": "Parameter names must be unique across querystring, header and path", + "message": "Cannot delete model 'Two', is referenced in method request: //ANY", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 400 + "HTTPStatusCode": 409 + } + }, + "delete-method-using-model-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 204 + } + }, + "delete-model-unused-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 202 } } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_put_method_validation": { - "recorded-date": "25-02-2023, 17:49:08", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_put_method_validation": { + "recorded-date": "15-04-2024, 21:24:40", "recorded-content": { "wrong-api": { "Error": { @@ -1192,8 +1389,8 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_method": { - "recorded-date": "13-03-2023, 23:31:44", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_update_method": { + "recorded-date": "15-04-2024, 21:26:26", "recorded-content": { "put-method-response": { "apiKeyRequired": false, @@ -1265,8 +1462,8 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_method_validation": { - "recorded-date": "13-03-2023, 23:36:56", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_update_method_validation": { + "recorded-date": "15-04-2024, 21:26:43", "recorded-content": { "wrong-rest-api": { "Error": { @@ -1311,12 +1508,25 @@ } }, "unsupported-operation": { - "apiKeyRequired": true, - "authorizationType": "NONE", - "httpMethod": "ANY", + "Error": { + "Code": "BadRequestException", + "Message": "Invalid patch operation specified. Must be one of: [replace]" + }, + "message": "Invalid patch operation specified. Must be one of: [replace]", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 200 + "HTTPStatusCode": 400 + } + }, + "unsupported-operation-2": { + "Error": { + "Code": "BadRequestException", + "Message": "Invalid patch operation specified. Must be one of: [replace]" + }, + "message": "Invalid patch operation specified. Must be one of: [replace]", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 } }, "unsupported-path": { @@ -1405,8 +1615,8 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_model_lifecycle": { - "recorded-date": "26-05-2023, 04:30:31", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiModels::test_model_lifecycle": { + "recorded-date": "15-04-2024, 21:02:34", "recorded-content": { "create-model": { "contentType": "application/json", @@ -1549,8 +1759,8 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_model_validation": { - "recorded-date": "09-03-2023, 19:13:20", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiModels::test_model_validation": { + "recorded-date": "15-04-2024, 20:33:53", "recorded-content": { "create-model-wrong-id": { "Error": { @@ -1653,8 +1863,8 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_model": { - "recorded-date": "09-03-2023, 19:39:34", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiModels::test_update_model": { + "recorded-date": "15-04-2024, 20:34:09", "recorded-content": { "update-model-wrong-id": { "Error": { @@ -1728,146 +1938,35 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_put_method_model": { - "recorded-date": "15-03-2023, 12:12:59", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_validators_crud_no_api": { + "recorded-date": "15-04-2024, 20:44:19", "recorded-content": { - "create-model": { - "contentType": "application/json", - "id": "", - "name": "", - "schema": { - "title": "", - "type": "object" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "create-model-2": { - "contentType": "application/json", - "id": "", - "name": "Two", - "schema": { - "title": "Two", - "type": "object" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "put-method-request-models": { - "apiKeyRequired": false, - "authorizationType": "NONE", - "httpMethod": "ANY", - "requestModels": { - "application/json": "" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "delete-model-used": { - "Error": { - "Code": "ConflictException", - "Message": "Cannot delete model '', is referenced in method request: //ANY" - }, - "message": "Cannot delete model '', is referenced in method request: //ANY", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 409 - } - }, - "update-method-model": { - "apiKeyRequired": false, - "authorizationType": "NONE", - "httpMethod": "ANY", - "requestModels": { - "application/json": "Two" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "delete-model-unused": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 202 - } - }, - "delete-model-used-2": { - "Error": { - "Code": "ConflictException", - "Message": "Cannot delete model 'Two', is referenced in method request: //ANY" - }, - "message": "Cannot delete model 'Two', is referenced in method request: //ANY", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 409 - } - }, - "put-method-2-request-models": { - "apiKeyRequired": false, - "authorizationType": "NONE", - "httpMethod": "ANY", - "requestModels": { - "application/json": "Two" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "delete-model-used-by-2-method": { + "wrong-rest-api-id-create-validator": { "Error": { - "Code": "ConflictException", - "Message": "Cannot delete model 'Two', is referenced in method request: /test/ANY" + "Code": "BadRequestException", + "Message": "Invalid REST API identifier specified" }, - "message": "Cannot delete model 'Two', is referenced in method request: /test/ANY", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 409 - } - }, - "update-method-model-2": { - "apiKeyRequired": false, - "authorizationType": "NONE", - "httpMethod": "ANY", + "message": "Invalid REST API identifier specified", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 200 + "HTTPStatusCode": 400 } }, - "delete-model-used-by-method-1": { + "wrong-rest-api-id-get-validators": { "Error": { - "Code": "ConflictException", - "Message": "Cannot delete model 'Two', is referenced in method request: //ANY" + "Code": "NotFoundException", + "Message": "Invalid API identifier specified 111111111111:test-fake-rest-id" }, - "message": "Cannot delete model 'Two', is referenced in method request: //ANY", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 409 - } - }, - "delete-method-using-model-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 204 - } - }, - "delete-model-unused-2": { + "message": "Invalid API identifier specified 111111111111:test-fake-rest-id", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 202 + "HTTPStatusCode": 404 } } } }, "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_request_validator_lifecycle": { - "recorded-date": "22-03-2023, 13:07:42", + "recorded-date": "15-04-2024, 20:44:21", "recorded-content": { "create-rest-api": { "apiKeySource": "HEADER", @@ -1881,6 +1980,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 201 @@ -1977,7 +2077,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_invalid_get_request_validator": { - "recorded-date": "22-03-2023, 13:08:19", + "recorded-date": "15-04-2024, 20:44:55", "recorded-content": { "get-request-validators-invalid-api-id": { "Error": { @@ -2004,7 +2104,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_invalid_get_request_validators": { - "recorded-date": "22-03-2023, 13:08:19", + "recorded-date": "15-04-2024, 20:44:55", "recorded-content": { "get-invalid-request-validators": { "Error": { @@ -2020,7 +2120,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_invalid_delete_request_validator": { - "recorded-date": "22-03-2023, 13:08:44", + "recorded-date": "15-04-2024, 20:45:24", "recorded-content": { "delete-request-validator-invalid-api-id": { "Error": { @@ -2047,7 +2147,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_create_request_validator_invalid_api_id": { - "recorded-date": "22-03-2023, 13:08:44", + "recorded-date": "15-04-2024, 20:45:24", "recorded-content": { "invalid-create-request-validator": { "Error": { @@ -2063,7 +2163,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_invalid_update_request_validator_operations": { - "recorded-date": "22-03-2023, 13:08:58", + "recorded-date": "15-04-2024, 20:46:01", "recorded-content": { "create-rest-api": { "apiKeySource": "HEADER", @@ -2077,6 +2177,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 201 @@ -2138,513 +2239,47 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_documentation_part_lifecycle": { - "recorded-date": "23-03-2023, 17:50:40", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayGatewayResponse::test_gateway_response_crud": { + "recorded-date": "15-04-2024, 20:46:20", "recorded-content": { - "create-documentation-part": { - "id": "", - "location": { - "type": "API" - }, - "properties": { - "description": "Sample API description" + "get-gateway-response-default": { + "defaultResponse": true, + "responseParameters": {}, + "responseTemplates": { + "application/json": "{\"message\":$context.error.messageString}" }, + "responseType": "MISSING_AUTHENTICATION_TOKEN", + "statusCode": "403", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 201 + "HTTPStatusCode": 200 } }, - "get-documentation-part": { - "id": "", - "location": { - "type": "API" + "put-gateway-response": { + "defaultResponse": false, + "responseParameters": { + "gatewayresponse.header.Access-Control-Allow-Origin": "'a.b.c'", + "gatewayresponse.header.x-request-header": "method.request.header.Accept", + "gatewayresponse.header.x-request-path": "method.request.path.petId", + "gatewayresponse.header.x-request-query": "method.request.querystring.q" }, - "properties": { - "description": "Sample API description" + "responseTemplates": { + "application/json": "{\n \"message\": $context.error.messageString,\n \"type\": \"$context.error.responseType\",\n \"stage\": \"$context.stage\",\n \"resourcePath\": \"$context.resourcePath\",\n \"stageVariables.a\": \"$stageVariables.a\",\n \"statusCode\": \"'404'\"\n}" }, + "responseType": "MISSING_AUTHENTICATION_TOKEN", + "statusCode": "404", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 200 + "HTTPStatusCode": 201 } }, - "get-documentation-parts": { + "get-gateway-responses": { "items": [ { - "id": "", - "location": { - "type": "API" - }, - "properties": { - "description": "Sample API description" - } - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "update-documentation-part": { - "id": "", - "location": { - "type": "API" - }, - "properties": { - "description": "Updated Sample API description" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "get-documentation-part-after-update": { - "id": "", - "location": { - "type": "API" - }, - "properties": { - "description": "Updated Sample API description" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "delete_documentation_part": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 202 - } - } - } - }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_get_documentation_part": { - "recorded-date": "23-03-2023, 17:51:12", - "recorded-content": { - "get-documentation-part-invalid-api-id": { - "Error": { - "Code": "NotFoundException", - "Message": "Invalid Documentation part identifier specified" - }, - "message": "Invalid Documentation part identifier specified", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 404 - } - }, - "get-documentation-part-invalid-doc-id": { - "Error": { - "Code": "NotFoundException", - "Message": "Invalid Documentation part identifier specified" - }, - "message": "Invalid Documentation part identifier specified", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 404 - } - } - } - }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_get_documentation_parts": { - "recorded-date": "23-03-2023, 17:51:12", - "recorded-content": { - "get-inavlid-documentation-parts": { - "Error": { - "Code": "NotFoundException", - "Message": "Invalid API identifier specified 111111111111:api_id" - }, - "message": "Invalid API identifier specified 111111111111:api_id", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 404 - } - } - } - }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_update_documentation_part": { - "recorded-date": "23-03-2023, 17:51:42", - "recorded-content": { - "update-documentation-part-invalid-api-id": { - "Error": { - "Code": "NotFoundException", - "Message": "Invalid Documentation part identifier specified" - }, - "message": "Invalid Documentation part identifier specified", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 404 - } - }, - "update-documentation-part-invalid-add-operation": { - "Error": { - "Code": "BadRequestException", - "Message": "Invalid patch path '/properties' specified for op 'add'. Please choose supported operations" - }, - "message": "Invalid patch path '/properties' specified for op 'add'. Please choose supported operations", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - }, - "update-documentation-part-invalid-path": { - "Error": { - "Code": "BadRequestException", - "Message": "Invalid patch path '/invalidPath' specified for op 'replace'. Must be one of: [/properties]" - }, - "message": "Invalid patch path '/invalidPath' specified for op 'replace'. Must be one of: [/properties]", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } - }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_create_documentation_part_operations": { - "recorded-date": "23-03-2023, 17:52:11", - "recorded-content": { - "create_documentation_part_invalid_api_id": { - "Error": { - "Code": "NotFoundException", - "Message": "Invalid API identifier specified 111111111111:api_id" - }, - "message": "Invalid API identifier specified 111111111111:api_id", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 404 - } - }, - "create_documentation_part_invalid_location_type": { - "Error": { - "Code": "ValidationException", - "Message": "1 validation error detected: Value 'INVALID' at 'createDocumentationPartInput.location.type' failed to satisfy constraint: Member must satisfy enum value set: [RESPONSE_BODY, RESPONSE, METHOD, MODEL, AUTHORIZER, RESPONSE_HEADER, RESOURCE, PATH_PARAMETER, REQUEST_BODY, QUERY_PARAMETER, API, REQUEST_HEADER]" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } - }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_delete_documentation_part": { - "recorded-date": "23-03-2023, 17:52:31", - "recorded-content": { - "delete_documentation_part_wrong_api_id": { - "Error": { - "Code": "NotFoundException", - "Message": "Invalid API identifier specified 111111111111:api_id" - }, - "message": "Invalid API identifier specified 111111111111:api_id", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 404 - } - }, - "delete_documentation_part": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 202 - } - }, - "delete_already_deleted_documentation_part": { - "Error": { - "Code": "NotFoundException", - "Message": "Invalid Documentation part identifier specified" - }, - "message": "Invalid Documentation part identifier specified", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 404 - } - } - } - }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_rest_api_compression": { - "recorded-date": "04-04-2023, 17:35:05", - "recorded-content": { - "enable-compression": { - "apiKeySource": "HEADER", - "createdDate": "datetime", - "description": "this is my api", - "disableExecuteApiEndpoint": false, - "endpointConfiguration": { - "types": [ - "EDGE" - ] - }, - "id": "", - "minimumCompressionSize": 10, - "name": "", - "tags": {}, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "disable-compression": { - "apiKeySource": "HEADER", - "createdDate": "datetime", - "description": "this is my api", - "disableExecuteApiEndpoint": false, - "endpointConfiguration": { - "types": [ - "EDGE" - ] - }, - "id": "", - "name": "", - "tags": {}, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "set-compression-zero": { - "apiKeySource": "HEADER", - "createdDate": "datetime", - "description": "this is my api", - "disableExecuteApiEndpoint": false, - "endpointConfiguration": { - "types": [ - "EDGE" - ] - }, - "id": "", - "minimumCompressionSize": 0, - "name": "", - "tags": {}, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "set-negative-compression": { - "Error": { - "Code": "BadRequestException", - "Message": "Invalid minimum compression size, must be between 0 and 10485760" - }, - "message": "Invalid minimum compression size, must be between 0 and 10485760", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - }, - "set-string-compression": { - "Error": { - "Code": "BadRequestException", - "Message": "Invalid minimum compression size, must be between 0 and 10485760" - }, - "message": "Invalid minimum compression size, must be between 0 and 10485760", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - }, - "unsupported-operation": { - "Error": { - "Code": "BadRequestException", - "Message": "Invalid patch operation specified. Must be 'add'|'remove'|'replace'" - }, - "message": "Invalid patch operation specified. Must be 'add'|'remove'|'replace'", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } - }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_invoke_test_method": { - "recorded-date": "01-07-2023, 18:52:50", - "recorded-content": { - "test-invoke-method-get": { - "body": { - "petId": "123" - }, - "headers": { - "Content-Type": "application/json" - }, - "latency": "latency", - "log": "log", - "multiValueHeaders": { - "Content-Type": [ - "application/json" - ] - }, - "status": 200, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "test-invoke-method-get-with-qs": { - "body": { - "petId": "123" - }, - "headers": { - "Content-Type": "application/json" - }, - "latency": "latency", - "log": "log", - "multiValueHeaders": { - "Content-Type": [ - "application/json" - ] - }, - "status": 200, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "test-invoke-method-post-with-body": { - "body": { - "petId": "123" - }, - "headers": { - "Content-Type": "application/json" - }, - "latency": "latency", - "log": "log", - "multiValueHeaders": { - "Content-Type": [ - "application/json" - ] - }, - "status": 200, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "resource-id-not-found": { - "Error": { - "Code": "NotFoundException", - "Message": "Invalid Resource identifier specified" - }, - "message": "Invalid Resource identifier specified", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 404 - } - }, - "rest-api-not-found": { - "Error": { - "Code": "NotFoundException", - "Message": "Invalid Resource identifier specified" - }, - "message": "Invalid Resource identifier specified", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 404 - } - } - } - }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_import_documentation_parts": { - "recorded-date": "26-06-2023, 12:01:38", - "recorded-content": { - "create-import-documentations_parts": [ - { - "id": "", - "location": { - "type": "API" - }, - "properties": { - "description": "API description", - "info": { - "description": "API info description 4", - "version": "API info version 3" - } - } - }, - { - "id": "", - "location": { - "type": "METHOD", - "path": "/", - "method": "GET" - }, - "properties": { - "description": "Method description." - } - }, - { - "id": "", - "location": { - "type": "MODEL", - "name": "" - }, - "properties": { - "title": " Schema" - } - }, - { - "id": "", - "location": { - "type": "RESPONSE", - "path": "/", - "method": "GET", - "statusCode": "200" - }, - "properties": { - "description": "200 response" - } - } - ], - "import-documentation-parts": { - "ids": [ - "", - "", - "", - "" - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayGatewayResponse::test_gateway_response_crud": { - "recorded-date": "04-09-2023, 20:13:20", - "recorded-content": { - "get-gateway-response-default": { - "defaultResponse": true, - "responseParameters": {}, - "responseTemplates": { - "application/json": "{\"message\":$context.error.messageString}" - }, - "responseType": "MISSING_AUTHENTICATION_TOKEN", - "statusCode": "403", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "put-gateway-response": { - "defaultResponse": false, - "responseParameters": { - "gatewayresponse.header.Access-Control-Allow-Origin": "'a.b.c'", - "gatewayresponse.header.x-request-header": "method.request.header.Accept", - "gatewayresponse.header.x-request-path": "method.request.path.petId", - "gatewayresponse.header.x-request-query": "method.request.querystring.q" - }, - "responseTemplates": { - "application/json": "{\n \"message\": $context.error.messageString,\n \"type\": \"$context.error.responseType\",\n \"stage\": \"$context.stage\",\n \"resourcePath\": \"$context.resourcePath\",\n \"stageVariables.a\": \"$stageVariables.a\",\n \"statusCode\": \"'404'\"\n}" - }, - "responseType": "MISSING_AUTHENTICATION_TOKEN", - "statusCode": "404", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 201 - } - }, - "get-gateway-responses": { - "items": [ - { - "defaultResponse": true, - "responseParameters": {}, - "responseTemplates": { - "application/json": "{\"message\":$context.error.messageString}" + "defaultResponse": true, + "responseParameters": {}, + "responseTemplates": { + "application/json": "{\"message\":$context.error.messageString}" }, "responseType": "ACCESS_DENIED", "statusCode": "403" @@ -2823,16 +2458,225 @@ "responseType": "UNSUPPORTED_MEDIA_TYPE", "statusCode": "415" }, - { - "defaultResponse": true, - "responseParameters": {}, - "responseTemplates": { - "application/json": "{\"message\":$context.error.messageString}" - }, - "responseType": "WAF_FILTERED", - "statusCode": "403" - } - ], + { + "defaultResponse": true, + "responseParameters": {}, + "responseTemplates": { + "application/json": "{\"message\":$context.error.messageString}" + }, + "responseType": "WAF_FILTERED", + "statusCode": "403" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "get-gateway-response": { + "defaultResponse": false, + "responseParameters": { + "gatewayresponse.header.Access-Control-Allow-Origin": "'a.b.c'", + "gatewayresponse.header.x-request-header": "method.request.header.Accept", + "gatewayresponse.header.x-request-path": "method.request.path.petId", + "gatewayresponse.header.x-request-query": "method.request.querystring.q" + }, + "responseTemplates": { + "application/json": "{\n \"message\": $context.error.messageString,\n \"type\": \"$context.error.responseType\",\n \"stage\": \"$context.stage\",\n \"resourcePath\": \"$context.resourcePath\",\n \"stageVariables.a\": \"$stageVariables.a\",\n \"statusCode\": \"'404'\"\n}" + }, + "responseType": "MISSING_AUTHENTICATION_TOKEN", + "statusCode": "404", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "delete-gateway-response": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 202 + } + }, + "get-deleted-gw-response": { + "defaultResponse": true, + "responseParameters": {}, + "responseTemplates": { + "application/json": "{\"message\":$context.error.messageString}" + }, + "responseType": "MISSING_AUTHENTICATION_TOKEN", + "statusCode": "403", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayGatewayResponse::test_gateway_response_validation": { + "recorded-date": "15-04-2024, 20:47:08", + "recorded-content": { + "get-gateway-responses-no-api": { + "Error": { + "Code": "NotFoundException", + "Message": "Invalid API identifier specified 111111111111:fake-api-id" + }, + "message": "Invalid API identifier specified 111111111111:fake-api-id", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 404 + } + }, + "get-gateway-response-no-api": { + "Error": { + "Code": "NotFoundException", + "Message": "Invalid API identifier specified 111111111111:fake-api-id" + }, + "message": "Invalid API identifier specified 111111111111:fake-api-id", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 404 + } + }, + "delete-gateway-response-no-api": { + "Error": { + "Code": "NotFoundException", + "Message": "Invalid API identifier specified 111111111111:fake-api-id" + }, + "message": "Invalid API identifier specified 111111111111:fake-api-id", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 404 + } + }, + "update-gateway-response-no-api": { + "Error": { + "Code": "NotFoundException", + "Message": "Invalid API identifier specified 111111111111:fake-api-id" + }, + "message": "Invalid API identifier specified 111111111111:fake-api-id", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 404 + } + }, + "delete-gateway-response-not-set": { + "Error": { + "Code": "NotFoundException", + "Message": "Gateway response type not defined on api" + }, + "message": "Gateway response type not defined on api", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 404 + } + }, + "get-gateway-response-wrong-response-type": { + "Error": { + "Code": "ValidationException", + "Message": "1 validation error detected: Value 'FAKE_RESPONSE_TYPE' at 'responseType' failed to satisfy constraint: Member must satisfy enum value set: [REQUEST_TOO_LARGE, RESOURCE_NOT_FOUND, AUTHORIZER_CONFIGURATION_ERROR, MISSING_AUTHENTICATION_TOKEN, BAD_REQUEST_BODY, INVALID_SIGNATURE, INVALID_API_KEY, BAD_REQUEST_PARAMETERS, AUTHORIZER_FAILURE, UNAUTHORIZED, INTEGRATION_TIMEOUT, ACCESS_DENIED, DEFAULT_4XX, DEFAULT_5XX, WAF_FILTERED, QUOTA_EXCEEDED, THROTTLED, API_CONFIGURATION_ERROR, UNSUPPORTED_MEDIA_TYPE, INTEGRATION_FAILURE, EXPIRED_TOKEN]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "delete-gateway-response-wrong-response-type": { + "Error": { + "Code": "ValidationException", + "Message": "1 validation error detected: Value 'FAKE_RESPONSE_TYPE' at 'responseType' failed to satisfy constraint: Member must satisfy enum value set: [REQUEST_TOO_LARGE, RESOURCE_NOT_FOUND, AUTHORIZER_CONFIGURATION_ERROR, MISSING_AUTHENTICATION_TOKEN, BAD_REQUEST_BODY, INVALID_SIGNATURE, INVALID_API_KEY, BAD_REQUEST_PARAMETERS, AUTHORIZER_FAILURE, UNAUTHORIZED, INTEGRATION_TIMEOUT, ACCESS_DENIED, DEFAULT_4XX, DEFAULT_5XX, WAF_FILTERED, QUOTA_EXCEEDED, THROTTLED, API_CONFIGURATION_ERROR, UNSUPPORTED_MEDIA_TYPE, INTEGRATION_FAILURE, EXPIRED_TOKEN]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "update-gateway-response-wrong-response-type": { + "Error": { + "Code": "ValidationException", + "Message": "1 validation error detected: Value 'FAKE_RESPONSE_TYPE' at 'responseType' failed to satisfy constraint: Member must satisfy enum value set: [REQUEST_TOO_LARGE, RESOURCE_NOT_FOUND, AUTHORIZER_CONFIGURATION_ERROR, MISSING_AUTHENTICATION_TOKEN, BAD_REQUEST_BODY, INVALID_SIGNATURE, INVALID_API_KEY, BAD_REQUEST_PARAMETERS, AUTHORIZER_FAILURE, UNAUTHORIZED, INTEGRATION_TIMEOUT, ACCESS_DENIED, DEFAULT_4XX, DEFAULT_5XX, WAF_FILTERED, QUOTA_EXCEEDED, THROTTLED, API_CONFIGURATION_ERROR, UNSUPPORTED_MEDIA_TYPE, INTEGRATION_FAILURE, EXPIRED_TOKEN]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "put-gateway-response-wrong-response-type": { + "Error": { + "Code": "ValidationException", + "Message": "1 validation error detected: Value 'FAKE_RESPONSE_TYPE' at 'responseType' failed to satisfy constraint: Member must satisfy enum value set: [REQUEST_TOO_LARGE, RESOURCE_NOT_FOUND, AUTHORIZER_CONFIGURATION_ERROR, MISSING_AUTHENTICATION_TOKEN, BAD_REQUEST_BODY, INVALID_SIGNATURE, INVALID_API_KEY, BAD_REQUEST_PARAMETERS, AUTHORIZER_FAILURE, UNAUTHORIZED, INTEGRATION_TIMEOUT, ACCESS_DENIED, DEFAULT_4XX, DEFAULT_5XX, WAF_FILTERED, QUOTA_EXCEEDED, THROTTLED, API_CONFIGURATION_ERROR, UNSUPPORTED_MEDIA_TYPE, INTEGRATION_FAILURE, EXPIRED_TOKEN]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayGatewayResponse::test_update_gateway_response": { + "recorded-date": "15-04-2024, 20:47:51", + "recorded-content": { + "update-gateway-response-not-set": { + "defaultResponse": false, + "responseParameters": {}, + "responseTemplates": { + "application/json": "{\"message\":$context.error.messageString}" + }, + "responseType": "DEFAULT_4XX", + "statusCode": "444", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "default-get-gateway-response": { + "defaultResponse": false, + "responseParameters": {}, + "responseTemplates": { + "application/json": "{\"message\":$context.error.messageString}" + }, + "responseType": "DEFAULT_4XX", + "statusCode": "444", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "put-gateway-response": { + "defaultResponse": false, + "responseParameters": { + "gatewayresponse.header.Access-Control-Allow-Origin": "'a.b.c'", + "gatewayresponse.header.x-request-header": "method.request.header.Accept", + "gatewayresponse.header.x-request-path": "method.request.path.petId", + "gatewayresponse.header.x-request-query": "method.request.querystring.q" + }, + "responseTemplates": { + "application/json": { + "application/json": "{\"message\":$context.error.messageString}" + } + }, + "responseType": "DEFAULT_4XX", + "statusCode": "404", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + }, + "update-gateway-response": { + "defaultResponse": false, + "responseParameters": { + "gatewayresponse.header.Access-Control-Allow-Origin": "'example.com'", + "gatewayresponse.header.x-request-header": "method.request.header.Accept", + "gatewayresponse.header.x-request-path": "method.request.path.petId", + "gatewayresponse.header.x-request-query": "method.request.querystring.q" + }, + "responseTemplates": { + "application/json": { + "application/json": "{\"message\":$context.error.messageString}" + }, + "application/xml": "$context.error.messageString$context.error.responseType" + }, + "responseType": "DEFAULT_4XX", + "statusCode": "444", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -2841,315 +2685,422 @@ "get-gateway-response": { "defaultResponse": false, "responseParameters": { - "gatewayresponse.header.Access-Control-Allow-Origin": "'a.b.c'", + "gatewayresponse.header.Access-Control-Allow-Origin": "'example.com'", "gatewayresponse.header.x-request-header": "method.request.header.Accept", "gatewayresponse.header.x-request-path": "method.request.path.petId", "gatewayresponse.header.x-request-query": "method.request.querystring.q" }, "responseTemplates": { - "application/json": "{\n \"message\": $context.error.messageString,\n \"type\": \"$context.error.responseType\",\n \"stage\": \"$context.stage\",\n \"resourcePath\": \"$context.resourcePath\",\n \"stageVariables.a\": \"$stageVariables.a\",\n \"statusCode\": \"'404'\"\n}" + "application/json": { + "application/json": "{\"message\":$context.error.messageString}" + }, + "application/xml": "$context.error.messageString$context.error.responseType" }, - "responseType": "MISSING_AUTHENTICATION_TOKEN", - "statusCode": "404", + "responseType": "DEFAULT_4XX", + "statusCode": "444", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "delete-gateway-response": { + "update-gateway-add-status-code": { + "Error": { + "Code": "BadRequestException", + "Message": "Invalid patch path /statusCode" + }, + "message": "Invalid patch path /statusCode", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 202 + "HTTPStatusCode": 400 } }, - "get-deleted-gw-response": { - "defaultResponse": true, - "responseParameters": {}, - "responseTemplates": { - "application/json": "{\"message\":$context.error.messageString}" + "update-gateway-remove-status-code": { + "Error": { + "Code": "BadRequestException", + "Message": "Invalid patch path /statusCode" }, - "responseType": "MISSING_AUTHENTICATION_TOKEN", - "statusCode": "403", + "message": "Invalid patch path /statusCode", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 200 + "HTTPStatusCode": 400 } - } - } - }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayGatewayResponse::test_gateway_response_validation": { - "recorded-date": "04-09-2023, 20:32:34", - "recorded-content": { - "get-gateway-responses-no-api": { + }, + "update-gateway-replace-invalid-parameter": { "Error": { "Code": "NotFoundException", - "Message": "Invalid API identifier specified 111111111111:fake-api-id" + "Message": "Invalid parameter name specified" }, - "message": "Invalid API identifier specified 111111111111:fake-api-id", + "message": "Invalid parameter name specified", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 404 } }, - "get-gateway-response-no-api": { + "update-gateway-no-path": { "Error": { - "Code": "NotFoundException", - "Message": "Invalid API identifier specified 111111111111:fake-api-id" + "Code": "BadRequestException", + "Message": "Invalid patch path null" }, - "message": "Invalid API identifier specified 111111111111:fake-api-id", + "message": "Invalid patch path null", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 404 + "HTTPStatusCode": 400 } }, - "delete-gateway-response-no-api": { + "update-gateway-wrong-op": { "Error": { - "Code": "NotFoundException", - "Message": "Invalid API identifier specified 111111111111:fake-api-id" + "Code": "ValidationException", + "Message": "1 validation error detected: Value 'wrong-op' at 'updateGatewayResponseInput.patchOperations.1.member.op' failed to satisfy constraint: Member must satisfy enum value set: [add, remove, move, test, replace, copy]" }, - "message": "Invalid API identifier specified 111111111111:fake-api-id", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 404 + "HTTPStatusCode": 400 } }, - "update-gateway-response-no-api": { + "update-gateway-wrong-path": { "Error": { - "Code": "NotFoundException", - "Message": "Invalid API identifier specified 111111111111:fake-api-id" + "Code": "BadRequestException", + "Message": "Invalid patch path /wrongPath" }, - "message": "Invalid API identifier specified 111111111111:fake-api-id", + "message": "Invalid patch path /wrongPath", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 404 + "HTTPStatusCode": 400 } }, - "delete-gateway-response-not-set": { + "update-gateway-replace-invalid-parameter-0-none": { "Error": { - "Code": "NotFoundException", - "Message": "Gateway response type not defined on api" + "Code": "BadRequestException", + "Message": "Invalid null or empty value in responseTemplates" }, - "message": "Gateway response type not defined on api", + "message": "Invalid null or empty value in responseTemplates", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 404 + "HTTPStatusCode": 400 } }, - "get-gateway-response-wrong-response-type": { + "update-gateway-replace-invalid-parameter-1-none": { "Error": { - "Code": "ValidationException", - "Message": "1 validation error detected: Value 'FAKE_RESPONSE_TYPE' at 'responseType' failed to satisfy constraint: Member must satisfy enum value set: [REQUEST_TOO_LARGE, RESOURCE_NOT_FOUND, AUTHORIZER_CONFIGURATION_ERROR, MISSING_AUTHENTICATION_TOKEN, BAD_REQUEST_BODY, INVALID_SIGNATURE, INVALID_API_KEY, BAD_REQUEST_PARAMETERS, AUTHORIZER_FAILURE, UNAUTHORIZED, INTEGRATION_TIMEOUT, ACCESS_DENIED, DEFAULT_4XX, DEFAULT_5XX, WAF_FILTERED, QUOTA_EXCEEDED, THROTTLED, API_CONFIGURATION_ERROR, UNSUPPORTED_MEDIA_TYPE, INTEGRATION_FAILURE, EXPIRED_TOKEN]" + "Code": "BadRequestException", + "Message": "Invalid null or empty value in responseParameters" }, + "message": "Invalid null or empty value in responseParameters", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 } + } + } + }, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApigatewayTestInvoke::test_invoke_test_method": { + "recorded-date": "15-04-2024, 20:48:35", + "recorded-content": { + "test-invoke-method-get": { + "body": { + "petId": "123" + }, + "headers": { + "Content-Type": "application/json" + }, + "latency": "latency", + "log": "log", + "multiValueHeaders": { + "Content-Type": [ + "application/json" + ] + }, + "status": 200, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } }, - "delete-gateway-response-wrong-response-type": { + "test-invoke-method-get-with-qs": { + "body": { + "petId": "123" + }, + "headers": { + "Content-Type": "application/json" + }, + "latency": "latency", + "log": "log", + "multiValueHeaders": { + "Content-Type": [ + "application/json" + ] + }, + "status": 200, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "test-invoke-method-post-with-body": { + "body": { + "petId": "123" + }, + "headers": { + "Content-Type": "application/json" + }, + "latency": "latency", + "log": "log", + "multiValueHeaders": { + "Content-Type": [ + "application/json" + ] + }, + "status": 200, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "resource-id-not-found": { "Error": { - "Code": "ValidationException", - "Message": "1 validation error detected: Value 'FAKE_RESPONSE_TYPE' at 'responseType' failed to satisfy constraint: Member must satisfy enum value set: [REQUEST_TOO_LARGE, RESOURCE_NOT_FOUND, AUTHORIZER_CONFIGURATION_ERROR, MISSING_AUTHENTICATION_TOKEN, BAD_REQUEST_BODY, INVALID_SIGNATURE, INVALID_API_KEY, BAD_REQUEST_PARAMETERS, AUTHORIZER_FAILURE, UNAUTHORIZED, INTEGRATION_TIMEOUT, ACCESS_DENIED, DEFAULT_4XX, DEFAULT_5XX, WAF_FILTERED, QUOTA_EXCEEDED, THROTTLED, API_CONFIGURATION_ERROR, UNSUPPORTED_MEDIA_TYPE, INTEGRATION_FAILURE, EXPIRED_TOKEN]" + "Code": "NotFoundException", + "Message": "Invalid Resource identifier specified" }, + "message": "Invalid Resource identifier specified", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 400 + "HTTPStatusCode": 404 } }, - "update-gateway-response-wrong-response-type": { + "rest-api-not-found": { + "Error": { + "Code": "NotFoundException", + "Message": "Invalid Resource identifier specified" + }, + "message": "Invalid Resource identifier specified", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 404 + } + } + } + }, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApigatewayIntegration::test_put_integration_wrong_type": { + "recorded-date": "15-04-2024, 20:49:25", + "recorded-content": { + "put-integration-wrong-type": { "Error": { "Code": "ValidationException", - "Message": "1 validation error detected: Value 'FAKE_RESPONSE_TYPE' at 'responseType' failed to satisfy constraint: Member must satisfy enum value set: [REQUEST_TOO_LARGE, RESOURCE_NOT_FOUND, AUTHORIZER_CONFIGURATION_ERROR, MISSING_AUTHENTICATION_TOKEN, BAD_REQUEST_BODY, INVALID_SIGNATURE, INVALID_API_KEY, BAD_REQUEST_PARAMETERS, AUTHORIZER_FAILURE, UNAUTHORIZED, INTEGRATION_TIMEOUT, ACCESS_DENIED, DEFAULT_4XX, DEFAULT_5XX, WAF_FILTERED, QUOTA_EXCEEDED, THROTTLED, API_CONFIGURATION_ERROR, UNSUPPORTED_MEDIA_TYPE, INTEGRATION_FAILURE, EXPIRED_TOKEN]" + "Message": "1 validation error detected: Value 'HTTPS_PROXY' at 'putIntegrationInput.type' failed to satisfy constraint: Member must satisfy enum value set: [HTTP, MOCK, AWS_PROXY, HTTP_PROXY, AWS]" }, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 } + } + } + }, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_doc_parts_crud_no_api": { + "recorded-date": "15-04-2024, 20:52:33", + "recorded-content": { + "wrong-rest-api-id-create-doc-part": { + "Error": { + "Code": "NotFoundException", + "Message": "Invalid API identifier specified 111111111111:test-fake-rest-id" + }, + "message": "Invalid API identifier specified 111111111111:test-fake-rest-id", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 404 + } }, - "put-gateway-response-wrong-response-type": { + "wrong-rest-api-id-get-doc-parts": { "Error": { - "Code": "ValidationException", - "Message": "1 validation error detected: Value 'FAKE_RESPONSE_TYPE' at 'responseType' failed to satisfy constraint: Member must satisfy enum value set: [REQUEST_TOO_LARGE, RESOURCE_NOT_FOUND, AUTHORIZER_CONFIGURATION_ERROR, MISSING_AUTHENTICATION_TOKEN, BAD_REQUEST_BODY, INVALID_SIGNATURE, INVALID_API_KEY, BAD_REQUEST_PARAMETERS, AUTHORIZER_FAILURE, UNAUTHORIZED, INTEGRATION_TIMEOUT, ACCESS_DENIED, DEFAULT_4XX, DEFAULT_5XX, WAF_FILTERED, QUOTA_EXCEEDED, THROTTLED, API_CONFIGURATION_ERROR, UNSUPPORTED_MEDIA_TYPE, INTEGRATION_FAILURE, EXPIRED_TOKEN]" + "Code": "NotFoundException", + "Message": "Invalid API identifier specified 111111111111:test-fake-rest-id" }, + "message": "Invalid API identifier specified 111111111111:test-fake-rest-id", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 400 + "HTTPStatusCode": 404 } } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayGatewayResponse::test_update_gateway_response": { - "recorded-date": "05-09-2023, 01:58:33", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_documentation_part_lifecycle": { + "recorded-date": "15-04-2024, 20:52:35", "recorded-content": { - "update-gateway-response-not-set": { - "defaultResponse": false, - "responseParameters": {}, - "responseTemplates": { - "application/json": "{\"message\":$context.error.messageString}" + "create-documentation-part": { + "id": "", + "location": { + "type": "API" + }, + "properties": { + "description": "Sample API description" }, - "responseType": "DEFAULT_4XX", - "statusCode": "444", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 200 + "HTTPStatusCode": 201 } }, - "default-get-gateway-response": { - "defaultResponse": false, - "responseParameters": {}, - "responseTemplates": { - "application/json": "{\"message\":$context.error.messageString}" + "get-documentation-part": { + "id": "", + "location": { + "type": "API" + }, + "properties": { + "description": "Sample API description" }, - "responseType": "DEFAULT_4XX", - "statusCode": "444", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "put-gateway-response": { - "defaultResponse": false, - "responseParameters": { - "gatewayresponse.header.Access-Control-Allow-Origin": "'a.b.c'", - "gatewayresponse.header.x-request-header": "method.request.header.Accept", - "gatewayresponse.header.x-request-path": "method.request.path.petId", - "gatewayresponse.header.x-request-query": "method.request.querystring.q" - }, - "responseTemplates": { - "application/json": { - "application/json": "{\"message\":$context.error.messageString}" + "get-documentation-parts": { + "items": [ + { + "id": "", + "location": { + "type": "API" + }, + "properties": { + "description": "Sample API description" + } } - }, - "responseType": "DEFAULT_4XX", - "statusCode": "404", + ], "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 201 + "HTTPStatusCode": 200 } }, - "update-gateway-response": { - "defaultResponse": false, - "responseParameters": { - "gatewayresponse.header.Access-Control-Allow-Origin": "'example.com'", - "gatewayresponse.header.x-request-header": "method.request.header.Accept", - "gatewayresponse.header.x-request-path": "method.request.path.petId", - "gatewayresponse.header.x-request-query": "method.request.querystring.q" + "update-documentation-part": { + "id": "", + "location": { + "type": "API" }, - "responseTemplates": { - "application/json": { - "application/json": "{\"message\":$context.error.messageString}" - }, - "application/xml": "$context.error.messageString$context.error.responseType" + "properties": { + "description": "Updated Sample API description" }, - "responseType": "DEFAULT_4XX", - "statusCode": "444", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "get-gateway-response": { - "defaultResponse": false, - "responseParameters": { - "gatewayresponse.header.Access-Control-Allow-Origin": "'example.com'", - "gatewayresponse.header.x-request-header": "method.request.header.Accept", - "gatewayresponse.header.x-request-path": "method.request.path.petId", - "gatewayresponse.header.x-request-query": "method.request.querystring.q" + "get-documentation-part-after-update": { + "id": "", + "location": { + "type": "API" }, - "responseTemplates": { - "application/json": { - "application/json": "{\"message\":$context.error.messageString}" - }, - "application/xml": "$context.error.messageString$context.error.responseType" + "properties": { + "description": "Updated Sample API description" }, - "responseType": "DEFAULT_4XX", - "statusCode": "444", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "update-gateway-add-status-code": { + "delete_documentation_part": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 202 + } + } + } + }, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_get_documentation_part": { + "recorded-date": "15-04-2024, 20:53:15", + "recorded-content": { + "get-documentation-part-invalid-api-id": { "Error": { - "Code": "BadRequestException", - "Message": "Invalid patch path /statusCode" + "Code": "NotFoundException", + "Message": "Invalid Documentation part identifier specified" }, - "message": "Invalid patch path /statusCode", + "message": "Invalid Documentation part identifier specified", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 400 + "HTTPStatusCode": 404 } }, - "update-gateway-remove-status-code": { + "get-documentation-part-invalid-doc-id": { "Error": { - "Code": "BadRequestException", - "Message": "Invalid patch path /statusCode" + "Code": "NotFoundException", + "Message": "Invalid Documentation part identifier specified" }, - "message": "Invalid patch path /statusCode", + "message": "Invalid Documentation part identifier specified", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 400 + "HTTPStatusCode": 404 } - }, - "update-gateway-replace-invalid-parameter": { + } + } + }, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_get_documentation_parts": { + "recorded-date": "15-04-2024, 20:53:15", + "recorded-content": { + "get-inavlid-documentation-parts": { "Error": { "Code": "NotFoundException", - "Message": "Invalid parameter name specified" + "Message": "Invalid API identifier specified 111111111111:api_id" }, - "message": "Invalid parameter name specified", + "message": "Invalid API identifier specified 111111111111:api_id", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 404 } - }, - "update-gateway-no-path": { + } + } + }, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_update_documentation_part": { + "recorded-date": "15-04-2024, 20:53:47", + "recorded-content": { + "update-documentation-part-invalid-api-id": { "Error": { - "Code": "BadRequestException", - "Message": "Invalid patch path null" + "Code": "NotFoundException", + "Message": "Invalid Documentation part identifier specified" }, - "message": "Invalid patch path null", + "message": "Invalid Documentation part identifier specified", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 400 + "HTTPStatusCode": 404 } }, - "update-gateway-wrong-op": { + "update-documentation-part-invalid-add-operation": { "Error": { - "Code": "ValidationException", - "Message": "1 validation error detected: Value 'wrong-op' at 'updateGatewayResponseInput.patchOperations.1.member.op' failed to satisfy constraint: Member must satisfy enum value set: [add, remove, move, test, replace, copy]" + "Code": "BadRequestException", + "Message": "Invalid patch path '/properties' specified for op 'add'. Please choose supported operations" }, + "message": "Invalid patch path '/properties' specified for op 'add'. Please choose supported operations", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 } }, - "update-gateway-wrong-path": { + "update-documentation-part-invalid-path": { "Error": { "Code": "BadRequestException", - "Message": "Invalid patch path /wrongPath" + "Message": "Invalid patch path '/invalidPath' specified for op 'replace'. Must be one of: [/properties]" }, - "message": "Invalid patch path /wrongPath", + "message": "Invalid patch path '/invalidPath' specified for op 'replace'. Must be one of: [/properties]", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 } - }, - "update-gateway-replace-invalid-parameter-0-none": { + } + } + }, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_create_documentation_part_operations": { + "recorded-date": "15-04-2024, 20:54:19", + "recorded-content": { + "create_documentation_part_invalid_api_id": { "Error": { - "Code": "BadRequestException", - "Message": "Invalid null or empty value in responseTemplates" + "Code": "NotFoundException", + "Message": "Invalid API identifier specified 111111111111:api_id" }, - "message": "Invalid null or empty value in responseTemplates", + "message": "Invalid API identifier specified 111111111111:api_id", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 400 + "HTTPStatusCode": 404 } }, - "update-gateway-replace-invalid-parameter-1-none": { + "create_documentation_part_invalid_location_type": { "Error": { - "Code": "BadRequestException", - "Message": "Invalid null or empty value in responseParameters" + "Code": "ValidationException", + "Message": "1 validation error detected: Value 'INVALID' at 'createDocumentationPartInput.location.type' failed to satisfy constraint: Member must satisfy enum value set: [RESPONSE_BODY, RESPONSE, METHOD, MODEL, AUTHORIZER, RESPONSE_HEADER, RESOURCE, PATH_PARAMETER, REQUEST_BODY, QUERY_PARAMETER, API, REQUEST_HEADER]" }, - "message": "Invalid null or empty value in responseParameters", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 @@ -3157,17 +3108,100 @@ } } }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApigatewayIntegration::test_put_integration_wrong_type": { - "recorded-date": "28-11-2023, 20:15:11", + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_delete_documentation_part": { + "recorded-date": "15-04-2024, 20:54:32", "recorded-content": { - "put-integration-wrong-type": { + "delete_documentation_part_wrong_api_id": { "Error": { - "Code": "ValidationException", - "Message": "1 validation error detected: Value 'HTTPS_PROXY' at 'putIntegrationInput.type' failed to satisfy constraint: Member must satisfy enum value set: [HTTP, MOCK, AWS_PROXY, HTTP_PROXY, AWS]" + "Code": "NotFoundException", + "Message": "Invalid API identifier specified 111111111111:api_id" }, + "message": "Invalid API identifier specified 111111111111:api_id", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 400 + "HTTPStatusCode": 404 + } + }, + "delete_documentation_part": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 202 + } + }, + "delete_already_deleted_documentation_part": { + "Error": { + "Code": "NotFoundException", + "Message": "Invalid Documentation part identifier specified" + }, + "message": "Invalid Documentation part identifier specified", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 404 + } + } + } + }, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_import_documentation_parts": { + "recorded-date": "15-04-2024, 20:57:15", + "recorded-content": { + "create-import-documentations_parts": [ + { + "id": "", + "location": { + "type": "API" + }, + "properties": { + "description": "API description", + "info": { + "description": "API info description 4", + "version": "API info version 3" + } + } + }, + { + "id": "", + "location": { + "type": "METHOD", + "path": "/", + "method": "GET" + }, + "properties": { + "description": "Method description." + } + }, + { + "id": "", + "location": { + "type": "MODEL", + "name": "" + }, + "properties": { + "title": " Schema" + } + }, + { + "id": "", + "location": { + "type": "RESPONSE", + "path": "/", + "method": "GET", + "statusCode": "200" + }, + "properties": { + "description": "200 response" + } + } + ], + "import-documentation-parts": { + "ids": [ + "", + "", + "", + "" + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 } } } diff --git a/tests/aws/services/apigateway/test_apigateway_api.validation.json b/tests/aws/services/apigateway/test_apigateway_api.validation.json index 1ae78d60f03e2..caa7c23453136 100644 --- a/tests/aws/services/apigateway/test_apigateway_api.validation.json +++ b/tests/aws/services/apigateway/test_apigateway_api.validation.json @@ -1,134 +1,134 @@ { - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_authorizer_crud_no_api": { - "last_validated_date": "2023-02-28T19:06:16+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiAuthorizer::test_authorizer_crud_no_api": { + "last_validated_date": "2024-04-15T18:43:45+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_create_proxy_resource": { - "last_validated_date": "2023-02-24T14:56:43+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_doc_parts_crud_no_api": { + "last_validated_date": "2024-04-15T20:52:33+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_create_proxy_resource_validation": { - "last_validated_date": "2023-02-24T15:17:00+00:00" - }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_create_resource_parent_invalid": { - "last_validated_date": "2023-02-24T15:39:00+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_documentation_part_lifecycle": { + "last_validated_date": "2024-04-15T20:52:34+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_create_rest_api_with_optional_params": { - "last_validated_date": "2023-04-04T15:58:40+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_import_documentation_parts": { + "last_validated_date": "2024-04-15T20:56:45+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_create_rest_api_with_tags": { - "last_validated_date": "2023-02-01T19:11:19+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_create_documentation_part_operations": { + "last_validated_date": "2024-04-15T20:53:48+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_delete_resource": { - "last_validated_date": "2023-02-23T19:41:28+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_delete_documentation_part": { + "last_validated_date": "2024-04-15T20:54:20+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_doc_arts_crud_no_api": { - "last_validated_date": "2023-02-28T19:11:32+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_get_documentation_part": { + "last_validated_date": "2024-04-15T20:52:39+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_get_api_case_insensitive": { - "last_validated_date": "2023-03-01T15:26:45+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_get_documentation_parts": { + "last_validated_date": "2024-04-15T20:53:15+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_invoke_test_method": { - "last_validated_date": "2023-07-01T16:52:50+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_update_documentation_part": { + "last_validated_date": "2024-04-15T20:53:16+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_list_and_delete_apis": { - "last_validated_date": "2023-02-01T19:16:52+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_method_lifecycle": { + "last_validated_date": "2024-04-15T21:22:46+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_method_lifecycle": { - "last_validated_date": "2023-03-15T11:06:26+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_method_request_parameters": { + "last_validated_date": "2024-04-15T21:22:51+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_method_request_parameters": { - "last_validated_date": "2023-02-24T18:25:22+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_put_method_model": { + "last_validated_date": "2024-04-15T21:23:29+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_model_lifecycle": { - "last_validated_date": "2023-05-26T02:30:31+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_put_method_validation": { + "last_validated_date": "2024-04-15T21:23:49+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_model_validation": { - "last_validated_date": "2023-03-09T18:13:20+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_update_method": { + "last_validated_date": "2024-04-15T21:24:42+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_put_method_model": { - "last_validated_date": "2023-03-15T11:12:59+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_update_method_validation": { + "last_validated_date": "2024-04-15T21:26:29+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_put_method_validation": { - "last_validated_date": "2023-02-25T16:49:08+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiModels::test_model_lifecycle": { + "last_validated_date": "2024-04-15T21:02:18+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_resource_lifecycle": { - "last_validated_date": "2023-02-23T21:08:31+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiModels::test_model_validation": { + "last_validated_date": "2024-04-15T20:33:07+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_method": { - "last_validated_date": "2023-03-13T22:31:44+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiModels::test_update_model": { + "last_validated_date": "2024-04-15T20:33:55+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_method_validation": { - "last_validated_date": "2023-03-13T22:36:56+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_create_request_validator_invalid_api_id": { + "last_validated_date": "2024-04-15T20:45:24+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_model": { - "last_validated_date": "2023-03-09T18:39:34+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_invalid_delete_request_validator": { + "last_validated_date": "2024-04-15T20:44:56+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_resource_behaviour": { - "last_validated_date": "2023-02-24T16:56:07+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_invalid_get_request_validator": { + "last_validated_date": "2024-04-15T20:44:24+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_rest_api_behaviour": { - "last_validated_date": "2023-02-01T19:27:17+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_invalid_get_request_validators": { + "last_validated_date": "2024-04-15T20:44:55+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_rest_api_compression": { - "last_validated_date": "2023-04-04T15:35:05+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_invalid_update_request_validator_operations": { + "last_validated_date": "2024-04-15T20:45:26+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_rest_api_invalid_api_id": { - "last_validated_date": "2023-02-01T21:26:45+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_request_validator_lifecycle": { + "last_validated_date": "2024-04-15T20:44:21+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_rest_api_operation_add_remove": { - "last_validated_date": "2023-02-01T19:11:40+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_validators_crud_no_api": { + "last_validated_date": "2024-04-15T20:44:19+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_validators_crud_no_api": { - "last_validated_date": "2023-02-28T19:18:59+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_create_proxy_resource": { + "last_validated_date": "2024-04-15T17:31:19+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_documentation_part_lifecycle": { - "last_validated_date": "2023-03-23T16:50:40+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_create_proxy_resource_validation": { + "last_validated_date": "2024-04-15T17:31:32+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_import_documentation_parts": { - "last_validated_date": "2023-06-26T10:01:38+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_create_resource_parent_invalid": { + "last_validated_date": "2024-04-15T17:30:24+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_create_documentation_part_operations": { - "last_validated_date": "2023-03-23T16:52:11+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_delete_resource": { + "last_validated_date": "2024-04-15T17:29:41+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_delete_documentation_part": { - "last_validated_date": "2023-03-23T16:52:31+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_resource_lifecycle": { + "last_validated_date": "2024-04-15T17:29:03+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_get_documentation_part": { - "last_validated_date": "2023-03-23T16:51:12+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_update_resource_behaviour": { + "last_validated_date": "2024-04-15T17:29:08+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_get_documentation_parts": { - "last_validated_date": "2023-03-23T16:51:12+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_create_rest_api_with_optional_params": { + "last_validated_date": "2024-04-15T15:10:32+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_update_documentation_part": { - "last_validated_date": "2023-03-23T16:51:42+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_create_rest_api_with_tags": { + "last_validated_date": "2024-04-15T15:11:51+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_create_request_validator_invalid_api_id": { - "last_validated_date": "2023-03-22T12:08:44+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_get_api_case_insensitive": { + "last_validated_date": "2024-04-15T15:09:49+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_invalid_delete_request_validator": { - "last_validated_date": "2023-03-22T12:08:44+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_list_and_delete_apis": { + "last_validated_date": "2024-04-15T15:08:47+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_invalid_get_request_validator": { - "last_validated_date": "2023-03-22T12:08:19+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_update_rest_api_behaviour": { + "last_validated_date": "2024-04-15T15:13:30+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_invalid_get_request_validators": { - "last_validated_date": "2023-03-22T12:08:19+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_update_rest_api_compression": { + "last_validated_date": "2024-04-15T15:12:36+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_invalid_update_request_validator_operations": { - "last_validated_date": "2023-03-22T12:08:58+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_update_rest_api_invalid_api_id": { + "last_validated_date": "2024-04-15T15:14:15+00:00" }, - "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_request_validator_lifecycle": { - "last_validated_date": "2023-03-22T12:07:42+00:00" + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_update_rest_api_operation_add_remove": { + "last_validated_date": "2024-04-15T15:12:34+00:00" }, "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayGatewayResponse::test_gateway_response_crud": { - "last_validated_date": "2023-09-04T18:13:20+00:00" + "last_validated_date": "2024-04-15T20:46:20+00:00" }, "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayGatewayResponse::test_gateway_response_validation": { - "last_validated_date": "2023-09-04T18:32:34+00:00" + "last_validated_date": "2024-04-15T20:46:24+00:00" }, "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayGatewayResponse::test_update_gateway_response": { - "last_validated_date": "2023-09-04T23:58:33+00:00" + "last_validated_date": "2024-04-15T20:47:11+00:00" }, "tests/aws/services/apigateway/test_apigateway_api.py::TestApigatewayIntegration::test_put_integration_wrong_type": { - "last_validated_date": "2023-11-28T19:15:11+00:00" + "last_validated_date": "2024-04-15T20:48:47+00:00" + }, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApigatewayTestInvoke::test_invoke_test_method": { + "last_validated_date": "2024-04-15T20:48:35+00:00" } } diff --git a/tests/aws/services/apigateway/test_apigateway_extended.py b/tests/aws/services/apigateway/test_apigateway_extended.py index 70d7a36e7f1fa..f259783f2587e 100644 --- a/tests/aws/services/apigateway/test_apigateway_extended.py +++ b/tests/aws/services/apigateway/test_apigateway_extended.py @@ -19,12 +19,7 @@ [TEST_IMPORT_PETSTORE_SWAGGER, TEST_IMPORT_PETS], ids=["TEST_IMPORT_PETSTORE_SWAGGER", "TEST_IMPORT_PETS"], ) -@markers.snapshot.skip_snapshot_verify( - paths=[ - "$..body.host", - "$..rootResourceId", - ] -) +@markers.snapshot.skip_snapshot_verify(paths=["$..body.host"]) def test_export_swagger_openapi(aws_client, snapshot, import_apigw, import_file, region_name): snapshot.add_transformer( [ @@ -63,15 +58,13 @@ def test_export_swagger_openapi(aws_client, snapshot, import_apigw, import_file, [TEST_IMPORT_PETSTORE_SWAGGER, TEST_IMPORT_PETS], ids=["TEST_IMPORT_PETSTORE_SWAGGER", "TEST_IMPORT_PETS"], ) -@markers.snapshot.skip_snapshot_verify( - paths=[ - "$..body.servers..url", - "$..rootResourceId", - ] -) +@markers.snapshot.skip_snapshot_verify(paths=["$..body.servers..url"]) def test_export_oas30_openapi(aws_client, snapshot, import_apigw, region_name, import_file): snapshot.add_transformer( - snapshot.transform.jsonpath("$.import-api.id", value_replacement="api-id") + [ + snapshot.transform.jsonpath("$.import-api.id", value_replacement="api-id"), + snapshot.transform.key_value("rootResourceId"), + ] ) spec_file = load_file(import_file) diff --git a/tests/aws/services/apigateway/test_apigateway_extended.snapshot.json b/tests/aws/services/apigateway/test_apigateway_extended.snapshot.json index 58a17fd1e1b81..8720263aea0f2 100644 --- a/tests/aws/services/apigateway/test_apigateway_extended.snapshot.json +++ b/tests/aws/services/apigateway/test_apigateway_extended.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_swagger_openapi[TEST_IMPORT_PETSTORE_SWAGGER]": { - "recorded-date": "26-03-2024, 11:32:19", + "recorded-date": "15-04-2024, 21:43:25", "recorded-content": { "import-api": { "apiKeySource": "HEADER", @@ -638,7 +638,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_swagger_openapi[TEST_IMPORT_PETS]": { - "recorded-date": "26-03-2024, 11:32:36", + "recorded-date": "15-04-2024, 21:43:56", "recorded-content": { "import-api": { "apiKeySource": "HEADER", @@ -782,7 +782,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_oas30_openapi[TEST_IMPORT_PETSTORE_SWAGGER]": { - "recorded-date": "26-03-2024, 11:32:50", + "recorded-date": "15-04-2024, 21:45:03", "recorded-content": { "import-api": { "apiKeySource": "HEADER", @@ -796,7 +796,7 @@ }, "id": "", "name": "PetStore", - "rootResourceId": "kb57q8ij35", + "rootResourceId": "", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 201 @@ -1140,7 +1140,6 @@ } }, "x-amazon-apigateway-integration": { - "type": "http", "httpMethod": "GET", "uri": "http://petstore.execute-api..amazonaws.com/petstore/pets", "responses": { @@ -1155,7 +1154,8 @@ "integration.request.querystring.page": "method.request.querystring.page", "integration.request.querystring.type": "method.request.querystring.type" }, - "passthroughBehavior": "when_no_match" + "passthroughBehavior": "when_no_match", + "type": "http" } }, "post": { @@ -1190,7 +1190,6 @@ } }, "x-amazon-apigateway-integration": { - "type": "http", "httpMethod": "POST", "uri": "http://petstore.execute-api..amazonaws.com/petstore/pets", "responses": { @@ -1201,7 +1200,8 @@ } } }, - "passthroughBehavior": "when_no_match" + "passthroughBehavior": "when_no_match", + "type": "http" } }, "options": { @@ -1235,7 +1235,6 @@ } }, "x-amazon-apigateway-integration": { - "type": "mock", "responses": { "default": { "statusCode": "200", @@ -1249,7 +1248,8 @@ "requestTemplates": { "application/json": "{\"statusCode\": 200}" }, - "passthroughBehavior": "when_no_match" + "passthroughBehavior": "when_no_match", + "type": "mock" } } }, @@ -1286,7 +1286,6 @@ } }, "x-amazon-apigateway-integration": { - "type": "http", "httpMethod": "GET", "uri": "http://petstore.execute-api..amazonaws.com/petstore/pets/{petId}", "responses": { @@ -1300,7 +1299,8 @@ "requestParameters": { "integration.request.path.petId": "method.request.path.petId" }, - "passthroughBehavior": "when_no_match" + "passthroughBehavior": "when_no_match", + "type": "http" } }, "options": { @@ -1344,7 +1344,6 @@ } }, "x-amazon-apigateway-integration": { - "type": "mock", "responses": { "default": { "statusCode": "200", @@ -1358,7 +1357,8 @@ "requestTemplates": { "application/json": "{\"statusCode\": 200}" }, - "passthroughBehavior": "when_no_match" + "passthroughBehavior": "when_no_match", + "type": "mock" } } }, @@ -1378,7 +1378,6 @@ } }, "x-amazon-apigateway-integration": { - "type": "mock", "responses": { "default": { "statusCode": "200", @@ -1393,7 +1392,8 @@ "requestTemplates": { "application/json": "{\"statusCode\": 200}" }, - "passthroughBehavior": "when_no_match" + "passthroughBehavior": "when_no_match", + "type": "mock" } } } @@ -1468,7 +1468,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_oas30_openapi[TEST_IMPORT_PETS]": { - "recorded-date": "26-03-2024, 11:32:58", + "recorded-date": "15-04-2024, 21:45:07", "recorded-content": { "import-api": { "apiKeySource": "HEADER", @@ -1481,7 +1481,7 @@ }, "id": "", "name": "Simple PetStore (Swagger)", - "rootResourceId": "vp8w0yxukk", + "rootResourceId": "", "version": "1.0.0", "ResponseMetadata": { "HTTPHeaders": {}, diff --git a/tests/aws/services/apigateway/test_apigateway_extended.validation.json b/tests/aws/services/apigateway/test_apigateway_extended.validation.json index 2782c237ad0df..e301c44599698 100644 --- a/tests/aws/services/apigateway/test_apigateway_extended.validation.json +++ b/tests/aws/services/apigateway/test_apigateway_extended.validation.json @@ -1,14 +1,14 @@ { "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_oas30_openapi[TEST_IMPORT_PETSTORE_SWAGGER]": { - "last_validated_date": "2024-03-26T11:32:49+00:00" + "last_validated_date": "2024-04-15T21:45:02+00:00" }, "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_oas30_openapi[TEST_IMPORT_PETS]": { - "last_validated_date": "2024-03-26T11:32:53+00:00" + "last_validated_date": "2024-04-15T21:45:04+00:00" }, "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_swagger_openapi[TEST_IMPORT_PETSTORE_SWAGGER]": { - "last_validated_date": "2024-03-26T11:32:19+00:00" + "last_validated_date": "2024-04-15T21:43:24+00:00" }, "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_swagger_openapi[TEST_IMPORT_PETS]": { - "last_validated_date": "2024-03-26T11:32:24+00:00" + "last_validated_date": "2024-04-15T21:43:30+00:00" } } diff --git a/tests/aws/services/apigateway/test_apigateway_import.py b/tests/aws/services/apigateway/test_apigateway_import.py index 64dc3bff14d57..ad401470f15a1 100644 --- a/tests/aws/services/apigateway/test_apigateway_import.py +++ b/tests/aws/services/apigateway/test_apigateway_import.py @@ -272,7 +272,6 @@ def test_import_rest_api(self, import_apigw, snapshot): "$.resources.items..resourceMethods.GET", # TODO: this is really weird, after importing, AWS returns them empty? "$.resources.items..resourceMethods.OPTIONS", "$.resources.items..resourceMethods.POST", - "$..rootResourceId", "$.get-authorizers.items[1].authorizerResultTtlInSeconds", ] ) @@ -470,6 +469,7 @@ def test_import_rest_apis_with_base_path_swagger( "$..cacheNamespace", # TODO: investigate why it's different "$.get-resources-oas30-srv-url.items..id", # TODO: even in overwrite, APIGW keeps the same ID if same path "$.get-resources-oas30-srv-url.items..parentId", # TODO: even in overwrite, APIGW keeps the same ID if same path + "$.put-rest-api-oas30-srv-url..rootResourceId", # TODO: because APIGW keeps the same above, id counting is different ] ) def test_import_rest_api_with_base_path_oas30( @@ -652,7 +652,6 @@ def test_import_with_circular_models( paths=[ "$.resources.items..resourceMethods.POST", # TODO: this is really weird, after importing, AWS returns them empty? - "$..rootResourceId", # TODO: newly added ] ) @markers.aws.validated diff --git a/tests/aws/services/apigateway/test_apigateway_import.snapshot.json b/tests/aws/services/apigateway/test_apigateway_import.snapshot.json index 37539d6c5a20a..0bc53ea89bfd5 100644 --- a/tests/aws/services/apigateway/test_apigateway_import.snapshot.json +++ b/tests/aws/services/apigateway/test_apigateway_import.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_api": { - "recorded-date": "03-06-2023, 13:16:32", + "recorded-date": "15-04-2024, 21:30:21", "recorded-content": { "import_rest_api": { "apiKeySource": "HEADER", @@ -16,6 +16,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "version": "1.0", "ResponseMetadata": { "HTTPHeaders": {}, @@ -25,7 +26,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_swagger_api": { - "recorded-date": "22-03-2024, 07:03:04", + "recorded-date": "15-04-2024, 21:31:02", "recorded-content": { "import-swagger": { "apiKeySource": "HEADER", @@ -771,7 +772,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_and_validate_rest_api[openapi.spec.tf.json]": { - "recorded-date": "03-06-2023, 13:29:17", + "recorded-date": "15-04-2024, 21:31:22", "recorded-content": { "import_tf_rest_api": { "apiKeySource": "HEADER", @@ -784,6 +785,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "version": "1", "ResponseMetadata": { "HTTPHeaders": {}, @@ -1006,7 +1008,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_and_validate_rest_api[swagger-mock-cors.json]": { - "recorded-date": "03-06-2023, 13:29:35", + "recorded-date": "15-04-2024, 21:31:41", "recorded-content": { "import_tf_rest_api": { "apiKeySource": "HEADER", @@ -1019,6 +1021,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "version": "1.0", "ResponseMetadata": { "HTTPHeaders": {}, @@ -1379,7 +1382,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_apis_with_base_path_swagger[ignore]": { - "recorded-date": "03-06-2023, 14:10:55", + "recorded-date": "15-04-2024, 21:33:04", "recorded-content": { "put-rest-api-swagger-json": { "apiKeySource": "HEADER", @@ -1392,6 +1395,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": {}, "version": "2", "ResponseMetadata": { @@ -1761,7 +1765,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_apis_with_base_path_swagger[prepend]": { - "recorded-date": "03-06-2023, 14:12:12", + "recorded-date": "15-04-2024, 21:34:01", "recorded-content": { "put-rest-api-swagger-json": { "apiKeySource": "HEADER", @@ -1774,6 +1778,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": {}, "version": "2", "ResponseMetadata": { @@ -2149,7 +2154,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_apis_with_base_path_swagger[split]": { - "recorded-date": "03-06-2023, 14:14:07", + "recorded-date": "15-04-2024, 21:34:50", "recorded-content": { "put-rest-api-swagger-json": { "apiKeySource": "HEADER", @@ -2162,6 +2167,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": {}, "version": "2", "ResponseMetadata": { @@ -2531,7 +2537,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_api_with_base_path_oas30[prepend]": { - "recorded-date": "04-06-2023, 00:45:31", + "recorded-date": "15-04-2024, 21:36:04", "recorded-content": { "put-rest-api-oas30-srv-var": { "apiKeySource": "HEADER", @@ -2544,6 +2550,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": {}, "version": "2.0", "ResponseMetadata": { @@ -2780,6 +2787,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": {}, "version": "2.0", "ResponseMetadata": { @@ -3014,7 +3022,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_api_with_base_path_oas30[split]": { - "recorded-date": "04-06-2023, 00:46:41", + "recorded-date": "15-04-2024, 21:36:26", "recorded-content": { "put-rest-api-oas30-srv-var": { "apiKeySource": "HEADER", @@ -3027,6 +3035,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": {}, "version": "2.0", "ResponseMetadata": { @@ -3257,6 +3266,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": {}, "version": "2.0", "ResponseMetadata": { @@ -3485,7 +3495,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_api_with_base_path_oas30[ignore]": { - "recorded-date": "04-06-2023, 00:45:06", + "recorded-date": "15-04-2024, 21:35:47", "recorded-content": { "put-rest-api-oas30-srv-var": { "apiKeySource": "HEADER", @@ -3498,6 +3508,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": {}, "version": "2.0", "ResponseMetadata": { @@ -3728,6 +3739,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": {}, "version": "2.0", "ResponseMetadata": { @@ -3763,7 +3775,7 @@ "httpMethod": "GET", "methodIntegration": { "cacheKeyParameters": [], - "cacheNamespace": "9zjs21", + "cacheNamespace": "", "integrationResponses": { "200": { "responseParameters": { @@ -3815,7 +3827,7 @@ }, "srv-url-integration-test-get": { "cacheKeyParameters": [], - "cacheNamespace": "9zjs21", + "cacheNamespace": "", "integrationResponses": { "200": { "responseParameters": { @@ -3856,7 +3868,7 @@ "httpMethod": "OPTIONS", "methodIntegration": { "cacheKeyParameters": [], - "cacheNamespace": "9zjs21", + "cacheNamespace": "", "integrationResponses": { "200": { "responseParameters": { @@ -3911,7 +3923,7 @@ }, "srv-url-integration-test-options": { "cacheKeyParameters": [], - "cacheNamespace": "9zjs21", + "cacheNamespace": "", "integrationResponses": { "200": { "responseParameters": { @@ -3950,7 +3962,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_global_api_key_authorizer": { - "recorded-date": "07-06-2023, 03:16:37", + "recorded-date": "15-04-2024, 21:37:12", "recorded-content": { "import-swagger": { "apiKeySource": "AUTHORIZER", @@ -3963,6 +3975,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "version": "1.0", "ResponseMetadata": { "HTTPHeaders": {}, @@ -4182,7 +4195,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_circular_models": { - "recorded-date": "07-06-2023, 02:26:07", + "recorded-date": "15-04-2024, 21:37:35", "recorded-content": { "import-api": { "apiKeySource": "HEADER", @@ -4195,6 +4208,7 @@ }, "id": "", "name": "Circular model reference", + "rootResourceId": "", "version": "1.0", "ResponseMetadata": { "HTTPHeaders": {}, @@ -4378,7 +4392,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_circular_models_and_request_validation": { - "recorded-date": "09-01-2024, 22:35:27", + "recorded-date": "15-04-2024, 21:38:12", "recorded-content": { "import-api": { "apiKeySource": "HEADER", @@ -4590,7 +4604,7 @@ } }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_http_method_integration": { - "recorded-date": "05-12-2023, 22:27:16", + "recorded-date": "15-04-2024, 21:39:47", "recorded-content": { "resources": { "items": [ @@ -4776,5 +4790,9 @@ } } } + }, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_stage_variables": { + "recorded-date": "15-04-2024, 21:38:55", + "recorded-content": {} } } diff --git a/tests/aws/services/apigateway/test_apigateway_import.validation.json b/tests/aws/services/apigateway/test_apigateway_import.validation.json index 14962a2e44c64..9ec1a89f26d40 100644 --- a/tests/aws/services/apigateway/test_apigateway_import.validation.json +++ b/tests/aws/services/apigateway/test_apigateway_import.validation.json @@ -1,44 +1,47 @@ { "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_and_validate_rest_api[openapi.spec.tf.json]": { - "last_validated_date": "2023-06-03T11:29:17+00:00" + "last_validated_date": "2024-04-15T21:31:20+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_and_validate_rest_api[swagger-mock-cors.json]": { - "last_validated_date": "2023-06-03T11:29:35+00:00" + "last_validated_date": "2024-04-15T21:31:41+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_api": { - "last_validated_date": "2023-06-03T11:16:32+00:00" + "last_validated_date": "2024-04-15T21:30:20+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_api_with_base_path_oas30[ignore]": { - "last_validated_date": "2023-06-03T22:45:06+00:00" + "last_validated_date": "2024-04-15T21:35:08+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_api_with_base_path_oas30[prepend]": { - "last_validated_date": "2023-06-03T22:45:31+00:00" + "last_validated_date": "2024-04-15T21:36:02+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_api_with_base_path_oas30[split]": { - "last_validated_date": "2023-06-03T22:46:41+00:00" + "last_validated_date": "2024-04-15T21:36:22+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_apis_with_base_path_swagger[ignore]": { - "last_validated_date": "2023-06-03T12:10:55+00:00" + "last_validated_date": "2024-04-15T21:32:25+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_apis_with_base_path_swagger[prepend]": { - "last_validated_date": "2023-06-03T12:12:12+00:00" + "last_validated_date": "2024-04-15T21:33:49+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_apis_with_base_path_swagger[split]": { - "last_validated_date": "2023-06-03T12:14:07+00:00" + "last_validated_date": "2024-04-15T21:34:46+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_swagger_api": { - "last_validated_date": "2024-03-22T07:03:03+00:00" + "last_validated_date": "2024-04-15T21:30:39+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_circular_models": { - "last_validated_date": "2023-06-07T00:26:07+00:00" + "last_validated_date": "2024-04-15T21:37:14+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_circular_models_and_request_validation": { - "last_validated_date": "2024-01-09T22:35:26+00:00" + "last_validated_date": "2024-04-15T21:37:44+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_global_api_key_authorizer": { - "last_validated_date": "2023-06-07T01:16:37+00:00" + "last_validated_date": "2024-04-15T21:36:29+00:00" }, "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_http_method_integration": { - "last_validated_date": "2023-12-05T21:27:16+00:00" + "last_validated_date": "2024-04-15T21:38:57+00:00" + }, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_stage_variables": { + "last_validated_date": "2024-04-15T21:38:14+00:00" } } diff --git a/tests/aws/services/apigateway/test_apigateway_integrations.py b/tests/aws/services/apigateway/test_apigateway_integrations.py index d2dcfdc0a1e5f..9be43adb1d9b5 100644 --- a/tests/aws/services/apigateway/test_apigateway_integrations.py +++ b/tests/aws/services/apigateway/test_apigateway_integrations.py @@ -501,15 +501,18 @@ def test_create_execute_api_vpc_endpoint( ): poll_sleep = 5 if is_aws_cloud() else 1 # TODO: create a re-usable ec2_api() transformer - snapshot.add_transformer(snapshot.transform.key_value("DnsName")) - snapshot.add_transformer(snapshot.transform.key_value("GroupId")) - snapshot.add_transformer(snapshot.transform.key_value("GroupName")) - snapshot.add_transformer(snapshot.transform.key_value("SubnetIds")) - snapshot.add_transformer(snapshot.transform.key_value("VpcId")) - snapshot.add_transformer(snapshot.transform.key_value("VpcEndpointId")) - snapshot.add_transformer(snapshot.transform.key_value("HostedZoneId")) - snapshot.add_transformer(snapshot.transform.key_value("id")) - snapshot.add_transformer(snapshot.transform.key_value("name")) + snapshot.add_transformers_list( + [ + snapshot.transform.key_value("DnsName"), + snapshot.transform.key_value("GroupId"), + snapshot.transform.key_value("GroupName"), + snapshot.transform.key_value("SubnetIds"), + snapshot.transform.key_value("VpcId"), + snapshot.transform.key_value("VpcEndpointId"), + snapshot.transform.key_value("HostedZoneId"), + *snapshot.transform.apigateway_api(), + ] + ) # create table table = dynamodb_create_table()["TableDescription"] diff --git a/tests/aws/services/apigateway/test_apigateway_integrations.snapshot.json b/tests/aws/services/apigateway/test_apigateway_integrations.snapshot.json index 04efd432c2c88..7ae2851968781 100644 --- a/tests/aws/services/apigateway/test_apigateway_integrations.snapshot.json +++ b/tests/aws/services/apigateway/test_apigateway_integrations.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/apigateway/test_apigateway_integrations.py::test_create_execute_api_vpc_endpoint": { - "recorded-date": "18-03-2023, 22:01:10", + "recorded-date": "15-04-2024, 23:07:07", "recorded-content": { "endpoint-details": { "CreationTimestamp": "timestamp", @@ -66,6 +66,7 @@ ], "Version": "2012-10-17" }, + "rootResourceId": "", "tags": {}, "ResponseMetadata": { "HTTPHeaders": {}, diff --git a/tests/aws/services/apigateway/test_apigateway_integrations.validation.json b/tests/aws/services/apigateway/test_apigateway_integrations.validation.json index 80f326d98be36..5503951fa4999 100644 --- a/tests/aws/services/apigateway/test_apigateway_integrations.validation.json +++ b/tests/aws/services/apigateway/test_apigateway_integrations.validation.json @@ -1,6 +1,6 @@ { "tests/aws/services/apigateway/test_apigateway_integrations.py::test_create_execute_api_vpc_endpoint": { - "last_validated_date": "2023-03-18T21:01:10+00:00" + "last_validated_date": "2024-04-15T23:07:07+00:00" }, "tests/aws/services/apigateway/test_apigateway_integrations.py::test_put_integration_responses": { "last_validated_date": "2023-05-26T17:44:45+00:00" diff --git a/tests/aws/services/cloudformation/api/test_transformers.py b/tests/aws/services/cloudformation/api/test_transformers.py index 1f49e59d48026..632552ee04c81 100644 --- a/tests/aws/services/cloudformation/api/test_transformers.py +++ b/tests/aws/services/cloudformation/api/test_transformers.py @@ -5,10 +5,13 @@ @markers.aws.validated @markers.snapshot.skip_snapshot_verify(paths=["$..tags"]) def test_duplicate_resources(deploy_cfn_template, s3_bucket, snapshot, aws_client): - snapshot.add_transformer(snapshot.transform.key_value("id")) - snapshot.add_transformer(snapshot.transform.key_value("name")) - snapshot.add_transformer(snapshot.transform.key_value("aws:cloudformation:stack-id")) - snapshot.add_transformer(snapshot.transform.key_value("aws:cloudformation:stack-name")) + snapshot.add_transformers_list( + [ + *snapshot.transform.apigateway_api(), + snapshot.transform.key_value("aws:cloudformation:stack-id"), + snapshot.transform.key_value("aws:cloudformation:stack-name"), + ] + ) # put API spec to S3 api_spec = """ @@ -52,3 +55,6 @@ def test_duplicate_resources(deploy_cfn_template, s3_bucket, snapshot, aws_clien result = aws_client.apigateway.get_rest_api(restApiId=api_id) assert result snapshot.match("api-details", result) + + resources = aws_client.apigateway.get_resources(restApiId=api_id) + snapshot.match("api-resources", resources) diff --git a/tests/aws/services/cloudformation/api/test_transformers.snapshot.json b/tests/aws/services/cloudformation/api/test_transformers.snapshot.json index 97bd20f7df5d3..fa7686ebabab3 100644 --- a/tests/aws/services/cloudformation/api/test_transformers.snapshot.json +++ b/tests/aws/services/cloudformation/api/test_transformers.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/cloudformation/api/test_transformers.py::test_duplicate_resources": { - "recorded-date": "21-02-2023, 09:45:29", + "recorded-date": "15-04-2024, 22:51:13", "recorded-content": { "api-details": { "apiKeySource": "HEADER", @@ -13,6 +13,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": { "aws:cloudformation:logical-id": "RestApi", "aws:cloudformation:stack-id": "", @@ -23,6 +24,18 @@ "HTTPHeaders": {}, "HTTPStatusCode": 200 } + }, + "api-resources": { + "items": [ + { + "id": "", + "path": "/" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } } } } diff --git a/tests/aws/services/cloudformation/api/test_transformers.validation.json b/tests/aws/services/cloudformation/api/test_transformers.validation.json index 489d124c8fce2..8af221fa6c60f 100644 --- a/tests/aws/services/cloudformation/api/test_transformers.validation.json +++ b/tests/aws/services/cloudformation/api/test_transformers.validation.json @@ -1,5 +1,5 @@ { "tests/aws/services/cloudformation/api/test_transformers.py::test_duplicate_resources": { - "last_validated_date": "2023-02-21T08:45:29+00:00" + "last_validated_date": "2024-04-15T22:51:13+00:00" } } diff --git a/tests/aws/services/cloudformation/resources/test_apigateway.snapshot.json b/tests/aws/services/cloudformation/resources/test_apigateway.snapshot.json index 35bba0e02b792..fdbc417ed1839 100644 --- a/tests/aws/services/cloudformation/resources/test_apigateway.snapshot.json +++ b/tests/aws/services/cloudformation/resources/test_apigateway.snapshot.json @@ -66,7 +66,7 @@ } }, "tests/aws/services/cloudformation/resources/test_apigateway.py::test_api_gateway_with_policy_as_dict": { - "recorded-date": "01-03-2023, 23:36:31", + "recorded-date": "15-04-2024, 22:59:53", "recorded-content": { "rest-api": { "apiKeySource": "HEADER", @@ -93,6 +93,7 @@ ], "Version": "2012-10-17" }, + "rootResourceId": "", "tags": { "aws:cloudformation:logical-id": "MyApi", "aws:cloudformation:stack-id": "arn:aws:cloudformation::111111111111:stack/stack-name/", @@ -106,7 +107,7 @@ } }, "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_deploy_apigateway_from_s3_swagger": { - "recorded-date": "02-06-2023, 18:26:01", + "recorded-date": "15-04-2024, 22:59:18", "recorded-content": { "rest-api": { "apiKeySource": "HEADER", @@ -119,6 +120,7 @@ }, "id": "", "name": "", + "rootResourceId": "", "tags": { "aws:cloudformation:logical-id": "ApiGatewayRestApi", "aws:cloudformation:stack-id": "arn:aws:cloudformation::111111111111:stack/stack-name/", diff --git a/tests/aws/services/cloudformation/resources/test_apigateway.validation.json b/tests/aws/services/cloudformation/resources/test_apigateway.validation.json index 32b75f848afe7..09601f2937c05 100644 --- a/tests/aws/services/cloudformation/resources/test_apigateway.validation.json +++ b/tests/aws/services/cloudformation/resources/test_apigateway.validation.json @@ -3,10 +3,10 @@ "last_validated_date": "2024-02-19T08:55:12+00:00" }, "tests/aws/services/cloudformation/resources/test_apigateway.py::test_api_gateway_with_policy_as_dict": { - "last_validated_date": "2023-03-01T22:36:31+00:00" + "last_validated_date": "2024-04-15T22:59:53+00:00" }, "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_deploy_apigateway_from_s3_swagger": { - "last_validated_date": "2023-06-02T16:26:01+00:00" + "last_validated_date": "2024-04-15T22:59:17+00:00" }, "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_deploy_apigateway_integration": { "last_validated_date": "2024-02-21T12:54:34+00:00" From 90dac798c4f1c7d6cdae970653119b714e1ce5ee Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Thu, 18 Apr 2024 00:54:25 +0200 Subject: [PATCH 075/169] fix SNS filtering when payload contains arrays (#10684) --- localstack/services/sns/publisher.py | 74 +++++- tests/aws/services/sns/test_sns.py | 226 ++++++++++++++---- tests/aws/services/sns/test_sns.snapshot.json | 136 +++++++---- .../aws/services/sns/test_sns.validation.json | 7 +- tests/unit/test_sns.py | 24 +- 5 files changed, 362 insertions(+), 105 deletions(-) diff --git a/localstack/services/sns/publisher.py b/localstack/services/sns/publisher.py index 022766ce4c1b4..245ecdbee5592 100644 --- a/localstack/services/sns/publisher.py +++ b/localstack/services/sns/publisher.py @@ -1142,13 +1142,14 @@ def _evaluate_nested_filter_policy_on_dict(self, filter_policy, payload: dict) - :return: True if the payload respect the filter policy, otherwise False """ flat_policy = self._flatten_dict(filter_policy) - flat_payload = self._flatten_dict(payload) + flat_payloads = self._flatten_dict_with_list(payload) for key, values in flat_policy.items(): if not any( self._evaluate_condition( flat_payload.get(key), condition, field_exists=key in flat_payload ) for condition in values + for flat_payload in flat_payloads ): return False return True @@ -1179,9 +1180,6 @@ def _evaluate_filter_policy_conditions_on_attribute( return False def _evaluate_condition(self, value, condition, field_exists: bool): - if isinstance(value, list): - return any(self._evaluate_condition(val, condition, field_exists) for val in value) - if not isinstance(condition, dict): return field_exists and value == condition elif (must_exist := condition.get("exists")) is not None: @@ -1242,21 +1240,81 @@ def _flatten_dict(nested_dict: dict): "field1.field2.field4": "val1" }` :param nested_dict: a (nested) dictionary - :return: flatten_dict: a dictionary with no nested dict inside, flattened to a single level + :return: a list of flattened dictionaries with no nested dict or list inside, flattened to a + single level, one list item for every list item encountered """ flatten = {} def _traverse(_policy: dict, parent_key=None): for key, values in _policy.items(): - pkey = key if not parent_key else f"{parent_key}.{key}" + flattened_parent_key = key if not parent_key else f"{parent_key}.{key}" if not isinstance(values, dict): - flatten[pkey] = values + flatten[flattened_parent_key] = values else: - _traverse(values, parent_key=pkey) + _traverse(values, parent_key=flattened_parent_key) _traverse(nested_dict) return flatten + @staticmethod + def _flatten_dict_with_list(nested_dict: dict) -> list[dict]: + """ + Takes a dictionary as input and will output the dictionary on a single level. + The dictionary can have lists containing other dictionaries, and one root level entry will be created for every + item in a list. + Input: + `{"field1": { + "field2: [ + {"field3: "val1", "field4": "val2"}, + {"field3: "val3", "field4": "val4"}, + } + ]}` + Output: + `[ + { + "field1.field2.field3": "val1", + "field1.field2.field4": "val2" + }, + { + "field1.field2.field3": "val3", + "field1.field2.field4": "val4" + }, + ]` + :param nested_dict: a (nested) dictionary + :return: flatten_dict: a dictionary with no nested dict inside, flattened to a single level + """ + flattened = [] + current_object = {} + + def _traverse(_object, parent_key=None): + if isinstance(_object, dict): + for key, values in _object.items(): + flattened_parent_key = key if not parent_key else f"{parent_key}.{key}" + _traverse(values, flattened_parent_key) + + # we don't have to worry about `parent_key` being None for list or any other type, because we have a check + # that the first object is always a dict, thus setting a parent key on first iteration + elif isinstance(_object, list): + for value in _object: + if isinstance(value, (dict, list)): + _traverse(value, parent_key=parent_key) + else: + current_object[parent_key] = value + + if current_object: + flattened.append({**current_object}) + current_object.clear() + else: + current_object[parent_key] = _object + + _traverse(nested_dict) + + # if the payload did not have any list, we manually append the current object + if not flattened: + flattened.append(current_object) + + return flattened + class PublishDispatcher: """ diff --git a/tests/aws/services/sns/test_sns.py b/tests/aws/services/sns/test_sns.py index e5456308f8b0c..de9b0e52e6356 100644 --- a/tests/aws/services/sns/test_sns.py +++ b/tests/aws/services/sns/test_sns.py @@ -1,5 +1,6 @@ import base64 import contextlib +import copy import json import logging import queue @@ -2801,6 +2802,35 @@ def check_subscription(): class TestSNSFilter: + @pytest.fixture + def sns_create_sqs_subscription_with_filter_policy( + self, sns_create_sqs_subscription, aws_client + ): + def _inner(topic_arn: str, queue_url: str, filter_scope: str, filter_policy: dict): + subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) + subscription_arn = subscription["SubscriptionArn"] + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicyScope", + AttributeValue=filter_scope, + ) + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=json.dumps(filter_policy), + ) + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="RawMessageDelivery", + AttributeValue="true", + ) + return subscription_arn + + yield _inner + @markers.aws.validated def test_filter_policy( self, sqs_create_queue, sns_create_topic, sns_create_sqs_subscription, snapshot, aws_client @@ -3045,18 +3075,21 @@ def get_filter_policy(): @markers.aws.validated def test_exists_filter_policy_attributes_array( - self, sqs_create_queue, sns_create_topic, sns_create_sqs_subscription, snapshot, aws_client + self, + sqs_create_queue, + sns_create_topic, + sns_create_sqs_subscription_with_filter_policy, + snapshot, + aws_client, ): topic_arn = sns_create_topic()["TopicArn"] queue_url = sqs_create_queue() - subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) - subscription_arn = subscription["SubscriptionArn"] - filter_policy = {"store": ["value1"]} - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicy", - AttributeValue=json.dumps(filter_policy), + subscription_arn = sns_create_sqs_subscription_with_filter_policy( + topic_arn=topic_arn, + queue_url=queue_url, + filter_scope="MessageAttributes", + filter_policy=filter_policy, ) response_attributes = aws_client.sns.get_subscription_attributes( @@ -3618,52 +3651,32 @@ def test_filter_policy_on_message_body_array_attributes( self, sqs_create_queue, sns_create_topic, - sns_create_sqs_subscription, + sns_create_sqs_subscription_with_filter_policy, snapshot, aws_client, ): topic_arn = sns_create_topic()["TopicArn"] queue_url_1 = sqs_create_queue() queue_url_2 = sqs_create_queue() - subscription_1 = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url_1) - subscription_2 = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url_2) - subscription_arn_1 = subscription_1["SubscriptionArn"] - subscription_arn_2 = subscription_2["SubscriptionArn"] filter_policy_1 = {"headers": {"route-to": ["queue1"]}} - filter_policy_2 = {"headers": {"route-to": ["queue2"]}} - for sub_arn, filter_policy in ( - (subscription_arn_1, filter_policy_1), - (subscription_arn_2, filter_policy_2), - ): - aws_client.sns.set_subscription_attributes( - SubscriptionArn=sub_arn, - AttributeName="FilterPolicyScope", - AttributeValue="MessageBody", - ) - - aws_client.sns.set_subscription_attributes( - SubscriptionArn=sub_arn, - AttributeName="FilterPolicy", - AttributeValue=json.dumps(filter_policy), - ) + sns_create_sqs_subscription_with_filter_policy( + topic_arn=topic_arn, + queue_url=queue_url_1, + filter_scope="MessageBody", + filter_policy=filter_policy_1, + ) - aws_client.sns.set_subscription_attributes( - SubscriptionArn=sub_arn, - AttributeName="RawMessageDelivery", - AttributeValue="true", - ) + filter_policy_2 = {"headers": {"route-to": ["queue2"]}} + sns_create_sqs_subscription_with_filter_policy( + topic_arn=topic_arn, + queue_url=queue_url_2, + filter_scope="MessageBody", + filter_policy=filter_policy_2, + ) queues = [queue_url_1, queue_url_2] - for i, queue_url in enumerate(queues): - response = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=1 - ) - snapshot.match(f"recv-init-{i}", response) - # assert there are no messages in the queue - assert "Messages" not in response or response["Messages"] == [] - # publish messages that satisfies the filter policy, assert that messages are received messages = [ {"headers": {"route-to": ["queue3"]}}, @@ -3707,6 +3720,135 @@ def get_messages(_queue_url: str, _recv_messages: list): recv_messages.sort(key=itemgetter("Body")) snapshot.match(f"messages-queue-{i}", {"Messages": recv_messages}) + @markers.aws.validated + def test_filter_policy_on_message_body_array_of_object_attributes( + self, + sqs_create_queue, + sns_create_topic, + sns_create_sqs_subscription_with_filter_policy, + snapshot, + aws_client, + ): + # example from https://aws.amazon.com/blogs/compute/introducing-payload-based-message-filtering-for-amazon-sns/ + topic_arn = sns_create_topic()["TopicArn"] + queue_url = sqs_create_queue() + # complex filter policy with different level of nesting + filter_policy = { + "Records": { + "s3": {"object": {"key": [{"prefix": "auto-"}]}}, + "eventName": [{"prefix": "ObjectCreated:"}], + } + } + + sns_create_sqs_subscription_with_filter_policy( + topic_arn=topic_arn, + queue_url=queue_url, + filter_scope="MessageBody", + filter_policy=filter_policy, + ) + + # stripped down events + s3_event_auto_insurance_created = { + "Records": [ + { + "eventSource": "aws:s3", + "eventTime": "2022-11-21T03:41:29.743Z", + "eventName": "ObjectCreated:Put", + "s3": { + "bucket": { + "name": "insurance-bucket-demo", + "arn": "arn:aws:s3:::insurance-bucket-demo", + }, + "object": { + "key": "auto-insurance-2314.xml", + "size": 17, + }, + }, + } + ] + } + # copy the object to modify it + s3_event_auto_insurance_removed = copy.deepcopy(s3_event_auto_insurance_created) + s3_event_auto_insurance_removed["Records"][0]["eventName"] = "ObjectRemoved:Delete" + + # copy the object to modify it + s3_event_home_insurance_created = copy.deepcopy(s3_event_auto_insurance_created) + s3_event_home_insurance_created["Records"][0]["s3"]["object"]["key"] = ( + "home-insurance-2314.xml" + ) + + # stripped down events + s3_event_multiple_records = { + "Records": [ + { + "eventSource": "aws:s3", + "eventName": "ObjectCreated:Put", + "s3": { + # this object is a list of list of dict, and it works in AWS + "object": [ + [ + { + "key": "auto-insurance-2314.xml", + "size": 17, + } + ] + ], + }, + }, + { + "eventSource": "aws:s3", + "eventName": "ObjectRemoved:Delete", + "s3": { + "object": { + "key": "home-insurance-2314.xml", + "size": 17, + } + }, + }, + ] + } + + messages = [ + s3_event_multiple_records, + s3_event_auto_insurance_removed, + s3_event_home_insurance_created, + s3_event_auto_insurance_created, + ] + for i, message in enumerate(messages): + aws_client.sns.publish( + TopicArn=topic_arn, + Message=json.dumps(message), + ) + + def get_messages(_queue_url: str, _received_messages: list): + # due to the random nature of receiving SQS messages, we need to consolidate a single object to match + sqs_response = aws_client.sqs.receive_message( + QueueUrl=_queue_url, + WaitTimeSeconds=1, + VisibilityTimeout=0, + MessageAttributeNames=["All"], + AttributeNames=["All"], + ) + for _message in sqs_response["Messages"]: + _received_messages.append(_message) + aws_client.sqs.delete_message( + QueueUrl=_queue_url, ReceiptHandle=_message["ReceiptHandle"] + ) + + assert len(_received_messages) == 2 + + received_messages = [] + retry( + get_messages, + retries=10, + sleep=0.1, + _queue_url=queue_url, + _received_messages=received_messages, + ) + # we need to sort the list (the order does not matter as we're not using FIFO) + received_messages.sort(key=itemgetter("Body")) + snapshot.match("messages", {"Messages": received_messages}) + class TestSNSPlatformEndpoint: @markers.aws.only_localstack diff --git a/tests/aws/services/sns/test_sns.snapshot.json b/tests/aws/services/sns/test_sns.snapshot.json index 6eb7a3fe5cfc4..6002a58044676 100644 --- a/tests/aws/services/sns/test_sns.snapshot.json +++ b/tests/aws/services/sns/test_sns.snapshot.json @@ -5024,7 +5024,7 @@ } }, "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_exists_filter_policy_attributes_array": { - "recorded-date": "02-04-2024, 22:27:18", + "recorded-date": "17-04-2024, 18:10:46", "recorded-content": { "subscription-attributes-policy": { "Attributes": { @@ -5039,7 +5039,7 @@ "Owner": "111111111111", "PendingConfirmation": "false", "Protocol": "sqs", - "RawMessageDelivery": "false", + "RawMessageDelivery": "true", "SubscriptionArn": "arn:aws:sns::111111111111::", "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/", "TopicArn": "arn:aws:sns::111111111111:" @@ -5058,25 +5058,9 @@ "messages-1": { "Messages": [ { - "Body": { - "Type": "Notification", - "MessageId": "", - "TopicArn": "arn:aws:sns::111111111111:", - "Message": "message-1", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "", - "SigningCertURL": "/SimpleNotificationService-", - "UnsubscribeURL": "/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns::111111111111::", - "MessageAttributes": { - "store": { - "Type": "String", - "Value": "value1" - } - } - }, + "Body": "message-1", "MD5OfBody": "", - "MessageId": "", + "MessageId": "", "ReceiptHandle": "" } ], @@ -5088,25 +5072,9 @@ "messages-2": { "Messages": [ { - "Body": { - "Type": "Notification", - "MessageId": "", - "TopicArn": "arn:aws:sns::111111111111:", - "Message": "message-2", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "", - "SigningCertURL": "/SimpleNotificationService-", - "UnsubscribeURL": "/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns::111111111111::", - "MessageAttributes": { - "store": { - "Type": "String.Array", - "Value": "[\"value1\", \"value2\"]" - } - } - }, + "Body": "message-2", "MD5OfBody": "", - "MessageId": "", + "MessageId": "", "ReceiptHandle": "" } ], @@ -5124,20 +5092,8 @@ } }, "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body_array_attributes": { - "recorded-date": "02-04-2024, 22:36:25", + "recorded-date": "17-04-2024, 21:33:10", "recorded-content": { - "recv-init-0": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "recv-init-1": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, "messages-queue-0": { "Messages": [ { @@ -5221,5 +5177,83 @@ ] } } + }, + "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body_array_of_object_attributes": { + "recorded-date": "17-04-2024, 21:32:41", + "recorded-content": { + "messages": { + "Messages": [ + { + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "1", + "SenderId": "", + "SentTimestamp": "timestamp" + }, + "Body": { + "Records": [ + { + "eventSource": "aws:s3", + "eventName": "ObjectCreated:Put", + "s3": { + "object": [ + [ + { + "key": "auto-insurance-2314.xml", + "size": 17 + } + ] + ] + } + }, + { + "eventSource": "aws:s3", + "eventName": "ObjectRemoved:Delete", + "s3": { + "object": { + "key": "home-insurance-2314.xml", + "size": 17 + } + } + } + ] + }, + "MD5OfBody": "", + "MessageId": "", + "ReceiptHandle": "" + }, + { + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "1", + "SenderId": "", + "SentTimestamp": "timestamp" + }, + "Body": { + "Records": [ + { + "eventSource": "aws:s3", + "eventTime": "date", + "eventName": "ObjectCreated:Put", + "s3": { + "bucket": { + "name": "", + "arn": "arn:aws:s3:::" + }, + "object": { + "key": "auto-insurance-2314.xml", + "size": 17 + } + } + } + ] + }, + "MD5OfBody": "", + "MessageId": "", + "ReceiptHandle": "" + } + ] + } + } } } diff --git a/tests/aws/services/sns/test_sns.validation.json b/tests/aws/services/sns/test_sns.validation.json index d122a43bb9c54..c07a5b165442d 100644 --- a/tests/aws/services/sns/test_sns.validation.json +++ b/tests/aws/services/sns/test_sns.validation.json @@ -3,7 +3,7 @@ "last_validated_date": "2023-11-09T20:04:02+00:00" }, "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_exists_filter_policy_attributes_array": { - "last_validated_date": "2024-04-02T22:27:17+00:00" + "last_validated_date": "2024-04-17T18:10:45+00:00" }, "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy": { "last_validated_date": "2024-01-25T18:07:57+00:00" @@ -18,7 +18,10 @@ "last_validated_date": "2023-11-09T19:58:29+00:00" }, "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body_array_attributes": { - "last_validated_date": "2024-04-02T22:36:24+00:00" + "last_validated_date": "2024-04-17T21:33:09+00:00" + }, + "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body_array_of_object_attributes": { + "last_validated_date": "2024-04-17T21:32:40+00:00" }, "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body_dot_attribute": { "last_validated_date": "2023-11-09T17:50:59+00:00" diff --git a/tests/unit/test_sns.py b/tests/unit/test_sns.py index 4a9820486555e..4dcb46580f492 100644 --- a/tests/unit/test_sns.py +++ b/tests/unit/test_sns.py @@ -609,13 +609,33 @@ def test_filter_policy_on_message_body(self): ({"f1": ["v3", "v4"], "f2": "v5"}, False), ), ), + ( + {"f1": {"f2": {"f3": {"f4": ["v1"]}}}}, + ( + ({"f1": {"f2": {"f3": {"f4": "v1"}}}}, True), + ({"f1": [{"f2": {"f3": {"f4": "v1"}}}]}, True), + ({"f1": [{"f2": [{"f3": {"f4": "v1"}}]}]}, True), + ({"f1": [{"f2": [[{"f3": {"f4": "v1"}}]]}]}, True), + ({"f1": [{"f2": [{"f3": {"f4": "v1"}, "f5": {"f6": "v2"}}]}]}, True), + ({"f1": [{"f2": [[{"f3": {"f4": "v2"}}, {"f3": {"f4": "v1"}}]]}]}, True), + ({"f1": [{"f2": {"f3": {"f4": "v2"}}}]}, False), + ({"f1": [{"f2": {"fx": {"f4": "v1"}}}]}, False), + ({"f1": [{"fx": {"f3": {"f4": "v1"}}}]}, False), + ({"fx": [{"f2": {"f3": {"f4": "v1"}}}]}, False), + ({"f1": [{"f2": [{"f3": {"f4": "v2"}, "f5": {"f6": "v3"}}]}]}, False), + ({"f1": [{"f2": [[{"f3": {"f4": "v2"}}, {"f3": {"f4": "v3"}}]]}]}, False), + ), + ), ] sub_filter = SubscriptionFilter() for filter_policy, messages in test_data: for message_body, expected in messages: - assert expected == sub_filter.check_filter_policy_on_message_body( - filter_policy, message_body=json.dumps(message_body) + assert ( + sub_filter.check_filter_policy_on_message_body( + filter_policy, message_body=json.dumps(message_body) + ) + == expected ), (filter_policy, message_body) @pytest.mark.parametrize("region", ["us-east-1", "eu-central-1", "us-west-2", "my-region"]) From 01ac73984395a889b19a5a01fd4cb467d9fae1e0 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Thu, 18 Apr 2024 01:42:23 +0200 Subject: [PATCH 076/169] fix DDB eventName for TransactWriteItems Update inserting data (#10685) --- localstack/services/dynamodb/provider.py | 4 +- tests/aws/services/dynamodb/test_dynamodb.py | 20 ++++++++- .../dynamodb/test_dynamodb.snapshot.json | 42 ++++++++++++++++--- .../dynamodb/test_dynamodb.validation.json | 2 +- 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/localstack/services/dynamodb/provider.py b/localstack/services/dynamodb/provider.py index c364c2f6aea99..6e2ed2480a620 100644 --- a/localstack/services/dynamodb/provider.py +++ b/localstack/services/dynamodb/provider.py @@ -1807,10 +1807,10 @@ def prepare_transact_write_item_records( record["dynamodb"]["StreamViewType"] = stream_type.stream_view_type record["eventID"] = short_uid() - record["eventName"] = "MODIFY" if updated_item else "INSERT" + record["eventName"] = "MODIFY" if existing_item else "INSERT" record["dynamodb"]["Keys"] = keys - if stream_type.needs_old_image: + if existing_item and stream_type.needs_old_image: record["dynamodb"]["OldImage"] = existing_item if stream_type.needs_new_image: record["dynamodb"]["NewImage"] = updated_item diff --git a/tests/aws/services/dynamodb/test_dynamodb.py b/tests/aws/services/dynamodb/test_dynamodb.py index 55028239a62ea..9293d84313477 100644 --- a/tests/aws/services/dynamodb/test_dynamodb.py +++ b/tests/aws/services/dynamodb/test_dynamodb.py @@ -2018,6 +2018,23 @@ def test_transact_write_items_streaming( ) snapshot.match("transact-write-response-update", response) + # use Update to write a new key + response = aws_client.dynamodb.transact_write_items( + TransactItems=[ + { + "Update": { + "TableName": table_name, + "Key": {"id": {"S": "NonExistentKey"}}, + "UpdateExpression": "SET attr1 = :v1", + "ExpressionAttributeValues": { + ":v1": {"S": "value1"}, + }, + } + }, + ] + ) + snapshot.match("transact-write-update-insert", response) + # delete the key response = aws_client.dynamodb.transact_write_items( TransactItems=[ @@ -2036,6 +2053,7 @@ def test_transact_write_items_streaming( # - PutItem # - TransactWriteItem on NewKey insert # - TransactWriteItem on NewKey update + # - TransactWriteItem on NonExistentKey insert # - TransactWriteItem on NewKey delete # - TransactWriteItem on Fred modify via Put # don't send an event when Fred is overwritten with the same value @@ -2052,7 +2070,7 @@ def _get_records_amount(record_amount: int): assert len(records) >= record_amount - retry(lambda: _get_records_amount(5), sleep=1, retries=3) + retry(lambda: _get_records_amount(6), sleep=1, retries=3) snapshot.match("get-records", {"Records": records}) @markers.aws.validated diff --git a/tests/aws/services/dynamodb/test_dynamodb.snapshot.json b/tests/aws/services/dynamodb/test_dynamodb.snapshot.json index 057c97c4782eb..6c0f0bf3bb8ac 100644 --- a/tests/aws/services/dynamodb/test_dynamodb.snapshot.json +++ b/tests/aws/services/dynamodb/test_dynamodb.snapshot.json @@ -857,7 +857,7 @@ } }, "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_transact_write_items_streaming": { - "recorded-date": "15-03-2024, 01:54:32", + "recorded-date": "17-04-2024, 22:45:49", "recorded-content": { "create-table": { "TableDescription": { @@ -944,6 +944,12 @@ "HTTPStatusCode": 200 } }, + "transact-write-update-insert": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, "transact-write-response-delete": { "ResponseMetadata": { "HTTPHeaders": {}, @@ -1032,6 +1038,32 @@ "eventSource": "aws:dynamodb", "eventVersion": "1.1" }, + { + "awsRegion": "", + "dynamodb": { + "ApproximateCreationDateTime": "datetime", + "Keys": { + "id": { + "S": "NonExistentKey" + } + }, + "NewImage": { + "attr1": { + "S": "value1" + }, + "id": { + "S": "NonExistentKey" + } + }, + "SequenceNumber": "", + "SizeBytes": 43, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventID": "", + "eventName": "INSERT", + "eventSource": "aws:dynamodb", + "eventVersion": "1.1" + }, { "awsRegion": "", "dynamodb": { @@ -1052,11 +1084,11 @@ "S": "NewKey" } }, - "SequenceNumber": "", + "SequenceNumber": "", "SizeBytes": 38, "StreamViewType": "NEW_AND_OLD_IMAGES" }, - "eventID": "", + "eventID": "", "eventName": "REMOVE", "eventSource": "aws:dynamodb", "eventVersion": "1.1" @@ -1083,11 +1115,11 @@ "S": "Fred" } }, - "SequenceNumber": "", + "SequenceNumber": "", "SizeBytes": 26, "StreamViewType": "NEW_AND_OLD_IMAGES" }, - "eventID": "", + "eventID": "", "eventName": "MODIFY", "eventSource": "aws:dynamodb", "eventVersion": "1.1" diff --git a/tests/aws/services/dynamodb/test_dynamodb.validation.json b/tests/aws/services/dynamodb/test_dynamodb.validation.json index f2eb8bd4ec09f..b183935cd6284 100644 --- a/tests/aws/services/dynamodb/test_dynamodb.validation.json +++ b/tests/aws/services/dynamodb/test_dynamodb.validation.json @@ -63,7 +63,7 @@ "last_validated_date": "2023-08-23T14:33:37+00:00" }, "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_transact_write_items_streaming": { - "last_validated_date": "2024-03-15T01:54:32+00:00" + "last_validated_date": "2024-04-17T22:45:49+00:00" }, "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_transact_write_items_streaming_for_different_tables": { "last_validated_date": "2024-04-02T21:45:36+00:00" From d858f004ea2498c0d580d47b9770017f46044577 Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Thu, 18 Apr 2024 13:01:22 +0200 Subject: [PATCH 077/169] Add experimental event ruler (#10615) --- Dockerfile | 3 +- Makefile | 3 +- localstack/config.py | 5 + localstack/packages/core.py | 78 +- localstack/services/events/event_ruler.py | 63 ++ localstack/services/events/packages.py | 26 + localstack/services/events/provider.py | 51 +- .../dynamodb_event_source_listener.py | 14 +- .../lambda_/event_source_listeners/utils.py | 12 +- pyproject.toml | 2 + requirements-dev.txt | 3 + requirements-runtime.txt | 3 + requirements-test.txt | 3 + requirements-typehint.txt | 3 + .../cloudformation/resources/test_lambda.py | 2 + .../exists_dynamodb.json5 | 24 + .../exists_dynamodb_NEG.json5 | 22 + .../services/events/test_event_patterns.py | 1 + .../events/test_event_patterns.snapshot.json | 16 + .../test_event_patterns.validation.json | 12 + ...test_lambda_integration_dynamodbstreams.py | 154 ++-- ..._integration_dynamodbstreams.snapshot.json | 776 ++++++++++++++---- ...ntegration_dynamodbstreams.validation.json | 34 +- .../services/lambda_/test_lambda_utils.py | 2 +- 24 files changed, 1076 insertions(+), 236 deletions(-) create mode 100644 localstack/services/events/event_ruler.py create mode 100644 localstack/services/events/packages.py create mode 100644 tests/aws/services/events/event_pattern_templates/exists_dynamodb.json5 create mode 100644 tests/aws/services/events/event_pattern_templates/exists_dynamodb_NEG.json5 diff --git a/Dockerfile b/Dockerfile index 956f8b026f7b8..5809b9a55f7c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -145,7 +145,8 @@ ARG TARGETARCH RUN --mount=type=cache,target=/var/cache/apt \ apt-get update && \ # Install dependencies to add additional repos - apt-get install -y gcc + # g++ is a workaround to fix the JPype1 compile error on ARM Linux "gcc: fatal error: cannot execute ‘cc1plus’" + apt-get install -y gcc g++ # upgrade python build tools RUN --mount=type=cache,target=/root/.cache \ diff --git a/Makefile b/Makefile index 97ecc0640b48c..7c668725e8968 100644 --- a/Makefile +++ b/Makefile @@ -191,9 +191,10 @@ docker-run-tests: ## Initializes the test environment and runs the tests in a docker-run-tests-s3-only: ## Initializes the test environment and runs the tests in a docker container for the S3 only image # TODO: We need node as it's a dependency of the InfraProvisioner at import time, remove when we do not need it anymore + # g++ is a workaround to fix the JPype1 compile error on ARM Linux "gcc: fatal error: cannot execute ‘cc1plus’" because the test dependencies include the runtime dependencies. docker run -e LOCALSTACK_INTERNAL_TEST_COLLECT_METRIC=1 --entrypoint= -v `pwd`/requirements-test.txt:/opt/code/localstack/requirements-test.txt -v `pwd`/tests/:/opt/code/localstack/tests/ -v `pwd`/target/:/opt/code/localstack/target/ -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/localstack:/var/lib/localstack \ $(IMAGE_NAME) \ - bash -c "make install-test && apt-get install -y --no-install-recommends gnupg && mkdir -p /etc/apt/keyrings && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main\" > /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install -y --no-install-recommends nodejs && DEBUG=$(DEBUG) PYTEST_LOGLEVEL=debug PYTEST_ARGS='$(PYTEST_ARGS)' TEST_PATH='$(TEST_PATH)' TINYBIRD_PYTEST_ARGS='$(TINYBIRD_PYTEST_ARGS)' TINYBIRD_DATASOURCE='$(TINYBIRD_DATASOURCE)' TINYBIRD_TOKEN='$(TINYBIRD_TOKEN)' TINYBIRD_URL='$(TINYBIRD_URL)' CI_COMMIT_BRANCH='$(CI_COMMIT_BRANCH)' CI_COMMIT_SHA='$(CI_COMMIT_SHA)' CI_JOB_URL='$(CI_JOB_URL)' CI_JOB_NAME='$(CI_JOB_NAME)' CI_JOB_ID='$(CI_JOB_ID)' CI='$(CI)' make test" + bash -c "apt-get update && apt-get install -y g++ && make install-test && apt-get install -y --no-install-recommends gnupg && mkdir -p /etc/apt/keyrings && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main\" > /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install -y --no-install-recommends nodejs && DEBUG=$(DEBUG) PYTEST_LOGLEVEL=debug PYTEST_ARGS='$(PYTEST_ARGS)' TEST_PATH='$(TEST_PATH)' TINYBIRD_PYTEST_ARGS='$(TINYBIRD_PYTEST_ARGS)' TINYBIRD_DATASOURCE='$(TINYBIRD_DATASOURCE)' TINYBIRD_TOKEN='$(TINYBIRD_TOKEN)' TINYBIRD_URL='$(TINYBIRD_URL)' CI_COMMIT_BRANCH='$(CI_COMMIT_BRANCH)' CI_COMMIT_SHA='$(CI_COMMIT_SHA)' CI_JOB_URL='$(CI_JOB_URL)' CI_JOB_NAME='$(CI_JOB_NAME)' CI_JOB_ID='$(CI_JOB_ID)' CI='$(CI)' make test" docker-run: ## Run Docker image locally diff --git a/localstack/config.py b/localstack/config.py index b60e912428727..34ff7bca0bc62 100644 --- a/localstack/config.py +++ b/localstack/config.py @@ -762,6 +762,11 @@ def populate_edge_configuration( # get-function call. INTERNAL_RESOURCE_ACCOUNT = os.environ.get("INTERNAL_RESOURCE_ACCOUNT") or "949334387222" +# Determine which implementation to use for the event rule / event filtering engine used by multiple services: +# EventBridge, EventBridge Pipes, Lambda Event Source Mapping, SNS +# Options: provider (default) | java +EVENT_RULE_ENGINE = os.environ.get("EVENT_RULE_ENGINE", "").strip() + # ----- # SERVICE-SPECIFIC CONFIGS BELOW # ----- diff --git a/localstack/packages/core.py b/localstack/packages/core.py index c1fb7cb7b62b3..153e89ab13484 100644 --- a/localstack/packages/core.py +++ b/localstack/packages/core.py @@ -4,13 +4,13 @@ from abc import ABC from functools import lru_cache from sys import version_info -from typing import Optional +from typing import Optional, Tuple import requests from localstack import config -from ..constants import LOCALSTACK_VENV_FOLDER +from ..constants import LOCALSTACK_VENV_FOLDER, MAVEN_REPO_URL from ..utils.archives import download_and_extract from ..utils.files import chmod_r, chown_r, mkdir, rm_rf from ..utils.http import download @@ -295,3 +295,77 @@ def _install(self, target: InstallTarget) -> None: def _setup_existing_installation(self, target: InstallTarget) -> None: """If the venv is already present, it just needs to be initialized once.""" self._prepare_installation(target) + + +class MavenDownloadInstaller(DownloadInstaller): + """The packageURL is easy copy/pastable from the Maven central repository and the first package URL + defines the package name and version. + Example package_url: pkg:maven/software.amazon.event.ruler/event-ruler@1.7.3 + => name: event-ruler + => version: 1.7.3 + """ + + # Example: software.amazon.event.ruler + group_id: str + # Example: event-ruler + artifact_id: str + + # Custom installation directory + install_dir_suffix: str | None + + def __init__(self, package_url: str, install_dir_suffix: str | None = None): + self.group_id, self.artifact_id, version = parse_maven_package_url(package_url) + super().__init__(self.artifact_id, version) + self.install_dir_suffix = install_dir_suffix + + def _get_download_url(self) -> str: + group_id_path = self.group_id.replace(".", "/") + return f"{MAVEN_REPO_URL}/{group_id_path}/{self.artifact_id}/{self.version}/{self.artifact_id}-{self.version}.jar" + + def _get_install_dir(self, target: InstallTarget) -> str: + """Allow to overwrite the default installation directory. + This enables downloading transitive dependencies into the same directory. + """ + if self.install_dir_suffix: + return os.path.join(target.value, self.install_dir_suffix) + else: + return super()._get_install_dir(target) + + +class MavenPackageInstaller(MavenDownloadInstaller): + """Package installer for downloading Maven JARs, including optional dependencies. + The first Maven package is used as main LPM package and other dependencies are installed additionally. + Follows the Maven naming conventions: https://maven.apache.org/guides/mini/guide-naming-conventions.html + """ + + # Installers for Maven dependencies + dependencies: list[MavenDownloadInstaller] + + def __init__(self, *package_urls: str): + super().__init__(package_urls[0]) + self.dependencies = [] + + # Create installers for dependencies + for package_url in package_urls[1:]: + install_dir_suffix = os.path.join(self.name, self.version) + self.dependencies.append(MavenDownloadInstaller(package_url, install_dir_suffix)) + + def _install(self, target: InstallTarget) -> None: + # Install all dependencies first + for dependency in self.dependencies: + dependency._install(target) + # Install the main Maven package once all dependencies are installed. + # This main package indicates whether all dependencies are installed. + super()._install(target) + + +def parse_maven_package_url(package_url: str) -> Tuple[str, str, str]: + """Example: parse_maven_package_url("pkg:maven/software.amazon.event.ruler/event-ruler@1.7.3") + -> software.amazon.event.ruler, event-ruler, 1.7.3 + """ + parts = package_url.split("/") + group_id = parts[1] + sub_parts = parts[2].split("@") + artifact_id = sub_parts[0] + version = sub_parts[1] + return group_id, artifact_id, version diff --git a/localstack/services/events/event_ruler.py b/localstack/services/events/event_ruler.py new file mode 100644 index 0000000000000..a9210689566cd --- /dev/null +++ b/localstack/services/events/event_ruler.py @@ -0,0 +1,63 @@ +import logging +import os +from functools import cache +from pathlib import Path + +from localstack import config +from localstack.services.events.packages import event_ruler_package +from localstack.services.events.utils import InvalidEventPatternException +from localstack.utils.objects import singleton_factory + +THIS_FOLDER = os.path.dirname(os.path.realpath(__file__)) + +LOG = logging.getLogger(__name__) + + +@singleton_factory +def start_jvm() -> None: + import jpype + from jpype import config as jpype_config + + # Workaround to unblock LocalStack shutdown. By default, JPype waits until all daemon threads are terminated, + # which blocks the LocalStack shutdown during testing because pytest runs LocalStack in a separate thread and + # `jpype.shutdownJVM()` only works from the main Python thread. + # Shutting down the JVM: https://jpype.readthedocs.io/en/latest/userguide.html#shutting-down-the-jvm + # JPype shutdown discussion: https://github.com/MPh-py/MPh/issues/15#issuecomment-778486669 + jpype_config.destroy_jvm = False + + if not jpype.isJVMStarted(): + event_ruler_libs_path = get_event_ruler_libs_path() + event_ruler_libs_pattern = event_ruler_libs_path.joinpath("*") + jpype.startJVM(classpath=[event_ruler_libs_pattern]) + + +@cache +def get_event_ruler_libs_path() -> Path: + installer = event_ruler_package.get_installer() + installer.install() + return Path(installer.get_installed_dir()) + + +def matches_rule(event: str, rule: str) -> bool: + """Invokes the AWS Event Ruler Java library: https://github.com/aws/event-ruler + There is a single static boolean method Ruler.matchesRule(event, rule) - + both arguments are provided as JSON strings. + """ + if config.EVENT_RULE_ENGINE != "java": + raise NotImplementedError("Set EVENT_RULE_ENGINE=java to enable the Java Event Ruler.") + + start_jvm() + import jpype.imports # noqa F401: required for importing Java modules + from jpype import java + + # Import of the Java class "Ruler" needs to happen after the JVM start + from software.amazon.event.ruler import Ruler + + try: + # "Static rule matching" is the easiest implementation to get started. + # "Matching with a machine" using a compiled machine is faster and enables rule validation before matching. + # https://github.com/aws/event-ruler?tab=readme-ov-file#matching-with-a-machine + return Ruler.matchesRule(event, rule) + except java.lang.Exception as e: + reason = e.args[0] + raise InvalidEventPatternException(reason=reason) from e diff --git a/localstack/services/events/packages.py b/localstack/services/events/packages.py new file mode 100644 index 0000000000000..5686b6844d454 --- /dev/null +++ b/localstack/services/events/packages.py @@ -0,0 +1,26 @@ +from localstack.packages import Package, PackageInstaller +from localstack.packages.core import MavenPackageInstaller + +# https://central.sonatype.com/artifact/software.amazon.event.ruler/event-ruler +EVENT_RULER_VERSION = "1.7.3" +# The dependent jackson.version is defined in the Maven POM File of event-ruler +JACKSON_VERSION = "2.16.2" + + +class EventRulerPackage(Package): + def __init__(self): + super().__init__("EventRulerLibs", EVENT_RULER_VERSION) + + def get_versions(self) -> list[str]: + return [EVENT_RULER_VERSION] + + def _get_installer(self, version: str) -> PackageInstaller: + return MavenPackageInstaller( + f"pkg:maven/software.amazon.event.ruler/event-ruler@{EVENT_RULER_VERSION}", + f"pkg:maven/com.fasterxml.jackson.core/jackson-annotations@{JACKSON_VERSION}", + f"pkg:maven/com.fasterxml.jackson.core/jackson-core@{JACKSON_VERSION}", + f"pkg:maven/com.fasterxml.jackson.core/jackson-databind@{JACKSON_VERSION}", + ) + + +event_ruler_package = EventRulerPackage() diff --git a/localstack/services/events/provider.py b/localstack/services/events/provider.py index 1d8b93a1c7bef..c0f683d549aa9 100644 --- a/localstack/services/events/provider.py +++ b/localstack/services/events/provider.py @@ -24,6 +24,7 @@ EventBusNameOrArn, EventPattern, EventsApi, + InvalidEventPatternException, PutRuleResponse, PutTargetsResponse, RoleArn, @@ -39,8 +40,12 @@ from localstack.constants import APPLICATION_AMZ_JSON_1_1 from localstack.http import route from localstack.services.edge import ROUTER +from localstack.services.events.event_ruler import matches_rule from localstack.services.events.models import EventsStore, events_stores from localstack.services.events.scheduler import JobScheduler +from localstack.services.events.utils import ( + InvalidEventPatternException as InternalInvalidEventPatternException, +) from localstack.services.events.utils import matches_event from localstack.services.moto import call_moto from localstack.services.plugins import ServiceLifecycleHook @@ -110,28 +115,44 @@ def test_event_pattern( """Test event pattern uses EventBridge event pattern matching: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html """ - event_pattern_dict = json.loads(event_pattern) - event_dict = json.loads(event) - - # TODO: unify all these different implementation below ;) - - # EventBridge implementation: - result = matches_event(event_pattern_dict, event_dict) - - # EventSourceMapping implementation: + if config.EVENT_RULE_ENGINE == "java": + try: + result = matches_rule(event, event_pattern) + except InternalInvalidEventPatternException as e: + raise InvalidEventPatternException(e.message) from e + else: + event_pattern_dict = json.loads(event_pattern) + event_dict = json.loads(event) + result = matches_event(event_pattern_dict, event_dict) + + # TODO: unify the different implementations below: + # event_pattern_dict = json.loads(event_pattern) + # event_dict = json.loads(event) + + # EventBridge: + # result = matches_event(event_pattern_dict, event_dict) + + # Lambda EventSourceMapping: + # from localstack.services.lambda_.event_source_listeners.utils import does_match_event + # # result = does_match_event(event_pattern_dict, event_dict) - # moto implementation: + # moto-ext EventBridge: # from moto.events.models import EventPattern as EventPatternMoto # # event_pattern = EventPatternMoto.load(event_pattern) # result = event_pattern.matches_event(event_dict) - # SNS: + # SNS: The SNS rule engine seems to differ slightly, for example not allowing the wildcard pattern. # from localstack.services.sns.publisher import SubscriptionFilter # subscription_filter = SubscriptionFilter() # result = subscription_filter._evaluate_nested_filter_policy_on_dict(event_pattern_dict, event_dict) + # moto-ext SNS: + # from moto.sns.utils import FilterPolicyMatcher + # filter_policy_matcher = FilterPolicyMatcher(event_pattern_dict, "MessageBody") + # result = filter_policy_matcher._body_based_match(event_dict) + return TestEventPatternResponse(Result=result) @staticmethod @@ -409,7 +430,13 @@ def filter_event_based_on_event_format( return False if rule_information.event_pattern._pattern: event_pattern = rule_information.event_pattern._pattern - if not matches_event(event_pattern, event): + if config.EVENT_RULE_ENGINE == "java": + event_str = json.dumps(event) + event_pattern_str = json.dumps(event_pattern) + match_result = matches_rule(event_str, event_pattern_str) + else: + match_result = matches_event(event_pattern, event) + if not match_result: return False return True diff --git a/localstack/services/lambda_/event_source_listeners/dynamodb_event_source_listener.py b/localstack/services/lambda_/event_source_listeners/dynamodb_event_source_listener.py index 73ca7e87a329a..a9724c9056ffb 100644 --- a/localstack/services/lambda_/event_source_listeners/dynamodb_event_source_listener.py +++ b/localstack/services/lambda_/event_source_listeners/dynamodb_event_source_listener.py @@ -50,9 +50,6 @@ def _filter_records( def _create_lambda_event_payload(self, stream_arn, records, shard_id=None): record_payloads = [] for record in records: - creation_time = record.get("dynamodb", {}).get("ApproximateCreationDateTime", None) - if creation_time is not None: - record["dynamodb"]["ApproximateCreationDateTime"] = creation_time.timestamp() record_payloads.append( { "eventID": record["eventID"], @@ -76,3 +73,14 @@ def _get_first_and_last_arrival_time(self, first_record, last_record): last_record.get("ApproximateArrivalTimestamp", datetime.datetime.utcnow()).isoformat() + "Z", ) + + def _transform_records(self, raw_records: list[dict]) -> list[dict]: + """Convert dynamodb.ApproximateCreationDateTime datetime to float""" + records_new = [] + for record in raw_records: + record_new = record.copy() + if creation_time := record.get("dynamodb", {}).get("ApproximateCreationDateTime"): + # convert datetime object to float timestamp + record_new["dynamodb"]["ApproximateCreationDateTime"] = creation_time.timestamp() + records_new.append(record_new) + return records_new diff --git a/localstack/services/lambda_/event_source_listeners/utils.py b/localstack/services/lambda_/event_source_listeners/utils.py index e32c604e70bcd..d111b3c8ed70d 100644 --- a/localstack/services/lambda_/event_source_listeners/utils.py +++ b/localstack/services/lambda_/event_source_listeners/utils.py @@ -2,7 +2,9 @@ import logging import re +from localstack import config from localstack.aws.api.lambda_ import FilterCriteria +from localstack.services.events.event_ruler import matches_rule from localstack.utils.strings import first_char_to_lower LOG = logging.getLogger(__name__) @@ -21,8 +23,14 @@ def filter_stream_records(records, filters: list[FilterCriteria]): for record in records: for filter in filters: for rule in filter["Filters"]: - filter_pattern: dict[str, any] = json.loads(rule["Pattern"]) - if does_match_event(filter_pattern, record): + if config.EVENT_RULE_ENGINE == "java": + event_str = json.dumps(record) + event_pattern_str = rule["Pattern"] + match_result = matches_rule(event_str, event_pattern_str) + else: + filter_pattern: dict[str, any] = json.loads(rule["Pattern"]) + match_result = does_match_event(filter_pattern, record) + if match_result: filtered_records.append(record) break return filtered_records diff --git a/pyproject.toml b/pyproject.toml index a769e4aed271e..cbe71963c4bf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,6 +85,8 @@ runtime = [ "aws-sam-translator>=1.15.1", "crontab>=0.22.6", "cryptography>=41.0.5", + # allow Python programs full access to Java class libraries. Used for opt-in event ruler. + "JPype1>=1.5.0", "json5>=0.9.11", "jsonpath-ng>=1.6.1", "jsonpath-rw>=1.4.0", diff --git a/requirements-dev.txt b/requirements-dev.txt index 4a3e5545519fe..10b47f7ac93f2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -216,6 +216,8 @@ jmespath==1.0.1 # botocore joserfc==0.9.0 # via moto-ext +jpype1==1.5.0 + # via localstack-core jschema-to-python==1.2.3 # via cfn-lint jsii==1.97.0 @@ -298,6 +300,7 @@ packaging==24.0 # apispec # build # docker + # jpype1 # pytest # pytest-rerunfailures pandoc==2.3 diff --git a/requirements-runtime.txt b/requirements-runtime.txt index f893976de212f..ad292ff736477 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -163,6 +163,8 @@ jmespath==1.0.1 # botocore joserfc==0.9.0 # via moto-ext +jpype1==1.5.0 + # via localstack-core (pyproject.toml) jschema-to-python==1.2.3 # via cfn-lint json5==0.9.25 @@ -229,6 +231,7 @@ packaging==24.0 # apispec # build # docker + # jpype1 pathable==0.4.3 # via jsonschema-path pbr==6.0.0 diff --git a/requirements-test.txt b/requirements-test.txt index efb8d297492c2..216f47725eb4f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -200,6 +200,8 @@ jmespath==1.0.1 # botocore joserfc==0.9.0 # via moto-ext +jpype1==1.5.0 + # via localstack-core jschema-to-python==1.2.3 # via cfn-lint jsii==1.97.0 @@ -278,6 +280,7 @@ packaging==24.0 # apispec # build # docker + # jpype1 # pytest # pytest-rerunfailures pathable==0.4.3 diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 0c628ea289950..eb138acd441ea 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -220,6 +220,8 @@ jmespath==1.0.1 # botocore joserfc==0.9.0 # via moto-ext +jpype1==1.5.0 + # via localstack-core jschema-to-python==1.2.3 # via cfn-lint jsii==1.97.0 @@ -494,6 +496,7 @@ packaging==24.0 # apispec # build # docker + # jpype1 # pytest # pytest-rerunfailures pandoc==2.3 diff --git a/tests/aws/services/cloudformation/resources/test_lambda.py b/tests/aws/services/cloudformation/resources/test_lambda.py index 1cb361daa9bc0..75b8be84a0632 100644 --- a/tests/aws/services/cloudformation/resources/test_lambda.py +++ b/tests/aws/services/cloudformation/resources/test_lambda.py @@ -569,6 +569,8 @@ def wait_logs(): with pytest.raises(aws_client.lambda_.exceptions.ResourceNotFoundException): aws_client.lambda_.get_event_source_mapping(UUID=esm_id) + # TODO: consider moving into the dedicated DynamoDB => Lambda tests + # tests.aws.services.lambda_.test_lambda_integration_dynamodbstreams.TestDynamoDBEventSourceMapping.test_dynamodb_event_filter @markers.aws.validated def test_lambda_dynamodb_event_filter( self, dynamodb_wait_for_table_active, deploy_cfn_template, aws_client diff --git a/tests/aws/services/events/event_pattern_templates/exists_dynamodb.json5 b/tests/aws/services/events/event_pattern_templates/exists_dynamodb.json5 new file mode 100644 index 0000000000000..00594198d50b4 --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/exists_dynamodb.json5 @@ -0,0 +1,24 @@ +// DynamoDB Stream Tutorial: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.Lambda.Tutorial2.html +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "dynamodb": { + "id": {"S": "test1234"}, + "presentKey": {"S": "test123"} + } + }, + "EventPattern": { + "dynamodb": { + // "Exists matching only works on leaf nodes. It does not work on intermediate nodes." + // https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-exists-matching + "presentKey": { + "S": [{"exists": true}] + } + } + } +} diff --git a/tests/aws/services/events/event_pattern_templates/exists_dynamodb_NEG.json5 b/tests/aws/services/events/event_pattern_templates/exists_dynamodb_NEG.json5 new file mode 100644 index 0000000000000..5c0976ac373eb --- /dev/null +++ b/tests/aws/services/events/event_pattern_templates/exists_dynamodb_NEG.json5 @@ -0,0 +1,22 @@ +// DynamoDB Stream Tutorial: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.Lambda.Tutorial2.html +{ + "Event": { + "id": "1", + "source": "test-source", + "detail-type": "test-detail-type", + "account": "123456789012", + "region": "us-east-2", + "time": "2022-07-13T13:48:01Z", + "dynamodb": { + "id": {"S": "test1234"}, + "presentKey": {"S": "test123"} + } + }, + "EventPattern": { + "dynamodb": { + // "Exists matching only works on leaf nodes. It does not work on intermediate nodes." + // https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-exists-matching + "presentKey": [{"exists": true}] + } + } +} diff --git a/tests/aws/services/events/test_event_patterns.py b/tests/aws/services/events/test_event_patterns.py index 6e3a0a49a3f99..7c31c3731bee2 100644 --- a/tests/aws/services/events/test_event_patterns.py +++ b/tests/aws/services/events/test_event_patterns.py @@ -70,6 +70,7 @@ def list_files_with_suffix(directory_path: str, suffix: str) -> List[str]: "content_wildcard_simplified", "dot_joining_event", "dot_joining_pattern", + "exists_dynamodb_NEG", "nested_json_NEG", "or-exists", "or-exists-parent", diff --git a/tests/aws/services/events/test_event_patterns.snapshot.json b/tests/aws/services/events/test_event_patterns.snapshot.json index 1fe5309849faa..1f50547e900b6 100644 --- a/tests/aws/services/events/test_event_patterns.snapshot.json +++ b/tests/aws/services/events/test_event_patterns.snapshot.json @@ -414,5 +414,21 @@ "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[list_within_dict]": { "recorded-date": "08-04-2024, 19:33:54", "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[exists_false_dynamodb_NEG]": { + "recorded-date": "09-04-2024, 16:47:27", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[exists_false_dynamodb]": { + "recorded-date": "09-04-2024, 16:21:59", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[exists_dynamodb]": { + "recorded-date": "09-04-2024, 16:51:30", + "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[exists_dynamodb_NEG]": { + "recorded-date": "09-04-2024, 16:51:59", + "recorded-content": {} } } diff --git a/tests/aws/services/events/test_event_patterns.validation.json b/tests/aws/services/events/test_event_patterns.validation.json index 12606f07e3c66..ef63c055e6d26 100644 --- a/tests/aws/services/events/test_event_patterns.validation.json +++ b/tests/aws/services/events/test_event_patterns.validation.json @@ -173,6 +173,18 @@ "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dynamodb]": { "last_validated_date": "2024-04-08T19:33:59+00:00" }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[exists_dynamodb]": { + "last_validated_date": "2024-04-09T16:51:30+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[exists_dynamodb_NEG]": { + "last_validated_date": "2024-04-09T16:51:59+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[exists_false_dynamodb]": { + "last_validated_date": "2024-04-09T16:21:59+00:00" + }, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[exists_false_dynamodb_NEG]": { + "last_validated_date": "2024-04-09T16:47:27+00:00" + }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[int_nolist_EXC]": { "last_validated_date": "2024-04-08T19:33:55+00:00" }, diff --git a/tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py b/tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py index c8cb7d0540c56..d7abab21d6e53 100644 --- a/tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py +++ b/tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py @@ -411,67 +411,110 @@ def verify_failure_received(): messages = retry(verify_failure_received, retries=15, sleep=sleep, sleep_before=5) snapshot.match("destination_queue_messages", messages) - @markers.aws.validated + # TODO: consider re-designing this test case because it currently does negative testing for the second event, + # which can be unreliable due to undetermined waiting times (i.e., retries). For reliable testing, we need + # a) strict event ordering and b) a final event that passes all filters to reliably determine the end of the test. + # The current behavior leads to hard-to-detect false negatives such as in this CI run: + # https://app.circleci.com/pipelines/github/localstack/localstack/24012/workflows/461664c2-0203-45f9-aec2-394666f48f03/jobs/197705/tests @pytest.mark.parametrize( + # Calls represents the expected number of Lambda invocations (either 1 or 2). + # Negative tests with calls=0 are unreliable due to undetermined waiting times. "item_to_put1, item_to_put2, filter, calls", [ # Test with filter, and two times same entry - ( - {"id": {"S": "test123"}, "id2": {"S": "test42"}}, - None, + pytest.param( + {"id": {"S": "id_value"}, "id2": {"S": "id2_value"}}, + # Inserting the same event (identified by PK) twice triggers a MODIFY event. + {"id": {"S": "id_value"}, "id2": {"S": "id2_value"}}, {"eventName": ["INSERT"]}, 1, + id="insert_same_entry_twice", ), # Test with OR filter - ( - {"id": {"S": "test123"}}, - {"id": {"S": "test123"}, "id2": {"S": "42test"}}, + pytest.param( + {"id": {"S": "id_value"}}, + {"id": {"S": "id_value"}, "id2": {"S": "id2_new_value"}}, {"eventName": ["INSERT", "MODIFY"]}, 2, + id="content_or_filter", ), # Test with 2 filters (AND), and two times same entry (second time modified aka MODIFY eventName) - ( - {"id": {"S": "test123"}}, - {"id": {"S": "test123"}, "id2": {"S": "42test"}}, + pytest.param( + {"id": {"S": "id_value"}}, + {"id": {"S": "id_value"}, "id2": {"S": "id2_new_value"}}, {"eventName": ["INSERT"], "eventSource": ["aws:dynamodb"]}, 1, + id="content_multiple_filters", ), - # Test exists filter - ( - {"id": {"S": "test123"}}, - {"id": {"S": "test1234"}, "presentKey": {"S": "test123"}}, - {"dynamodb": {"NewImage": {"presentKey": [{"exists": False}]}}}, + # Test content filter using the DynamoDB data type "S" + pytest.param( + {"id": {"S": "id_value_1"}, "presentKey": {"S": "presentValue"}}, + {"id": {"S": "id_value_2"}}, + # Omitting the "S" does NOT match: {"dynamodb": {"NewImage": {"presentKey": ["presentValue"]}}} + {"dynamodb": {"NewImage": {"presentKey": {"S": ["presentValue"]}}}}, 1, + id="content_filter_type", ), - # numeric filters - # NOTE: numeric filters seem not to work with DynamoDB as the values are represented as string - # and it looks like that there is no conversion happening - # I leave the test here in case this changes in future. - ( - {"id": {"S": "test123"}, "numericFilter": {"N": "123"}}, - {"id": {"S": "test1234"}, "numericFilter": {"N": "12"}}, - {"dynamodb": {"NewImage": {"numericFilter": {"N": [{"numeric": [">", 100]}]}}}}, - 0, - ), - ( - {"id": {"S": "test123"}, "numericFilter": {"N": "100"}}, - {"id": {"S": "test1234"}, "numericFilter": {"N": "12"}}, - { - "dynamodb": { - "NewImage": {"numericFilter": {"N": [{"numeric": [">=", 100, "<", 200]}]}} - } - }, - 0, + # Test exists filter using the DynamoDB data type "S" + pytest.param( + {"id": {"S": "id_value_1"}, "presentKey": {"S": "presentValue"}}, + {"id": {"S": "id_value_2"}}, + # Omitting the "S" does NOT match: {"dynamodb": {"NewImage": {"presentKey": [{"exists": True}]}}} + {"dynamodb": {"NewImage": {"presentKey": {"S": [{"exists": True}]}}}}, + 1, + id="exists_filter_type", ), + # TODO: Fix native LocalStack implementation for exists + # pytest.param( + # {"id": {"S": "id_value_1"}}, + # {"id": {"S": "id_value_2"}, "presentKey": {"S": "presentValue"}}, + # {"dynamodb": {"NewImage": {"presentKey": [{"exists": False}]}}}, + # 2, + # id="exists_false_filter", + # ), + # numeric filter + # NOTE: numeric filters do not work with DynamoDB because all values are represented as string + # and not converted to numbers for filtering. + # The following AWS tutorial has a note about numeric filtering, which does not apply to DynamoDB strings: + # https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.Lambda.Tutorial2.html + # TODO: Fix native LocalStack implementation for anything-but + # pytest.param( + # {"id": {"S": "id_value_1"}, "numericFilter": {"N": "42"}}, + # {"id": {"S": "id_value_2"}, "numericFilter": {"N": "101"}}, + # { + # "dynamodb": { + # "NewImage": { + # "numericFilter": { + # # Filtering passes if at least one of the filter conditions matches + # "N": [{"numeric": [">", 100]}, {"anything-but": "101"}] + # } + # } + # } + # }, + # 1, + # id="numeric_filter", + # ), # Prefix - ( - {"id": {"S": "test123"}, "prefix": {"S": "us-1-testtest"}}, - {"id": {"S": "test1234"}, "prefix": {"S": "testtest"}}, + pytest.param( + {"id": {"S": "id_value_1"}, "prefix": {"S": "us-1-other-suffix"}}, + {"id": {"S": "id_value_1"}, "prefix": {"S": "other-suffix"}}, {"dynamodb": {"NewImage": {"prefix": {"S": [{"prefix": "us-1"}]}}}}, 1, + id="prefix_filter", + ), + # DynamoDB ApproximateCreationDateTime (datetime) gets converted into a float BEFORE filtering + # https://docs.aws.amazon.com/lambda/latest/dg/invocation-eventfiltering.html#filtering-ddb + # Using a numeric operator implicitly checks whether ApproximateCreationDateTime is a numeric type + pytest.param( + {"id": {"S": "id_value_1"}}, + {"id": {"S": "id_value_2"}}, + {"dynamodb": {"ApproximateCreationDateTime": [{"numeric": [">", 0]}]}}, + 2, + id="date_time_conversion", ), ], ) + @markers.aws.validated def test_dynamodb_event_filter( self, create_lambda_function, @@ -486,6 +529,14 @@ def test_dynamodb_event_filter( snapshot, aws_client, ): + """Test event filtering for DynamoDB streams: + https://docs.aws.amazon.com/lambda/latest/dg/invocation-eventfiltering.html#filtering-ddb + + Slow against AWS taking ~2min per test case. + + Test assumption: The first item MUST always match the filter and the second item CAN match the filter. + => This enables two-step testing (i.e., snapshots between inserts) but is unreliable and should be revised. + """ function_name = f"lambda_func-{short_uid()}" table_name = f"test-table-{short_uid()}" max_retries = 50 @@ -493,7 +544,7 @@ def test_dynamodb_event_filter( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, role=lambda_su_role, ) table_creation_response = dynamodb_create_table(table_name=table_name, partition_key="id") @@ -530,28 +581,22 @@ def test_dynamodb_event_filter( snapshot.match("create_event_source_mapping_response", create_event_source_mapping_response) _await_event_source_mapping_enabled(aws_client.lambda_, event_source_uuid) + + # Insert item_to_put1 aws_client.dynamodb.put_item(TableName=table_name, Item=item_to_put1) def assert_lambda_called(): events = get_lambda_log_events(function_name, logs_client=aws_client.logs) - if calls > 0: - assert len(events) == 1 - else: - # negative test for 'numeric' filter - assert len(events) == 0 + assert len(events) == 1 return events events = retry(assert_lambda_called, retries=max_retries) snapshot.match("lambda-log-events", events) - # Following lines are relevant if variables are set via parametrize - if item_to_put2: - # putting a new item (item_to_put2) a second time is a 'INSERT' request - aws_client.dynamodb.put_item(TableName=table_name, Item=item_to_put2) - else: - # putting the same item (item_to_put1) a second time is a 'MODIFY' request (at least in Localstack) - aws_client.dynamodb.put_item(TableName=table_name, Item=item_to_put1) - # depending on the parametrize values the filter (and the items to put) the lambda might be called multiple times + # Insert item_to_put2 + aws_client.dynamodb.put_item(TableName=table_name, Item=item_to_put2) + + # The Lambda might be called multiple times depending on the items to put and filter. if calls > 1: def assert_events_called_multiple(): @@ -564,6 +609,13 @@ def assert_events_called_multiple(): else: # lambda wasn't called a second time, so no new records should be found events = retry(assert_lambda_called, retries=max_retries) + + # Validate events containing either one or two records + for event in events: + for record in event["Records"]: + if creation_time := record.get("dynamodb", {}).get("ApproximateCreationDateTime"): + # Ensure the timestamp is in the right format (e.g., no unserializable datetime) + assert isinstance(creation_time, float) snapshot.match("lambda-multiple-log-events", events) @markers.aws.validated diff --git a/tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.snapshot.json b/tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.snapshot.json index 981f413b22aed..9cb250ca6ecc0 100644 --- a/tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.snapshot.json +++ b/tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.snapshot.json @@ -405,8 +405,120 @@ } } }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put10-None-filter0-1]": { - "recorded-date": "27-02-2023, 18:37:56", + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_invalid_event_filter[single-string]": { + "recorded-date": "27-02-2023, 18:44:12", + "recorded-content": { + "exception_event_source_creation": { + "Error": { + "Code": "InvalidParameterValueException", + "Message": "Invalid filter pattern definition." + }, + "Type": "User", + "message": "Invalid filter pattern definition.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_invalid_event_filter[[{\"eventName\": [\"INSERT\"=123}]]": { + "recorded-date": "27-02-2023, 18:44:25", + "recorded-content": { + "exception_event_source_creation": { + "Error": { + "Code": "InvalidParameterValueException", + "Message": "Invalid filter pattern definition." + }, + "Type": "User", + "message": "Invalid filter pattern definition.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_duplicate_event_source_mappings": { + "recorded-date": "04-01-2024, 17:25:30", + "recorded-content": { + "create-table-result": { + "TableDescription": { + "AttributeDefinitions": [ + { + "AttributeName": "my_partition_key", + "AttributeType": "S" + } + ], + "BillingModeSummary": { + "BillingMode": "PAY_PER_REQUEST" + }, + "CreationDateTime": "datetime", + "DeletionProtectionEnabled": false, + "ItemCount": 0, + "KeySchema": [ + { + "AttributeName": "my_partition_key", + "KeyType": "HASH" + } + ], + "ProvisionedThroughput": { + "NumberOfDecreasesToday": 0, + "ReadCapacityUnits": 0, + "WriteCapacityUnits": 0 + }, + "TableArn": "arn:aws:dynamodb::111111111111:table/", + "TableId": "", + "TableName": "", + "TableSizeBytes": 0, + "TableStatus": "CREATING" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "create": { + "BatchSize": 100, + "BisectBatchOnFunctionError": false, + "DestinationConfig": { + "OnFailure": {} + }, + "EventSourceArn": "arn:aws:dynamodb::111111111111:table//stream/", + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionResponseTypes": [], + "LastModified": "datetime", + "LastProcessingResult": "No records processed", + "MaximumBatchingWindowInSeconds": 1, + "MaximumRecordAgeInSeconds": -1, + "MaximumRetryAttempts": 1, + "ParallelizationFactor": 1, + "StartingPosition": "TRIM_HORIZON", + "State": "Creating", + "StateTransitionReason": "User action", + "TumblingWindowInSeconds": 0, + "UUID": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 202 + } + }, + "error": { + "Error": { + "Code": "ResourceConflictException", + "Message": "The event source arn (\" arn:aws:dynamodb::111111111111:table//stream/ \") and function (\" \") provided mapping already exists. Please update or delete the existing mapping with UUID " + }, + "Type": "User", + "message": "The event source arn (\" arn:aws:dynamodb::111111111111:table//stream/ \") and function (\" \") provided mapping already exists. Please update or delete the existing mapping with UUID ", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 409 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[insert_same_entry_twice]": { + "recorded-date": "11-04-2024, 20:49:00", "recorded-content": { "table_creation_response": { "TableDescription": { @@ -420,6 +532,7 @@ "BillingMode": "PAY_PER_REQUEST" }, "CreationDateTime": "datetime", + "DeletionProtectionEnabled": false, "ItemCount": 0, "KeySchema": [ { @@ -492,19 +605,19 @@ "ApproximateCreationDateTime": "", "Keys": { "id": { - "S": "test123" + "S": "id_value" } }, "NewImage": { "id2": { - "S": "test42" + "S": "id2_value" }, "id": { - "S": "test123" + "S": "id_value" } }, "SequenceNumber": "", - "SizeBytes": 27, + "SizeBytes": 32, "StreamViewType": "NEW_AND_OLD_IMAGES" }, "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" @@ -525,19 +638,19 @@ "ApproximateCreationDateTime": "", "Keys": { "id": { - "S": "test123" + "S": "id_value" } }, "NewImage": { "id2": { - "S": "test42" + "S": "id2_value" }, "id": { - "S": "test123" + "S": "id_value" } }, "SequenceNumber": "", - "SizeBytes": 27, + "SizeBytes": 32, "StreamViewType": "NEW_AND_OLD_IMAGES" }, "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" @@ -547,8 +660,8 @@ ] } }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put11-item_to_put21-filter1-2]": { - "recorded-date": "27-02-2023, 18:39:17", + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[content_or_filter]": { + "recorded-date": "11-04-2024, 20:50:44", "recorded-content": { "table_creation_response": { "TableDescription": { @@ -562,6 +675,7 @@ "BillingMode": "PAY_PER_REQUEST" }, "CreationDateTime": "datetime", + "DeletionProtectionEnabled": false, "ItemCount": 0, "KeySchema": [ { @@ -635,16 +749,16 @@ "ApproximateCreationDateTime": "", "Keys": { "id": { - "S": "test123" + "S": "id_value" } }, "NewImage": { "id": { - "S": "test123" + "S": "id_value" } }, "SequenceNumber": "", - "SizeBytes": 18, + "SizeBytes": 20, "StreamViewType": "NEW_AND_OLD_IMAGES" }, "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" @@ -665,16 +779,16 @@ "ApproximateCreationDateTime": "", "Keys": { "id": { - "S": "test123" + "S": "id_value" } }, "NewImage": { "id": { - "S": "test123" + "S": "id_value" } }, "SequenceNumber": "", - "SizeBytes": 18, + "SizeBytes": 20, "StreamViewType": "NEW_AND_OLD_IMAGES" }, "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" @@ -693,24 +807,24 @@ "ApproximateCreationDateTime": "", "Keys": { "id": { - "S": "test123" + "S": "id_value" } }, "NewImage": { "id2": { - "S": "42test" + "S": "id2_new_value" }, "id": { - "S": "test123" + "S": "id_value" } }, "OldImage": { "id": { - "S": "test123" + "S": "id_value" } }, "SequenceNumber": "", - "SizeBytes": 36, + "SizeBytes": 46, "StreamViewType": "NEW_AND_OLD_IMAGES" }, "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" @@ -720,8 +834,8 @@ ] } }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put12-item_to_put22-filter2-1]": { - "recorded-date": "27-02-2023, 18:40:56", + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[content_multiple_filters]": { + "recorded-date": "11-04-2024, 20:52:04", "recorded-content": { "table_creation_response": { "TableDescription": { @@ -735,6 +849,7 @@ "BillingMode": "PAY_PER_REQUEST" }, "CreationDateTime": "datetime", + "DeletionProtectionEnabled": false, "ItemCount": 0, "KeySchema": [ { @@ -810,16 +925,16 @@ "ApproximateCreationDateTime": "", "Keys": { "id": { - "S": "test123" + "S": "id_value" } }, "NewImage": { "id": { - "S": "test123" + "S": "id_value" } }, "SequenceNumber": "", - "SizeBytes": 18, + "SizeBytes": 20, "StreamViewType": "NEW_AND_OLD_IMAGES" }, "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" @@ -840,16 +955,16 @@ "ApproximateCreationDateTime": "", "Keys": { "id": { - "S": "test123" + "S": "id_value" } }, "NewImage": { "id": { - "S": "test123" + "S": "id_value" } }, "SequenceNumber": "", - "SizeBytes": 18, + "SizeBytes": 20, "StreamViewType": "NEW_AND_OLD_IMAGES" }, "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" @@ -859,8 +974,8 @@ ] } }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put13-item_to_put23-filter3-1]": { - "recorded-date": "27-02-2023, 18:42:11", + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[content_filter_type]": { + "recorded-date": "11-04-2024, 20:53:04", "recorded-content": { "table_creation_response": { "TableDescription": { @@ -874,6 +989,7 @@ "BillingMode": "PAY_PER_REQUEST" }, "CreationDateTime": "datetime", + "DeletionProtectionEnabled": false, "ItemCount": 0, "KeySchema": [ { @@ -910,11 +1026,11 @@ "Pattern": { "dynamodb": { "NewImage": { - "presentKey": [ - { - "exists": false - } - ] + "presentKey": { + "S": [ + "presentValue" + ] + } } } } @@ -952,16 +1068,19 @@ "ApproximateCreationDateTime": "", "Keys": { "id": { - "S": "test123" + "S": "id_value_1" } }, "NewImage": { "id": { - "S": "test123" + "S": "id_value_1" + }, + "presentKey": { + "S": "presentValue" } }, "SequenceNumber": "", - "SizeBytes": 18, + "SizeBytes": 46, "StreamViewType": "NEW_AND_OLD_IMAGES" }, "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" @@ -982,16 +1101,19 @@ "ApproximateCreationDateTime": "", "Keys": { "id": { - "S": "test123" + "S": "id_value_1" } }, "NewImage": { "id": { - "S": "test123" + "S": "id_value_1" + }, + "presentKey": { + "S": "presentValue" } }, "SequenceNumber": "", - "SizeBytes": 18, + "SizeBytes": 46, "StreamViewType": "NEW_AND_OLD_IMAGES" }, "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" @@ -1001,8 +1123,8 @@ ] } }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put14-item_to_put24-filter4-0]": { - "recorded-date": "27-02-2023, 18:42:34", + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[exists_filter_type]": { + "recorded-date": "11-04-2024, 20:54:44", "recorded-content": { "table_creation_response": { "TableDescription": { @@ -1016,6 +1138,7 @@ "BillingMode": "PAY_PER_REQUEST" }, "CreationDateTime": "datetime", + "DeletionProtectionEnabled": false, "ItemCount": 0, "KeySchema": [ { @@ -1052,13 +1175,10 @@ "Pattern": { "dynamodb": { "NewImage": { - "numericFilter": { - "N": [ + "presentKey": { + "S": [ { - "numeric": [ - ">", - 100 - ] + "exists": true } ] } @@ -1086,12 +1206,76 @@ "HTTPStatusCode": 202 } }, - "lambda-log-events": [], - "lambda-multiple-log-events": [] + "lambda-log-events": [ + { + "Records": [ + { + "eventID": "", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "", + "dynamodb": { + "ApproximateCreationDateTime": "", + "Keys": { + "id": { + "S": "id_value_1" + } + }, + "NewImage": { + "id": { + "S": "id_value_1" + }, + "presentKey": { + "S": "presentValue" + } + }, + "SequenceNumber": "", + "SizeBytes": 46, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" + } + ] + } + ], + "lambda-multiple-log-events": [ + { + "Records": [ + { + "eventID": "", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "", + "dynamodb": { + "ApproximateCreationDateTime": "", + "Keys": { + "id": { + "S": "id_value_1" + } + }, + "NewImage": { + "id": { + "S": "id_value_1" + }, + "presentKey": { + "S": "presentValue" + } + }, + "SequenceNumber": "", + "SizeBytes": 46, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" + } + ] + } + ] } }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put15-item_to_put25-filter5-0]": { - "recorded-date": "27-02-2023, 18:42:58", + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[exists_false_filter]": { + "recorded-date": "11-04-2024, 20:56:31", "recorded-content": { "table_creation_response": { "TableDescription": { @@ -1105,6 +1289,7 @@ "BillingMode": "PAY_PER_REQUEST" }, "CreationDateTime": "datetime", + "DeletionProtectionEnabled": false, "ItemCount": 0, "KeySchema": [ { @@ -1141,18 +1326,11 @@ "Pattern": { "dynamodb": { "NewImage": { - "numericFilter": { - "N": [ - { - "numeric": [ - ">=", - 100, - "<", - 200 - ] - } - ] - } + "presentKey": [ + { + "exists": false + } + ] } } } @@ -1177,37 +1355,127 @@ "HTTPStatusCode": 202 } }, - "lambda-log-events": [], - "lambda-multiple-log-events": [] - } - }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put16-item_to_put26-filter6-1]": { - "recorded-date": "27-02-2023, 18:43:59", - "recorded-content": { - "table_creation_response": { - "TableDescription": { - "AttributeDefinitions": [ + "lambda-log-events": [ + { + "Records": [ { - "AttributeName": "id", - "AttributeType": "S" + "eventID": "", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "", + "dynamodb": { + "ApproximateCreationDateTime": "", + "Keys": { + "id": { + "S": "id_value_1" + } + }, + "NewImage": { + "id": { + "S": "id_value_1" + } + }, + "SequenceNumber": "", + "SizeBytes": 24, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" } - ], - "BillingModeSummary": { - "BillingMode": "PAY_PER_REQUEST" - }, - "CreationDateTime": "datetime", - "ItemCount": 0, - "KeySchema": [ + ] + } + ], + "lambda-multiple-log-events": [ + { + "Records": [ { - "AttributeName": "id", - "KeyType": "HASH" - } - ], - "ProvisionedThroughput": { - "NumberOfDecreasesToday": 0, - "ReadCapacityUnits": 0, - "WriteCapacityUnits": 0 - }, + "eventID": "", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "", + "dynamodb": { + "ApproximateCreationDateTime": "", + "Keys": { + "id": { + "S": "id_value_1" + } + }, + "NewImage": { + "id": { + "S": "id_value_1" + } + }, + "SequenceNumber": "", + "SizeBytes": 24, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" + } + ] + }, + { + "Records": [ + { + "eventID": "", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "", + "dynamodb": { + "ApproximateCreationDateTime": "", + "Keys": { + "id": { + "S": "id_value_2" + } + }, + "NewImage": { + "id": { + "S": "id_value_2" + }, + "presentKey": { + "S": "presentValue" + } + }, + "SequenceNumber": "", + "SizeBytes": 46, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" + } + ] + } + ] + } + }, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[numeric_filter]": { + "recorded-date": "11-04-2024, 20:57:39", + "recorded-content": { + "table_creation_response": { + "TableDescription": { + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "BillingModeSummary": { + "BillingMode": "PAY_PER_REQUEST" + }, + "CreationDateTime": "datetime", + "DeletionProtectionEnabled": false, + "ItemCount": 0, + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "ProvisionedThroughput": { + "NumberOfDecreasesToday": 0, + "ReadCapacityUnits": 0, + "WriteCapacityUnits": 0 + }, "TableArn": "arn:aws:dynamodb::111111111111:table/", "TableId": "", "TableName": "", @@ -1232,10 +1500,16 @@ "Pattern": { "dynamodb": { "NewImage": { - "prefix": { - "S": [ + "numericFilter": { + "N": [ { - "prefix": "us-1" + "numeric": [ + ">", + 100 + ] + }, + { + "anything-but": "101" } ] } @@ -1276,19 +1550,19 @@ "ApproximateCreationDateTime": "", "Keys": { "id": { - "S": "test123" + "S": "id_value_1" } }, "NewImage": { - "prefix": { - "S": "us-1-testtest" + "numericFilter": { + "N": "42" }, "id": { - "S": "test123" + "S": "id_value_1" } }, "SequenceNumber": "", - "SizeBytes": 37, + "SizeBytes": 39, "StreamViewType": "NEW_AND_OLD_IMAGES" }, "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" @@ -1309,19 +1583,19 @@ "ApproximateCreationDateTime": "", "Keys": { "id": { - "S": "test123" + "S": "id_value_1" } }, "NewImage": { - "prefix": { - "S": "us-1-testtest" + "numericFilter": { + "N": "42" }, "id": { - "S": "test123" + "S": "id_value_1" } }, "SequenceNumber": "", - "SizeBytes": 37, + "SizeBytes": 39, "StreamViewType": "NEW_AND_OLD_IMAGES" }, "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" @@ -1331,48 +1605,165 @@ ] } }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_invalid_event_filter[single-string]": { - "recorded-date": "27-02-2023, 18:44:12", + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[prefix_filter]": { + "recorded-date": "11-04-2024, 20:59:06", "recorded-content": { - "exception_event_source_creation": { - "Error": { - "Code": "InvalidParameterValueException", - "Message": "Invalid filter pattern definition." + "table_creation_response": { + "TableDescription": { + "AttributeDefinitions": [ + { + "AttributeName": "id", + "AttributeType": "S" + } + ], + "BillingModeSummary": { + "BillingMode": "PAY_PER_REQUEST" + }, + "CreationDateTime": "datetime", + "DeletionProtectionEnabled": false, + "ItemCount": 0, + "KeySchema": [ + { + "AttributeName": "id", + "KeyType": "HASH" + } + ], + "ProvisionedThroughput": { + "NumberOfDecreasesToday": 0, + "ReadCapacityUnits": 0, + "WriteCapacityUnits": 0 + }, + "TableArn": "arn:aws:dynamodb::111111111111:table/", + "TableId": "", + "TableName": "", + "TableSizeBytes": 0, + "TableStatus": "CREATING" }, - "Type": "User", - "message": "Invalid filter pattern definition.", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 400 + "HTTPStatusCode": 200 } - } - } - }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_invalid_event_filter[[{\"eventName\": [\"INSERT\"=123}]]": { - "recorded-date": "27-02-2023, 18:44:25", - "recorded-content": { - "exception_event_source_creation": { - "Error": { - "Code": "InvalidParameterValueException", - "Message": "Invalid filter pattern definition." + }, + "create_event_source_mapping_response": { + "BatchSize": 1, + "BisectBatchOnFunctionError": false, + "DestinationConfig": { + "OnFailure": {} }, - "Type": "User", - "message": "Invalid filter pattern definition.", + "EventSourceArn": "arn:aws:dynamodb::111111111111:table//stream/", + "FilterCriteria": { + "Filters": [ + { + "Pattern": { + "dynamodb": { + "NewImage": { + "prefix": { + "S": [ + { + "prefix": "us-1" + } + ] + } + } + } + } + } + ] + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionResponseTypes": [], + "LastModified": "datetime", + "LastProcessingResult": "No records processed", + "MaximumBatchingWindowInSeconds": 1, + "MaximumRecordAgeInSeconds": -1, + "MaximumRetryAttempts": 1, + "ParallelizationFactor": 1, + "StartingPosition": "TRIM_HORIZON", + "State": "Creating", + "StateTransitionReason": "User action", + "TumblingWindowInSeconds": 0, + "UUID": "", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 400 + "HTTPStatusCode": 202 } - } + }, + "lambda-log-events": [ + { + "Records": [ + { + "eventID": "", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "", + "dynamodb": { + "ApproximateCreationDateTime": "", + "Keys": { + "id": { + "S": "id_value_1" + } + }, + "NewImage": { + "prefix": { + "S": "us-1-other-suffix" + }, + "id": { + "S": "id_value_1" + } + }, + "SequenceNumber": "", + "SizeBytes": 47, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" + } + ] + } + ], + "lambda-multiple-log-events": [ + { + "Records": [ + { + "eventID": "", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "", + "dynamodb": { + "ApproximateCreationDateTime": "", + "Keys": { + "id": { + "S": "id_value_1" + } + }, + "NewImage": { + "prefix": { + "S": "us-1-other-suffix" + }, + "id": { + "S": "id_value_1" + } + }, + "SequenceNumber": "", + "SizeBytes": 47, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" + } + ] + } + ] } }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_duplicate_event_source_mappings": { - "recorded-date": "04-01-2024, 17:25:30", + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[date_time_conversion]": { + "recorded-date": "11-04-2024, 21:00:11", "recorded-content": { - "create-table-result": { + "table_creation_response": { "TableDescription": { "AttributeDefinitions": [ { - "AttributeName": "my_partition_key", + "AttributeName": "id", "AttributeType": "S" } ], @@ -1384,7 +1775,7 @@ "ItemCount": 0, "KeySchema": [ { - "AttributeName": "my_partition_key", + "AttributeName": "id", "KeyType": "HASH" } ], @@ -1404,13 +1795,24 @@ "HTTPStatusCode": 200 } }, - "create": { - "BatchSize": 100, + "create_event_source_mapping_response": { + "BatchSize": 1, "BisectBatchOnFunctionError": false, "DestinationConfig": { "OnFailure": {} }, "EventSourceArn": "arn:aws:dynamodb::111111111111:table//stream/", + "FilterCriteria": { + "Filters": [ + { + "Pattern": { + "dynamodb": { + "ApproximateCreationDateTime": "" + } + } + } + ] + }, "FunctionArn": "arn:aws:lambda::111111111111:function:", "FunctionResponseTypes": [], "LastModified": "datetime", @@ -1429,18 +1831,94 @@ "HTTPStatusCode": 202 } }, - "error": { - "Error": { - "Code": "ResourceConflictException", - "Message": "The event source arn (\" arn:aws:dynamodb::111111111111:table//stream/ \") and function (\" \") provided mapping already exists. Please update or delete the existing mapping with UUID " + "lambda-log-events": [ + { + "Records": [ + { + "eventID": "", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "", + "dynamodb": { + "ApproximateCreationDateTime": "", + "Keys": { + "id": { + "S": "id_value_1" + } + }, + "NewImage": { + "id": { + "S": "id_value_1" + } + }, + "SequenceNumber": "", + "SizeBytes": 24, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" + } + ] + } + ], + "lambda-multiple-log-events": [ + { + "Records": [ + { + "eventID": "", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "", + "dynamodb": { + "ApproximateCreationDateTime": "", + "Keys": { + "id": { + "S": "id_value_1" + } + }, + "NewImage": { + "id": { + "S": "id_value_1" + } + }, + "SequenceNumber": "", + "SizeBytes": 24, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" + } + ] }, - "Type": "User", - "message": "The event source arn (\" arn:aws:dynamodb::111111111111:table//stream/ \") and function (\" \") provided mapping already exists. Please update or delete the existing mapping with UUID ", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 409 + { + "Records": [ + { + "eventID": "", + "eventName": "INSERT", + "eventVersion": "1.1", + "eventSource": "aws:dynamodb", + "awsRegion": "", + "dynamodb": { + "ApproximateCreationDateTime": "", + "Keys": { + "id": { + "S": "id_value_2" + } + }, + "NewImage": { + "id": { + "S": "id_value_2" + } + }, + "SequenceNumber": "", + "SizeBytes": 24, + "StreamViewType": "NEW_AND_OLD_IMAGES" + }, + "eventSourceARN": "arn:aws:dynamodb::111111111111:table//stream/" + } + ] } - } + ] } } } diff --git a/tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.validation.json b/tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.validation.json index 2e677efa25b78..42bd226dae248 100644 --- a/tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.validation.json +++ b/tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.validation.json @@ -8,26 +8,32 @@ "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_duplicate_event_source_mappings": { "last_validated_date": "2024-01-04T23:38:14+00:00" }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put10-None-filter0-1]": { - "last_validated_date": "2023-02-27T17:37:56+00:00" + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[content_filter_type]": { + "last_validated_date": "2024-04-11T20:53:03+00:00" }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put11-item_to_put21-filter1-2]": { - "last_validated_date": "2023-02-27T17:39:17+00:00" + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[content_multiple_filters]": { + "last_validated_date": "2024-04-11T20:52:03+00:00" }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put12-item_to_put22-filter2-1]": { - "last_validated_date": "2023-02-27T17:40:56+00:00" + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[content_or_filter]": { + "last_validated_date": "2024-04-11T20:50:43+00:00" }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put13-item_to_put23-filter3-1]": { - "last_validated_date": "2023-02-27T17:42:11+00:00" + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[date_time_conversion]": { + "last_validated_date": "2024-04-11T21:00:10+00:00" }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put14-item_to_put24-filter4-0]": { - "last_validated_date": "2023-02-27T17:42:34+00:00" + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[exists_false_filter]": { + "last_validated_date": "2024-04-11T20:56:30+00:00" }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put15-item_to_put25-filter5-0]": { - "last_validated_date": "2023-02-27T17:42:58+00:00" + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[exists_filter_type]": { + "last_validated_date": "2024-04-11T20:54:43+00:00" }, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put16-item_to_put26-filter6-1]": { - "last_validated_date": "2023-02-27T17:43:59+00:00" + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[insert_same_entry_twice]": { + "last_validated_date": "2024-04-11T20:48:59+00:00" + }, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[numeric_filter]": { + "last_validated_date": "2024-04-11T20:57:38+00:00" + }, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[prefix_filter]": { + "last_validated_date": "2024-04-11T20:59:05+00:00" }, "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_source_mapping": { "last_validated_date": "2023-04-26T08:48:20+00:00" diff --git a/tests/unit/services/lambda_/test_lambda_utils.py b/tests/unit/services/lambda_/test_lambda_utils.py index c159a1debe5fb..59e401d4ab0c6 100644 --- a/tests/unit/services/lambda_/test_lambda_utils.py +++ b/tests/unit/services/lambda_/test_lambda_utils.py @@ -136,7 +136,7 @@ def test_no_match_partial(self): filters = [ { "Filters": [ - {"Pattern": json.dumps({"partitionKey": "2", "data": {"City": ["Seattle"]}})} + {"Pattern": json.dumps({"partitionKey": ["2"], "data": {"City": ["Seattle"]}})} ] } ] From 0a255e2ecb6012d6a6c5faae6c25d83ed66c755f Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Thu, 18 Apr 2024 14:38:14 +0100 Subject: [PATCH 078/169] StepFunctions: Fix Heartbeat Callback Locking (#10663) --- .../asl/eval/callback/callback.py | 12 +- .../testing/snapshots/transformer_utility.py | 9 + tests/aws/services/stepfunctions/utils.py | 2 +- .../v2/callback/test_callback.py | 170 ++- .../v2/callback/test_callback.snapshot.json | 1217 ++++++++++++++++- .../v2/callback/test_callback.validation.json | 14 +- .../error_handling/test_task_service_sqs.py | 19 +- .../test_task_service_sqs.snapshot.json | 20 +- .../test_task_service_sqs.validation.json | 2 +- .../v2/services/test_sns_task_service.py | 8 +- .../v2/services/test_sqs_task_service.py | 4 +- .../test_sqs_task_service.snapshot.json | 88 +- .../test_sqs_task_service.validation.json | 4 +- .../v2/timeouts/test_heartbeats.py | 9 +- .../v2/timeouts/test_heartbeats.snapshot.json | 12 +- .../timeouts/test_heartbeats.validation.json | 6 +- 16 files changed, 1484 insertions(+), 112 deletions(-) diff --git a/localstack/services/stepfunctions/asl/eval/callback/callback.py b/localstack/services/stepfunctions/asl/eval/callback/callback.py index fe48f303df3a1..4cb3b77895594 100644 --- a/localstack/services/stepfunctions/asl/eval/callback/callback.py +++ b/localstack/services/stepfunctions/asl/eval/callback/callback.py @@ -1,6 +1,6 @@ import abc from collections import OrderedDict -from threading import Event +from threading import Event, Lock from typing import Final, Optional from localstack.aws.api.stepfunctions import ActivityDoesNotExist, Arn @@ -51,19 +51,25 @@ class CallbackConsumerLeft(CallbackConsumerError): class HeartbeatEndpoint: + _mutex: Final[Lock] _next_heartbeat_event: Final[Event] _heartbeat_seconds: Final[int] def __init__(self, heartbeat_seconds: int): + self._mutex = Lock() self._next_heartbeat_event = Event() self._heartbeat_seconds = heartbeat_seconds def clear_and_wait(self) -> bool: - self._next_heartbeat_event.clear() + with self._mutex: + if self._next_heartbeat_event.is_set(): + self._next_heartbeat_event.clear() + return True return self._next_heartbeat_event.wait(timeout=self._heartbeat_seconds) def notify(self): - self._next_heartbeat_event.set() + with self._mutex: + self._next_heartbeat_event.set() class HeartbeatTimeoutError(TimeoutError): diff --git a/localstack/testing/snapshots/transformer_utility.py b/localstack/testing/snapshots/transformer_utility.py index 411107d60fbc3..c32bbd1dc7ec0 100644 --- a/localstack/testing/snapshots/transformer_utility.py +++ b/localstack/testing/snapshots/transformer_utility.py @@ -629,6 +629,15 @@ def sfn_map_run_arn(map_run_arn: LongArn, index: int) -> list[RegexTransformer]: RegexTransformer(arn_parts[1], f""), ] + @staticmethod + def sfn_sqs_integration(): + return [ + *TransformerUtility.sqs_api(), + # Transform MD5OfMessageBody value bindings as in StepFunctions these are not deterministic + # about the input message. + TransformerUtility.key_value("MD5OfMessageBody"), + ] + @staticmethod def stepfunctions_api(): return [ diff --git a/tests/aws/services/stepfunctions/utils.py b/tests/aws/services/stepfunctions/utils.py index d7bd7f3cd0371..d39f616c45bf7 100644 --- a/tests/aws/services/stepfunctions/utils.py +++ b/tests/aws/services/stepfunctions/utils.py @@ -332,7 +332,7 @@ def create_and_record_events( definition, execution_input, ): - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) sfn_snapshot.add_transformers_list( [ JsonpathTransformer( diff --git a/tests/aws/services/stepfunctions/v2/callback/test_callback.py b/tests/aws/services/stepfunctions/v2/callback/test_callback.py index 4adaab52e926e..dd8807318df67 100644 --- a/tests/aws/services/stepfunctions/v2/callback/test_callback.py +++ b/tests/aws/services/stepfunctions/v2/callback/test_callback.py @@ -3,6 +3,7 @@ from localstack_snapshot.snapshots.transformer import JsonpathTransformer, RegexTransformer +from localstack.services.stepfunctions.asl.eval.count_down_latch import CountDownLatch from localstack.testing.pytest import markers from localstack.utils.strings import short_uid from localstack.utils.sync import retry @@ -13,10 +14,37 @@ from tests.aws.services.stepfunctions.templates.timeouts.timeout_templates import ( TimeoutTemplates as TT, ) -from tests.aws.services.stepfunctions.utils import create, create_and_record_execution +from tests.aws.services.stepfunctions.utils import ( + await_execution_terminated, + create, + create_and_record_execution, +) from tests.aws.test_notifications import PUBLICATION_RETRIES, PUBLICATION_TIMEOUT +def _handle_sqs_task_token_with_heartbeats_and_success(aws_client, queue_url) -> None: + # Handle the state machine task token published in the sqs queue, by submitting 10 heartbeat + # notifications and a task success notification. Snapshot the response of each call. + + # Read the expected sqs message and extract the body. + def _get_message_body(): + receive_message_response = aws_client.sqs.receive_message( + QueueUrl=queue_url, MaxNumberOfMessages=1 + ) + return receive_message_response["Messages"][0]["Body"] + + message_body_str = retry(_get_message_body, retries=100, sleep=1) + message_body = json.loads(message_body_str) + + # Send the heartbeat notifications. + task_token = message_body["TaskToken"] + for i in range(10): + aws_client.stepfunctions.send_task_heartbeat(taskToken=task_token) + + # Send the task success notification. + aws_client.stepfunctions.send_task_success(taskToken=task_token, output=message_body_str) + + @markers.snapshot.skip_snapshot_verify( paths=[ "$..loggingConfiguration", @@ -26,7 +54,6 @@ ] ) class TestCallback: - @markers.snapshot.skip_snapshot_verify(paths=["$..MD5OfMessageBody"]) @markers.aws.needs_fixing def test_sqs_wait_for_task_token( self, @@ -37,7 +64,7 @@ def test_sqs_wait_for_task_token( sqs_send_task_success_state_machine, sfn_snapshot, ): - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) sfn_snapshot.add_transformer( JsonpathTransformer( jsonpath="$..TaskToken", @@ -67,7 +94,6 @@ def test_sqs_wait_for_task_token( exec_input, ) - @markers.snapshot.skip_snapshot_verify(paths=["$..MD5OfMessageBody"]) @markers.aws.needs_fixing def test_sqs_wait_for_task_token_timeout( self, @@ -78,7 +104,7 @@ def test_sqs_wait_for_task_token_timeout( sqs_send_task_success_state_machine, sfn_snapshot, ): - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) sfn_snapshot.add_transformer( JsonpathTransformer( jsonpath="$..TaskToken", @@ -106,7 +132,6 @@ def test_sqs_wait_for_task_token_timeout( exec_input, ) - @markers.snapshot.skip_snapshot_verify(paths=["$..MD5OfMessageBody"]) @markers.aws.needs_fixing def test_sqs_failure_in_wait_for_task_token( self, @@ -117,7 +142,7 @@ def test_sqs_failure_in_wait_for_task_token( sqs_send_task_failure_state_machine, sfn_snapshot, ): - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) sfn_snapshot.add_transformer( JsonpathTransformer( jsonpath="$..TaskToken", @@ -147,7 +172,6 @@ def test_sqs_failure_in_wait_for_task_token( exec_input, ) - @markers.snapshot.skip_snapshot_verify(paths=["$..MD5OfMessageBody"]) @markers.aws.needs_fixing def test_sqs_wait_for_task_tok_with_heartbeat( self, @@ -158,7 +182,7 @@ def test_sqs_wait_for_task_tok_with_heartbeat( sqs_send_heartbeat_and_task_success_state_machine, sfn_snapshot, ): - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) sfn_snapshot.add_transformer( JsonpathTransformer( jsonpath="$..TaskToken", @@ -208,7 +232,7 @@ def test_sns_publish_wait_for_task_token( replace_reference=True, ) ) - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) sfn_snapshot.add_transformer(sfn_snapshot.transform.sns_api()) topic_info = sns_create_topic() @@ -471,3 +495,129 @@ def test_start_execution_sync_delegate_timeout( definition, exec_input, ) + + @markers.aws.validated + def test_multiple_heartbeat_notifications( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sqs_create_queue, + sfn_snapshot, + ): + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..TaskToken", + replacement="task_token", + replace_reference=True, + ) + ) + + queue_name = f"queue-{short_uid()}" + queue_url = sqs_create_queue(QueueName=queue_name) + sfn_snapshot.add_transformer(RegexTransformer(queue_url, "sqs_queue_url")) + sfn_snapshot.add_transformer(RegexTransformer(queue_name, "sqs_queue_name")) + + task_token_consumer_thread = threading.Thread( + target=_handle_sqs_task_token_with_heartbeats_and_success, args=(aws_client, queue_url) + ) + task_token_consumer_thread.start() + + template = CT.load_sfn_template( + TT.SERVICE_SQS_SEND_AND_WAIT_FOR_TASK_TOKEN_WITH_HEARTBEAT_PATH + ) + definition = json.dumps(template) + + exec_input = json.dumps( + {"QueueUrl": queue_url, "Message": "txt", "HeartbeatSecondsPath": 120} + ) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + + task_token_consumer_thread.join(timeout=300) + + @markers.aws.validated + def test_multiple_executions_and_heartbeat_notifications( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sqs_create_queue, + sfn_snapshot, + ): + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..TaskToken", + replacement="a_task_token", + replace_reference=False, + ) + ) + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..MessageId", + replacement="a_message_id", + replace_reference=False, + ) + ) + + queue_name = f"queue-{short_uid()}" + queue_url = sqs_create_queue(QueueName=queue_name) + sfn_snapshot.add_transformer(RegexTransformer(queue_url, "sqs_queue_url")) + sfn_snapshot.add_transformer(RegexTransformer(queue_name, "sqs_queue_name")) + + sfn_role_arn = create_iam_role_for_sfn() + + template = CT.load_sfn_template( + TT.SERVICE_SQS_SEND_AND_WAIT_FOR_TASK_TOKEN_WITH_HEARTBEAT_PATH + ) + definition = json.dumps(template) + + creation_response = create_state_machine( + name=f"state_machine_{short_uid()}", definition=definition, roleArn=sfn_role_arn + ) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sm_create_arn(creation_response, 0)) + state_machine_arn = creation_response["stateMachineArn"] + + exec_input = json.dumps( + {"QueueUrl": queue_url, "Message": "txt", "HeartbeatSecondsPath": 120} + ) + + # Launch multiple execution of the same state machine. + execution_count = 6 + execution_arns = list() + for _ in range(execution_count): + execution_arn = aws_client.stepfunctions.start_execution( + stateMachineArn=state_machine_arn, input=exec_input + )["executionArn"] + execution_arns.append(execution_arn) + + # Launch one sqs task token handler per each execution, and await for all the terminate handling the task. + task_token_handler_latch = CountDownLatch(execution_count) + + def _sqs_task_token_handler(): + _handle_sqs_task_token_with_heartbeats_and_success(aws_client, queue_url) + task_token_handler_latch.count_down() + + for _ in range(execution_count): + inner_handler_thread = threading.Thread(target=_sqs_task_token_handler, args=()) + inner_handler_thread.start() + + task_token_handler_latch.wait() + + # For each execution, await terminate and record the event executions. + for i, execution_arn in enumerate(execution_arns): + await_execution_terminated( + stepfunctions_client=aws_client.stepfunctions, execution_arn=execution_arn + ) + execution_history = aws_client.stepfunctions.get_execution_history( + executionArn=execution_arn + ) + sfn_snapshot.match(f"execution_history_{i}", execution_history) diff --git a/tests/aws/services/stepfunctions/v2/callback/test_callback.snapshot.json b/tests/aws/services/stepfunctions/v2/callback/test_callback.snapshot.json index 024487932bbea..8a8b4d85a24d6 100644 --- a/tests/aws/services/stepfunctions/v2/callback/test_callback.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/callback/test_callback.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_token": { - "recorded-date": "25-06-2023, 14:30:26", + "recorded-date": "18-04-2024, 06:19:31", "recorded-content": { "get_execution_history": { "events": [ @@ -69,24 +69,28 @@ "previousEventId": 4, "taskSubmittedEventDetails": { "output": { - "MD5OfMessageBody": "c1b8689efa515ca4871a83ed8e2dd5fc", + "MD5OfMessageBody": "", "MessageId": "", "SdkHttpMetadata": { "AllHttpHeaders": { "x-amzn-RequestId": [ "" ], + "connection": [ + "keep-alive" + ], "Content-Length": [ - "378" + "106" ], "Date": "date", "Content-Type": [ - "text/xml" + "application/x-amz-json-1.0" ] }, "HttpHeaders": { - "Content-Length": "378", - "Content-Type": "text/xml", + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", "Date": "date", "x-amzn-RequestId": "" }, @@ -153,7 +157,7 @@ } }, "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_token_timeout": { - "recorded-date": "10-03-2024, 16:42:16", + "recorded-date": "18-04-2024, 06:20:28", "recorded-content": { "get_execution_history": { "events": [ @@ -223,7 +227,7 @@ "previousEventId": 4, "taskSubmittedEventDetails": { "output": { - "MD5OfMessageBody": "1761bdfa678f40d2d350e7bdf487f6f6", + "MD5OfMessageBody": "", "MessageId": "", "SdkHttpMetadata": { "AllHttpHeaders": { @@ -292,7 +296,7 @@ } }, "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_failure_in_wait_for_task_token": { - "recorded-date": "25-06-2023, 14:31:14", + "recorded-date": "18-04-2024, 06:24:24", "recorded-content": { "get_execution_history": { "events": [ @@ -361,24 +365,28 @@ "previousEventId": 4, "taskSubmittedEventDetails": { "output": { - "MD5OfMessageBody": "103af427820fd12011fe1cb454fdcc0a", + "MD5OfMessageBody": "", "MessageId": "", "SdkHttpMetadata": { "AllHttpHeaders": { "x-amzn-RequestId": [ "" ], + "connection": [ + "keep-alive" + ], "Content-Length": [ - "378" + "106" ], "Date": "date", "Content-Type": [ - "text/xml" + "application/x-amz-json-1.0" ] }, "HttpHeaders": { - "Content-Length": "378", - "Content-Type": "text/xml", + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", "Date": "date", "x-amzn-RequestId": "" }, @@ -428,7 +436,7 @@ } }, "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_tok_with_heartbeat": { - "recorded-date": "25-06-2023, 14:34:21", + "recorded-date": "18-04-2024, 06:25:26", "recorded-content": { "get_execution_history": { "events": [ @@ -499,24 +507,28 @@ "previousEventId": 4, "taskSubmittedEventDetails": { "output": { - "MD5OfMessageBody": "5c22318ff95a90991fb6e16a19a89dc1", + "MD5OfMessageBody": "", "MessageId": "", "SdkHttpMetadata": { "AllHttpHeaders": { "x-amzn-RequestId": [ "" ], + "connection": [ + "keep-alive" + ], "Content-Length": [ - "378" + "106" ], "Date": "date", "Content-Type": [ - "text/xml" + "application/x-amz-json-1.0" ] }, "HttpHeaders": { - "Content-Length": "378", - "Content-Type": "text/xml", + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", "Date": "date", "x-amzn-RequestId": "" }, @@ -1507,5 +1519,1170 @@ } ] } + }, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_multiple_heartbeat_notifications": { + "recorded-date": "18-04-2024, 06:17:37", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "txt", + "HeartbeatSecondsPath": 120 + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "arn:aws:iam::111111111111:role/" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "txt", + "HeartbeatSecondsPath": 120 + }, + "inputDetails": { + "truncated": false + }, + "name": "SendMessageWithWait" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "heartbeatInSeconds": 120, + "parameters": { + "MessageBody": { + "Message": "txt", + "TaskToken": "" + }, + "QueueUrl": "sqs_queue_url" + }, + "region": "", + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs", + "timeoutInSeconds": 600 + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "MD5OfMessageBody": "", + "MessageId": "", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskSucceededEventDetails": { + "output": { + "Message": "txt", + "TaskToken": "" + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 7, + "previousEventId": 6, + "stateExitedEventDetails": { + "name": "SendMessageWithWait", + "output": { + "Message": "txt", + "TaskToken": "" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "Message": "txt", + "TaskToken": "" + }, + "outputDetails": { + "truncated": false + } + }, + "id": 8, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_multiple_executions_and_heartbeat_notifications": { + "recorded-date": "18-04-2024, 06:18:29", + "recorded-content": { + "execution_history_0": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "txt", + "HeartbeatSecondsPath": 120 + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "arn:aws:iam::111111111111:role/" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "txt", + "HeartbeatSecondsPath": 120 + }, + "inputDetails": { + "truncated": false + }, + "name": "SendMessageWithWait" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "heartbeatInSeconds": 120, + "parameters": { + "MessageBody": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "QueueUrl": "sqs_queue_url" + }, + "region": "", + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs", + "timeoutInSeconds": 600 + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "MD5OfMessageBody": "", + "MessageId": "a_message_id", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskSucceededEventDetails": { + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 7, + "previousEventId": 6, + "stateExitedEventDetails": { + "name": "SendMessageWithWait", + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + } + }, + "id": 8, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execution_history_1": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "txt", + "HeartbeatSecondsPath": 120 + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "arn:aws:iam::111111111111:role/" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "txt", + "HeartbeatSecondsPath": 120 + }, + "inputDetails": { + "truncated": false + }, + "name": "SendMessageWithWait" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "heartbeatInSeconds": 120, + "parameters": { + "MessageBody": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "QueueUrl": "sqs_queue_url" + }, + "region": "", + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs", + "timeoutInSeconds": 600 + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "MD5OfMessageBody": "", + "MessageId": "a_message_id", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskSucceededEventDetails": { + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 7, + "previousEventId": 6, + "stateExitedEventDetails": { + "name": "SendMessageWithWait", + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + } + }, + "id": 8, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execution_history_2": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "txt", + "HeartbeatSecondsPath": 120 + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "arn:aws:iam::111111111111:role/" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "txt", + "HeartbeatSecondsPath": 120 + }, + "inputDetails": { + "truncated": false + }, + "name": "SendMessageWithWait" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "heartbeatInSeconds": 120, + "parameters": { + "MessageBody": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "QueueUrl": "sqs_queue_url" + }, + "region": "", + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs", + "timeoutInSeconds": 600 + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "MD5OfMessageBody": "", + "MessageId": "a_message_id", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskSucceededEventDetails": { + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 7, + "previousEventId": 6, + "stateExitedEventDetails": { + "name": "SendMessageWithWait", + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + } + }, + "id": 8, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execution_history_3": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "txt", + "HeartbeatSecondsPath": 120 + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "arn:aws:iam::111111111111:role/" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "txt", + "HeartbeatSecondsPath": 120 + }, + "inputDetails": { + "truncated": false + }, + "name": "SendMessageWithWait" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "heartbeatInSeconds": 120, + "parameters": { + "MessageBody": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "QueueUrl": "sqs_queue_url" + }, + "region": "", + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs", + "timeoutInSeconds": 600 + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "MD5OfMessageBody": "", + "MessageId": "a_message_id", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskSucceededEventDetails": { + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 7, + "previousEventId": 6, + "stateExitedEventDetails": { + "name": "SendMessageWithWait", + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + } + }, + "id": 8, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execution_history_4": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "txt", + "HeartbeatSecondsPath": 120 + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "arn:aws:iam::111111111111:role/" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "txt", + "HeartbeatSecondsPath": 120 + }, + "inputDetails": { + "truncated": false + }, + "name": "SendMessageWithWait" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "heartbeatInSeconds": 120, + "parameters": { + "MessageBody": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "QueueUrl": "sqs_queue_url" + }, + "region": "", + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs", + "timeoutInSeconds": 600 + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "MD5OfMessageBody": "", + "MessageId": "a_message_id", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskSucceededEventDetails": { + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 7, + "previousEventId": 6, + "stateExitedEventDetails": { + "name": "SendMessageWithWait", + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + } + }, + "id": 8, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "execution_history_5": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "txt", + "HeartbeatSecondsPath": 120 + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "arn:aws:iam::111111111111:role/" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "txt", + "HeartbeatSecondsPath": 120 + }, + "inputDetails": { + "truncated": false + }, + "name": "SendMessageWithWait" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "heartbeatInSeconds": 120, + "parameters": { + "MessageBody": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "QueueUrl": "sqs_queue_url" + }, + "region": "", + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs", + "timeoutInSeconds": 600 + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "MD5OfMessageBody": "", + "MessageId": "a_message_id", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskSucceededEventDetails": { + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 7, + "previousEventId": 6, + "stateExitedEventDetails": { + "name": "SendMessageWithWait", + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "Message": "txt", + "TaskToken": "a_task_token" + }, + "outputDetails": { + "truncated": false + } + }, + "id": 8, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/stepfunctions/v2/callback/test_callback.validation.json b/tests/aws/services/stepfunctions/v2/callback/test_callback.validation.json index 90a7c138c831b..2aaa9c1e7e0b7 100644 --- a/tests/aws/services/stepfunctions/v2/callback/test_callback.validation.json +++ b/tests/aws/services/stepfunctions/v2/callback/test_callback.validation.json @@ -1,18 +1,24 @@ { + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_multiple_executions_and_heartbeat_notifications": { + "last_validated_date": "2024-04-18T06:18:29+00:00" + }, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_multiple_heartbeat_notifications": { + "last_validated_date": "2024-04-18T06:17:37+00:00" + }, "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sns_publish_wait_for_task_token": { "last_validated_date": "2024-02-01T20:51:35+00:00" }, "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_failure_in_wait_for_task_token": { - "last_validated_date": "2023-06-25T12:31:14+00:00" + "last_validated_date": "2024-04-18T06:24:24+00:00" }, "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_tok_with_heartbeat": { - "last_validated_date": "2023-06-25T12:34:21+00:00" + "last_validated_date": "2024-04-18T06:25:26+00:00" }, "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_token": { - "last_validated_date": "2023-06-25T12:30:26+00:00" + "last_validated_date": "2024-04-18T06:19:31+00:00" }, "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_token_timeout": { - "last_validated_date": "2024-03-10T16:42:16+00:00" + "last_validated_date": "2024-04-18T06:20:28+00:00" }, "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_start_execution_sync": { "last_validated_date": "2023-06-30T12:42:19+00:00" diff --git a/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.py b/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.py index 7450f33af55ef..42f5ee2614eaf 100644 --- a/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.py +++ b/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.py @@ -36,7 +36,7 @@ def test_send_message_no_such_queue( create_state_machine, sfn_snapshot, ): - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) queue_name = f"queue-{short_uid()}" queue_url = f"http://no-such-queue-{short_uid()}" @@ -65,7 +65,7 @@ def test_send_message_no_such_queue_no_catch( create_state_machine, sfn_snapshot, ): - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) queue_name = f"queue-{short_uid()}" queue_url = f"http://no-such-queue-{short_uid()}" @@ -98,7 +98,7 @@ def test_send_message_empty_body( sqs_create_queue, sfn_snapshot, ): - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) queue_name = f"queue-{short_uid()}" queue_url = sqs_create_queue(QueueName=queue_name) @@ -118,7 +118,6 @@ def test_send_message_empty_body( exec_input, ) - @markers.snapshot.skip_snapshot_verify(paths=["$..MD5OfMessageBody"]) @markers.aws.validated def test_sqs_failure_in_wait_for_task_tok( self, @@ -127,10 +126,10 @@ def test_sqs_failure_in_wait_for_task_tok( create_state_machine, sqs_create_queue, sqs_send_task_failure_state_machine, - snapshot, + sfn_snapshot, ): - snapshot.add_transformer(snapshot.transform.sqs_api()) - snapshot.add_transformer( + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) + sfn_snapshot.add_transformer( JsonpathTransformer( jsonpath="$..TaskToken", replacement="task_token", @@ -140,8 +139,8 @@ def test_sqs_failure_in_wait_for_task_tok( queue_name = f"queue-{short_uid()}" queue_url = sqs_create_queue(QueueName=queue_name) - snapshot.add_transformer(RegexTransformer(queue_url, "")) - snapshot.add_transformer(RegexTransformer(queue_name, "")) + sfn_snapshot.add_transformer(RegexTransformer(queue_url, "")) + sfn_snapshot.add_transformer(RegexTransformer(queue_name, "")) sqs_send_task_failure_state_machine(queue_url) @@ -155,7 +154,7 @@ def test_sqs_failure_in_wait_for_task_tok( aws_client.stepfunctions, create_iam_role_for_sfn, create_state_machine, - snapshot, + sfn_snapshot, definition, exec_input, ) diff --git a/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.snapshot.json b/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.snapshot.json index 6ea9a0e154d78..1920c15d53fef 100644 --- a/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.snapshot.json @@ -398,7 +398,7 @@ } }, "tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.py::TestTaskServiceSqs::test_sqs_failure_in_wait_for_task_tok": { - "recorded-date": "29-05-2023, 11:21:47", + "recorded-date": "18-04-2024, 06:27:04", "recorded-content": { "get_execution_history": { "events": [ @@ -467,27 +467,31 @@ "previousEventId": 4, "taskSubmittedEventDetails": { "output": { - "MD5OfMessageBody": "e4abda1911cb5527bdde8f990de60801", + "MD5OfMessageBody": "", "MessageId": "", "SdkHttpMetadata": { "AllHttpHeaders": { "x-amzn-RequestId": [ "" ], + "connection": [ + "keep-alive" + ], "Content-Length": [ - "378" + "106" ], "Date": [ - "Mon, 29 May 2023 09:21:46 GMT" + "Thu, 18 Apr 2024 06:27:01 GMT" ], "Content-Type": [ - "text/xml" + "application/x-amz-json-1.0" ] }, "HttpHeaders": { - "Content-Length": "378", - "Content-Type": "text/xml", - "Date": "Mon, 29 May 2023 09:21:46 GMT", + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "Thu, 18 Apr 2024 06:27:01 GMT", "x-amzn-RequestId": "" }, "HttpStatusCode": 200 diff --git a/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.validation.json b/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.validation.json index 7020d23518ece..02d0be8bbb02f 100644 --- a/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.validation.json +++ b/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.validation.json @@ -9,6 +9,6 @@ "last_validated_date": "2023-06-22T11:31:50+00:00" }, "tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.py::TestTaskServiceSqs::test_sqs_failure_in_wait_for_task_tok": { - "last_validated_date": "2023-05-29T09:21:47+00:00" + "last_validated_date": "2024-04-18T06:27:04+00:00" } } diff --git a/tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py b/tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py index 0bbcb5302bd14..ff36b95432d29 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py +++ b/tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py @@ -53,7 +53,7 @@ def test_fifo_message_attribute( sns_create_topic, sfn_snapshot, ): - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) fifo_topic_name = f"topic-{short_uid()}.fifo" sns_topic = sns_create_topic(Name=fifo_topic_name, Attributes={"FifoTopic": "true"}) topic_arn = sns_topic["TopicArn"] @@ -88,7 +88,7 @@ def test_publish_base( sfn_snapshot, message, ): - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) sns_topic = sns_create_topic() topic_arn = sns_topic["TopicArn"] @@ -123,7 +123,7 @@ def test_publish_message_attributes( message_value, ): sfn_snapshot.add_transformer(sfn_snapshot.transform.sns_api()) - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) topic_info = sns_create_topic() topic_arn = topic_info["TopicArn"] @@ -181,7 +181,7 @@ def test_publish_base_error_topic_arn( sns_create_topic, sfn_snapshot, ): - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) sns_topic = sns_create_topic() topic_arn = sns_topic["TopicArn"] diff --git a/tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.py b/tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.py index e8e9d54fd014a..59c4200a0e2e8 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.py +++ b/tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.py @@ -33,7 +33,7 @@ def test_send_message( sqs_create_queue, sfn_snapshot, ): - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) queue_name = f"queue-{short_uid()}" queue_url = sqs_create_queue(QueueName=queue_name) @@ -67,7 +67,7 @@ def test_send_message_unsupported_parameters( sqs_create_queue, sfn_snapshot, ): - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) queue_name = f"queue-{short_uid()}" queue_url = sqs_create_queue(QueueName=queue_name) diff --git a/tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.snapshot.json b/tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.snapshot.json index a0b1b56251e2f..7ed06bd5ae526 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.py::TestTaskServiceSqs::test_send_message": { - "recorded-date": "22-06-2023, 13:41:17", + "recorded-date": "18-04-2024, 06:37:09", "recorded-content": { "get_execution_history": { "events": [ @@ -66,24 +66,28 @@ "previousEventId": 4, "taskSucceededEventDetails": { "output": { - "MD5OfMessageBody": "3caa9a099bc4e58ae7923f68c5d7d081", + "MD5OfMessageBody": "", "MessageId": "", "SdkHttpMetadata": { "AllHttpHeaders": { "x-amzn-RequestId": [ "" ], + "connection": [ + "keep-alive" + ], "Content-Length": [ - "378" + "106" ], "Date": "date", "Content-Type": [ - "text/xml" + "application/x-amz-json-1.0" ] }, "HttpHeaders": { - "Content-Length": "378", - "Content-Type": "text/xml", + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", "Date": "date", "x-amzn-RequestId": "" }, @@ -108,24 +112,28 @@ "stateExitedEventDetails": { "name": "SendSQS", "output": { - "MD5OfMessageBody": "3caa9a099bc4e58ae7923f68c5d7d081", + "MD5OfMessageBody": "", "MessageId": "", "SdkHttpMetadata": { "AllHttpHeaders": { "x-amzn-RequestId": [ "" ], + "connection": [ + "keep-alive" + ], "Content-Length": [ - "378" + "106" ], "Date": "date", "Content-Type": [ - "text/xml" + "application/x-amz-json-1.0" ] }, "HttpHeaders": { - "Content-Length": "378", - "Content-Type": "text/xml", + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", "Date": "date", "x-amzn-RequestId": "" }, @@ -145,24 +153,28 @@ { "executionSucceededEventDetails": { "output": { - "MD5OfMessageBody": "3caa9a099bc4e58ae7923f68c5d7d081", + "MD5OfMessageBody": "", "MessageId": "", "SdkHttpMetadata": { "AllHttpHeaders": { "x-amzn-RequestId": [ "" ], + "connection": [ + "keep-alive" + ], "Content-Length": [ - "378" + "106" ], "Date": "date", "Content-Type": [ - "text/xml" + "application/x-amz-json-1.0" ] }, "HttpHeaders": { - "Content-Length": "378", - "Content-Type": "text/xml", + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", "Date": "date", "x-amzn-RequestId": "" }, @@ -190,7 +202,7 @@ } }, "tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.py::TestTaskServiceSqs::test_send_message_unsupported_parameters": { - "recorded-date": "22-06-2023, 13:41:34", + "recorded-date": "18-04-2024, 06:37:24", "recorded-content": { "get_execution_history": { "events": [ @@ -262,24 +274,28 @@ "previousEventId": 4, "taskSucceededEventDetails": { "output": { - "MD5OfMessageBody": "098f6bcd4621d373cade4e832627b4f6", + "MD5OfMessageBody": "", "MessageId": "", "SdkHttpMetadata": { "AllHttpHeaders": { "x-amzn-RequestId": [ "" ], + "connection": [ + "keep-alive" + ], "Content-Length": [ - "378" + "106" ], "Date": "date", "Content-Type": [ - "text/xml" + "application/x-amz-json-1.0" ] }, "HttpHeaders": { - "Content-Length": "378", - "Content-Type": "text/xml", + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", "Date": "date", "x-amzn-RequestId": "" }, @@ -304,24 +320,28 @@ "stateExitedEventDetails": { "name": "SendSQS", "output": { - "MD5OfMessageBody": "098f6bcd4621d373cade4e832627b4f6", + "MD5OfMessageBody": "", "MessageId": "", "SdkHttpMetadata": { "AllHttpHeaders": { "x-amzn-RequestId": [ "" ], + "connection": [ + "keep-alive" + ], "Content-Length": [ - "378" + "106" ], "Date": "date", "Content-Type": [ - "text/xml" + "application/x-amz-json-1.0" ] }, "HttpHeaders": { - "Content-Length": "378", - "Content-Type": "text/xml", + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", "Date": "date", "x-amzn-RequestId": "" }, @@ -341,24 +361,28 @@ { "executionSucceededEventDetails": { "output": { - "MD5OfMessageBody": "098f6bcd4621d373cade4e832627b4f6", + "MD5OfMessageBody": "", "MessageId": "", "SdkHttpMetadata": { "AllHttpHeaders": { "x-amzn-RequestId": [ "" ], + "connection": [ + "keep-alive" + ], "Content-Length": [ - "378" + "106" ], "Date": "date", "Content-Type": [ - "text/xml" + "application/x-amz-json-1.0" ] }, "HttpHeaders": { - "Content-Length": "378", - "Content-Type": "text/xml", + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", "Date": "date", "x-amzn-RequestId": "" }, diff --git a/tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.validation.json b/tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.validation.json index 698c5c5d0dda1..da4ccffc8e6e0 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.validation.json +++ b/tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.validation.json @@ -1,8 +1,8 @@ { "tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.py::TestTaskServiceSqs::test_send_message": { - "last_validated_date": "2023-06-22T11:41:17+00:00" + "last_validated_date": "2024-04-18T06:37:09+00:00" }, "tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.py::TestTaskServiceSqs::test_send_message_unsupported_parameters": { - "last_validated_date": "2023-06-22T11:41:34+00:00" + "last_validated_date": "2024-04-18T06:37:24+00:00" } } diff --git a/tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py b/tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py index 568b8275bfcd9..8fe0af4fa5f72 100644 --- a/tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py +++ b/tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py @@ -19,7 +19,6 @@ ] ) class TestHeartbeats: - @markers.snapshot.skip_snapshot_verify(paths=["$..MD5OfMessageBody"]) @markers.aws.validated def test_heartbeat_timeout( self, @@ -30,7 +29,7 @@ def test_heartbeat_timeout( sqs_send_task_success_state_machine, sfn_snapshot, ): - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) sfn_snapshot.add_transformer( JsonpathTransformer( jsonpath="$..TaskToken", @@ -58,7 +57,6 @@ def test_heartbeat_timeout( exec_input, ) - @markers.snapshot.skip_snapshot_verify(paths=["$..MD5OfMessageBody"]) @markers.aws.validated def test_heartbeat_path_timeout( self, @@ -69,7 +67,7 @@ def test_heartbeat_path_timeout( sqs_send_task_success_state_machine, sfn_snapshot, ): - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) sfn_snapshot.add_transformer( JsonpathTransformer( jsonpath="$..TaskToken", @@ -101,7 +99,6 @@ def test_heartbeat_path_timeout( exec_input, ) - @markers.snapshot.skip_snapshot_verify(paths=["$..MD5OfMessageBody"]) @markers.aws.validated def test_heartbeat_no_timeout( self, @@ -112,7 +109,7 @@ def test_heartbeat_no_timeout( sqs_send_task_success_state_machine, sfn_snapshot, ): - sfn_snapshot.add_transformer(sfn_snapshot.transform.sqs_api()) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) sfn_snapshot.add_transformer( JsonpathTransformer( jsonpath="$..TaskToken", diff --git a/tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.snapshot.json b/tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.snapshot.json index 284a37a139e38..b71c35ef68f6b 100644 --- a/tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py::TestHeartbeats::test_heartbeat_timeout": { - "recorded-date": "10-03-2024, 16:39:54", + "recorded-date": "18-04-2024, 06:27:58", "recorded-content": { "get_execution_history": { "events": [ @@ -71,7 +71,7 @@ "previousEventId": 4, "taskSubmittedEventDetails": { "output": { - "MD5OfMessageBody": "2e28057614d59d8f7dfcdb8c9ac712f0", + "MD5OfMessageBody": "", "MessageId": "", "SdkHttpMetadata": { "AllHttpHeaders": { @@ -140,7 +140,7 @@ } }, "tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py::TestHeartbeats::test_heartbeat_path_timeout": { - "recorded-date": "10-03-2024, 16:40:13", + "recorded-date": "18-04-2024, 06:28:23", "recorded-content": { "get_execution_history": { "events": [ @@ -213,7 +213,7 @@ "previousEventId": 4, "taskSubmittedEventDetails": { "output": { - "MD5OfMessageBody": "e23368b7448139be32aa2f1fbd962673", + "MD5OfMessageBody": "", "MessageId": "", "SdkHttpMetadata": { "AllHttpHeaders": { @@ -282,7 +282,7 @@ } }, "tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py::TestHeartbeats::test_heartbeat_no_timeout": { - "recorded-date": "10-03-2024, 16:40:31", + "recorded-date": "18-04-2024, 06:29:19", "recorded-content": { "get_execution_history": { "events": [ @@ -352,7 +352,7 @@ "previousEventId": 4, "taskSubmittedEventDetails": { "output": { - "MD5OfMessageBody": "54d831cd55f1c29412fd982a3f7cb682", + "MD5OfMessageBody": "", "MessageId": "", "SdkHttpMetadata": { "AllHttpHeaders": { diff --git a/tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.validation.json b/tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.validation.json index 00e6e4aa048fc..926efd4d3401d 100644 --- a/tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.validation.json +++ b/tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.validation.json @@ -1,11 +1,11 @@ { "tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py::TestHeartbeats::test_heartbeat_no_timeout": { - "last_validated_date": "2024-03-10T16:40:31+00:00" + "last_validated_date": "2024-04-18T06:29:19+00:00" }, "tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py::TestHeartbeats::test_heartbeat_path_timeout": { - "last_validated_date": "2024-03-10T16:40:13+00:00" + "last_validated_date": "2024-04-18T06:28:23+00:00" }, "tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py::TestHeartbeats::test_heartbeat_timeout": { - "last_validated_date": "2024-03-10T16:39:54+00:00" + "last_validated_date": "2024-04-18T06:27:58+00:00" } } From 10c0c9a4a169ee02105bdef2106fee6a7b47d72b Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Thu, 18 Apr 2024 15:58:08 +0200 Subject: [PATCH 079/169] Fix missing config in list for CLI (#10689) --- localstack/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/localstack/config.py b/localstack/config.py index 34ff7bca0bc62..56d84930ab587 100644 --- a/localstack/config.py +++ b/localstack/config.py @@ -1134,6 +1134,7 @@ def use_custom_dns(): "DYNAMODB_WRITE_ERROR_PROBABILITY", "EAGER_SERVICE_LOADING", "ENABLE_CONFIG_UPDATES", + "EVENT_RULE_ENGINE", "EXTRA_CORS_ALLOWED_HEADERS", "EXTRA_CORS_ALLOWED_ORIGINS", "EXTRA_CORS_EXPOSE_HEADERS", From f6c21e30cd112832e4b56cd6831df7e5b947c666 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Thu, 18 Apr 2024 18:49:36 +0200 Subject: [PATCH 080/169] improve user request route detection for APIGW in CORS (#10688) Co-authored-by: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> --- localstack/aws/handlers/cors.py | 5 ++- tests/integration/test_security.py | 66 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/localstack/aws/handlers/cors.py b/localstack/aws/handlers/cors.py index ed5c0b5a4c374..88a5456e6416a 100644 --- a/localstack/aws/handlers/cors.py +++ b/localstack/aws/handlers/cors.py @@ -152,8 +152,9 @@ def should_enforce_self_managed_service(context: RequestContext) -> bool: if not config.DISABLE_CUSTOM_CORS_APIGATEWAY: # we don't check for service_name == "apigw" here because ``.execute-api.`` can be either apigw v1 or v2 - is_user_request = ( - ".execute-api." in context.request.host or PATH_USER_REQUEST in context.request.path + path = context.request.path + is_user_request = ".execute-api." in context.request.host or ( + path.startswith("/restapis/") and f"/{PATH_USER_REQUEST}" in context.request.path ) if is_user_request: return False diff --git a/tests/integration/test_security.py b/tests/integration/test_security.py index e3cccff1d95c7..b26ba218bb7d0 100644 --- a/tests/integration/test_security.py +++ b/tests/integration/test_security.py @@ -153,3 +153,69 @@ def test_no_cors_without_origin_header(self): response = requests.get(f"{config.internal_service_url()}/2015-03-31/functions/") assert response.status_code == 200 assert "access-control-allow-origin" not in response.headers + + def test_cors_apigw_not_applied(self, aws_client): + # make sure the service is loaded and has registered routes + aws_client.apigateway.get_rest_apis() + + cors_headers = [ + "access-control-allow-headers", + "access-control-allow-methods", + "access-control-allow-origin", + "access-control-allow-credentials", + ] + + # assert that the registered routes for APIGW are handled by themselves on the CORS/CRSF level + # note: we have tests for asserting proper return of CORS headers in APIGW itself when configured to do so + # + rest_api_url_user_request = ( + f"{config.internal_service_url()}/restapis/myapiid/stage/_user_request_" + ) + response = requests.get( + rest_api_url_user_request, + verify=False, + headers={ + "Origin": "https://app.localstack.cloud", + }, + ) + assert response.status_code == 404 + assert not any(response.headers.get(cors_header) for cors_header in cors_headers) + + rest_api_url_host = f"{config.internal_service_url()}/stage" + host_header = "myapiid.execute-api.localhost.localstack.cloud:4566" + response = requests.get( + rest_api_url_host, + verify=False, + headers={ + "Origin": "https://app.localstack.cloud", + "Host": host_header, + }, + ) + + assert response.status_code == 404 + assert not any(response.headers.get(cors_header) for cors_header in cors_headers) + + # now we give it a try with a route from the provider defined in the specs: GetRestApi, and an authorized origin + rest_api_url = f"{config.internal_service_url()}/restapis/myapiid" + response = requests.get( + rest_api_url, + verify=False, + headers={ + "Origin": "https://app.localstack.cloud", + "Authorization": "AWS4-HMAC-SHA256 Credential=test/20240418/us-east-1/apigateway/aws4_request, SignedHeaders=accept;host;x-amz-date, Signature=88259852931bc389bd7c2e1fb8b700b935e9d6b14bd2ef72efc3a9b20b415701", + }, + ) + assert response.status_code == 404 + assert all(response.headers.get(cors_header) for cors_header in cors_headers) + + # now do the same from an unauthorized Origin + response = requests.get( + rest_api_url, + verify=False, + headers={ + "Origin": "http://localhost:4200", + "Authorization": "AWS4-HMAC-SHA256 Credential=test/20240418/us-east-1/apigateway/aws4_request, SignedHeaders=accept;host;x-amz-date, Signature=88259852931bc389bd7c2e1fb8b700b935e9d6b14bd2ef72efc3a9b20b415701", + }, + ) + assert response.status_code == 403 + assert not any(response.headers.get(cors_header) for cors_header in cors_headers) From e88797a94aec61f301b3af9f8a0df43bb9db2ce6 Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Fri, 19 Apr 2024 12:54:36 +0200 Subject: [PATCH 081/169] fix external service ports manager unit test flakiness (#10695) --- tests/unit/test_common.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_common.py b/tests/unit/test_common.py index de52385e837d9..3091fd5dbc4be 100644 --- a/tests/unit/test_common.py +++ b/tests/unit/test_common.py @@ -584,10 +584,13 @@ def test_reserve_any_port_within_range( def test_reserve_port_all_reserved( self, external_service_ports_manager: ExternalServicePortsManager ): - external_service_ports_manager.reserve_port() - external_service_ports_manager.reserve_port() + # the external service ports manager fixture only has 2 ports available, + # reserving 3 has to raise an error, but this could also happen earlier + # (if one of the ports is blocked by something else, like a previous test) with pytest.raises(PortNotAvailableException): external_service_ports_manager.reserve_port() + external_service_ports_manager.reserve_port() + external_service_ports_manager.reserve_port() def test_reserve_same_port_twice( self, external_service_ports_manager: ExternalServicePortsManager From 51b397f33fca199c31f9ceba8abf93ffd2cf0883 Mon Sep 17 00:00:00 2001 From: Bojan Miletic <4889922+Morijarti@users.noreply.github.com> Date: Fri, 19 Apr 2024 16:53:45 +0200 Subject: [PATCH 082/169] Implementation of Lambda Advanced Configuration attribute (#10591) --- localstack/services/lambda_/api_utils.py | 1 + .../lambda_/invocation/lambda_models.py | 3 + localstack/services/lambda_/provider.py | 62 +- localstack/services/lambda_/runtimes.py | 4 +- ..._lambda_destination_scenario.snapshot.json | 10 +- ...ambda_destination_scenario.validation.json | 2 +- .../test_note_taking.snapshot.json | 22 +- .../test_note_taking.validation.json | 2 +- .../cloudformation/resources/test_lambda.py | 1 + .../resources/test_lambda.snapshot.json | 57 +- .../resources/test_lambda.validation.json | 33 +- tests/aws/services/lambda_/test_lambda.py | 106 +- .../lambda_/test_lambda.snapshot.json | 291 +- .../lambda_/test_lambda.validation.json | 134 +- tests/aws/services/lambda_/test_lambda_api.py | 315 +- .../lambda_/test_lambda_api.snapshot.json | 2825 +++++++++++++++-- .../lambda_/test_lambda_api.validation.json | 246 +- 17 files changed, 3395 insertions(+), 719 deletions(-) diff --git a/localstack/services/lambda_/api_utils.py b/localstack/services/lambda_/api_utils.py index 2aaa130dd08a0..6a66fe0229c12 100644 --- a/localstack/services/lambda_/api_utils.py +++ b/localstack/services/lambda_/api_utils.py @@ -488,6 +488,7 @@ def map_config_out( EphemeralStorage=EphemeralStorage(Size=version.config.ephemeral_storage.size), SnapStart=version.config.snap_start, RuntimeVersionConfig=version.config.runtime_version_config, + LoggingConfig=version.config.logging_config, **optional_kwargs, ) return func_conf diff --git a/localstack/services/lambda_/invocation/lambda_models.py b/localstack/services/lambda_/invocation/lambda_models.py index 5ea6b7095e599..5a98bff1a442c 100644 --- a/localstack/services/lambda_/invocation/lambda_models.py +++ b/localstack/services/lambda_/invocation/lambda_models.py @@ -27,6 +27,7 @@ InvocationType, InvokeMode, LastUpdateStatus, + LoggingConfig, PackageType, ProvisionedConcurrencyStatusEnum, Runtime, @@ -558,6 +559,8 @@ class VersionFunctionConfiguration: # file_system_configs: FileSystemConfig vpc_config: Optional[VpcConfig] = None + logging_config: LoggingConfig = dataclasses.field(default_factory=dict) + @dataclasses.dataclass(frozen=True) class FunctionVersion: diff --git a/localstack/services/lambda_/provider.py b/localstack/services/lambda_/provider.py index 1d143478ccf0c..6cc56acde0f56 100644 --- a/localstack/services/lambda_/provider.py +++ b/localstack/services/lambda_/provider.py @@ -83,6 +83,8 @@ ListProvisionedConcurrencyConfigsResponse, ListTagsResponse, ListVersionsByFunctionResponse, + LogFormat, + LoggingConfig, LogType, MasterRegion, MaxFunctionEventInvokeConfigListItems, @@ -636,7 +638,7 @@ def _validate_layers(self, new_layers: list[str], region: str, account_id: str): if layer_version_str is None: raise ValidationException( f"1 validation error detected: Value '[{layer_version_arn}]'" - + r" at 'layers' failed to satisfy constraint: Member must satisfy constraint: [Member must have length less than or equal to 140, Member must have length greater than or equal to 1, Member must satisfy regular expression pattern: (arn:[a-zA-Z0-9-]+:lambda:[a-zA-Z0-9-]+:\d{12}:layer:[a-zA-Z0-9-_]+:[0-9]+)|(arn:[a-zA-Z0-9-]+:lambda:::awslayer:[a-zA-Z0-9-_]+), Member must not be null]", + + r" at 'layers' failed to satisfy constraint: Member must satisfy constraint: [Member must have length less than or equal to 140, Member must have length greater than or equal to 1, Member must satisfy regular expression pattern: (arn:[a-zA-Z0-9-]+:lambda:[a-z]{2}((-gov)|(-iso(b?)))?-[a-z]+-\d{1}:\d{12}:layer:[a-zA-Z0-9-_]+:[0-9]+)|(arn:[a-zA-Z0-9-]+:lambda:::awslayer:[a-zA-Z0-9-_]+), Member must not be null]", ) state = lambda_stores[layer_account_id][layer_region] @@ -867,6 +869,33 @@ def create_function( ) # Runtime management controls are not available when providing a custom image runtime_version_config = None + if "LoggingConfig" in request: + logging_config = request["LoggingConfig"] + LOG.warning( + "Advanced Lambda Logging Configuration is currently mocked " + "and will not impact the logging behavior. " + "Please create a feature request if needed." + ) + + # when switching to JSON, app and system level log is auto set to INFO + if logging_config.get("LogFormat", None) == LogFormat.JSON: + logging_config = { + "ApplicationLogLevel": "INFO", + "SystemLogLevel": "INFO", + "LogGroup": f"/aws/lambda/{function_name}", + } | logging_config + else: + logging_config = ( + LoggingConfig( + LogFormat=LogFormat.Text, LogGroup=f"/aws/lambda/{function_name}" + ) + | logging_config + ) + + else: + logging_config = LoggingConfig( + LogFormat=LogFormat.Text, LogGroup=f"/aws/lambda/{function_name}" + ) version = FunctionVersion( id=arn, @@ -906,6 +935,7 @@ def create_function( code=StateReasonCode.Creating, reason="The function is being created.", ), + logging_config=logging_config, ), ) fn.versions["$LATEST"] = version @@ -1044,6 +1074,36 @@ def update_function_configuration( working_directory=new_image_config.get("WorkingDirectory"), ) + if "LoggingConfig" in request: + logging_config = request["LoggingConfig"] + LOG.warning( + "Advanced Lambda Logging Configuration is currently mocked " + "and will not impact the logging behavior. " + "Please create a feature request if needed." + ) + + # when switching to JSON, app and system level log is auto set to INFO + if logging_config.get("LogFormat", None) == LogFormat.JSON: + logging_config = { + "ApplicationLogLevel": "INFO", + "SystemLogLevel": "INFO", + } | logging_config + + last_config = latest_version_config.logging_config + + # add partial update + new_logging_config = last_config | logging_config + + # in case we switched from JSON to Text we need to remove LogLevel keys + if ( + new_logging_config.get("LogFormat") == LogFormat.Text + and last_config.get("LogFormat") == LogFormat.JSON + ): + new_logging_config.pop("ApplicationLogLevel", None) + new_logging_config.pop("SystemLogLevel", None) + + replace_kwargs["logging_config"] = new_logging_config + if "TracingConfig" in request: new_mode = request.get("TracingConfig", {}).get("Mode") if new_mode: diff --git a/localstack/services/lambda_/runtimes.py b/localstack/services/lambda_/runtimes.py index fc4d864ceccfd..5cd425cb509df 100644 --- a/localstack/services/lambda_/runtimes.py +++ b/localstack/services/lambda_/runtimes.py @@ -81,7 +81,7 @@ SUPPORTED_RUNTIMES: list[Runtime] = list(set(IMAGE_MAPPING.keys()) - set(DEPRECATED_RUNTIMES)) # A temporary list of missing runtimes not yet supported in LocalStack. Used for modular updates. -MISSING_RUNTIMES = [] +MISSING_RUNTIMES = [Runtime.ruby3_3] # An unordered list of all Lambda runtimes supported by LocalStack. ALL_RUNTIMES: list[Runtime] = list(IMAGE_MAPPING.keys()) @@ -130,6 +130,6 @@ SNAP_START_SUPPORTED_RUNTIMES = [Runtime.java11, Runtime.java17, Runtime.java21] # An ordered list of all Lambda runtimes considered valid by AWS. Matching snapshots in test_create_lambda_exceptions -VALID_RUNTIMES: str = "[nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, java8.al2, ruby3.2, python3.8, python3.9]" +VALID_RUNTIMES: str = "[nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9]" # An ordered list of all Lambda runtimes for layers considered valid by AWS. Matching snapshots in test_layer_exceptions VALID_LAYER_RUNTIMES: str = "[ruby2.6, dotnetcore1.0, python3.7, nodejs8.10, nasa, ruby2.7, python2.7-greengrass, dotnetcore2.0, python3.8, java21, dotnet6, dotnetcore2.1, python3.9, java11, nodejs6.10, provided, dotnetcore3.1, dotnet8, java17, nodejs, nodejs4.3, java8.al2, go1.x, nodejs20.x, go1.9, byol, nodejs10.x, provided.al2023, python3.10, java8, nodejs12.x, python3.11, nodejs8.x, python3.12, nodejs14.x, nodejs8.9, python3.13, nodejs16.x, provided.al2, nodejs4.3-edge, nodejs18.x, ruby3.2, python3.4, ruby3.3, ruby2.5, python3.6, python2.7]" diff --git a/tests/aws/scenario/lambda_destination/test_lambda_destination_scenario.snapshot.json b/tests/aws/scenario/lambda_destination/test_lambda_destination_scenario.snapshot.json index b4d5231651ddd..dec5cdda4a11d 100644 --- a/tests/aws/scenario/lambda_destination/test_lambda_destination_scenario.snapshot.json +++ b/tests/aws/scenario/lambda_destination/test_lambda_destination_scenario.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/scenario/lambda_destination/test_lambda_destination_scenario.py::TestLambdaDestinationScenario::test_infra": { - "recorded-date": "04-10-2023, 16:04:47", + "recorded-date": "17-04-2024, 07:04:49", "recorded-content": { "get_fn_1": { "Code": { @@ -22,6 +22,10 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -71,6 +75,10 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", diff --git a/tests/aws/scenario/lambda_destination/test_lambda_destination_scenario.validation.json b/tests/aws/scenario/lambda_destination/test_lambda_destination_scenario.validation.json index accd99a0f1f0a..319c8dc4b1d18 100644 --- a/tests/aws/scenario/lambda_destination/test_lambda_destination_scenario.validation.json +++ b/tests/aws/scenario/lambda_destination/test_lambda_destination_scenario.validation.json @@ -3,6 +3,6 @@ "last_validated_date": "2023-10-04T14:05:34+00:00" }, "tests/aws/scenario/lambda_destination/test_lambda_destination_scenario.py::TestLambdaDestinationScenario::test_infra": { - "last_validated_date": "2023-10-04T14:04:47+00:00" + "last_validated_date": "2024-04-17T07:04:49+00:00" } } diff --git a/tests/aws/scenario/note_taking/test_note_taking.snapshot.json b/tests/aws/scenario/note_taking/test_note_taking.snapshot.json index a501d8cdefe96..e6f49404136af 100644 --- a/tests/aws/scenario/note_taking/test_note_taking.snapshot.json +++ b/tests/aws/scenario/note_taking/test_note_taking.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/scenario/note_taking/test_note_taking.py::TestNoteTakingScenario::test_validate_infra_setup": { - "recorded-date": "28-09-2023, 16:08:28", + "recorded-date": "16-04-2024, 08:42:29", "recorded-content": { "describe_stack_resources": { "StackResources": [ @@ -536,6 +536,10 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -590,6 +594,10 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -644,6 +652,10 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -698,6 +710,10 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -752,6 +768,10 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", diff --git a/tests/aws/scenario/note_taking/test_note_taking.validation.json b/tests/aws/scenario/note_taking/test_note_taking.validation.json index 6a8f1cd42dcc7..de09ce6687062 100644 --- a/tests/aws/scenario/note_taking/test_note_taking.validation.json +++ b/tests/aws/scenario/note_taking/test_note_taking.validation.json @@ -1,5 +1,5 @@ { "tests/aws/scenario/note_taking/test_note_taking.py::TestNoteTakingScenario::test_validate_infra_setup": { - "last_validated_date": "2023-09-28T14:08:28+00:00" + "last_validated_date": "2024-04-16T08:42:29+00:00" } } diff --git a/tests/aws/services/cloudformation/resources/test_lambda.py b/tests/aws/services/cloudformation/resources/test_lambda.py index 75b8be84a0632..a99ce3cf410e6 100644 --- a/tests/aws/services/cloudformation/resources/test_lambda.py +++ b/tests/aws/services/cloudformation/resources/test_lambda.py @@ -106,6 +106,7 @@ def test_lambda_w_dynamodb_event_filter_update(deploy_cfn_template, snapshot, aw "$..access-control-expose-headers", "$..server", "$..content-length", + "$..InvokeMode", ] ) @markers.aws.validated diff --git a/tests/aws/services/cloudformation/resources/test_lambda.snapshot.json b/tests/aws/services/cloudformation/resources/test_lambda.snapshot.json index 224304b9e15bc..4982d3b1ef1fc 100644 --- a/tests/aws/services/cloudformation/resources/test_lambda.snapshot.json +++ b/tests/aws/services/cloudformation/resources/test_lambda.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/cloudformation/resources/test_lambda.py::test_cfn_function_url": { - "recorded-date": "27-02-2023, 17:27:12", + "recorded-date": "16-04-2024, 08:16:02", "recorded-content": { "url_resource": { "StackResourceDetail": { @@ -26,6 +26,7 @@ "CreationTime": "date", "FunctionArn": "arn:aws:lambda::111111111111:function:", "FunctionUrl": "", + "InvokeMode": "BUFFERED", "LastModifiedTime": "date", "ResponseMetadata": { "HTTPHeaders": {}, @@ -49,6 +50,7 @@ "CreationTime": "date", "FunctionArn": "arn:aws:lambda::111111111111:function:", "FunctionUrl": "", + "InvokeMode": "BUFFERED", "LastModifiedTime": "date", "ResponseMetadata": { "HTTPHeaders": {}, @@ -66,7 +68,7 @@ } }, "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_alias": { - "recorded-date": "08-09-2023, 11:43:01", + "recorded-date": "09-04-2024, 07:19:19", "recorded-content": { "stack_resource_descriptions": { "StackResources": [ @@ -138,7 +140,7 @@ } }, "tests/aws/services/cloudformation/resources/test_lambda.py::TestCfnLambdaIntegrations::test_cfn_lambda_permissions": { - "recorded-date": "27-02-2023, 17:34:08", + "recorded-date": "09-04-2024, 07:26:03", "recorded-content": { "stack_resources": { "StackResources": [ @@ -228,6 +230,10 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -271,7 +277,10 @@ "numMinDelayRetries": 0, "backoffFunction": "linear" }, - "disableSubscriptionOverrides": false + "disableSubscriptionOverrides": false, + "defaultRequestPolicy": { + "headerContentType": "text/plain; charset=UTF-8" + } } }, "Owner": "111111111111", @@ -344,7 +353,7 @@ } }, "tests/aws/services/cloudformation/resources/test_lambda.py::TestCfnLambdaIntegrations::test_cfn_lambda_sqs_source": { - "recorded-date": "27-02-2023, 17:38:51", + "recorded-date": "09-04-2024, 07:29:12", "recorded-content": { "stack_resources": { "StackResources": [ @@ -459,6 +468,10 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -577,7 +590,7 @@ } }, "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_code_signing_config": { - "recorded-date": "27-02-2023, 17:28:57", + "recorded-date": "09-04-2024, 07:19:51", "recorded-content": { "stack_resource_descriptions": { "StackResources": [ @@ -622,7 +635,7 @@ } }, "tests/aws/services/cloudformation/resources/test_lambda.py::test_event_invoke_config": { - "recorded-date": "21-11-2023, 11:00:12", + "recorded-date": "09-04-2024, 07:20:36", "recorded-content": { "event_invoke_config": { "DestinationConfig": { @@ -641,7 +654,7 @@ } }, "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_version": { - "recorded-date": "27-02-2023, 17:31:32", + "recorded-date": "09-04-2024, 07:21:37", "recorded-content": { "stack_resources": { "StackResources": [ @@ -703,6 +716,10 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -732,6 +749,10 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -773,6 +794,10 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -800,7 +825,7 @@ } }, "tests/aws/services/cloudformation/resources/test_lambda.py::TestCfnLambdaIntegrations::test_cfn_lambda_dynamodb_source": { - "recorded-date": "19-12-2023, 08:08:47", + "recorded-date": "09-04-2024, 07:34:20", "recorded-content": { "stack_resources": { "StackResources": [ @@ -918,6 +943,10 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -1093,7 +1122,7 @@ } }, "tests/aws/services/cloudformation/resources/test_lambda.py::TestCfnLambdaIntegrations::test_cfn_lambda_kinesis_source": { - "recorded-date": "27-02-2023, 17:44:31", + "recorded-date": "09-04-2024, 07:37:55", "recorded-content": { "stack_resources": { "StackResources": [ @@ -1215,6 +1244,10 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -1358,7 +1391,7 @@ } }, "tests/aws/services/cloudformation/resources/test_lambda.py::test_multiple_lambda_permissions_for_singlefn": { - "recorded-date": "09-03-2023, 22:07:56", + "recorded-date": "09-04-2024, 07:25:05", "recorded-content": { "policy": { "Policy": { @@ -1394,7 +1427,7 @@ } }, "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_cfn_dead_letter_config_async_invocation": { - "recorded-date": "04-12-2023, 12:46:52", + "recorded-date": "09-04-2024, 07:39:50", "recorded-content": { "failed-async-lambda": { "Messages": [ diff --git a/tests/aws/services/cloudformation/resources/test_lambda.validation.json b/tests/aws/services/cloudformation/resources/test_lambda.validation.json index 1a4ede1167750..bed5f0c9c325a 100644 --- a/tests/aws/services/cloudformation/resources/test_lambda.validation.json +++ b/tests/aws/services/cloudformation/resources/test_lambda.validation.json @@ -1,41 +1,50 @@ { "tests/aws/services/cloudformation/resources/test_lambda.py::TestCfnLambdaIntegrations::test_cfn_lambda_dynamodb_source": { - "last_validated_date": "2023-12-19T07:08:47+00:00" + "last_validated_date": "2024-04-09T07:34:20+00:00" }, "tests/aws/services/cloudformation/resources/test_lambda.py::TestCfnLambdaIntegrations::test_cfn_lambda_kinesis_source": { - "last_validated_date": "2023-02-27T16:44:31+00:00" + "last_validated_date": "2024-04-09T07:37:55+00:00" }, "tests/aws/services/cloudformation/resources/test_lambda.py::TestCfnLambdaIntegrations::test_cfn_lambda_permissions": { - "last_validated_date": "2023-02-27T16:34:08+00:00" + "last_validated_date": "2024-04-09T07:26:03+00:00" }, "tests/aws/services/cloudformation/resources/test_lambda.py::TestCfnLambdaIntegrations::test_cfn_lambda_sqs_source": { - "last_validated_date": "2023-02-27T16:38:51+00:00" + "last_validated_date": "2024-04-09T07:29:12+00:00" }, "tests/aws/services/cloudformation/resources/test_lambda.py::TestCfnLambdaIntegrations::test_lambda_dynamodb_event_filter": { - "last_validated_date": "2024-03-12T18:17:17+00:00" + "last_validated_date": "2024-04-09T07:31:17+00:00" }, "tests/aws/services/cloudformation/resources/test_lambda.py::test_cfn_function_url": { - "last_validated_date": "2023-02-27T16:27:12+00:00" + "last_validated_date": "2024-04-16T08:16:02+00:00" }, "tests/aws/services/cloudformation/resources/test_lambda.py::test_event_invoke_config": { - "last_validated_date": "2023-11-21T10:00:12+00:00" + "last_validated_date": "2024-04-09T07:20:36+00:00" }, "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_alias": { - "last_validated_date": "2023-09-08T09:43:01+00:00" + "last_validated_date": "2024-04-09T07:19:19+00:00" }, "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_cfn_dead_letter_config_async_invocation": { - "last_validated_date": "2023-12-04T11:46:52+00:00" + "last_validated_date": "2024-04-09T07:39:50+00:00" + }, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_cfn_run": { + "last_validated_date": "2024-04-09T07:22:32+00:00" }, "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_code_signing_config": { - "last_validated_date": "2023-02-27T16:28:57+00:00" + "last_validated_date": "2024-04-09T07:19:51+00:00" }, "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_version": { - "last_validated_date": "2023-02-27T16:31:32+00:00" + "last_validated_date": "2024-04-09T07:21:37+00:00" }, "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_w_dynamodb_event_filter_update": { "last_validated_date": "2024-04-11T18:29:12+00:00" }, "tests/aws/services/cloudformation/resources/test_lambda.py::test_multiple_lambda_permissions_for_singlefn": { - "last_validated_date": "2023-03-09T21:07:56+00:00" + "last_validated_date": "2024-04-09T07:25:05+00:00" + }, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_python_lambda_code_deployed_via_s3": { + "last_validated_date": "2024-04-09T07:38:32+00:00" + }, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_update_lambda_permissions": { + "last_validated_date": "2024-04-09T07:23:41+00:00" } } diff --git a/tests/aws/services/lambda_/test_lambda.py b/tests/aws/services/lambda_/test_lambda.py index fb060c89b84bb..668f99032ab53 100644 --- a/tests/aws/services/lambda_/test_lambda.py +++ b/tests/aws/services/lambda_/test_lambda.py @@ -185,7 +185,7 @@ def test_large_payloads(self, caplog, create_lambda_function, aws_client): create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) large_value = "test123456" * 100 * 1000 * 5 payload = {"test": large_value} # 5MB payload @@ -205,7 +205,7 @@ def test_lambda_large_response(self, caplog, create_lambda_function, aws_client) create_lambda_function( handler_file=TEST_LAMBDA_CUSTOM_RESPONSE_SIZE, func_name=function_name, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) response_size = 6 * 1024 * 1024 # actually + 100 is the upper limit payload = {"bytenum": response_size} # 6MB response size @@ -221,7 +221,7 @@ def test_lambda_too_large_response(self, create_lambda_function, aws_client, sna create_lambda_function( handler_file=TEST_LAMBDA_CUSTOM_RESPONSE_SIZE, func_name=function_name, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) response_size = 7 * 1024 * 1024 # 7MB response size (i.e. over 6MB limit) payload = {"bytenum": response_size} @@ -261,7 +261,7 @@ def test_lambda_too_large_response_but_with_custom_limit( create_lambda_function( handler_file=TEST_LAMBDA_CUSTOM_RESPONSE_SIZE, func_name=function_name, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) response_size = 7 * 1024 * 1024 # 7MB response size (i.e. over 6MB limit) payload = {"bytenum": response_size} @@ -280,7 +280,7 @@ def test_function_state(self, lambda_su_role, snapshot, create_lambda_function_a # create_response is the original create call response, even though the fixture waits until it's not pending create_response = create_lambda_function_aws( FunctionName=function_name, - Runtime=Runtime.python3_10, + Runtime=Runtime.python3_12, Handler="handler.handler", Role=lambda_su_role, Code={"ZipFile": zip_file}, @@ -324,7 +324,7 @@ def test_assume_role( create_result = create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ROLE, func_name=function_name, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) # Example: arn:aws:iam::111111111111:role/lambda-autogenerated-d5da4d52 create_role_resource = arns.extract_resource_from_arn( @@ -361,7 +361,7 @@ def test_lambda_different_iam_keys_environment( create_result = create_lambda_function( func_name=function_name, handler_file=TEST_LAMBDA_SLEEP_ENVIRONMENT, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, role=lambda_su_role, ) snapshot.match("create-result", create_result) @@ -437,7 +437,7 @@ def test_runtime_introspection_x86(self, create_lambda_function, snapshot, aws_c create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_INTROSPECT_PYTHON, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, timeout=9, Architectures=[Architecture.x86_64], ) @@ -460,7 +460,7 @@ def test_runtime_introspection_arm(self, create_lambda_function, snapshot, aws_c create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_INTROSPECT_PYTHON, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, timeout=9, Architectures=[Architecture.arm64], ) @@ -475,14 +475,14 @@ def test_runtime_ulimits(self, create_lambda_function, snapshot, monkeypatch, aw monkeypatch.setattr( config, "LAMBDA_DOCKER_FLAGS", - "--ulimit nofile=1024:1024 --ulimit nproc=1024:1024 --ulimit core=-1:-1 --ulimit stack=8388608:-1 --ulimit memlock=65536:65536", + "--ulimit nofile=1024:1024 --ulimit nproc=742:742 --ulimit core=-1:-1 --ulimit stack=8388608:-1 --ulimit memlock=65536:65536", ) func_name = f"test_lambda_ulimits_{short_uid()}" create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_ULIMITS, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) invoke_result = aws_client.lambda_.invoke(FunctionName=func_name) @@ -503,7 +503,7 @@ def test_ignore_architecture(self, create_lambda_function, monkeypatch, aws_clie create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_INTROSPECT_PYTHON, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, Architectures=[non_native_architecture], ) @@ -514,8 +514,6 @@ def test_ignore_architecture(self, create_lambda_function, monkeypatch, aws_clie @markers.snapshot.skip_snapshot_verify( paths=[ - "$..LoggingConfig", - "$..CreateFunctionResponse.LoggingConfig", "$..RuntimeVersionConfig.RuntimeVersionArn", ] ) @@ -601,7 +599,7 @@ def test_lambda_invoke_with_timeout(self, create_lambda_function, snapshot, aws_ create_result = create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_TIMEOUT_PYTHON, - runtime=Runtime.python3_8, + runtime=Runtime.python3_12, client=aws_client.lambda_, timeout=1, ) @@ -638,7 +636,7 @@ def test_lambda_invoke_no_timeout(self, create_lambda_function, snapshot, aws_cl create_result = create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_TIMEOUT_PYTHON, - runtime=Runtime.python3_8, + runtime=Runtime.python3_12, client=aws_client.lambda_, timeout=2, ) @@ -686,7 +684,7 @@ def test_lambda_invoke_timed_out_environment_reuse( create_result = create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_TIMEOUT_ENV_PYTHON, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, client=aws_client.lambda_, timeout=1, ) @@ -749,7 +747,6 @@ def assert_events(): @markers.aws.validated @markers.snapshot.skip_snapshot_verify( paths=[ - "$..CreateFunctionResponse.LoggingConfig", # not set directly on init in lambda, but only on runtime processes "$..Payload.environment.AWS_ACCESS_KEY_ID", "$..Payload.environment.AWS_SECRET_ACCESS_KEY", @@ -870,7 +867,7 @@ def test_lambda_url_invocation(self, create_lambda_function, snapshot, returnval create_lambda_function( func_name=function_name, handler_file=handler_file, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) url_config = aws_client.lambda_.create_function_url_config( @@ -927,7 +924,7 @@ def test_lambda_url_echo_invoke( create_lambda_function( func_name=function_name, zip_file=testutil.create_zip_file(TEST_LAMBDA_URL, get_content=True), - runtime=Runtime.nodejs16_x, + runtime=Runtime.nodejs20_x, handler="lambda_url.handler", ) @@ -989,7 +986,7 @@ def test_lambda_url_headers_and_status(self, create_lambda_function, aws_client) create_lambda_function( func_name=function_name, zip_file=testutil.create_zip_file(TEST_LAMBDA_PYTHON_ECHO_JSON_BODY, get_content=True), - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, handler="lambda_echo_json_body.handler", ) url_config = aws_client.lambda_.create_function_url_config( @@ -1083,7 +1080,7 @@ def test_lambda_url_invocation_exception(self, create_lambda_function, snapshot, create_lambda_function( func_name=function_name, handler_file=TEST_LAMBDA_PYTHON_UNHANDLED_ERROR, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) get_fn_result = aws_client.lambda_.get_function(FunctionName=function_name) snapshot.match("get_fn_result", get_fn_result) @@ -1212,7 +1209,7 @@ def test_lambda_permission_url_invocation(self, create_lambda_function, snapshot create_lambda_function( func_name=function_name, zip_file=testutil.create_zip_file(TEST_LAMBDA_URL, get_content=True), - runtime=Runtime.nodejs18_x, + runtime=Runtime.nodejs20_x, handler="lambda_url.handler", ) url_config = aws_client.lambda_.create_function_url_config( @@ -1333,7 +1330,7 @@ def test_invocation_type_event_error(self, create_lambda_function, snapshot, aws creation_response = create_lambda_function( func_name=function_name, handler_file=TEST_LAMBDA_PYTHON_UNHANDLED_ERROR, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) snapshot.match("creation_response", creation_response) invocation_response = aws_client.lambda_.invoke( @@ -1383,14 +1380,14 @@ def test_invocation_with_qualifier( zip_file = create_lambda_archive( load_file(TEST_LAMBDA_PYTHON), get_content=True, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) aws_client.s3.upload_fileobj(BytesIO(zip_file), s3_bucket, bucket_key) # create lambda function response = create_lambda_function_aws( FunctionName=function_name, - Runtime=Runtime.python3_10, + Runtime=Runtime.python3_12, Role=lambda_su_role, Publish=True, Handler="handler.handler", @@ -1436,14 +1433,14 @@ def test_upload_lambda_from_s3( zip_file = testutil.create_lambda_archive( load_file(TEST_LAMBDA_PYTHON), get_content=True, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) aws_client.s3.upload_fileobj(BytesIO(zip_file), s3_bucket, bucket_key) # create lambda function create_response = create_lambda_function_aws( FunctionName=function_name, - Runtime=Runtime.python3_10, + Runtime=Runtime.python3_12, Handler="handler.handler", Role=lambda_su_role, Code={"S3Bucket": s3_bucket, "S3Key": bucket_key}, @@ -1469,7 +1466,7 @@ def test_lambda_with_context( func_name=function_name, handler_file=TEST_LAMBDA_INTEGRATION_NODEJS, handler="lambda_integration.handler", - runtime=Runtime.nodejs16_x, + runtime=Runtime.nodejs20_x, ) snapshot.match("creation", creation_response) ctx = { @@ -1511,7 +1508,7 @@ def test_lambda_runtime_error(self, aws_client, create_lambda_function, snapshot func_name=function_name, handler_file=TEST_LAMBDA_PYTHON_RUNTIME_ERROR, handler="lambda_runtime_error.handler", - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) result = aws_client.lambda_.invoke( @@ -1532,7 +1529,7 @@ def test_lambda_runtime_exit(self, aws_client, create_lambda_function, snapshot) func_name=function_name, handler_file=TEST_LAMBDA_PYTHON_RUNTIME_EXIT, handler="lambda_runtime_exit.handler", - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) result = aws_client.lambda_.invoke( @@ -1553,7 +1550,7 @@ def test_lambda_runtime_exit_segfault(self, aws_client, create_lambda_function, func_name=function_name, handler_file=TEST_LAMBDA_PYTHON_RUNTIME_EXIT_SEGFAULT, handler="lambda_runtime_exit_segfault.handler", - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) result = aws_client.lambda_.invoke( @@ -1571,7 +1568,7 @@ def test_lambda_handler_error(self, aws_client, create_lambda_function, snapshot func_name=function_name, handler_file=TEST_LAMBDA_PYTHON_HANDLER_ERROR, handler="lambda_handler_error.handler", - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) result = aws_client.lambda_.invoke( @@ -1592,7 +1589,7 @@ def test_lambda_handler_exit(self, aws_client, create_lambda_function, snapshot) func_name=function_name, handler_file=TEST_LAMBDA_PYTHON_HANDLER_EXIT, handler="lambda_handler_exit.handler", - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) result = aws_client.lambda_.invoke( @@ -1613,7 +1610,7 @@ def test_lambda_runtime_wrapper_not_found(self, aws_client, create_lambda_functi func_name=function_name, handler_file=TEST_LAMBDA_PYTHON_ECHO, handler="lambda_echo.handler", - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, envvars={"AWS_LAMBDA_EXEC_WRAPPER": "/idontexist.sh"}, ) @@ -1639,7 +1636,7 @@ def test_lambda_runtime_startup_timeout( func_name=function_name, handler_file=TEST_LAMBDA_PYTHON_ECHO, handler="lambda_echo.handler", - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) client_config = Config( @@ -1669,7 +1666,7 @@ def test_lambda_runtime_startup_error( func_name=function_name, handler_file=TEST_LAMBDA_PYTHON_ECHO, handler="lambda_echo.handler", - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) client_config = Config( @@ -1713,7 +1710,7 @@ def test_lambda_invoke_payload_encoding_error( func_name=function_name, handler_file=TEST_LAMBDA_PYTHON_ECHO, handler="lambda_echo.handler", - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) client_config = Config( @@ -1739,7 +1736,7 @@ def test_delete_lambda_during_sync_invoke(self, aws_client, create_lambda_functi create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_SLEEP_ENVIRONMENT, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, Timeout=30, ) @@ -1808,7 +1805,7 @@ def test_cross_account_access( func_arn = create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_INTROSPECT_PYTHON, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, )["CreateFunctionResponse"]["FunctionArn"] layer_name = f"layer-{short_uid()}" @@ -1928,7 +1925,7 @@ def test_lambda_concurrency_crud(self, snapshot, create_lambda_function, aws_cli create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_PYTHON_ECHO, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) default_concurrency_result = aws_client.lambda_.get_function_concurrency( @@ -1969,7 +1966,7 @@ def test_lambda_concurrency_block(self, snapshot, create_lambda_function, aws_cl create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_PYTHON_ECHO, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, ) # reserved concurrency @@ -2044,7 +2041,7 @@ def test_lambda_provisioned_concurrency_moves_with_alias( create_result = create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_INVOCATION_TYPE, - runtime=Runtime.python3_8, + runtime=Runtime.python3_12, client=aws_client.lambda_, timeout=2, ) @@ -2175,7 +2172,7 @@ def test_provisioned_concurrency(self, create_lambda_function, snapshot, aws_cli create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_INVOCATION_TYPE, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, client=aws_client.lambda_, ) @@ -2221,7 +2218,7 @@ def test_lambda_provisioned_concurrency_scheduling( create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_INVOCATION_TYPE, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, timeout=10, ) @@ -2289,7 +2286,7 @@ def test_reserved_concurrency_async_queue(self, create_lambda_function, snapshot create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_INTROSPECT_PYTHON, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, client=aws_client.lambda_, timeout=20, ) @@ -2369,7 +2366,7 @@ def test_reserved_concurrency( create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_INTROSPECT_PYTHON, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, client=aws_client.lambda_, timeout=20, ) @@ -2425,7 +2422,7 @@ def test_reserved_provisioned_overlap(self, create_lambda_function, snapshot, aw create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_INVOCATION_TYPE, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, client=aws_client.lambda_, ) @@ -2507,7 +2504,7 @@ def test_lambda_versions_with_code_changes( Code={"ZipFile": zip_file_v1}, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_10, + Runtime=Runtime.python3_12, Description="No version :(", ) snapshot.match("create_response", create_response) @@ -2569,7 +2566,6 @@ def test_lambda_versions_with_code_changes( ) snapshot.match("invocation_result_v1_end", invocation_result_v1) - @markers.snapshot.skip_snapshot_verify(paths=["$..LoggingConfig"]) @markers.aws.validated def test_lambda_handler_update(self, aws_client, create_lambda_function, snapshot): func_name = f"test_lambda_{short_uid()}" @@ -2625,7 +2621,7 @@ def test_lambda_alias_moving( Code={"ZipFile": zip_file_v1}, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_10, + Runtime=Runtime.python3_12, Description="No version :(", ) snapshot.match("create_response", create_response) @@ -2712,7 +2708,7 @@ def test_alias_routingconfig( Code={"ZipFile": zip_file_v1}, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_10, + Runtime=Runtime.python3_12, Description="First version :)", Publish=True, ) @@ -2769,7 +2765,7 @@ def test_request_id_invoke(self, aws_client, create_lambda_function, snapshot): create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_PYTHON_REQUEST_ID, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, client=aws_client.lambda_, ) @@ -2811,7 +2807,7 @@ def test_request_id_invoke_url(self, aws_client, create_lambda_function, snapsho create_lambda_function( func_name=fn_name, handler_file=handler_file, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, client=aws_client.lambda_, ) @@ -2870,7 +2866,7 @@ def test_request_id_async_invoke_with_retry( create_lambda_function( func_name=func_name, handler_file=TEST_LAMBDA_CONTEXT_REQID, - runtime=Runtime.python3_10, + runtime=Runtime.python3_12, client=aws_client.lambda_, ) diff --git a/tests/aws/services/lambda_/test_lambda.snapshot.json b/tests/aws/services/lambda_/test_lambda.snapshot.json index 91f068fd40009..b3a86ad228c54 100644 --- a/tests/aws/services/lambda_/test_lambda.snapshot.json +++ b/tests/aws/services/lambda_/test_lambda.snapshot.json @@ -152,7 +152,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_cache_local[nodejs]": { - "recorded-date": "20-11-2023, 22:57:11", + "recorded-date": "08-04-2024, 16:55:57", "recorded-content": { "first_invoke_result": { "ExecutedVersion": "$LATEST", @@ -179,7 +179,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_cache_local[python]": { - "recorded-date": "20-11-2023, 22:57:14", + "recorded-date": "08-04-2024, 16:56:00", "recorded-content": { "first_invoke_result": { "ExecutedVersion": "$LATEST", @@ -206,7 +206,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_invoke_with_timeout": { - "recorded-date": "20-11-2023, 22:04:37", + "recorded-date": "08-04-2024, 16:56:05", "recorded-content": { "create-result": { "CreateEventSourceMappingResponse": null, @@ -227,11 +227,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.8", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -268,7 +272,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_invoke_no_timeout": { - "recorded-date": "20-11-2023, 22:04:53", + "recorded-date": "08-04-2024, 16:56:14", "recorded-content": { "create-result": { "CreateEventSourceMappingResponse": null, @@ -289,11 +293,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.8", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -327,7 +335,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_function_state": { - "recorded-date": "20-11-2023, 22:03:44", + "recorded-date": "08-04-2024, 16:55:21", "recorded-content": { "create-fn-response": { "Architectures": [ @@ -343,11 +351,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -388,11 +400,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -415,7 +431,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_with_logs[python3.10]": { - "recorded-date": "20-11-2023, 22:05:55", + "recorded-date": "08-04-2024, 16:57:44", "recorded-content": { "invoke": { "ExecutedVersion": "$LATEST", @@ -438,7 +454,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_with_logs[nodejs16.x]": { - "recorded-date": "20-11-2023, 22:05:51", + "recorded-date": "08-04-2024, 16:57:41", "recorded-content": { "invoke": { "ExecutedVersion": "$LATEST", @@ -475,7 +491,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_request_response[python3.10]": { - "recorded-date": "20-11-2023, 22:06:02", + "recorded-date": "08-04-2024, 16:57:51", "recorded-content": { "invoke-result": { "ExecutedVersion": "$LATEST", @@ -489,7 +505,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_request_response[nodejs16.x]": { - "recorded-date": "20-11-2023, 22:05:58", + "recorded-date": "08-04-2024, 16:57:48", "recorded-content": { "invoke-result": { "ExecutedVersion": "$LATEST", @@ -516,7 +532,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_event[python3.10]": { - "recorded-date": "20-11-2023, 22:06:20", + "recorded-date": "08-04-2024, 16:58:09", "recorded-content": { "invoke-result": { "Payload": "", @@ -529,7 +545,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_event[nodejs16.x]": { - "recorded-date": "20-11-2023, 22:06:10", + "recorded-date": "08-04-2024, 16:57:59", "recorded-content": { "invoke-result": { "Payload": "", @@ -673,7 +689,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_with_qualifier": { - "recorded-date": "20-11-2023, 22:06:33", + "recorded-date": "08-04-2024, 16:58:22", "recorded-content": { "creation-response": { "Architectures": [ @@ -689,11 +705,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -749,7 +769,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_upload_lambda_from_s3": { - "recorded-date": "20-11-2023, 22:06:39", + "recorded-date": "08-04-2024, 16:58:27", "recorded-content": { "creation-response": { "Architectures": [ @@ -765,11 +785,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -893,7 +917,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_lambda_concurrency_block": { - "recorded-date": "20-11-2023, 23:01:42", + "recorded-date": "08-04-2024, 17:02:07", "recorded-content": { "v1_result": { "Architectures": [ @@ -913,11 +937,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1005,7 +1033,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaVersions::test_lambda_versions_with_code_changes": { - "recorded-date": "20-11-2023, 22:08:09", + "recorded-date": "08-04-2024, 17:10:52", "recorded-content": { "create_response": { "Architectures": [ @@ -1021,11 +1049,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1061,11 +1093,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1101,11 +1137,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1165,11 +1205,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1215,11 +1259,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1295,7 +1343,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaAliases::test_lambda_alias_moving": { - "recorded-date": "20-11-2023, 22:08:16", + "recorded-date": "08-04-2024, 17:11:05", "recorded-content": { "create_response": { "Architectures": [ @@ -1311,11 +1359,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1390,11 +1442,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1441,11 +1497,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1516,7 +1576,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaAliases::test_alias_routingconfig": { - "recorded-date": "20-11-2023, 22:08:28", + "recorded-date": "08-04-2024, 17:11:17", "recorded-content": { "create_alias_response": { "AliasArn": "arn:aws:lambda::111111111111:function:", @@ -1537,7 +1597,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_lambda_different_iam_keys_environment": { - "recorded-date": "20-11-2023, 22:04:01", + "recorded-date": "08-04-2024, 16:55:38", "recorded-content": { "create-result": { "CreateEventSourceMappingResponse": null, @@ -1558,11 +1618,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2086,7 +2150,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_invoke_timed_out_environment_reuse": { - "recorded-date": "20-11-2023, 22:05:01", + "recorded-date": "08-04-2024, 16:56:22", "recorded-content": { "create-result": { "CreateEventSourceMappingResponse": null, @@ -2107,11 +2171,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2210,7 +2278,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation_exception": { - "recorded-date": "20-11-2023, 22:05:44", + "recorded-date": "08-04-2024, 16:57:23", "recorded-content": { "get_fn_result": { "Code": { @@ -2235,11 +2303,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2290,7 +2362,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_lambda_concurrency_crud": { - "recorded-date": "20-11-2023, 22:59:12", + "recorded-date": "08-04-2024, 16:59:43", "recorded-content": { "get_function_concurrency_default": { "ResponseMetadata": { @@ -2321,7 +2393,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaPermissions::test_lambda_permission_url_invocation": { - "recorded-date": "20-11-2023, 22:05:48", + "recorded-date": "08-04-2024, 16:57:38", "recorded-content": { "lambda_url_invocation_missing_permission": { "Message": "Forbidden" @@ -2329,7 +2401,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[dict]": { - "recorded-date": "20-11-2023, 22:05:05", + "recorded-date": "08-04-2024, 16:56:29", "recorded-content": { "create_lambda_url_config": { "AuthType": "NONE", @@ -2372,7 +2444,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[http-response]": { - "recorded-date": "20-11-2023, 22:05:09", + "recorded-date": "08-04-2024, 16:56:33", "recorded-content": { "create_lambda_url_config": { "AuthType": "NONE", @@ -2413,7 +2485,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[http-response-json]": { - "recorded-date": "20-11-2023, 22:05:13", + "recorded-date": "08-04-2024, 16:56:37", "recorded-content": { "create_lambda_url_config": { "AuthType": "NONE", @@ -2456,7 +2528,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[list-mixed]": { - "recorded-date": "20-11-2023, 22:05:18", + "recorded-date": "08-04-2024, 16:56:41", "recorded-content": { "create_lambda_url_config": { "AuthType": "NONE", @@ -2497,7 +2569,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[string]": { - "recorded-date": "20-11-2023, 22:05:22", + "recorded-date": "08-04-2024, 16:56:45", "recorded-content": { "create_lambda_url_config": { "AuthType": "NONE", @@ -2538,7 +2610,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[integer]": { - "recorded-date": "20-11-2023, 22:05:27", + "recorded-date": "08-04-2024, 16:56:49", "recorded-content": { "create_lambda_url_config": { "AuthType": "NONE", @@ -2579,7 +2651,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[float]": { - "recorded-date": "20-11-2023, 22:05:31", + "recorded-date": "08-04-2024, 16:56:53", "recorded-content": { "create_lambda_url_config": { "AuthType": "NONE", @@ -2620,7 +2692,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[boolean]": { - "recorded-date": "20-11-2023, 22:05:35", + "recorded-date": "08-04-2024, 16:56:57", "recorded-content": { "create_lambda_url_config": { "AuthType": "NONE", @@ -2661,7 +2733,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_runtime_introspection_arm": { - "recorded-date": "20-11-2023, 22:04:08", + "recorded-date": "08-04-2024, 16:55:44", "recorded-content": { "invoke_runtime_arm_introspection": { "ExecutedVersion": "$LATEST", @@ -2700,7 +2772,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_runtime_introspection_x86": { - "recorded-date": "20-11-2023, 22:04:05", + "recorded-date": "08-04-2024, 16:55:41", "recorded-content": { "invoke_runtime_x86_introspection": { "ExecutedVersion": "$LATEST", @@ -2739,7 +2811,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_runtime_ulimits": { - "recorded-date": "20-11-2023, 22:04:12", + "recorded-date": "16-04-2024, 08:12:12", "recorded-content": { "invoke_runtime_ulimits": { "ExecutedVersion": "$LATEST", @@ -2773,8 +2845,8 @@ 1024 ], "RLIMIT_NPROC": [ - 1024, - 1024 + 742, + 742 ], "RLIMIT_RSS": [ -1, @@ -2794,7 +2866,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_reserved_concurrency": { - "recorded-date": "20-11-2023, 23:04:50", + "recorded-date": "08-04-2024, 17:08:11", "recorded-content": { "fn": { "Architectures": [ @@ -2814,11 +2886,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2882,7 +2958,7 @@ }, "msg": { "Attributes": { - "AWSTraceHeader": "Root=1-655bd800-070a20a96026637c4070de01;Sampled=0", + "AWSTraceHeader": "Root=1-6614247a-4513533344453f9a2d077845;Parent=282ed520d6ca75c8;Sampled=0", "ApproximateFirstReceiveTimestamp": "timestamp", "ApproximateReceiveCount": "1", "SenderId": "", @@ -2906,7 +2982,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_reserved_concurrency_async_queue": { - "recorded-date": "20-11-2023, 23:04:37", + "recorded-date": "08-04-2024, 17:07:59", "recorded-content": { "fn": { "Architectures": [ @@ -2926,11 +3002,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2972,7 +3052,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_provisioned_concurrency": { - "recorded-date": "20-11-2023, 23:03:55", + "recorded-date": "08-04-2024, 17:04:21", "recorded-content": { "put_provisioned_5": { "AllocatedProvisionedConcurrentExecutions": 0, @@ -3010,7 +3090,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_reserved_provisioned_overlap": { - "recorded-date": "20-11-2023, 23:07:15", + "recorded-date": "08-04-2024, 17:10:37", "recorded-content": { "put_provisioned_5": { "AllocatedProvisionedConcurrentExecutions": 0, @@ -3096,11 +3176,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.10", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -3146,11 +3230,11 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestRequestIdHandling::test_request_id_format": { - "recorded-date": "20-11-2023, 22:08:28", + "recorded-date": "08-04-2024, 17:11:17", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda.py::TestRequestIdHandling::test_request_id_invoke": { - "recorded-date": "20-11-2023, 22:08:37", + "recorded-date": "08-04-2024, 17:11:26", "recorded-content": { "invoke_result": { "ExecutedVersion": "$LATEST", @@ -3172,7 +3256,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestRequestIdHandling::test_request_id_async_invoke_with_retry": { - "recorded-date": "20-11-2023, 22:10:54", + "recorded-date": "08-04-2024, 17:13:39", "recorded-content": { "invoke_result": { "Payload": "", @@ -3189,7 +3273,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestRequestIdHandling::test_request_id_invoke_url": { - "recorded-date": "20-11-2023, 22:08:49", + "recorded-date": "08-04-2024, 17:11:35", "recorded-content": { "create_lambda_url_config": { "AuthType": "NONE", @@ -3249,7 +3333,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_runtime_error": { - "recorded-date": "20-11-2023, 22:06:43", + "recorded-date": "16-04-2024, 08:08:32", "recorded-content": { "invocation_error": { "ExecutedVersion": "$LATEST", @@ -3259,13 +3343,13 @@ "errorType": "Exception", "requestId": "", "stackTrace": [ - " File \"/var/lang/lib/python3.10/importlib/__init__.py\", line 126, in import_module\n return _bootstrap._gcd_import(name[level:], package, level)\n", - " File \"\", line 1050, in _gcd_import\n", - " File \"\", line 1027, in _find_and_load\n", - " File \"\", line 1006, in _find_and_load_unlocked\n", - " File \"\", line 688, in _load_unlocked\n", - " File \"\", line 883, in exec_module\n", - " File \"\", line 241, in _call_with_frames_removed\n", + " File \"/var/lang/lib/python3.12/importlib/__init__.py\", line 90, in import_module\n return _bootstrap._gcd_import(name[level:], package, level)\n", + " File \"\", line 1387, in _gcd_import\n", + " File \"\", line 1360, in _find_and_load\n", + " File \"\", line 1331, in _find_and_load_unlocked\n", + " File \"\", line 935, in _load_unlocked\n", + " File \"\", line 995, in exec_module\n", + " File \"\", line 488, in _call_with_frames_removed\n", " File \"/var/task/lambda_runtime_error.py\", line 1, in \n raise Exception(\"Runtime startup fails\")\n" ] }, @@ -3278,7 +3362,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invoke_exceptions": { - "recorded-date": "20-11-2023, 22:05:55", + "recorded-date": "08-04-2024, 16:57:45", "recorded-content": { "invoke_function_doesnotexist": { "Error": { @@ -3295,7 +3379,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_runtime_wrapper_not_found": { - "recorded-date": "20-11-2023, 22:07:04", + "recorded-date": "08-04-2024, 16:59:29", "recorded-content": { "invocation_error": { "ExecutedVersion": "$LATEST", @@ -3313,7 +3397,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_runtime_exit": { - "recorded-date": "20-11-2023, 22:06:48", + "recorded-date": "08-04-2024, 16:58:35", "recorded-content": { "invocation_error": { "ExecutedVersion": "$LATEST", @@ -3331,7 +3415,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_handler_exit": { - "recorded-date": "20-11-2023, 22:07:01", + "recorded-date": "08-04-2024, 16:59:26", "recorded-content": { "invocation_error": { "ExecutedVersion": "$LATEST", @@ -3349,7 +3433,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_handler_error": { - "recorded-date": "20-11-2023, 22:06:57", + "recorded-date": "08-04-2024, 16:59:23", "recorded-content": { "invocation_error": { "ExecutedVersion": "$LATEST", @@ -3371,14 +3455,13 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_runtime_exit_segfault": { - "recorded-date": "20-11-2023, 22:06:54", + "recorded-date": "08-04-2024, 16:59:20", "recorded-content": { "invocation_error": { "ExecutedVersion": "$LATEST", "FunctionError": "Unhandled", "Payload": { - "errorType": "Runtime.ExitError", - "errorMessage": "RequestId: Error: Runtime exited with error: signal: segmentation fault" + "errorMessage": "date Task timed out after 30.13 seconds" }, "StatusCode": 200, "ResponseMetadata": { @@ -3389,7 +3472,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_invoke_payload_encoding_error[body-n\\x87r\\x9e\\xe9\\xb5\\xd7I\\xee\\x9bmt]": { - "recorded-date": "20-11-2023, 22:07:07", + "recorded-date": "08-04-2024, 16:59:32", "recorded-content": { "invoke_function_invalid_payload_body": { "Error": { @@ -3406,7 +3489,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_invoke_payload_encoding_error[message-\\x99\\xeb,j\\x07\\xa1zYh]": { - "recorded-date": "20-11-2023, 22:07:10", + "recorded-date": "08-04-2024, 16:59:35", "recorded-content": { "invoke_function_invalid_payload_message": { "Error": { @@ -3423,23 +3506,23 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_assume_role[1]": { - "recorded-date": "20-11-2023, 22:03:49", + "recorded-date": "08-04-2024, 16:55:26", "recorded-content": { "invoke-result-assumed-role-arn": "arn:aws:sts::111111111111:assumed-role/lambda-autogenerated-/@lambda_function" } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_assume_role[2]": { - "recorded-date": "20-11-2023, 22:03:54", + "recorded-date": "08-04-2024, 16:55:32", "recorded-content": { "invoke-result-assumed-role-arn": "arn:aws:sts::111111111111:assumed-role/lambda-autogenerated-/" } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_large_payloads": { - "recorded-date": "20-11-2023, 23:16:40", + "recorded-date": "08-04-2024, 16:55:07", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_mixed_architecture": { - "recorded-date": "16-02-2024, 11:16:11", + "recorded-date": "08-04-2024, 16:55:53", "recorded-content": { "create_function_response": { "CreateEventSourceMappingResponse": null, @@ -3540,7 +3623,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_lambda_provisioned_concurrency_scheduling": { - "recorded-date": "28-11-2023, 16:48:42", + "recorded-date": "16-04-2024, 08:03:42", "recorded-content": { "get_provisioned_postwait": { "AllocatedProvisionedConcurrentExecutions": 1, @@ -3556,7 +3639,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_init_environment": { - "recorded-date": "13-02-2024, 12:42:59", + "recorded-date": "08-04-2024, 16:56:25", "recorded-content": { "create-result": { "CreateEventSourceMappingResponse": null, @@ -3651,7 +3734,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_lambda_too_large_response": { - "recorded-date": "13-02-2024, 15:49:49", + "recorded-date": "08-04-2024, 16:55:18", "recorded-content": { "invoke_result": { "ExecutedVersion": "$LATEST", @@ -3682,7 +3765,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaVersions::test_lambda_handler_update": { - "recorded-date": "14-02-2024, 14:32:32", + "recorded-date": "08-04-2024, 17:10:59", "recorded-content": { "invoke_result_handler_one": { "ExecutedVersion": "$LATEST", @@ -3818,7 +3901,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invalid_invoke_mode": { - "recorded-date": "19-02-2024, 15:12:50", + "recorded-date": "08-04-2024, 16:57:26", "recorded-content": { "invoke_function_invalid_invoke_type": { "Error": { @@ -3833,7 +3916,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_update_function_url_config": { - "recorded-date": "19-02-2024, 15:49:43", + "recorded-date": "08-04-2024, 16:57:18", "recorded-content": { "create_lambda_url_config": { "AuthType": "NONE", @@ -3909,7 +3992,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_invoke[RESPONSE_STREAM]": { - "recorded-date": "19-02-2024, 16:23:18", + "recorded-date": "08-04-2024, 16:57:10", "recorded-content": { "create_lambda_url_config": { "AuthType": "NONE", @@ -3943,7 +4026,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_invoke[BUFFERED]": { - "recorded-date": "19-02-2024, 16:23:23", + "recorded-date": "08-04-2024, 16:57:05", "recorded-content": { "create_lambda_url_config": { "AuthType": "NONE", @@ -3977,7 +4060,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_invoke[None]": { - "recorded-date": "19-02-2024, 16:23:28", + "recorded-date": "08-04-2024, 16:57:02", "recorded-content": { "create_lambda_url_config": { "AuthType": "NONE", @@ -4010,7 +4093,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_http_fixture_default": { - "recorded-date": "29-03-2024, 14:57:46", + "recorded-date": "08-04-2024, 16:57:31", "recorded-content": { "url_response": { "args": { @@ -4042,7 +4125,7 @@ } }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_http_fixture_trim_x_headers": { - "recorded-date": "29-03-2024, 15:03:37", + "recorded-date": "08-04-2024, 16:57:34", "recorded-content": { "url_response": { "args": { @@ -4066,5 +4149,17 @@ "path": "/path/1" } } + }, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_lambda_large_response": { + "recorded-date": "08-04-2024, 16:55:12", + "recorded-content": {} + }, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_headers_and_status": { + "recorded-date": "08-04-2024, 16:57:14", + "recorded-content": {} + }, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaMultiAccounts::test_cross_account_access": { + "recorded-date": "08-04-2024, 16:59:40", + "recorded-content": {} } } diff --git a/tests/aws/services/lambda_/test_lambda.validation.json b/tests/aws/services/lambda_/test_lambda.validation.json index fa6b800f08675..3841acdb6dead 100644 --- a/tests/aws/services/lambda_/test_lambda.validation.json +++ b/tests/aws/services/lambda_/test_lambda.validation.json @@ -1,108 +1,108 @@ { "tests/aws/services/lambda_/test_lambda.py::TestLambdaAliases::test_alias_routingconfig": { - "last_validated_date": "2023-11-20T21:08:28+00:00" + "last_validated_date": "2024-04-08T17:11:17+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaAliases::test_lambda_alias_moving": { - "last_validated_date": "2023-11-20T21:08:16+00:00" + "last_validated_date": "2024-04-08T17:11:05+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_assume_role[1]": { - "last_validated_date": "2023-11-20T21:03:49+00:00" + "last_validated_date": "2024-04-08T16:55:25+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_assume_role[2]": { - "last_validated_date": "2023-11-20T21:03:54+00:00" + "last_validated_date": "2024-04-08T16:55:31+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_function_state": { - "last_validated_date": "2023-11-20T21:03:44+00:00" + "last_validated_date": "2024-04-08T16:55:20+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_lambda_different_iam_keys_environment": { - "last_validated_date": "2023-11-20T21:04:01+00:00" + "last_validated_date": "2024-04-08T16:55:37+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_lambda_large_response": { - "last_validated_date": "2024-02-13T09:12:01+00:00" + "last_validated_date": "2024-04-08T16:55:11+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_lambda_too_large_response": { - "last_validated_date": "2024-02-13T15:50:25+00:00" + "last_validated_date": "2024-04-08T16:55:18+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_large_payloads": { - "last_validated_date": "2023-11-20T22:16:40+00:00" + "last_validated_date": "2024-04-08T16:55:06+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_cache_local[nodejs]": { - "last_validated_date": "2023-11-20T21:57:11+00:00" + "last_validated_date": "2024-04-08T16:55:56+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_cache_local[python]": { - "last_validated_date": "2023-11-20T21:57:14+00:00" + "last_validated_date": "2024-04-08T16:55:59+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_init_environment": { - "last_validated_date": "2024-02-13T12:42:58+00:00" + "last_validated_date": "2024-04-08T16:56:25+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_invoke_no_timeout": { - "last_validated_date": "2023-11-20T21:04:53+00:00" + "last_validated_date": "2024-04-08T16:56:14+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_invoke_timed_out_environment_reuse": { - "last_validated_date": "2023-11-20T21:05:01+00:00" + "last_validated_date": "2024-04-08T16:56:22+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_invoke_with_timeout": { - "last_validated_date": "2023-11-20T21:04:37+00:00" + "last_validated_date": "2024-04-08T16:56:04+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_mixed_architecture": { - "last_validated_date": "2024-02-16T11:16:43+00:00" + "last_validated_date": "2024-04-08T16:55:52+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_runtime_introspection_arm": { - "last_validated_date": "2023-11-20T21:04:08+00:00" + "last_validated_date": "2024-04-08T16:55:44+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_runtime_introspection_x86": { - "last_validated_date": "2023-11-20T21:04:05+00:00" + "last_validated_date": "2024-04-08T16:55:41+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_runtime_ulimits": { - "last_validated_date": "2023-11-20T21:04:12+00:00" + "last_validated_date": "2024-04-16T08:12:11+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_lambda_concurrency_block": { - "last_validated_date": "2023-11-20T22:01:42+00:00" + "last_validated_date": "2024-04-08T17:02:06+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_lambda_concurrency_crud": { - "last_validated_date": "2023-11-20T21:59:12+00:00" + "last_validated_date": "2024-04-08T16:59:42+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_lambda_provisioned_concurrency_moves_with_alias": { "last_validated_date": "2023-03-21T07:47:38+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_lambda_provisioned_concurrency_scheduling": { - "last_validated_date": "2023-11-28T15:48:42+00:00" + "last_validated_date": "2024-04-16T08:03:41+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_provisioned_concurrency": { - "last_validated_date": "2023-11-20T22:03:55+00:00" + "last_validated_date": "2024-04-08T17:04:20+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_reserved_concurrency": { - "last_validated_date": "2023-11-20T22:04:50+00:00" + "last_validated_date": "2024-04-08T17:08:10+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_reserved_concurrency_async_queue": { - "last_validated_date": "2023-11-20T22:04:37+00:00" + "last_validated_date": "2024-04-08T17:07:56+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_reserved_provisioned_overlap": { - "last_validated_date": "2023-11-20T22:07:15+00:00" + "last_validated_date": "2024-04-08T17:10:36+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_handler_error": { - "last_validated_date": "2023-11-20T21:06:57+00:00" + "last_validated_date": "2024-04-08T16:59:22+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_handler_exit": { - "last_validated_date": "2023-11-20T21:07:01+00:00" + "last_validated_date": "2024-04-08T16:59:25+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_invoke_payload_encoding_error[body-n\\x87r\\x9e\\xe9\\xb5\\xd7I\\xee\\x9bmt]": { - "last_validated_date": "2023-11-20T21:07:07+00:00" + "last_validated_date": "2024-04-08T16:59:31+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_invoke_payload_encoding_error[message-\\x99\\xeb,j\\x07\\xa1zYh]": { - "last_validated_date": "2023-11-20T21:07:10+00:00" + "last_validated_date": "2024-04-08T16:59:34+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_runtime_error": { - "last_validated_date": "2023-11-20T21:06:43+00:00" + "last_validated_date": "2024-04-16T08:08:31+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_runtime_exit": { - "last_validated_date": "2023-11-20T21:06:48+00:00" + "last_validated_date": "2024-04-08T16:58:35+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_runtime_exit_segfault": { - "last_validated_date": "2023-11-20T21:06:54+00:00" + "last_validated_date": "2024-04-08T16:59:19+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_runtime_wrapper_not_found": { - "last_validated_date": "2023-11-20T21:07:04+00:00" + "last_validated_date": "2024-04-08T16:59:28+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_dry_run[nodejs16.x]": { "last_validated_date": "2022-09-09T17:15:38+00:00" @@ -111,114 +111,114 @@ "last_validated_date": "2023-04-26T17:37:23+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_event[nodejs16.x]": { - "last_validated_date": "2023-11-20T21:06:10+00:00" + "last_validated_date": "2024-04-08T16:57:59+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_event[python3.10]": { - "last_validated_date": "2023-11-20T21:06:20+00:00" + "last_validated_date": "2024-04-08T16:58:09+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_event_error": { "last_validated_date": "2023-09-04T20:49:02+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_request_response[nodejs16.x]": { - "last_validated_date": "2023-11-20T21:05:58+00:00" + "last_validated_date": "2024-04-08T16:57:47+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_request_response[python3.10]": { - "last_validated_date": "2023-11-20T21:06:02+00:00" + "last_validated_date": "2024-04-08T16:57:50+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_with_logs[nodejs16.x]": { - "last_validated_date": "2023-11-20T21:05:51+00:00" + "last_validated_date": "2024-04-08T16:57:40+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_with_logs[python3.10]": { - "last_validated_date": "2023-11-20T21:05:55+00:00" + "last_validated_date": "2024-04-08T16:57:44+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_with_qualifier": { - "last_validated_date": "2023-11-20T21:06:33+00:00" + "last_validated_date": "2024-04-08T16:58:20+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invoke_exceptions": { - "last_validated_date": "2023-11-20T21:05:55+00:00" + "last_validated_date": "2024-04-08T16:57:45+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_lambda_with_context": { "last_validated_date": "2022-09-09T18:19:33+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_upload_lambda_from_s3": { - "last_validated_date": "2023-11-20T21:06:39+00:00" + "last_validated_date": "2024-04-08T16:58:25+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaPermissions::test_lambda_permission_url_invocation": { - "last_validated_date": "2023-11-20T21:05:48+00:00" + "last_validated_date": "2024-04-08T16:57:37+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_update_function_url_config": { - "last_validated_date": "2024-02-19T15:49:42+00:00" + "last_validated_date": "2024-04-08T16:57:17+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_http_fixture": { "last_validated_date": "2024-03-28T22:20:14+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_http_fixture_default": { - "last_validated_date": "2024-03-29T14:57:45+00:00" + "last_validated_date": "2024-04-08T16:57:30+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_http_fixture_trim_x_headers": { - "last_validated_date": "2024-03-29T15:03:36+00:00" + "last_validated_date": "2024-04-08T16:57:34+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_invoke[BUFFERED]": { - "last_validated_date": "2024-02-19T16:25:40+00:00" + "last_validated_date": "2024-04-08T16:57:05+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_invoke[None]": { - "last_validated_date": "2024-02-19T16:25:50+00:00" + "last_validated_date": "2024-04-08T16:57:01+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_invoke[RESPONSE_STREAM]": { - "last_validated_date": "2024-02-19T16:28:35+00:00" + "last_validated_date": "2024-04-08T16:57:09+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_headers_and_status": { - "last_validated_date": "2024-02-04T16:00:29+00:00" + "last_validated_date": "2024-04-08T16:57:13+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invalid_invoke_mode": { - "last_validated_date": "2024-02-19T15:12:49+00:00" + "last_validated_date": "2024-04-08T16:57:25+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[boolean]": { - "last_validated_date": "2023-11-20T21:05:35+00:00" + "last_validated_date": "2024-04-08T16:56:56+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[dict]": { - "last_validated_date": "2023-11-20T21:05:05+00:00" + "last_validated_date": "2024-04-08T16:56:29+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[float]": { - "last_validated_date": "2023-11-20T21:05:31+00:00" + "last_validated_date": "2024-04-08T16:56:52+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[http-response-json]": { - "last_validated_date": "2023-11-20T21:05:13+00:00" + "last_validated_date": "2024-04-08T16:56:36+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[http-response]": { - "last_validated_date": "2023-11-20T21:05:09+00:00" + "last_validated_date": "2024-04-08T16:56:32+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[integer]": { - "last_validated_date": "2023-11-20T21:05:27+00:00" + "last_validated_date": "2024-04-08T16:56:48+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[list-mixed]": { - "last_validated_date": "2023-11-20T21:05:18+00:00" + "last_validated_date": "2024-04-08T16:56:40+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[string]": { - "last_validated_date": "2023-11-20T21:05:22+00:00" + "last_validated_date": "2024-04-08T16:56:44+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation_exception": { - "last_validated_date": "2023-11-20T21:05:44+00:00" + "last_validated_date": "2024-04-08T16:57:22+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_non_existing_url": { "last_validated_date": "2024-04-11T17:16:39+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaVersions::test_lambda_handler_update": { - "last_validated_date": "2024-02-14T14:32:31+00:00" + "last_validated_date": "2024-04-08T17:10:58+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestLambdaVersions::test_lambda_versions_with_code_changes": { - "last_validated_date": "2023-11-20T21:08:09+00:00" + "last_validated_date": "2024-04-08T17:10:52+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestRequestIdHandling::test_request_id_async_invoke_with_retry": { - "last_validated_date": "2023-11-20T21:10:54+00:00" + "last_validated_date": "2024-04-08T17:13:38+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestRequestIdHandling::test_request_id_format": { - "last_validated_date": "2023-11-20T21:08:28+00:00" + "last_validated_date": "2024-04-08T17:11:17+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestRequestIdHandling::test_request_id_invoke": { - "last_validated_date": "2023-11-20T21:08:37+00:00" + "last_validated_date": "2024-04-08T17:11:26+00:00" }, "tests/aws/services/lambda_/test_lambda.py::TestRequestIdHandling::test_request_id_invoke_url": { - "last_validated_date": "2023-11-20T21:08:49+00:00" + "last_validated_date": "2024-04-08T17:11:34+00:00" } } diff --git a/tests/aws/services/lambda_/test_lambda_api.py b/tests/aws/services/lambda_/test_lambda_api.py index ed2b894c0ff35..758a33f6e63e7 100644 --- a/tests/aws/services/lambda_/test_lambda_api.py +++ b/tests/aws/services/lambda_/test_lambda_api.py @@ -26,7 +26,7 @@ from localstack_snapshot.snapshots.transformer import SortingTransformer from localstack import config -from localstack.aws.api.lambda_ import Architecture, Runtime +from localstack.aws.api.lambda_ import Architecture, LogFormat, Runtime from localstack.services.lambda_.api_utils import ARCHITECTURES from localstack.services.lambda_.runtimes import ( ALL_RUNTIMES, @@ -77,10 +77,147 @@ def environment_length_bytes(e: dict) -> int: return string_length_bytes(serialized_environment) +class TestLoggingConfig: + @markers.aws.validated + def test_function_advanced_logging_configuration( + self, snapshot, create_lambda_function, lambda_su_role, aws_client + ): + function_name = f"fn-{short_uid()}" + create_response = create_lambda_function( + handler_file=TEST_LAMBDA_PYTHON_ECHO, + func_name=function_name, + runtime=Runtime.python3_12, + role=lambda_su_role, + MemorySize=256, + Timeout=5, + LoggingConfig={ + "LogFormat": LogFormat.JSON, + }, + ) + + snapshot.match("create_response", create_response) + aws_client.lambda_.get_waiter("function_active_v2").wait(FunctionName=function_name) + + get_function_response = aws_client.lambda_.get_function(FunctionName=function_name) + snapshot.match("get_function_response", get_function_response) + + function_config = aws_client.lambda_.get_function_configuration(FunctionName=function_name) + snapshot.match("function_config", function_config) + + advanced_config = { + "LogFormat": LogFormat.JSON, + "ApplicationLogLevel": "INFO", + "SystemLogLevel": "INFO", + "LogGroup": "cool_lambda", + } + updated_config = aws_client.lambda_.update_function_configuration( + FunctionName=function_name, LoggingConfig=advanced_config + ) + snapshot.match("updated_config", updated_config) + + aws_client.lambda_.get_waiter("function_updated_v2").wait(FunctionName=function_name) + + received_conf = aws_client.lambda_.get_function_configuration( + FunctionName=function_name, + ) + snapshot.match("received_config", received_conf) + + @markers.aws.validated + def test_advanced_logging_configuration_format_switch( + self, snapshot, create_lambda_function, lambda_su_role, aws_client + ): + function_name = f"fn-{short_uid()}" + create_response = create_lambda_function( + handler_file=TEST_LAMBDA_PYTHON_ECHO, + func_name=function_name, + runtime=Runtime.python3_12, + role=lambda_su_role, + MemorySize=256, + Timeout=5, + ) + + snapshot.match("create_response", create_response) + aws_client.lambda_.get_waiter("function_active_v2").wait(FunctionName=function_name) + + get_function_response = aws_client.lambda_.get_function(FunctionName=function_name) + snapshot.match("get_function_response", get_function_response) + + function_config = aws_client.lambda_.get_function_configuration(FunctionName=function_name) + snapshot.match("function_config", function_config) + + updated_config = aws_client.lambda_.update_function_configuration( + FunctionName=function_name, LoggingConfig={"LogFormat": LogFormat.JSON} + ) + snapshot.match("updated_config", updated_config) + + aws_client.lambda_.get_waiter("function_updated_v2").wait(FunctionName=function_name) + + received_conf = aws_client.lambda_.get_function_configuration( + FunctionName=function_name, + ) + snapshot.match("received_config", received_conf) + + updated_config = aws_client.lambda_.update_function_configuration( + FunctionName=function_name, LoggingConfig={"LogFormat": LogFormat.Text} + ) + snapshot.match("updated_config_v2", updated_config) + + aws_client.lambda_.get_waiter("function_updated_v2").wait(FunctionName=function_name) + + received_conf = aws_client.lambda_.get_function_configuration( + FunctionName=function_name, + ) + snapshot.match("received_config_v2", received_conf) + + @markers.aws.validated + @pytest.mark.parametrize( + "partial_config", + [ + {"LogFormat": LogFormat.JSON}, + {"LogFormat": LogFormat.JSON, "ApplicationLogLevel": "DEBUG"}, + {"LogFormat": LogFormat.JSON, "SystemLogLevel": "DEBUG"}, + {"LogGroup": "cool_lambda"}, + ], + ) + def test_function_partial_advanced_logging_configuration_update( + self, snapshot, create_lambda_function, lambda_su_role, aws_client, partial_config + ): + function_name = f"fn-{short_uid()}" + create_response = create_lambda_function( + handler_file=TEST_LAMBDA_PYTHON_ECHO, + func_name=function_name, + runtime=Runtime.python3_12, + role=lambda_su_role, + MemorySize=256, + Timeout=5, + ) + + snapshot.match("create_response", create_response) + aws_client.lambda_.get_waiter("function_active_v2").wait(FunctionName=function_name) + + get_function_response = aws_client.lambda_.get_function(FunctionName=function_name) + snapshot.match("get_function_response", get_function_response) + + function_config = aws_client.lambda_.get_function_configuration(FunctionName=function_name) + snapshot.match("function_config", function_config) + + updated_config = aws_client.lambda_.update_function_configuration( + FunctionName=function_name, LoggingConfig=partial_config + ) + snapshot.match("updated_config", updated_config) + + aws_client.lambda_.get_waiter("function_updated_v2").wait(FunctionName=function_name) + + received_conf = aws_client.lambda_.get_function_configuration( + FunctionName=function_name, + ) + snapshot.match("received_config", received_conf) + + class TestLambdaFunction: @markers.snapshot.skip_snapshot_verify( # The RuntimeVersionArn is currently a hardcoded id and therefore does not reflect the ARN resource update - # from python3.9 to python3.8 in update_func_conf_response. + # for different runtime versions" paths=["$..RuntimeVersionConfig.RuntimeVersionArn"] ) @markers.aws.validated @@ -90,7 +227,7 @@ def test_function_lifecycle(self, snapshot, create_lambda_function, lambda_su_ro create_response = create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, role=lambda_su_role, MemorySize=256, Timeout=5, @@ -104,7 +241,7 @@ def test_function_lifecycle(self, snapshot, create_lambda_function, lambda_su_ro update_func_conf_response = aws_client.lambda_.update_function_configuration( FunctionName=function_name, - Runtime=Runtime.python3_8, + Runtime=Runtime.python3_11, Description="Changed-Description", MemorySize=512, Timeout=10, @@ -148,7 +285,7 @@ def test_redundant_updates(self, create_lambda_function, snapshot, aws_client): create_response = create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, Description="Initial description", ) snapshot.match("create_response", create_response) @@ -224,7 +361,7 @@ def test_ops_on_nonexisting_version( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, Description="Initial description", ) with pytest.raises(aws_client.lambda_.exceptions.ResourceNotFoundException) as e: @@ -240,7 +377,7 @@ def test_delete_on_nonexisting_version(self, create_lambda_function, snapshot, a create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, Description="Initial description", ) # it seems delete function on a random qualifier is idempotent @@ -299,7 +436,7 @@ def test_get_function_wrong_region( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, Description="Initial description", ) wrong_region = ( @@ -324,7 +461,7 @@ def test_lambda_code_location_zipfile( Code={"ZipFile": zip_file_bytes}, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, ) snapshot.match("create-response-zip-file", create_response) get_function_response = aws_client.lambda_.get_function(FunctionName=function_name) @@ -373,7 +510,7 @@ def test_lambda_code_location_s3( Code={"S3Bucket": s3_bucket, "S3Key": bucket_key}, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, ) snapshot.match("create_response_s3", create_response) get_function_response = aws_client.lambda_.get_function(FunctionName=function_name) @@ -415,7 +552,6 @@ def test_lambda_code_location_s3( paths=[ "function_arn_other_account_exc..Error.Message", "$..CodeSha256", - "$..CreateFunctionResponse.LoggingConfig", ] ) @markers.aws.validated @@ -538,7 +674,7 @@ def test_create_lambda_exceptions(self, lambda_su_role, snapshot, aws_client): Code={"ZipFile": zip_file_bytes}, PackageType="Zip", Role="r1", - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, ) snapshot.match("invalid_role_arn_exc", e.value.response) # test invalid runtimes @@ -571,7 +707,7 @@ def test_create_lambda_exceptions(self, lambda_su_role, snapshot, aws_client): Code={"ZipFile": zip_file_bytes}, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, Architectures=[], ) snapshot.match("empty_architectures", e.value) @@ -584,7 +720,7 @@ def test_create_lambda_exceptions(self, lambda_su_role, snapshot, aws_client): Code={"ZipFile": zip_file_bytes}, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, Architectures=[Architecture.x86_64, Architecture.arm64], ) snapshot.match("multiple_architectures", e.value.response) @@ -597,7 +733,7 @@ def test_create_lambda_exceptions(self, lambda_su_role, snapshot, aws_client): Code={"ZipFile": zip_file_bytes}, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, Architectures=["X86_64"], ) snapshot.match("uppercase_architecture", e.value.response) @@ -627,7 +763,7 @@ def test_update_lambda_exceptions( Code={"ZipFile": zip_file_bytes}, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, ) with pytest.raises(ClientError) as e: aws_client.lambda_.update_function_configuration( @@ -663,7 +799,7 @@ def test_list_functions(self, create_lambda_function, lambda_su_role, snapshot, create_response = create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name_1, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, role=lambda_su_role, Publish=True, ) @@ -672,7 +808,7 @@ def test_list_functions(self, create_lambda_function, lambda_su_role, snapshot, create_response = create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name_2, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, role=lambda_su_role, ) snapshot.match("create_response_2", create_response) @@ -758,7 +894,7 @@ def test_vpc_config( create_response = create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, role=lambda_su_role, MemorySize=256, Timeout=5, @@ -830,7 +966,6 @@ def test_invalid_invoke(self, aws_client, snapshot): not is_docker_runtime_executor(), reason="Test will fail against other executors as they are not patched to take longer for the update", ) - @markers.snapshot.skip_snapshot_verify(paths=["$..LoggingConfig"]) @markers.aws.validated def test_lambda_concurrent_code_updates( self, aws_client, create_lambda_function_aws, lambda_su_role, snapshot, monkeypatch @@ -883,7 +1018,6 @@ def _runtime_client_path(*args, **kwargs): not is_docker_runtime_executor(), reason="Test will fail against other executors as they are not patched to take longer for the update", ) - @markers.snapshot.skip_snapshot_verify(paths=["$..LoggingConfig"]) @markers.aws.validated def test_lambda_concurrent_config_updates( self, aws_client, create_lambda_function, lambda_su_role, snapshot, monkeypatch @@ -1055,7 +1189,7 @@ def test_lambda_zip_file_to_image( create_image_response = create_lambda_function_aws( FunctionName=function_name, Role=lambda_su_role, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, Handler="handler.handler", Code={ "ZipFile": create_lambda_archive( @@ -1237,7 +1371,7 @@ def test_publish_version_on_create( }, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, Publish=True, ) snapshot.match("create_response", create_response) @@ -1289,7 +1423,7 @@ def test_version_lifecycle( }, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, Description="No version :(", ) snapshot.match("create_response", create_response) @@ -1366,7 +1500,7 @@ def test_publish_with_wrong_sha256( }, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, ) snapshot.match("create_response", create_response) @@ -1402,7 +1536,7 @@ def test_publish_with_update( }, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, ) snapshot.match("create_response", create_response) @@ -1455,7 +1589,7 @@ def test_alias_lifecycle( }, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, Environment={"Variables": {"testenv": "staging"}}, ) snapshot.match("create_response", create_response) @@ -1614,7 +1748,7 @@ def test_notfound_and_invalid_routingconfigs( }, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, Publish=True, Environment={"Variables": {"testenv": "staging"}}, ) @@ -1759,7 +1893,6 @@ def test_notfound_and_invalid_routingconfigs( ) snapshot.match("alias_does_not_exist_esc", e.value.response) - @markers.snapshot.skip_snapshot_verify(paths=["$..LoggingConfig"]) @markers.aws.validated def test_alias_naming(self, aws_client, snapshot, create_lambda_function_aws, lambda_su_role): """ @@ -1776,7 +1909,7 @@ def test_alias_naming(self, aws_client, snapshot, create_lambda_function_aws, la }, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, Environment={"Variables": {"testenv": "staging"}}, ) snapshot.match("create_response", create_response) @@ -1830,7 +1963,7 @@ def test_function_revisions_basic(self, create_lambda_function, snapshot, aws_cl func_name=function_name, zip_file=zip_file_content, handler="index.handler", - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) snapshot.match("create_function_response_rev1", create_function_response) rev1_create_function = create_function_response["CreateFunctionResponse"]["RevisionId"] @@ -1904,7 +2037,7 @@ def test_function_revisions_version_and_alias( create_function_response = create_lambda_function( func_name=function_name, handler_file=TEST_LAMBDA_PYTHON_ECHO, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) snapshot.match("create_function_response_rev1", create_function_response) rev1_create_function = create_function_response["CreateFunctionResponse"]["RevisionId"] @@ -1995,7 +2128,7 @@ def test_function_revisions_permissions(self, create_lambda_function, snapshot, create_lambda_function( func_name=function_name, handler_file=TEST_LAMBDA_PYTHON_ECHO, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) # rev2: created function becomes active @@ -2057,7 +2190,7 @@ def fn_arn(self, create_lambda_function, aws_client): create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) yield aws_client.lambda_.get_function(FunctionName=function_name)["Configuration"][ @@ -2072,7 +2205,7 @@ def test_create_tag_on_fn_create(self, create_lambda_function, snapshot, aws_cli create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, Tags={"testtag": custom_tag}, ) get_function_result = aws_client.lambda_.get_function(FunctionName=function_name) @@ -2176,7 +2309,7 @@ def test_lambda_eventinvokeconfig_lifecycle( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, role=lambda_su_role, ) @@ -2291,7 +2424,7 @@ def test_lambda_eventinvokeconfig_exceptions( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, role=lambda_su_role, ) get_fn_result = lambda_client.get_function(FunctionName=function_name) @@ -2300,7 +2433,7 @@ def test_lambda_eventinvokeconfig_exceptions( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name_2, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, role=lambda_su_role, ) get_fn_result_2 = lambda_client.get_function(FunctionName=function_name_2) @@ -2639,7 +2772,7 @@ def test_function_concurrency_exceptions(self, create_lambda_function, snapshot, create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) fn = aws_client.lambda_.get_function_configuration( FunctionName=function_name, Qualifier="$LATEST" @@ -2676,7 +2809,7 @@ def test_function_concurrency_limits( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) account_settings = aws_client.lambda_.get_account_settings() @@ -2712,7 +2845,7 @@ def test_function_concurrency(self, create_lambda_function, snapshot, aws_client create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) # Disable the function by throttling all incoming events. @@ -2767,7 +2900,7 @@ def test_provisioned_concurrency_exceptions( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) publish_version_result = lambda_client.publish_version(FunctionName=function_name) @@ -2925,7 +3058,7 @@ def test_provisioned_concurrency_limits( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) publish_version_result = lambda_client.publish_version(FunctionName=function_name) @@ -2975,7 +3108,7 @@ def test_lambda_provisioned_lifecycle( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) publish_version_result = aws_client.lambda_.publish_version(FunctionName=function_name) function_version = publish_version_result["Version"] @@ -3078,7 +3211,7 @@ def test_permission_exceptions(self, create_lambda_function, account_id, snapsho create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) # invalid statement id @@ -3241,7 +3374,7 @@ def test_add_lambda_permission_aws( lambda_create_response = create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) snapshot.match("create_lambda", lambda_create_response) # create lambda permission @@ -3270,7 +3403,7 @@ def test_lambda_permission_fn_versioning( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) # create lambda permission @@ -3399,7 +3532,7 @@ def test_add_lambda_permission_fields( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) resp = aws_client.lambda_.add_permission( @@ -3473,7 +3606,7 @@ def test_remove_multi_permissions(self, create_lambda_function, snapshot, aws_cl create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) action = "lambda:InvokeFunction" @@ -3542,7 +3675,7 @@ def test_create_multiple_lambda_permissions(self, create_lambda_function, snapsh create_lambda_function( func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, handler_file=TEST_LAMBDA_PYTHON_ECHO, ) @@ -3599,7 +3732,7 @@ def test_url_config_exceptions(self, create_lambda_function, snapshot, aws_clien create_lambda_function( func_name=function_name, zip_file=testutil.create_zip_file(TEST_LAMBDA_NODEJS, get_content=True), - runtime=Runtime.nodejs14_x, + runtime=Runtime.nodejs20_x, handler="lambda_handler.handler", ) fn_arn = aws_client.lambda_.get_function(FunctionName=function_name)["Configuration"][ @@ -3717,7 +3850,7 @@ def test_url_config_list_paging(self, create_lambda_function, snapshot, aws_clie create_lambda_function( func_name=function_name, zip_file=testutil.create_zip_file(TEST_LAMBDA_NODEJS, get_content=True), - runtime=Runtime.nodejs14_x, + runtime=Runtime.nodejs20_x, handler="lambda_handler.handler", ) @@ -3785,7 +3918,7 @@ def test_url_config_lifecycle(self, create_lambda_function, snapshot, aws_client create_lambda_function( func_name=function_name, zip_file=testutil.create_zip_file(TEST_LAMBDA_NODEJS, get_content=True), - runtime=Runtime.nodejs14_x, + runtime=Runtime.nodejs20_x, handler="lambda_handler.handler", ) @@ -3835,7 +3968,7 @@ def test_oversized_request_create_lambda(self, lambda_su_role, snapshot, aws_cli # upload zip file to S3 zip_file = testutil.create_lambda_archive( - code_str, get_content=True, runtime=Runtime.python3_9 + code_str, get_content=True, runtime=Runtime.python3_12 ) # enlarge the request beyond its limit while accounting for the zip file size @@ -3849,7 +3982,7 @@ def test_oversized_request_create_lambda(self, lambda_su_role, snapshot, aws_cli with pytest.raises(ClientError) as e: aws_client.lambda_.create_function( FunctionName=function_name, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, Handler="handler.handler", Role=lambda_su_role, Code={"ZipFile": zip_file}, @@ -3868,14 +4001,14 @@ def test_oversized_zipped_create_lambda(self, lambda_su_role, snapshot, aws_clie # upload zip file to S3 zip_file = testutil.create_lambda_archive( - code_str, get_content=True, runtime=Runtime.python3_9 + code_str, get_content=True, runtime=Runtime.python3_12 ) # create lambda function with pytest.raises(ClientError) as e: aws_client.lambda_.create_function( FunctionName=function_name, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, Handler="handler.handler", Role=lambda_su_role, Code={"ZipFile": zip_file}, @@ -3893,7 +4026,7 @@ def test_oversized_unzipped_lambda(self, s3_bucket, lambda_su_role, snapshot, aw # upload zip file to S3 zip_file = testutil.create_lambda_archive( - code_str, get_content=True, runtime=Runtime.python3_9 + code_str, get_content=True, runtime=Runtime.python3_12 ) aws_client.s3.upload_fileobj(BytesIO(zip_file), s3_bucket, bucket_key) @@ -3901,7 +4034,7 @@ def test_oversized_unzipped_lambda(self, s3_bucket, lambda_su_role, snapshot, aw with pytest.raises(aws_client.lambda_.exceptions.InvalidParameterValueException) as e: aws_client.lambda_.create_function( FunctionName=function_name, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, Handler="handler.handler", Role=lambda_su_role, Code={"S3Bucket": s3_bucket, "S3Key": bucket_key}, @@ -3920,14 +4053,14 @@ def test_large_lambda(self, s3_bucket, lambda_su_role, snapshot, cleanups, aws_c # upload zip file to S3 zip_file = testutil.create_lambda_archive( - code_str, get_content=True, runtime=Runtime.python3_9 + code_str, get_content=True, runtime=Runtime.python3_12 ) aws_client.s3.upload_fileobj(BytesIO(zip_file), s3_bucket, bucket_key) # create lambda function result = aws_client.lambda_.create_function( FunctionName=function_name, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, Handler="handler.handler", Role=lambda_su_role, Code={"S3Bucket": s3_bucket, "S3Key": bucket_key}, @@ -3959,7 +4092,7 @@ def test_large_environment_variables_fails(self, create_lambda_function, snapsho create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, envvars={ "LARGE_VAR": large_envvar, }, @@ -3994,7 +4127,7 @@ def test_large_environment_fails_multiple_keys( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, envvars=env, ) @@ -4022,7 +4155,7 @@ def test_lambda_envvars_near_limit_succeeds(self, create_lambda_function, snapsh res = create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, envvars={ "LARGE_VAR": large_envvar, }, @@ -4046,7 +4179,7 @@ def test_function_code_signing_config( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) response = aws_client.lambda_.create_code_signing_config( @@ -4079,7 +4212,11 @@ def test_function_code_signing_config( snapshot.match("get_function_code_signing_config", response) response = aws_client.lambda_.list_code_signing_configs() - snapshot.match("list_code_signing_configs", response) + + # TODO we should snapshot match entire response not just last element in list + # issue is that AWS creates 3 list entries where we only have one + # I believe on their end that they are keeping each configuration version as separate entry + snapshot.match("list_code_signing_configs", response["CodeSigningConfigs"][-1]) response = aws_client.lambda_.list_functions_by_code_signing_config( CodeSigningConfigArn=code_signing_arn @@ -4107,7 +4244,7 @@ def test_code_signing_not_found_excs( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) response = aws_client.lambda_.create_code_signing_config( @@ -4240,7 +4377,7 @@ def test_account_settings_total_code_size( zip_file=zip_file_content, handler="index.handler", func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) acc_settings1 = aws_client.lambda_.get_account_settings() assert ( @@ -4327,7 +4464,7 @@ def test_account_settings_total_code_size_config_update( create_lambda_function( handler_file=TEST_LAMBDA_NODEJS, func_name=function_name, - runtime=Runtime.nodejs16_x, + runtime=Runtime.nodejs18_x, ) acc_settings1 = aws_client.lambda_.get_account_settings() assert ( @@ -4346,7 +4483,7 @@ def test_account_settings_total_code_size_config_update( # 2) update function configuration (i.e., code remains identical) aws_client.lambda_.update_function_configuration( - FunctionName=function_name, Runtime=Runtime.nodejs18_x + FunctionName=function_name, Runtime=Runtime.nodejs20_x ) aws_client.lambda_.get_waiter("function_updated_v2").wait(FunctionName=function_name) acc_settings2 = aws_client.lambda_.get_account_settings() @@ -4458,7 +4595,7 @@ def test_event_source_mapping_lifecycle( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, role=lambda_su_role, ) # "minimal" @@ -4560,7 +4697,7 @@ def test_create_event_source_validation( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, role=lambda_su_role, ) @@ -4589,7 +4726,7 @@ def test_tag_exceptions(self, create_lambda_function, snapshot, account_id, aws_ create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) function_arn = aws_client.lambda_.get_function(FunctionName=function_name)["Configuration"][ "FunctionArn" @@ -4650,7 +4787,7 @@ def test_tag_limits(self, create_lambda_function, snapshot, aws_client): create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) function_arn = aws_client.lambda_.get_function(FunctionName=function_name)["Configuration"][ "FunctionArn" @@ -4686,7 +4823,7 @@ def test_tag_versions(self, create_lambda_function, snapshot, aws_client): create_function_result = create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, Tags={"key_a": "value_a"}, ) function_arn = create_function_result["CreateFunctionResponse"]["FunctionArn"] @@ -4729,7 +4866,7 @@ def snapshot_tags_for_resource(resource_arn: str, snapshot_suffix: str): create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, Tags={"key_a": "value_a"}, ) fn_arn = aws_client.lambda_.get_function(FunctionName=function_name)["Configuration"][ @@ -4807,7 +4944,7 @@ def test_layer_exceptions(self, snapshot, dummylayer, cleanups, aws_client): publish_result = aws_client.lambda_.publish_layer_version( LayerName=layer_name, - CompatibleRuntimes=[Runtime.python3_9], + CompatibleRuntimes=[Runtime.python3_12], Content={"ZipFile": dummylayer}, CompatibleArchitectures=[Architecture.x86_64], ) @@ -4913,7 +5050,7 @@ def test_layer_exceptions(self, snapshot, dummylayer, cleanups, aws_client): aws_client.lambda_.publish_layer_version( LayerName=f"testlayer-2-{short_uid()}", Content={"ZipFile": dummylayer}, - CompatibleRuntimes=["invalidruntime", "invalidruntime2", Runtime.nodejs16_x], + CompatibleRuntimes=["invalidruntime", "invalidruntime2", Runtime.nodejs20_x], CompatibleArchitectures=["invalidarch", Architecture.x86_64], ) snapshot.match("publish_layer_version_exc_partially_invalid_values", e.value.response) @@ -4975,7 +5112,7 @@ def test_layer_function_exceptions( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) get_fn_result = aws_client.lambda_.get_function(FunctionName=function_name) snapshot.match("get_fn_result", get_fn_result) @@ -5065,7 +5202,7 @@ def test_layer_function_quota_exception( """ layer_arns = [] for n in range(6): - layer_name_N = f"testlayer-{n+1}-{short_uid()}" + layer_name_N = f"testlayer-{n + 1}-{short_uid()}" publish_result_N = aws_client.lambda_.publish_layer_version( LayerName=layer_name_N, CompatibleRuntimes=[], @@ -5114,7 +5251,7 @@ def test_layer_lifecycle( create_lambda_function( handler_file=TEST_LAMBDA_PYTHON_ECHO, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) get_fn_result = aws_client.lambda_.get_function(FunctionName=function_name) snapshot.match("get_fn_result", get_fn_result) @@ -5126,7 +5263,7 @@ def test_layer_lifecycle( publish_result = aws_client.lambda_.publish_layer_version( LayerName=layer_name, - CompatibleRuntimes=[Runtime.python3_9], + CompatibleRuntimes=[Runtime.python3_12], LicenseInfo=license_info, Description=description, Content={"ZipFile": dummylayer}, @@ -5142,7 +5279,7 @@ def test_layer_lifecycle( # note: we don't even need to change anything for a second version to be published publish_result_2 = aws_client.lambda_.publish_layer_version( LayerName=layer_name, - CompatibleRuntimes=[Runtime.python3_9], + CompatibleRuntimes=[Runtime.python3_12], LicenseInfo=license_info, Description=description, Content={"ZipFile": dummylayer}, @@ -5208,7 +5345,7 @@ def test_layer_lifecycle( # creating a new layer version should still increment the previous version publish_result_3 = aws_client.lambda_.publish_layer_version( LayerName=layer_name, - CompatibleRuntimes=[Runtime.python3_9], + CompatibleRuntimes=[Runtime.python3_12], LicenseInfo=license_info, Description=description, Content={"ZipFile": dummylayer}, @@ -5250,7 +5387,7 @@ def test_layer_policy_exceptions(self, snapshot, dummylayer, cleanups, aws_clien publish_result = aws_client.lambda_.publish_layer_version( LayerName=layer_name, - CompatibleRuntimes=[Runtime.python3_9], + CompatibleRuntimes=[Runtime.python3_12], Content={"ZipFile": dummylayer}, CompatibleArchitectures=[Architecture.x86_64], ) @@ -5381,7 +5518,7 @@ def test_layer_policy_lifecycle( publish_result = aws_client.lambda_.publish_layer_version( LayerName=layer_name, - CompatibleRuntimes=[Runtime.python3_9], + CompatibleRuntimes=[Runtime.python3_12], Content={"ZipFile": dummylayer}, CompatibleArchitectures=[Architecture.x86_64], ) @@ -5474,7 +5611,7 @@ def test_snapstart_lifecycle(self, create_lambda_function, snapshot, aws_client, snapshot.match("get_function_response_version_1", get_function_response) @markers.aws.validated - @pytest.mark.parametrize("runtime", [Runtime.java11, Runtime.java17]) + @pytest.mark.parametrize("runtime", [Runtime.java21, Runtime.java17]) def test_snapstart_update_function_configuration( self, create_lambda_function, snapshot, aws_client, runtime ): @@ -5509,7 +5646,7 @@ def test_snapstart_exceptions(self, lambda_su_role, snapshot, aws_client): Code={"ZipFile": zip_file_bytes}, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, SnapStart={"ApplyOn": "PublishedVersions"}, ) snapshot.match("create_function_unsupported_snapstart_runtime", e.value.response) @@ -5521,7 +5658,7 @@ def test_snapstart_exceptions(self, lambda_su_role, snapshot, aws_client): Code={"ZipFile": zip_file_bytes}, PackageType="Zip", Role=lambda_su_role, - Runtime=Runtime.java11, + Runtime=Runtime.java21, SnapStart={"ApplyOn": "invalidOption"}, ) snapshot.match("create_function_invalid_snapstart_apply", e.value.response) diff --git a/tests/aws/services/lambda_/test_lambda_api.snapshot.json b/tests/aws/services/lambda_/test_lambda_api.snapshot.json index 92ca832b7cb09..477298392fb06 100644 --- a/tests/aws/services/lambda_/test_lambda_api.snapshot.json +++ b/tests/aws/services/lambda_/test_lambda_api.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_function_lifecycle": { - "recorded-date": "20-11-2023, 16:46:00", + "recorded-date": "10-04-2024, 08:59:22", "recorded-content": { "create_response": { "CreateEventSourceMappingResponse": null, @@ -21,11 +21,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 256, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -70,11 +74,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 256, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -116,11 +124,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 512, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.8", + "Runtime": "python3.11", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -164,11 +176,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 512, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.8", + "Runtime": "python3.11", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -210,11 +226,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 512, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.8", + "Runtime": "python3.11", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -258,11 +278,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 512, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.8", + "Runtime": "python3.11", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -303,7 +327,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_redundant_updates": { - "recorded-date": "20-11-2023, 16:46:07", + "recorded-date": "10-04-2024, 08:59:29", "recorded-content": { "create_response": { "CreateEventSourceMappingResponse": null, @@ -324,11 +348,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -370,11 +398,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -411,11 +443,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -457,11 +493,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -501,11 +541,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -547,11 +591,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -574,7 +622,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_with_arn_qualifier_mismatch[delete_function]": { - "recorded-date": "20-11-2023, 16:46:08", + "recorded-date": "10-04-2024, 08:59:30", "recorded-content": { "not_match_exception": { "Error": { @@ -603,7 +651,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_with_arn_qualifier_mismatch[get_function]": { - "recorded-date": "20-11-2023, 16:46:08", + "recorded-date": "10-04-2024, 08:59:30", "recorded-content": { "not_match_exception": { "Error": { @@ -632,7 +680,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_with_arn_qualifier_mismatch[get_function_configuration]": { - "recorded-date": "20-11-2023, 16:46:08", + "recorded-date": "10-04-2024, 08:59:30", "recorded-content": { "not_match_exception": { "Error": { @@ -661,7 +709,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_version[get_function]": { - "recorded-date": "20-11-2023, 16:46:11", + "recorded-date": "10-04-2024, 08:59:33", "recorded-content": { "version_not_found_exception": { "Error": { @@ -678,7 +726,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_version[get_function_configuration]": { - "recorded-date": "20-11-2023, 16:46:14", + "recorded-date": "10-04-2024, 08:59:36", "recorded-content": { "version_not_found_exception": { "Error": { @@ -695,7 +743,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_version[get_function_event_invoke_config]": { - "recorded-date": "20-11-2023, 16:46:18", + "recorded-date": "10-04-2024, 08:59:39", "recorded-content": { "version_not_found_exception": { "Error": { @@ -712,7 +760,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_delete_on_nonexisting_version": { - "recorded-date": "20-11-2023, 16:46:21", + "recorded-date": "10-04-2024, 08:59:42", "recorded-content": { "delete_function_response_non_existent": { "Error": { @@ -741,7 +789,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[delete_function]": { - "recorded-date": "20-11-2023, 16:46:22", + "recorded-date": "10-04-2024, 08:59:43", "recorded-content": { "not_found_exception": { "Error": { @@ -758,7 +806,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function]": { - "recorded-date": "20-11-2023, 16:46:22", + "recorded-date": "10-04-2024, 08:59:43", "recorded-content": { "not_found_exception": { "Error": { @@ -775,7 +823,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function_configuration]": { - "recorded-date": "20-11-2023, 16:46:22", + "recorded-date": "10-04-2024, 08:59:43", "recorded-content": { "not_found_exception": { "Error": { @@ -792,7 +840,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function_url_config]": { - "recorded-date": "20-11-2023, 16:46:22", + "recorded-date": "10-04-2024, 08:59:44", "recorded-content": { "not_found_exception": { "Error": { @@ -809,7 +857,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function_code_signing_config]": { - "recorded-date": "20-11-2023, 16:46:23", + "recorded-date": "10-04-2024, 08:59:44", "recorded-content": { "not_found_exception": { "Error": { @@ -826,7 +874,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function_event_invoke_config]": { - "recorded-date": "20-11-2023, 16:46:23", + "recorded-date": "10-04-2024, 08:59:44", "recorded-content": { "not_found_exception": { "Error": { @@ -843,7 +891,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function_concurrency]": { - "recorded-date": "20-11-2023, 16:46:23", + "recorded-date": "10-04-2024, 08:59:44", "recorded-content": { "not_found_exception": { "Error": { @@ -860,7 +908,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function]": { - "recorded-date": "20-11-2023, 16:46:26", + "recorded-date": "10-04-2024, 08:59:48", "recorded-content": { "wrong_region_exception": { "Error": { @@ -877,7 +925,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function_configuration]": { - "recorded-date": "20-11-2023, 16:46:29", + "recorded-date": "10-04-2024, 08:59:51", "recorded-content": { "wrong_region_exception": { "Error": { @@ -894,7 +942,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function_url_config]": { - "recorded-date": "20-11-2023, 16:46:32", + "recorded-date": "10-04-2024, 08:59:54", "recorded-content": { "wrong_region_exception": { "Error": { @@ -911,7 +959,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function_code_signing_config]": { - "recorded-date": "20-11-2023, 16:46:35", + "recorded-date": "10-04-2024, 08:59:57", "recorded-content": { "wrong_region_exception": { "Error": { @@ -928,7 +976,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function_event_invoke_config]": { - "recorded-date": "20-11-2023, 16:46:38", + "recorded-date": "10-04-2024, 09:00:00", "recorded-content": { "wrong_region_exception": { "Error": { @@ -945,7 +993,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function_concurrency]": { - "recorded-date": "20-11-2023, 16:46:41", + "recorded-date": "10-04-2024, 09:00:03", "recorded-content": { "wrong_region_exception": { "Error": { @@ -962,7 +1010,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[delete_function]": { - "recorded-date": "20-11-2023, 16:46:45", + "recorded-date": "10-04-2024, 09:00:06", "recorded-content": { "wrong_region_exception": { "Error": { @@ -979,7 +1027,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[invoke]": { - "recorded-date": "20-11-2023, 16:46:48", + "recorded-date": "10-04-2024, 09:00:09", "recorded-content": { "wrong_region_exception": { "Error": { @@ -996,7 +1044,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_code_location_zipfile": { - "recorded-date": "20-11-2023, 16:46:52", + "recorded-date": "10-04-2024, 09:00:13", "recorded-content": { "create-response-zip-file": { "Architectures": [ @@ -1012,11 +1060,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1057,11 +1109,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1098,11 +1154,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1143,11 +1203,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1170,7 +1234,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_code_location_s3": { - "recorded-date": "20-11-2023, 16:46:59", + "recorded-date": "10-04-2024, 09:00:19", "recorded-content": { "create_response_s3": { "Architectures": [ @@ -1186,11 +1250,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1231,11 +1299,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1272,11 +1344,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1317,11 +1393,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1344,7 +1424,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaVersions::test_publish_version_on_create": { - "recorded-date": "20-11-2023, 16:56:44", + "recorded-date": "10-04-2024, 09:12:04", "recorded-content": { "create_response": { "Architectures": [ @@ -1360,11 +1440,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1405,11 +1489,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1449,11 +1537,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1493,11 +1585,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1533,11 +1629,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "SnapStart": { "ApplyOn": "None", "OptimizationStatus": "Off" @@ -1562,11 +1662,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "SnapStart": { "ApplyOn": "None", "OptimizationStatus": "Off" @@ -1598,11 +1702,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1637,11 +1745,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "SnapStart": { "ApplyOn": "None", "OptimizationStatus": "Off" @@ -1666,11 +1778,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "SnapStart": { "ApplyOn": "None", "OptimizationStatus": "Off" @@ -1690,7 +1806,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaVersions::test_version_lifecycle": { - "recorded-date": "20-11-2023, 16:56:51", + "recorded-date": "10-04-2024, 09:12:12", "recorded-content": { "create_response": { "Architectures": [ @@ -1706,11 +1822,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1751,11 +1871,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1791,11 +1915,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "SnapStart": { "ApplyOn": "None", "OptimizationStatus": "Off" @@ -1829,11 +1957,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1872,11 +2004,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1911,11 +2047,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1954,11 +2094,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -1993,11 +2137,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2033,11 +2181,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2076,11 +2228,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2115,11 +2271,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2153,11 +2313,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2192,11 +2356,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "SnapStart": { "ApplyOn": "None", "OptimizationStatus": "Off" @@ -2221,11 +2389,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "SnapStart": { "ApplyOn": "None", "OptimizationStatus": "Off" @@ -2250,11 +2422,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "SnapStart": { "ApplyOn": "None", "OptimizationStatus": "Off" @@ -2391,7 +2567,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaVersions::test_publish_with_wrong_sha256": { - "recorded-date": "20-11-2023, 16:56:54", + "recorded-date": "10-04-2024, 09:12:15", "recorded-content": { "create_response": { "Architectures": [ @@ -2407,11 +2583,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2452,11 +2632,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2503,11 +2687,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2529,7 +2717,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaVersions::test_publish_with_update": { - "recorded-date": "20-11-2023, 16:56:57", + "recorded-date": "10-04-2024, 09:12:18", "recorded-content": { "create_response": { "Architectures": [ @@ -2545,11 +2733,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2590,11 +2782,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2628,11 +2824,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2672,11 +2872,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2720,11 +2924,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2747,7 +2955,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAlias::test_alias_lifecycle": { - "recorded-date": "20-11-2023, 16:57:06", + "recorded-date": "10-04-2024, 09:12:28", "recorded-content": { "create_response": { "Architectures": [ @@ -2768,11 +2976,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2813,11 +3025,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2856,11 +3072,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2936,11 +3156,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -2985,11 +3209,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -3319,7 +3547,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAlias::test_notfound_and_invalid_routingconfigs": { - "recorded-date": "20-11-2023, 16:57:21", + "recorded-date": "10-04-2024, 09:12:41", "recorded-content": { "create_response": { "Architectures": [ @@ -3340,11 +3568,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -3385,11 +3617,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -3428,11 +3664,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -3617,7 +3857,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTag::test_create_tag_on_fn_create": { - "recorded-date": "20-11-2023, 16:57:41", + "recorded-date": "10-04-2024, 09:13:05", "recorded-content": { "get_function_result": { "Code": { @@ -3642,11 +3882,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -3681,7 +3925,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTag::test_tag_lifecycle": { - "recorded-date": "20-11-2023, 16:57:46", + "recorded-date": "10-04-2024, 09:13:10", "recorded-content": { "tag_single_response": { "ResponseMetadata": { @@ -3796,7 +4040,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTag::test_tag_nonexisting_resource": { - "recorded-date": "20-11-2023, 16:57:50", + "recorded-date": "10-04-2024, 09:13:14", "recorded-content": { "pre_delete_get_function": { "Code": { @@ -3821,11 +4065,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -3884,7 +4132,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventInvokeConfig::test_lambda_eventinvokeconfig_exceptions": { - "recorded-date": "20-11-2023, 16:58:16", + "recorded-date": "10-04-2024, 09:13:39", "recorded-content": { "fn_version_result": { "Architectures": [ @@ -3904,11 +4152,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -4541,7 +4793,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaReservedConcurrency::test_function_concurrency_exceptions": { - "recorded-date": "20-11-2023, 16:58:20", + "recorded-date": "10-04-2024, 09:13:44", "recorded-content": { "put_function_concurrency_with_function_name_doesnotexist": { "Error": { @@ -4582,7 +4834,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaReservedConcurrency::test_function_concurrency": { - "recorded-date": "20-11-2023, 18:04:09", + "recorded-date": "10-04-2024, 09:13:51", "recorded-content": { "put_function_concurrency_with_reserved_0": { "ReservedConcurrentExecutions": 0, @@ -4620,7 +4872,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_add_lambda_permission_aws": { - "recorded-date": "20-11-2023, 16:58:43", + "recorded-date": "10-04-2024, 09:16:23", "recorded-content": { "create_lambda": { "CreateEventSourceMappingResponse": null, @@ -4641,11 +4893,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -4717,7 +4973,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_remove_multi_permissions": { - "recorded-date": "20-11-2023, 16:58:58", + "recorded-date": "10-04-2024, 09:16:38", "recorded-content": { "add_permission_1": { "Statement": { @@ -4861,7 +5117,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_create_multiple_lambda_permissions": { - "recorded-date": "20-11-2023, 16:59:01", + "recorded-date": "10-04-2024, 09:16:41", "recorded-content": { "add_permission_response_1": { "Statement": { @@ -4927,7 +5183,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaUrl::test_url_config_lifecycle": { - "recorded-date": "20-11-2023, 16:59:22", + "recorded-date": "10-04-2024, 09:17:00", "recorded-content": { "url_creation": { "AuthType": "NONE", @@ -5006,7 +5262,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_large_lambda": { - "recorded-date": "21-11-2023, 10:44:33", + "recorded-date": "10-04-2024, 09:18:29", "recorded-content": { "create_function_large_zip": { "Architectures": [ @@ -5022,11 +5278,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -5050,7 +5310,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_large_environment_variables_fails": { - "recorded-date": "20-11-2023, 17:00:32", + "recorded-date": "10-04-2024, 09:18:46", "recorded-content": { "failed_create_fn_result": { "Error": { @@ -5067,7 +5327,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_large_environment_fails_multiple_keys": { - "recorded-date": "20-11-2023, 17:00:49", + "recorded-date": "10-04-2024, 09:19:04", "recorded-content": { "failured_create_fn_result_multi_key": { "Error": { @@ -5084,7 +5344,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_lambda_envvars_near_limit_succeeds": { - "recorded-date": "20-11-2023, 17:00:52", + "recorded-date": "10-04-2024, 09:19:07", "recorded-content": { "successful_create_fn_result": { "CreateEventSourceMappingResponse": null, @@ -5107,11 +5367,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -5136,7 +5400,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestCodeSigningConfig::test_function_code_signing_config": { - "recorded-date": "20-11-2023, 17:00:57", + "recorded-date": "10-04-2024, 09:19:11", "recorded-content": { "create_code_signing_config": { "CodeSigningConfig": { @@ -5215,26 +5479,18 @@ } }, "list_code_signing_configs": { - "CodeSigningConfigs": [ - { - "AllowedPublishers": { - "SigningProfileVersionArns": [ - "arn:aws:signer::111111111111:/signing-profiles/test" - ] - }, - "CodeSigningConfigArn": "arn:aws:lambda::111111111111:code-signing-config:", - "CodeSigningConfigId": "", - "CodeSigningPolicies": { - "UntrustedArtifactOnDeployment": "Warn" - }, - "Description": "Testing CodeSigning Config", - "LastModified": "date" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } + "AllowedPublishers": { + "SigningProfileVersionArns": [ + "arn:aws:signer::111111111111:/signing-profiles/test" + ] + }, + "CodeSigningConfigArn": "arn:aws:lambda::111111111111:code-signing-config:", + "CodeSigningConfigId": "", + "CodeSigningPolicies": { + "UntrustedArtifactOnDeployment": "Warn" + }, + "Description": "Testing CodeSigning Config", + "LastModified": "date" }, "list_functions_by_code_signing_config": { "FunctionArns": [ @@ -5260,7 +5516,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestCodeSigningConfig::test_code_signing_not_found_excs": { - "recorded-date": "20-11-2023, 17:01:02", + "recorded-date": "10-04-2024, 09:19:16", "recorded-content": { "create_code_signing_config": { "CodeSigningConfig": { @@ -5439,7 +5695,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAccountSettings::test_account_settings": { - "recorded-date": "20-11-2023, 17:01:02", + "recorded-date": "10-04-2024, 09:19:17", "recorded-content": { "acc_settings_modded": { "AccountLimit": [ @@ -5461,7 +5717,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventSourceMappings::test_event_source_mapping_exceptions": { - "recorded-date": "20-11-2023, 17:01:28", + "recorded-date": "10-04-2024, 09:19:37", "recorded-content": { "get_unknown_uuid": { "Error": { @@ -5526,7 +5782,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventSourceMappings::test_event_source_mapping_lifecycle": { - "recorded-date": "20-11-2023, 17:01:43", + "recorded-date": "10-04-2024, 09:19:52", "recorded-content": { "update_table_response": { "TableDescription": { @@ -5655,7 +5911,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTags::test_tag_exceptions": { - "recorded-date": "20-11-2023, 17:02:01", + "recorded-date": "10-04-2024, 09:22:07", "recorded-content": { "tag_lambda_invalidarn": { "Error": { @@ -5729,7 +5985,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTags::test_tag_limits": { - "recorded-date": "20-11-2023, 17:02:05", + "recorded-date": "10-04-2024, 09:22:11", "recorded-content": { "tag_lambda_too_many_tags": { "Error": { @@ -5830,11 +6086,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -5921,7 +6181,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTags::test_tag_lifecycle": { - "recorded-date": "20-11-2023, 17:02:14", + "recorded-date": "10-04-2024, 09:22:20", "recorded-content": { "list_tags_response_postfncreate": { "Tags": { @@ -5955,11 +6215,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -6024,11 +6288,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -6097,11 +6365,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -6162,11 +6434,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -6221,11 +6497,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -6260,7 +6540,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaProvisionedConcurrency::test_provisioned_concurrency_exceptions": { - "recorded-date": "20-11-2023, 16:58:30", + "recorded-date": "10-04-2024, 09:13:57", "recorded-content": { "publish_version_result": { "Architectures": [ @@ -6280,11 +6560,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -6509,7 +6793,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaProvisionedConcurrency::test_lambda_provisioned_lifecycle": { - "recorded-date": "20-11-2023, 18:06:43", + "recorded-date": "10-04-2024, 09:16:15", "recorded-content": { "publish_version_result": { "Architectures": [ @@ -6529,11 +6813,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -6655,7 +6943,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_oversized_request_create_lambda": { - "recorded-date": "21-11-2023, 10:41:36", + "recorded-date": "10-04-2024, 09:17:14", "recorded-content": { "invalid_param_exc": { "Error": { @@ -6670,7 +6958,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTags::test_tag_versions": { - "recorded-date": "20-11-2023, 17:02:09", + "recorded-date": "10-04-2024, 09:22:14", "recorded-content": { "tag_resource_exception": { "Error": { @@ -6699,7 +6987,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaUrl::test_url_config_list_paging": { - "recorded-date": "20-11-2023, 16:59:17", + "recorded-date": "10-04-2024, 09:16:55", "recorded-content": { "fn_version_result": { "Architectures": [ @@ -6719,11 +7007,15 @@ "Handler": "lambda_handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "nodejs14.x", + "Runtime": "nodejs20.x", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -6819,7 +7111,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaUrl::test_url_config_exceptions": { - "recorded-date": "20-11-2023, 16:59:12", + "recorded-date": "10-04-2024, 09:16:50", "recorded-content": { "fn_version_result": { "Architectures": [ @@ -6839,11 +7131,15 @@ "Handler": "lambda_handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "nodejs14.x", + "Runtime": "nodejs20.x", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -7253,7 +7549,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_permission_exceptions": { - "recorded-date": "20-11-2023, 16:58:39", + "recorded-date": "10-04-2024, 09:16:20", "recorded-content": { "add_permission_invalid_statement_id": { "Error": { @@ -7434,7 +7730,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_oversized_unzipped_lambda": { - "recorded-date": "21-11-2023, 10:43:40", + "recorded-date": "10-04-2024, 09:17:47", "recorded-content": { "invalid_param_exc": { "Error": { @@ -7451,7 +7747,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_create_lambda_exceptions": { - "recorded-date": "12-03-2024, 15:42:35", + "recorded-date": "10-04-2024, 09:00:26", "recorded-content": { "invalid_role_arn_exc": { "Error": { @@ -7466,10 +7762,10 @@ "invalid_runtime_exc": { "Error": { "Code": "InvalidParameterValueException", - "Message": "Value non-existent-runtime at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN" + "Message": "Value non-existent-runtime at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN" }, "Type": "User", - "message": "Value non-existent-runtime at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN", + "message": "Value non-existent-runtime at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 @@ -7478,10 +7774,10 @@ "uppercase_runtime_exc": { "Error": { "Code": "InvalidParameterValueException", - "Message": "Value PYTHON3.9 at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN" + "Message": "Value PYTHON3.9 at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN" }, "Type": "User", - "message": "Value PYTHON3.9 at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN", + "message": "Value PYTHON3.9 at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 @@ -7523,7 +7819,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_update_lambda_exceptions": { - "recorded-date": "12-03-2024, 16:08:14", + "recorded-date": "10-04-2024, 09:00:29", "recorded-content": { "invalid_role_arn_exc": { "Error": { @@ -7538,10 +7834,10 @@ "invalid_runtime_exc": { "Error": { "Code": "InvalidParameterValueException", - "Message": "Value non-existent-runtime at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN" + "Message": "Value non-existent-runtime at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN" }, "Type": "User", - "message": "Value non-existent-runtime at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN", + "message": "Value non-existent-runtime at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 @@ -7550,10 +7846,10 @@ "uppercase_runtime_exc": { "Error": { "Code": "InvalidParameterValueException", - "Message": "Value PYTHON3.9 at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN" + "Message": "Value PYTHON3.9 at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN" }, "Type": "User", - "message": "Value PYTHON3.9 at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN", + "message": "Value PYTHON3.9 at 'runtime' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9] or be a valid ARN", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 @@ -7562,7 +7858,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_list_functions": { - "recorded-date": "20-11-2023, 16:47:16", + "recorded-date": "10-04-2024, 09:00:42", "recorded-content": { "create_response_1": { "CreateEventSourceMappingResponse": null, @@ -7583,11 +7879,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -7628,11 +7928,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -7683,11 +7987,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "SnapStart": { "ApplyOn": "None", "OptimizationStatus": "Off" @@ -7715,11 +8023,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "SnapStart": { "ApplyOn": "None", "OptimizationStatus": "Off" @@ -7747,11 +8059,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "SnapStart": { "ApplyOn": "None", "OptimizationStatus": "Off" @@ -7783,11 +8099,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "SnapStart": { "ApplyOn": "None", "OptimizationStatus": "Off" @@ -7815,11 +8135,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "SnapStart": { "ApplyOn": "None", "OptimizationStatus": "Off" @@ -7835,14 +8159,14 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_exceptions": { - "recorded-date": "12-03-2024, 15:42:16", + "recorded-date": "10-04-2024, 09:22:40", "recorded-content": { "publish_result": { "CompatibleArchitectures": [ "x86_64" ], "CompatibleRuntimes": [ - "python3.9" + "python3.12" ], "Content": { "CodeSha256": "", @@ -8013,7 +8337,7 @@ "publish_layer_version_exc_invalid_runtime_arch": { "Error": { "Code": "ValidationException", - "Message": "2 validation errors detected: Value '[invalidruntime]' at 'compatibleRuntimes' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, java8.al2, ruby3.2, python3.8, python3.9]; Value '[invalidarch]' at 'compatibleArchitectures' failed to satisfy constraint: Member must satisfy constraint: [Member must satisfy enum value set: [x86_64, arm64]]" + "Message": "2 validation errors detected: Value '[invalidruntime]' at 'compatibleRuntimes' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9]; Value '[invalidarch]' at 'compatibleArchitectures' failed to satisfy constraint: Member must satisfy constraint: [Member must satisfy enum value set: [x86_64, arm64]]" }, "ResponseMetadata": { "HTTPHeaders": {}, @@ -8023,7 +8347,7 @@ "publish_layer_version_exc_partially_invalid_values": { "Error": { "Code": "ValidationException", - "Message": "2 validation errors detected: Value '[invalidruntime, invalidruntime2, nodejs16.x]' at 'compatibleRuntimes' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, java8.al2, ruby3.2, python3.8, python3.9]; Value '[invalidarch, x86_64]' at 'compatibleArchitectures' failed to satisfy constraint: Member must satisfy constraint: [Member must satisfy enum value set: [x86_64, arm64]]" + "Message": "2 validation errors detected: Value '[invalidruntime, invalidruntime2, nodejs20.x]' at 'compatibleRuntimes' failed to satisfy constraint: Member must satisfy enum value set: [nodejs20.x, provided.al2023, python3.12, java17, nodejs16.x, dotnet8, python3.10, java11, python3.11, dotnet6, java21, nodejs18.x, provided.al2, ruby3.3, java8.al2, ruby3.2, python3.8, python3.9]; Value '[invalidarch, x86_64]' at 'compatibleArchitectures' failed to satisfy constraint: Member must satisfy constraint: [Member must satisfy enum value set: [x86_64, arm64]]" }, "ResponseMetadata": { "HTTPHeaders": {}, @@ -8033,7 +8357,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_lifecycle": { - "recorded-date": "20-11-2023, 17:04:38", + "recorded-date": "10-04-2024, 09:24:17", "recorded-content": { "get_fn_result": { "Code": { @@ -8058,11 +8382,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -8100,11 +8428,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -8128,7 +8460,7 @@ "x86_64" ], "CompatibleRuntimes": [ - "python3.9" + "python3.12" ], "Content": { "CodeSha256": "", @@ -8151,7 +8483,7 @@ "x86_64" ], "CompatibleRuntimes": [ - "python3.9" + "python3.12" ], "Content": { "CodeSha256": "", @@ -8195,11 +8527,15 @@ "CodeSize": "" } ], + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -8242,11 +8578,15 @@ "CodeSize": "" } ], + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -8270,7 +8610,7 @@ "x86_64" ], "CompatibleRuntimes": [ - "python3.9" + "python3.12" ], "Content": { "CodeSha256": "", @@ -8293,7 +8633,7 @@ "x86_64" ], "CompatibleRuntimes": [ - "python3.9" + "python3.12" ], "Content": { "CodeSha256": "", @@ -8318,7 +8658,7 @@ "x86_64" ], "CompatibleRuntimes": [ - "python3.9" + "python3.12" ], "CreatedDate": "date", "Description": "", @@ -8331,7 +8671,7 @@ "x86_64" ], "CompatibleRuntimes": [ - "python3.9" + "python3.12" ], "CreatedDate": "date", "Description": "", @@ -8375,11 +8715,15 @@ "CodeSize": "" } ], + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -8416,7 +8760,7 @@ "x86_64" ], "CompatibleRuntimes": [ - "python3.9" + "python3.12" ], "Content": { "CodeSha256": "", @@ -8437,14 +8781,14 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_policy_exceptions": { - "recorded-date": "20-11-2023, 17:04:54", + "recorded-date": "10-04-2024, 09:24:29", "recorded-content": { "publish_result": { "CompatibleArchitectures": [ "x86_64" ], "CompatibleRuntimes": [ - "python3.9" + "python3.12" ], "Content": { "CodeSha256": "", @@ -8620,14 +8964,14 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_policy_lifecycle": { - "recorded-date": "20-11-2023, 17:05:00", + "recorded-date": "10-04-2024, 09:24:35", "recorded-content": { "publish_result": { "CompatibleArchitectures": [ "x86_64" ], "CompatibleRuntimes": [ - "python3.9" + "python3.12" ], "Content": { "CodeSha256": "", @@ -8748,7 +9092,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_s3_content": { - "recorded-date": "20-11-2023, 17:04:46", + "recorded-date": "10-04-2024, 09:24:23", "recorded-content": { "publish_layer_result": { "Content": { @@ -8769,7 +9113,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_function_exceptions": { - "recorded-date": "20-11-2023, 17:03:22", + "recorded-date": "10-04-2024, 09:23:19", "recorded-content": { "publish_result": { "CompatibleArchitectures": [ @@ -8854,11 +9198,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -8941,7 +9289,7 @@ "add_layer_arn_without_version_exc": { "Error": { "Code": "ValidationException", - "Message": "1 validation error detected: Value '[arn:aws:lambda::111111111111:layer:]' at 'layers' failed to satisfy constraint: Member must satisfy constraint: [Member must have length less than or equal to 140, Member must have length greater than or equal to 1, Member must satisfy regular expression pattern: (arn:[a-zA-Z0-9-]+:lambda:[a-zA-Z0-9-]+:\\d{12}:layer:[a-zA-Z0-9-_]+:[0-9]+)|(arn:[a-zA-Z0-9-]+:lambda:::awslayer:[a-zA-Z0-9-_]+), Member must not be null]" + "Message": "1 validation error detected: Value '[arn:aws:lambda::111111111111:layer:]' at 'layers' failed to satisfy constraint: Member must satisfy constraint: [Member must have length less than or equal to 140, Member must have length greater than or equal to 1, Member must satisfy regular expression pattern: (arn:[a-zA-Z0-9-]+:lambda:[a-z]{2}((-gov)|(-iso(b?)))?-[a-z]+-\\d{1}:\\d{12}:layer:[a-zA-Z0-9-_]+:[0-9]+)|(arn:[a-zA-Z0-9-]+:lambda:::awslayer:[a-zA-Z0-9-_]+), Member must not be null]" }, "ResponseMetadata": { "HTTPHeaders": {}, @@ -8963,7 +9311,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaImages::test_lambda_image_crud": { - "recorded-date": "20-11-2023, 16:55:07", + "recorded-date": "10-04-2024, 09:10:21", "recorded-content": { "create-image-response": { "Architectures": [ @@ -8983,6 +9331,10 @@ "FunctionArn": "arn:aws:lambda::111111111111:function:", "FunctionName": "", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -9029,6 +9381,10 @@ "FunctionName": "", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -9068,6 +9424,10 @@ "FunctionName": "", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -9120,6 +9480,10 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -9164,6 +9528,10 @@ "FunctionName": "", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -9203,6 +9571,10 @@ "FunctionName": "", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -9225,7 +9597,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaImages::test_lambda_image_and_image_config_crud": { - "recorded-date": "20-11-2023, 16:55:56", + "recorded-date": "10-04-2024, 09:11:14", "recorded-content": { "create-image-with-config-response": { "Architectures": [ @@ -9257,6 +9629,10 @@ } }, "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -9315,6 +9691,10 @@ }, "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -9366,6 +9746,10 @@ }, "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -9415,8 +9799,12 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", - "MemorySize": 128, - "PackageType": "Image", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 128, + "PackageType": "Image", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", "SnapStart": { @@ -9468,6 +9856,10 @@ }, "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -9516,6 +9908,10 @@ }, "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -9556,6 +9952,10 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -9600,6 +10000,10 @@ "FunctionName": "", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -9639,6 +10043,10 @@ "FunctionName": "", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -9661,7 +10069,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaImages::test_lambda_zip_file_to_image": { - "recorded-date": "20-11-2023, 16:55:22", + "recorded-date": "10-04-2024, 09:10:38", "recorded-content": { "create-image-response": { "Architectures": [ @@ -9677,11 +10085,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -9722,11 +10134,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -9761,11 +10177,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -9816,11 +10236,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -9855,11 +10279,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -9881,7 +10309,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAccountSettings::test_account_settings_total_code_size": { - "recorded-date": "20-11-2023, 17:01:20", + "recorded-date": "10-04-2024, 09:19:29", "recorded-content": { "total_code_size_diff_create_function": 276, "total_code_size_diff_update_function": 276, @@ -9890,7 +10318,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAccountSettings::test_account_settings_total_code_size_config_update": { - "recorded-date": "20-11-2023, 17:01:26", + "recorded-date": "10-04-2024, 09:19:35", "recorded-content": { "is_total_code_size_diff_create_function_more_than_200": true, "total_code_size_diff_update_function_configuration": 0, @@ -9898,7 +10326,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventInvokeConfig::test_lambda_eventinvokeconfig_lifecycle": { - "recorded-date": "20-11-2023, 16:57:57", + "recorded-date": "10-04-2024, 09:13:21", "recorded-content": { "put_invokeconfig_retries_0": { "DestinationConfig": { @@ -10017,11 +10445,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -10261,7 +10693,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_add_lambda_permission_fields": { - "recorded-date": "20-11-2023, 16:58:54", + "recorded-date": "10-04-2024, 09:16:33", "recorded-content": { "add_permission_principal_wildcard": { "Statement": { @@ -10345,9 +10777,9 @@ "Resource": "arn:aws:lambda::111111111111:function:", "Condition": { "StringEquals": { + "AWS:SourceAccount": "111111111111", "lambda:FunctionUrlAuthType": "NONE", - "aws:PrincipalOrgID": "o-1234567890", - "AWS:SourceAccount": "111111111111" + "aws:PrincipalOrgID": "o-1234567890" }, "ArnLike": { "AWS:SourceArn": "arn:aws:s3:::" @@ -10380,7 +10812,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_lambda_permission_fn_versioning": { - "recorded-date": "20-11-2023, 16:58:49", + "recorded-date": "10-04-2024, 09:16:29", "recorded-content": { "add_permission": { "Statement": { @@ -10447,11 +10879,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -10493,11 +10929,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -11013,7 +11453,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaRevisions::test_function_revisions_basic": { - "recorded-date": "20-11-2023, 16:57:28", + "recorded-date": "10-04-2024, 09:12:53", "recorded-content": { "create_function_response_rev1": { "CreateEventSourceMappingResponse": null, @@ -11034,11 +11474,15 @@ "FunctionName": "", "Handler": "index.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -11083,11 +11527,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -11139,11 +11587,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -11185,11 +11637,15 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -11241,6 +11697,10 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -11287,6 +11747,10 @@ "Handler": "index.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -11314,7 +11778,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaRevisions::test_function_revisions_version_and_alias": { - "recorded-date": "20-11-2023, 16:57:33", + "recorded-date": "10-04-2024, 09:12:58", "recorded-content": { "create_function_response_rev1": { "CreateEventSourceMappingResponse": null, @@ -11335,11 +11799,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -11384,11 +11852,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -11438,11 +11910,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -11484,11 +11960,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -11531,11 +12011,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -11589,11 +12073,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -11636,11 +12124,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -11686,7 +12178,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaRevisions::test_function_revisions_permissions": { - "recorded-date": "20-11-2023, 16:57:37", + "recorded-date": "10-04-2024, 09:13:02", "recorded-content": { "add_permission_revision_exception": { "Error": { @@ -11758,7 +12250,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_function_quota_exception": { - "recorded-date": "20-11-2023, 17:04:11", + "recorded-date": "10-04-2024, 09:23:59", "recorded-content": { "create_function_with_six_layers": { "Error": { @@ -11775,7 +12267,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventSourceMappings::test_create_event_source_validation": { - "recorded-date": "20-11-2023, 17:01:56", + "recorded-date": "10-04-2024, 09:22:02", "recorded-content": { "error": { "Error": { @@ -11792,15 +12284,15 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_exceptions": { - "recorded-date": "20-11-2023, 17:08:17", + "recorded-date": "10-04-2024, 09:30:32", "recorded-content": { "create_function_unsupported_snapstart_runtime": { "Error": { "Code": "InvalidParameterValueException", - "Message": "python3.9 is not supported for SnapStart enabled functions." + "Message": "python3.12 is not supported for SnapStart enabled functions." }, "Type": "User", - "message": "python3.9 is not supported for SnapStart enabled functions.", + "message": "python3.12 is not supported for SnapStart enabled functions.", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 400 @@ -11819,7 +12311,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaImages::test_lambda_image_versions": { - "recorded-date": "20-11-2023, 16:56:31", + "recorded-date": "10-04-2024, 09:11:51", "recorded-content": { "create_image_response": { "Architectures": [ @@ -11839,6 +12331,10 @@ "FunctionArn": "arn:aws:lambda::111111111111:function:", "FunctionName": "", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -11885,6 +12381,10 @@ "FunctionName": "", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -11925,6 +12425,10 @@ "FunctionArn": "arn:aws:lambda::111111111111:function::$LATEST", "FunctionName": "", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -11957,6 +12461,10 @@ "FunctionArn": "arn:aws:lambda::111111111111:function::1", "FunctionName": "", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -11998,6 +12506,10 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -12042,6 +12554,10 @@ "FunctionName": "", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -12093,6 +12609,10 @@ "FunctionName": "", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -12133,6 +12653,10 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -12177,6 +12701,10 @@ "FunctionName": "", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -12216,6 +12744,10 @@ "FunctionName": "", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Image", "RevisionId": "", @@ -12238,7 +12770,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java11]": { - "recorded-date": "20-11-2023, 17:53:04", + "recorded-date": "10-04-2024, 09:26:29", "recorded-content": { "create_function_response": { "CreateEventSourceMappingResponse": null, @@ -12259,6 +12791,10 @@ "FunctionName": "", "Handler": "cloud.localstack.sample.LambdaHandlerWithLib", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -12308,6 +12844,10 @@ "Handler": "cloud.localstack.sample.LambdaHandlerWithLib", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -12355,6 +12895,10 @@ "Handler": "cloud.localstack.sample.LambdaHandlerWithLib", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -12382,7 +12926,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java17]": { - "recorded-date": "20-11-2023, 17:54:39", + "recorded-date": "10-04-2024, 09:28:30", "recorded-content": { "create_function_response": { "CreateEventSourceMappingResponse": null, @@ -12403,6 +12947,10 @@ "FunctionName": "", "Handler": "cloud.localstack.sample.LambdaHandlerWithLib", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -12452,6 +13000,10 @@ "Handler": "cloud.localstack.sample.LambdaHandlerWithLib", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -12499,6 +13051,10 @@ "Handler": "cloud.localstack.sample.LambdaHandlerWithLib", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -12619,7 +13175,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java17]": { - "recorded-date": "20-11-2023, 17:08:17", + "recorded-date": "10-04-2024, 09:30:32", "recorded-content": { "create_function_response": { "CreateEventSourceMappingResponse": null, @@ -12640,6 +13196,10 @@ "FunctionName": "", "Handler": "cloud.localstack.sample.LambdaHandlerWithLib", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -12686,6 +13246,10 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -12712,7 +13276,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_vpc_config": { - "recorded-date": "20-11-2023, 17:50:21", + "recorded-date": "10-04-2024, 09:09:00", "recorded-content": { "create_response": { "CreateEventSourceMappingResponse": null, @@ -12733,11 +13297,15 @@ "FunctionName": "", "Handler": "handler.handler", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 256, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -12792,11 +13360,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 256, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -12846,11 +13418,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 256, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -12902,11 +13478,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 256, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -12956,11 +13536,15 @@ "LastUpdateStatus": "InProgress", "LastUpdateStatusReason": "The function is being created.", "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 256, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -13008,11 +13592,15 @@ "Handler": "handler.handler", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 256, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -13082,7 +13670,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_compatibilities[runtimes0]": { - "recorded-date": "21-03-2024, 08:24:39", + "recorded-date": "10-04-2024, 09:22:25", "recorded-content": { "publish_result": { "CompatibleArchitectures": [ @@ -13123,7 +13711,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_compatibilities[runtimes1]": { - "recorded-date": "21-03-2024, 08:24:44", + "recorded-date": "10-04-2024, 09:22:29", "recorded-content": { "publish_result": { "CompatibleArchitectures": [ @@ -13161,7 +13749,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaProvisionedConcurrency::test_provisioned_concurrency_limits": { - "recorded-date": "20-11-2023, 16:58:33", + "recorded-date": "10-04-2024, 09:14:00", "recorded-content": { "put_provisioned_concurrency_account_limit_exceeded": { "Error": { @@ -13188,7 +13776,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaReservedConcurrency::test_function_concurrency_limits": { - "recorded-date": "20-11-2023, 16:58:23", + "recorded-date": "10-04-2024, 09:13:47", "recorded-content": { "put_function_concurrency_account_limit_exceeded": { "Error": { @@ -13215,7 +13803,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java21]": { - "recorded-date": "20-11-2023, 17:56:15", + "recorded-date": "10-04-2024, 09:30:25", "recorded-content": { "create_function_response": { "CreateEventSourceMappingResponse": null, @@ -13236,6 +13824,10 @@ "FunctionName": "", "Handler": "cloud.localstack.sample.LambdaHandlerWithLib", "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -13285,7 +13877,11 @@ "Handler": "cloud.localstack.sample.LambdaHandlerWithLib", "LastModified": "date", "LastUpdateStatus": "Successful", - "MemorySize": 128, + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", @@ -13332,6 +13928,10 @@ "Handler": "cloud.localstack.sample.LambdaHandlerWithLib", "LastModified": "date", "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, "MemorySize": 128, "PackageType": "Zip", "RevisionId": "", @@ -13359,7 +13959,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_oversized_zipped_create_lambda": { - "recorded-date": "21-11-2023, 10:43:03", + "recorded-date": "10-04-2024, 09:17:26", "recorded-content": { "invalid_param_exc": { "Error": { @@ -13374,7 +13974,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_function_arns": { - "recorded-date": "27-03-2024, 09:37:39", + "recorded-date": "10-04-2024, 09:00:25", "recorded-content": { "create-function-arn-response": { "CreateEventSourceMappingResponse": null, @@ -13531,7 +14131,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventSourceMappings::test_function_name_variations": { - "recorded-date": "15-12-2023, 10:58:47", + "recorded-date": "10-04-2024, 09:21:48", "recorded-content": { "name_only_create_esm": { "BatchSize": 10, @@ -13641,7 +14241,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAlias::test_alias_naming": { - "recorded-date": "13-02-2024, 08:44:31", + "recorded-date": "10-04-2024, 09:12:45", "recorded-content": { "create_response": { "Architectures": [ @@ -13670,7 +14270,7 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -13719,7 +14319,7 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.9", + "Runtime": "python3.12", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -13773,7 +14373,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_invalid_invoke": { - "recorded-date": "28-03-2024, 08:44:26", + "recorded-date": "10-04-2024, 09:09:00", "recorded-content": { "invoke_function_name_pattern_exc": { "Error": { @@ -13787,10 +14387,10 @@ } } }, - "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_concurrent_config_updates": { - "recorded-date": "08-04-2024, 13:14:54", + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java21]": { + "recorded-date": "10-04-2024, 09:30:28", "recorded-content": { - "create-function-response": { + "create_function_response": { "CreateEventSourceMappingResponse": null, "CreateFunctionResponse": { "Architectures": [ @@ -13807,7 +14407,7 @@ }, "FunctionArn": "arn:aws:lambda::111111111111:function:", "FunctionName": "", - "Handler": "handler.handler", + "Handler": "cloud.localstack.sample.LambdaHandlerWithLib", "LastModified": "date", "LoggingConfig": { "LogFormat": "Text", @@ -13817,7 +14417,7 @@ "PackageType": "Zip", "RevisionId": "", "Role": "arn:aws:iam::111111111111:role/", - "Runtime": "python3.12", + "Runtime": "java21", "RuntimeVersionConfig": { "RuntimeVersionArn": "arn:aws:lambda:::runtime:" }, @@ -13839,22 +14439,57 @@ } } }, - "update-during-in-progress-update-exc": { - "Error": { - "Code": "ResourceConflictException", - "Message": "The operation cannot be performed at this time. An update is in progress for resource: arn:aws:lambda::111111111111:function:" + "update_function_response": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} }, - "Type": "User", - "message": "The operation cannot be performed at this time. An update is in progress for resource: arn:aws:lambda::111111111111:function:", + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "cloud.localstack.sample.LambdaHandlerWithLib", + "LastModified": "date", + "LastUpdateStatus": "InProgress", + "LastUpdateStatusReason": "The function is being created.", + "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 128, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "java21", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "PublishedVersions", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 30, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", "ResponseMetadata": { "HTTPHeaders": {}, - "HTTPStatusCode": 409 + "HTTPStatusCode": 200 } } } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_concurrent_code_updates": { - "recorded-date": "09-04-2024, 11:23:28", + "recorded-date": "10-04-2024, 09:09:04", "recorded-content": { "create-function-response": { "Architectures": [ @@ -13912,5 +14547,1641 @@ } } } + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_concurrent_config_updates": { + "recorded-date": "10-04-2024, 09:09:09", + "recorded-content": { + "create-function-response": { + "CreateEventSourceMappingResponse": null, + "CreateFunctionResponse": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 128, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Pending", + "StateReason": "The function is being created.", + "StateReasonCode": "Creating", + "Timeout": 30, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + } + }, + "update-during-in-progress-update-exc": { + "Error": { + "Code": "ResourceConflictException", + "Message": "The operation cannot be performed at this time. An update is in progress for resource: arn:aws:lambda::111111111111:function:" + }, + "Type": "User", + "message": "The operation cannot be performed at this time. An update is in progress for resource: arn:aws:lambda::111111111111:function:", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 409 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_function_advanced_logging_configuration": { + "recorded-date": "19-04-2024, 08:20:18", + "recorded-content": { + "create_response": { + "CreateEventSourceMappingResponse": null, + "CreateFunctionResponse": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LoggingConfig": { + "ApplicationLogLevel": "INFO", + "LogFormat": "JSON", + "LogGroup": "/aws/lambda/", + "SystemLogLevel": "INFO" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Pending", + "StateReason": "The function is being created.", + "StateReasonCode": "Creating", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + } + }, + "get_function_response": { + "Code": { + "Location": "", + "RepositoryType": "S3" + }, + "Configuration": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "ApplicationLogLevel": "INFO", + "LogFormat": "JSON", + "LogGroup": "/aws/lambda/", + "SystemLogLevel": "INFO" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "function_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "ApplicationLogLevel": "INFO", + "LogFormat": "JSON", + "LogGroup": "/aws/lambda/", + "SystemLogLevel": "INFO" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "updated_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "InProgress", + "LastUpdateStatusReason": "The function is being created.", + "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "ApplicationLogLevel": "INFO", + "LogFormat": "JSON", + "LogGroup": "cool_lambda", + "SystemLogLevel": "INFO" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "received_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "ApplicationLogLevel": "INFO", + "LogFormat": "JSON", + "LogGroup": "cool_lambda", + "SystemLogLevel": "INFO" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_advanced_logging_configuration_format_switch": { + "recorded-date": "17-04-2024, 14:16:55", + "recorded-content": { + "create_response": { + "CreateEventSourceMappingResponse": null, + "CreateFunctionResponse": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Pending", + "StateReason": "The function is being created.", + "StateReasonCode": "Creating", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + } + }, + "get_function_response": { + "Code": { + "Location": "", + "RepositoryType": "S3" + }, + "Configuration": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "function_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "updated_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "InProgress", + "LastUpdateStatusReason": "The function is being created.", + "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "ApplicationLogLevel": "INFO", + "LogFormat": "JSON", + "LogGroup": "/aws/lambda/", + "SystemLogLevel": "INFO" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "received_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "ApplicationLogLevel": "INFO", + "LogFormat": "JSON", + "LogGroup": "/aws/lambda/", + "SystemLogLevel": "INFO" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "updated_config_v2": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "InProgress", + "LastUpdateStatusReason": "The function is being created.", + "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "received_config_v2": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_function_partial_advanced_logging_configuration_update[partial_config0]": { + "recorded-date": "17-04-2024, 14:17:01", + "recorded-content": { + "create_response": { + "CreateEventSourceMappingResponse": null, + "CreateFunctionResponse": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Pending", + "StateReason": "The function is being created.", + "StateReasonCode": "Creating", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + } + }, + "get_function_response": { + "Code": { + "Location": "", + "RepositoryType": "S3" + }, + "Configuration": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "function_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "updated_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "InProgress", + "LastUpdateStatusReason": "The function is being created.", + "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "ApplicationLogLevel": "INFO", + "LogFormat": "JSON", + "LogGroup": "/aws/lambda/", + "SystemLogLevel": "INFO" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "received_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "ApplicationLogLevel": "INFO", + "LogFormat": "JSON", + "LogGroup": "/aws/lambda/", + "SystemLogLevel": "INFO" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_function_partial_advanced_logging_configuration_update[partial_config1]": { + "recorded-date": "17-04-2024, 14:17:06", + "recorded-content": { + "create_response": { + "CreateEventSourceMappingResponse": null, + "CreateFunctionResponse": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Pending", + "StateReason": "The function is being created.", + "StateReasonCode": "Creating", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + } + }, + "get_function_response": { + "Code": { + "Location": "", + "RepositoryType": "S3" + }, + "Configuration": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "function_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "updated_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "InProgress", + "LastUpdateStatusReason": "The function is being created.", + "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "ApplicationLogLevel": "DEBUG", + "LogFormat": "JSON", + "LogGroup": "/aws/lambda/", + "SystemLogLevel": "INFO" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "received_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "ApplicationLogLevel": "DEBUG", + "LogFormat": "JSON", + "LogGroup": "/aws/lambda/", + "SystemLogLevel": "INFO" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_function_partial_advanced_logging_configuration_update[partial_config2]": { + "recorded-date": "17-04-2024, 14:17:11", + "recorded-content": { + "create_response": { + "CreateEventSourceMappingResponse": null, + "CreateFunctionResponse": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Pending", + "StateReason": "The function is being created.", + "StateReasonCode": "Creating", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + } + }, + "get_function_response": { + "Code": { + "Location": "", + "RepositoryType": "S3" + }, + "Configuration": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "function_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "updated_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "InProgress", + "LastUpdateStatusReason": "The function is being created.", + "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "ApplicationLogLevel": "INFO", + "LogFormat": "JSON", + "LogGroup": "/aws/lambda/", + "SystemLogLevel": "DEBUG" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "received_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "ApplicationLogLevel": "INFO", + "LogFormat": "JSON", + "LogGroup": "/aws/lambda/", + "SystemLogLevel": "DEBUG" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_function_partial_advanced_logging_configuration_update[partial_config3]": { + "recorded-date": "17-04-2024, 14:17:17", + "recorded-content": { + "create_response": { + "CreateEventSourceMappingResponse": null, + "CreateFunctionResponse": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Pending", + "StateReason": "The function is being created.", + "StateReasonCode": "Creating", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + } + }, + "get_function_response": { + "Code": { + "Location": "", + "RepositoryType": "S3" + }, + "Configuration": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "function_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "updated_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "InProgress", + "LastUpdateStatusReason": "The function is being created.", + "LastUpdateStatusReasonCode": "Creating", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "cool_lambda" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "received_config": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": {} + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "handler.handler", + "LastModified": "date", + "LastUpdateStatus": "Successful", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "cool_lambda" + }, + "MemorySize": 256, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "python3.12", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Active", + "Timeout": 5, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/lambda_/test_lambda_api.validation.json b/tests/aws/services/lambda_/test_lambda_api.validation.json index e699d6a4c7bac..30b64705585d2 100644 --- a/tests/aws/services/lambda_/test_lambda_api.validation.json +++ b/tests/aws/services/lambda_/test_lambda_api.validation.json @@ -1,311 +1,353 @@ { "tests/aws/services/lambda_/test_lambda_api.py::TestCodeSigningConfig::test_code_signing_not_found_excs": { - "last_validated_date": "2023-11-20T16:01:02+00:00" + "last_validated_date": "2024-04-10T09:19:15+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestCodeSigningConfig::test_function_code_signing_config": { - "last_validated_date": "2023-11-20T16:00:57+00:00" + "last_validated_date": "2024-04-10T09:19:10+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAccountSettings::test_account_settings": { - "last_validated_date": "2023-11-20T16:01:02+00:00" + "last_validated_date": "2024-04-10T09:19:17+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAccountSettings::test_account_settings_total_code_size": { - "last_validated_date": "2023-11-20T16:01:20+00:00" + "last_validated_date": "2024-04-10T09:19:28+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAccountSettings::test_account_settings_total_code_size_config_update": { - "last_validated_date": "2023-11-20T16:01:26+00:00" + "last_validated_date": "2024-04-10T09:19:34+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAlias::test_alias_lifecycle": { - "last_validated_date": "2023-11-20T15:57:06+00:00" + "last_validated_date": "2024-04-10T09:12:27+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAlias::test_alias_naming": { - "last_validated_date": "2024-02-13T08:47:11+00:00" + "last_validated_date": "2024-04-10T09:12:45+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAlias::test_notfound_and_invalid_routingconfigs": { - "last_validated_date": "2023-11-20T15:57:21+00:00" + "last_validated_date": "2024-04-10T09:12:41+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventInvokeConfig::test_lambda_eventinvokeconfig_exceptions": { - "last_validated_date": "2023-11-20T15:58:16+00:00" + "last_validated_date": "2024-04-10T09:13:37+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventInvokeConfig::test_lambda_eventinvokeconfig_lifecycle": { - "last_validated_date": "2023-11-20T15:57:57+00:00" + "last_validated_date": "2024-04-10T09:13:20+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventSourceMappings::test_create_event_source_validation": { - "last_validated_date": "2023-11-20T16:01:56+00:00" + "last_validated_date": "2024-04-10T09:21:59+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventSourceMappings::test_event_source_mapping_exceptions": { - "last_validated_date": "2023-11-20T16:01:28+00:00" + "last_validated_date": "2024-04-10T09:19:37+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventSourceMappings::test_event_source_mapping_lifecycle": { - "last_validated_date": "2023-11-20T16:01:43+00:00" + "last_validated_date": "2024-04-10T09:19:50+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventSourceMappings::test_function_name_variations": { - "last_validated_date": "2023-12-15T09:58:47+00:00" + "last_validated_date": "2024-04-10T09:21:43+00:00" + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_advance_logging_configuration_format_switch": { + "last_validated_date": "2024-04-10T08:58:47+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_create_lambda_exceptions": { - "last_validated_date": "2024-03-12T15:42:35+00:00" + "last_validated_date": "2024-04-10T09:00:26+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_delete_on_nonexisting_version": { - "last_validated_date": "2023-11-20T15:46:21+00:00" + "last_validated_date": "2024-04-10T08:59:42+00:00" + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_function_advanced_configuration": { + "last_validated_date": "2024-03-28T09:54:41+00:00" + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_function_advanced_logging_configuration": { + "last_validated_date": "2024-04-10T08:59:14+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_function_arns": { - "last_validated_date": "2024-03-27T09:37:34+00:00" + "last_validated_date": "2024-04-10T09:00:24+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_function_lifecycle": { - "last_validated_date": "2023-11-20T15:46:00+00:00" + "last_validated_date": "2024-04-10T08:59:22+00:00" + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_function_partial_advanced_logging_configuration_update[partial_config0]": { + "last_validated_date": "2024-04-10T08:58:53+00:00" + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_function_partial_advanced_logging_configuration_update[partial_config1]": { + "last_validated_date": "2024-04-10T08:58:58+00:00" + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_function_partial_advanced_logging_configuration_update[partial_config2]": { + "last_validated_date": "2024-04-10T08:59:03+00:00" + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_function_partial_advanced_logging_configuration_update[partial_config3]": { + "last_validated_date": "2024-04-10T08:59:09+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[delete_function]": { - "last_validated_date": "2023-11-20T15:46:45+00:00" + "last_validated_date": "2024-04-10T09:00:05+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function]": { - "last_validated_date": "2023-11-20T15:46:26+00:00" + "last_validated_date": "2024-04-10T08:59:46+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function_code_signing_config]": { - "last_validated_date": "2023-11-20T15:46:35+00:00" + "last_validated_date": "2024-04-10T08:59:56+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function_concurrency]": { - "last_validated_date": "2023-11-20T15:46:41+00:00" + "last_validated_date": "2024-04-10T09:00:02+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function_configuration]": { - "last_validated_date": "2023-11-20T15:46:29+00:00" + "last_validated_date": "2024-04-10T08:59:50+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function_event_invoke_config]": { - "last_validated_date": "2023-11-20T15:46:38+00:00" + "last_validated_date": "2024-04-10T08:59:59+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function_url_config]": { - "last_validated_date": "2023-11-20T15:46:32+00:00" + "last_validated_date": "2024-04-10T08:59:53+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[invoke]": { - "last_validated_date": "2023-11-20T15:46:48+00:00" + "last_validated_date": "2024-04-10T09:00:08+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_invalid_invoke": { - "last_validated_date": "2024-03-28T08:45:03+00:00" + "last_validated_date": "2024-04-10T09:09:00+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_code_location_s3": { - "last_validated_date": "2023-11-20T15:46:59+00:00" + "last_validated_date": "2024-04-10T09:00:17+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_code_location_zipfile": { - "last_validated_date": "2023-11-20T15:46:52+00:00" + "last_validated_date": "2024-04-10T09:00:12+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_concurrent_code_updates": { - "last_validated_date": "2024-04-09T11:23:28+00:00" + "last_validated_date": "2024-04-10T09:09:04+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_concurrent_config_updates": { - "last_validated_date": "2024-04-08T13:14:54+00:00" + "last_validated_date": "2024-04-10T09:09:08+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_list_functions": { - "last_validated_date": "2023-11-20T15:47:16+00:00" + "last_validated_date": "2024-04-10T09:00:40+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[delete_function]": { - "last_validated_date": "2023-11-20T15:46:22+00:00" + "last_validated_date": "2024-04-10T08:59:43+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function]": { - "last_validated_date": "2023-11-20T15:46:22+00:00" + "last_validated_date": "2024-04-10T08:59:43+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function_code_signing_config]": { - "last_validated_date": "2023-11-20T15:46:23+00:00" + "last_validated_date": "2024-04-10T08:59:44+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function_concurrency]": { - "last_validated_date": "2023-11-20T15:46:23+00:00" + "last_validated_date": "2024-04-10T08:59:44+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function_configuration]": { - "last_validated_date": "2023-11-20T15:46:22+00:00" + "last_validated_date": "2024-04-10T08:59:43+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function_event_invoke_config]": { - "last_validated_date": "2023-11-20T15:46:23+00:00" + "last_validated_date": "2024-04-10T08:59:44+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function_url_config]": { - "last_validated_date": "2023-11-20T15:46:22+00:00" + "last_validated_date": "2024-04-10T08:59:44+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_version[get_function]": { - "last_validated_date": "2023-11-20T15:46:11+00:00" + "last_validated_date": "2024-04-10T08:59:32+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_version[get_function_configuration]": { - "last_validated_date": "2023-11-20T15:46:14+00:00" + "last_validated_date": "2024-04-10T08:59:35+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_version[get_function_event_invoke_config]": { - "last_validated_date": "2023-11-20T15:46:18+00:00" + "last_validated_date": "2024-04-10T08:59:38+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_with_arn_qualifier_mismatch[delete_function]": { - "last_validated_date": "2023-11-20T15:46:08+00:00" + "last_validated_date": "2024-04-10T08:59:30+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_with_arn_qualifier_mismatch[get_function]": { - "last_validated_date": "2023-11-20T15:46:08+00:00" + "last_validated_date": "2024-04-10T08:59:30+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_with_arn_qualifier_mismatch[get_function_configuration]": { - "last_validated_date": "2023-11-20T15:46:08+00:00" + "last_validated_date": "2024-04-10T08:59:30+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_redundant_updates": { - "last_validated_date": "2023-11-20T15:46:07+00:00" + "last_validated_date": "2024-04-10T08:59:28+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_update_lambda_exceptions": { - "last_validated_date": "2024-03-12T16:08:14+00:00" + "last_validated_date": "2024-04-10T09:00:29+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_vpc_config": { - "last_validated_date": "2023-11-20T16:50:21+00:00" + "last_validated_date": "2024-04-10T09:08:55+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaImages::test_lambda_image_and_image_config_crud": { - "last_validated_date": "2023-11-20T15:55:56+00:00" + "last_validated_date": "2024-04-10T09:11:13+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaImages::test_lambda_image_crud": { - "last_validated_date": "2023-11-20T15:55:07+00:00" + "last_validated_date": "2024-04-10T09:10:21+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaImages::test_lambda_image_versions": { - "last_validated_date": "2023-11-20T15:56:31+00:00" + "last_validated_date": "2024-04-10T09:11:50+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaImages::test_lambda_zip_file_to_image": { - "last_validated_date": "2023-11-20T15:55:22+00:00" + "last_validated_date": "2024-04-10T09:10:37+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_compatibilities[runtimes0]": { - "last_validated_date": "2024-03-21T08:24:38+00:00" + "last_validated_date": "2024-04-10T09:22:25+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_compatibilities[runtimes1]": { - "last_validated_date": "2024-03-21T08:24:43+00:00" + "last_validated_date": "2024-04-10T09:22:29+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_exceptions": { - "last_validated_date": "2024-03-12T15:42:16+00:00" + "last_validated_date": "2024-04-10T09:22:39+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_function_exceptions": { - "last_validated_date": "2023-11-20T16:03:22+00:00" + "last_validated_date": "2024-04-10T09:23:18+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_function_quota_exception": { - "last_validated_date": "2023-11-20T16:04:11+00:00" + "last_validated_date": "2024-04-10T09:23:58+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_lifecycle": { - "last_validated_date": "2023-11-20T16:04:38+00:00" + "last_validated_date": "2024-04-10T09:24:16+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_policy_exceptions": { - "last_validated_date": "2023-11-20T16:04:54+00:00" + "last_validated_date": "2024-04-10T09:24:29+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_policy_lifecycle": { - "last_validated_date": "2023-11-20T16:05:00+00:00" + "last_validated_date": "2024-04-10T09:24:34+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_s3_content": { - "last_validated_date": "2023-11-20T16:04:46+00:00" + "last_validated_date": "2024-04-10T09:24:22+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_add_lambda_permission_aws": { - "last_validated_date": "2023-11-20T15:58:43+00:00" + "last_validated_date": "2024-04-10T09:16:22+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_add_lambda_permission_fields": { - "last_validated_date": "2023-11-20T15:58:54+00:00" + "last_validated_date": "2024-04-10T09:16:33+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_create_multiple_lambda_permissions": { - "last_validated_date": "2023-11-20T15:59:01+00:00" + "last_validated_date": "2024-04-10T09:16:40+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_lambda_permission_fn_versioning": { - "last_validated_date": "2023-11-20T15:58:49+00:00" + "last_validated_date": "2024-04-10T09:16:28+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_permission_exceptions": { - "last_validated_date": "2023-11-20T15:58:39+00:00" + "last_validated_date": "2024-04-10T09:16:19+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_remove_multi_permissions": { - "last_validated_date": "2023-11-20T15:58:58+00:00" + "last_validated_date": "2024-04-10T09:16:37+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaProvisionedConcurrency::test_lambda_provisioned_lifecycle": { - "last_validated_date": "2023-11-20T17:06:43+00:00" + "last_validated_date": "2024-04-10T09:16:14+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaProvisionedConcurrency::test_provisioned_concurrency_exceptions": { - "last_validated_date": "2023-11-20T15:58:30+00:00" + "last_validated_date": "2024-04-10T09:13:56+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaProvisionedConcurrency::test_provisioned_concurrency_limits": { - "last_validated_date": "2023-11-20T15:58:33+00:00" + "last_validated_date": "2024-04-10T09:13:59+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaReservedConcurrency::test_function_concurrency": { - "last_validated_date": "2023-11-20T17:04:09+00:00" + "last_validated_date": "2024-04-10T09:13:50+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaReservedConcurrency::test_function_concurrency_exceptions": { - "last_validated_date": "2023-11-20T15:58:20+00:00" + "last_validated_date": "2024-04-10T09:13:43+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaReservedConcurrency::test_function_concurrency_limits": { - "last_validated_date": "2023-11-20T15:58:23+00:00" + "last_validated_date": "2024-04-10T09:13:46+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaRevisions::test_function_revisions_basic": { - "last_validated_date": "2023-11-20T15:57:28+00:00" + "last_validated_date": "2024-04-10T09:12:52+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaRevisions::test_function_revisions_permissions": { - "last_validated_date": "2023-11-20T15:57:37+00:00" + "last_validated_date": "2024-04-10T09:13:01+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaRevisions::test_function_revisions_version_and_alias": { - "last_validated_date": "2023-11-20T15:57:33+00:00" + "last_validated_date": "2024-04-10T09:12:57+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_lambda_envvars_near_limit_succeeds": { - "last_validated_date": "2023-11-20T16:00:52+00:00" + "last_validated_date": "2024-04-10T09:19:06+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_large_environment_fails_multiple_keys": { - "last_validated_date": "2023-11-20T16:00:49+00:00" + "last_validated_date": "2024-04-10T09:19:04+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_large_environment_variables_fails": { - "last_validated_date": "2023-11-20T16:00:32+00:00" + "last_validated_date": "2024-04-10T09:18:46+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_large_lambda": { - "last_validated_date": "2023-11-21T09:44:33+00:00" + "last_validated_date": "2024-04-10T09:18:27+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_oversized_request_create_lambda": { - "last_validated_date": "2023-11-21T09:41:36+00:00" + "last_validated_date": "2024-04-10T09:17:14+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_oversized_unzipped_lambda": { - "last_validated_date": "2023-11-21T09:43:40+00:00" + "last_validated_date": "2024-04-10T09:17:46+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_oversized_zipped_create_lambda": { - "last_validated_date": "2023-11-21T09:43:03+00:00" + "last_validated_date": "2024-04-10T09:17:26+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_exceptions": { - "last_validated_date": "2023-11-20T16:08:17+00:00" + "last_validated_date": "2024-04-10T09:30:32+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java11]": { - "last_validated_date": "2023-11-20T16:53:04+00:00" + "last_validated_date": "2024-04-10T09:26:28+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java17]": { - "last_validated_date": "2023-11-20T16:54:39+00:00" + "last_validated_date": "2024-04-10T09:28:29+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java21]": { - "last_validated_date": "2023-11-20T16:56:15+00:00" + "last_validated_date": "2024-04-10T09:30:24+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java11]": { "last_validated_date": "2023-11-20T16:08:13+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java17]": { - "last_validated_date": "2023-11-20T16:08:17+00:00" + "last_validated_date": "2024-04-10T09:30:31+00:00" + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java21]": { + "last_validated_date": "2024-04-10T09:30:28+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTag::test_create_tag_on_fn_create": { - "last_validated_date": "2023-11-20T15:57:41+00:00" + "last_validated_date": "2024-04-10T09:13:04+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTag::test_tag_lifecycle": { - "last_validated_date": "2023-11-20T15:57:46+00:00" + "last_validated_date": "2024-04-10T09:13:09+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTag::test_tag_nonexisting_resource": { - "last_validated_date": "2023-11-20T15:57:50+00:00" + "last_validated_date": "2024-04-10T09:13:13+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTags::test_tag_exceptions": { - "last_validated_date": "2023-11-20T16:02:01+00:00" + "last_validated_date": "2024-04-10T09:22:06+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTags::test_tag_lifecycle": { - "last_validated_date": "2023-11-20T16:02:14+00:00" + "last_validated_date": "2024-04-10T09:22:19+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTags::test_tag_limits": { - "last_validated_date": "2023-11-20T16:02:05+00:00" + "last_validated_date": "2024-04-10T09:22:10+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTags::test_tag_versions": { - "last_validated_date": "2023-11-20T16:02:09+00:00" + "last_validated_date": "2024-04-10T09:22:13+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaUrl::test_url_config_exceptions": { - "last_validated_date": "2023-11-20T15:59:12+00:00" + "last_validated_date": "2024-04-10T09:16:49+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaUrl::test_url_config_lifecycle": { - "last_validated_date": "2023-11-20T15:59:22+00:00" + "last_validated_date": "2024-04-10T09:16:59+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaUrl::test_url_config_list_paging": { - "last_validated_date": "2023-11-20T15:59:17+00:00" + "last_validated_date": "2024-04-10T09:16:55+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaVersions::test_publish_version_on_create": { - "last_validated_date": "2023-11-20T15:56:44+00:00" + "last_validated_date": "2024-04-10T09:12:04+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaVersions::test_publish_with_update": { - "last_validated_date": "2023-11-20T15:56:57+00:00" + "last_validated_date": "2024-04-10T09:12:17+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaVersions::test_publish_with_wrong_sha256": { - "last_validated_date": "2023-11-20T15:56:54+00:00" + "last_validated_date": "2024-04-10T09:12:14+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaVersions::test_version_lifecycle": { - "last_validated_date": "2023-11-20T15:56:51+00:00" + "last_validated_date": "2024-04-10T09:12:11+00:00" + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_advanced_logging_configuration_format_switch": { + "last_validated_date": "2024-04-17T14:16:55+00:00" + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_function_advanced_logging_configuration": { + "last_validated_date": "2024-04-19T08:20:18+00:00" + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_function_partial_advanced_logging_configuration_update[partial_config0]": { + "last_validated_date": "2024-04-17T14:17:00+00:00" + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_function_partial_advanced_logging_configuration_update[partial_config1]": { + "last_validated_date": "2024-04-17T14:17:05+00:00" + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_function_partial_advanced_logging_configuration_update[partial_config2]": { + "last_validated_date": "2024-04-17T14:17:11+00:00" + }, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_function_partial_advanced_logging_configuration_update[partial_config3]": { + "last_validated_date": "2024-04-17T14:17:16+00:00" } } From 76844307217403c9337277753943da67236c6eb5 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Mon, 22 Apr 2024 07:23:33 +0200 Subject: [PATCH 083/169] Update CODEOWNERS (#10702) --- CODEOWNERS | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 32201ad040a2e..ab2248f0ea357 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -105,10 +105,10 @@ /tests/aws/services/acm/ @alexrashed # apigateway -/localstack/aws/api/apigateway/ @bentsku -/localstack/services/apigateway/ @bentsku -/tests/aws/services/apigateway/ @bentsku -/tests/unit/test_apigateway.py @bentsku +/localstack/aws/api/apigateway/ @bentsku @cloutierMat +/localstack/services/apigateway/ @bentsku @cloutierMat +/tests/aws/services/apigateway/ @bentsku @cloutierMat +/tests/unit/test_apigateway.py @bentsku @cloutierMat # cloudformation /localstack/aws/api/cloudformation/ @dominikschubert @pinzon @simonrw @Morijarti From 2cd0ef6510ca8ef565ab652888f84f51eb1b8776 Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Mon, 22 Apr 2024 21:25:17 +0200 Subject: [PATCH 084/169] Add Ruby 3.3 Lambda runtime (#10704) --- localstack/services/lambda_/runtimes.py | 5 +- .../lambda_/test_lambda_api.snapshot.json | 5 +- .../lambda_/test_lambda_api.validation.json | 4 +- .../services/lambda_/test_lambda_common.py | 1 + .../lambda_/test_lambda_common.snapshot.json | 278 +++++++++++++++++- .../test_lambda_common.validation.json | 25 +- 6 files changed, 302 insertions(+), 16 deletions(-) diff --git a/localstack/services/lambda_/runtimes.py b/localstack/services/lambda_/runtimes.py index 5cd425cb509df..2e103e884c132 100644 --- a/localstack/services/lambda_/runtimes.py +++ b/localstack/services/lambda_/runtimes.py @@ -57,7 +57,7 @@ Runtime.dotnet6: "dotnet:6", Runtime.dotnetcore3_1: "dotnet:core3.1", # deprecated Apr 3, 2023 => Apr 3, 2023 => May 3, 2023 Runtime.go1_x: "go:1", # deprecated Jan 8, 2024 => Feb 8, 2024 => Mar 12, 2024 - # "ruby3.3": "ruby:3.3", expected April 2024 + Runtime.ruby3_3: "ruby:3.3", Runtime.ruby3_2: "ruby:3.2", Runtime.ruby2_7: "ruby:2.7", # deprecated Dec 7, 2023 => Jan 9, 2024 => Feb 8, 2024 Runtime.provided_al2023: "provided:al2023", @@ -81,7 +81,7 @@ SUPPORTED_RUNTIMES: list[Runtime] = list(set(IMAGE_MAPPING.keys()) - set(DEPRECATED_RUNTIMES)) # A temporary list of missing runtimes not yet supported in LocalStack. Used for modular updates. -MISSING_RUNTIMES = [Runtime.ruby3_3] +MISSING_RUNTIMES = [] # An unordered list of all Lambda runtimes supported by LocalStack. ALL_RUNTIMES: list[Runtime] = list(IMAGE_MAPPING.keys()) @@ -109,6 +109,7 @@ ], "ruby": [ Runtime.ruby3_2, + Runtime.ruby3_3, ], "dotnet": [ Runtime.dotnet6, diff --git a/tests/aws/services/lambda_/test_lambda_api.snapshot.json b/tests/aws/services/lambda_/test_lambda_api.snapshot.json index 477298392fb06..a859940cf64c7 100644 --- a/tests/aws/services/lambda_/test_lambda_api.snapshot.json +++ b/tests/aws/services/lambda_/test_lambda_api.snapshot.json @@ -13670,7 +13670,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_compatibilities[runtimes0]": { - "recorded-date": "10-04-2024, 09:22:25", + "recorded-date": "22-04-2024, 10:39:35", "recorded-content": { "publish_result": { "CompatibleArchitectures": [ @@ -13711,7 +13711,7 @@ } }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_compatibilities[runtimes1]": { - "recorded-date": "10-04-2024, 09:22:29", + "recorded-date": "22-04-2024, 10:39:39", "recorded-content": { "publish_result": { "CompatibleArchitectures": [ @@ -13725,6 +13725,7 @@ "dotnet6", "dotnetcore3.1", "go1.x", + "ruby3.3", "ruby3.2", "ruby2.7", "provided.al2023", diff --git a/tests/aws/services/lambda_/test_lambda_api.validation.json b/tests/aws/services/lambda_/test_lambda_api.validation.json index 30b64705585d2..c2667b0819dd5 100644 --- a/tests/aws/services/lambda_/test_lambda_api.validation.json +++ b/tests/aws/services/lambda_/test_lambda_api.validation.json @@ -177,10 +177,10 @@ "last_validated_date": "2024-04-10T09:10:37+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_compatibilities[runtimes0]": { - "last_validated_date": "2024-04-10T09:22:25+00:00" + "last_validated_date": "2024-04-22T10:39:35+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_compatibilities[runtimes1]": { - "last_validated_date": "2024-04-10T09:22:29+00:00" + "last_validated_date": "2024-04-22T10:39:39+00:00" }, "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_exceptions": { "last_validated_date": "2024-04-10T09:22:39+00:00" diff --git a/tests/aws/services/lambda_/test_lambda_common.py b/tests/aws/services/lambda_/test_lambda_common.py index 900a926091f0a..e3a72f835aa9b 100644 --- a/tests/aws/services/lambda_/test_lambda_common.py +++ b/tests/aws/services/lambda_/test_lambda_common.py @@ -255,6 +255,7 @@ def test_runtime_wrapper_invoke(self, multiruntime_lambda, snapshot, tmp_path, a assert invocation_result_payload["environment"]["WRAPPER_VAR"] == test_value +@markers.lambda_runtime_update class TestLambdaCallingLocalstack: """=> Keep these tests synchronized with `test_lambda_endpoint_injection.py` in ext!""" diff --git a/tests/aws/services/lambda_/test_lambda_common.snapshot.json b/tests/aws/services/lambda_/test_lambda_common.snapshot.json index 3c785bfebb803..bc5e0a00d48f0 100644 --- a/tests/aws/services/lambda_/test_lambda_common.snapshot.json +++ b/tests/aws/services/lambda_/test_lambda_common.snapshot.json @@ -16,7 +16,7 @@ "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[ruby3.2]": { - "recorded-date": "20-03-2024, 21:05:54", + "recorded-date": "22-04-2024, 10:10:53", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.11]": { @@ -616,7 +616,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[ruby3.2]": { - "recorded-date": "20-03-2024, 21:07:08", + "recorded-date": "22-04-2024, 10:11:01", "recorded-content": { "create_function_result": { "Architectures": [ @@ -2543,7 +2543,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[ruby3.2]": { - "recorded-date": "20-03-2024, 21:08:50", + "recorded-date": "22-04-2024, 10:11:07", "recorded-content": { "create_function_result": { "Architectures": [ @@ -3449,7 +3449,7 @@ } }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[ruby3.2]": { - "recorded-date": "20-03-2024, 21:10:02", + "recorded-date": "22-04-2024, 10:11:12", "recorded-content": { "create_function_result": { "Architectures": [ @@ -4064,7 +4064,7 @@ "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[ruby3.2]": { - "recorded-date": "20-03-2024, 21:11:03", + "recorded-date": "22-04-2024, 10:11:19", "recorded-content": {} }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[dotnet8]": { @@ -4356,5 +4356,273 @@ "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[dotnet8]": { "recorded-date": "20-03-2024, 21:12:38", "recorded-content": {} + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[ruby3.3]": { + "recorded-date": "22-04-2024, 10:10:58", + "recorded-content": {} + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[ruby3.3]": { + "recorded-date": "22-04-2024, 10:11:04", + "recorded-content": { + "create_function_result": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": { + "TEST_KEY": "TEST_VAL" + } + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "function.handler", + "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 1024, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "ruby3.3", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Pending", + "StateReason": "The function is being created.", + "StateReasonCode": "Creating", + "Timeout": 3, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + }, + "invocation_result_payload": { + "ctx": { + "aws_request_id": "", + "function_name": "", + "function_version": "$LATEST", + "invoked_function_arn": "arn:aws:lambda::111111111111:function:", + "log_group_name": "/aws/lambda/", + "log_stream_name": "", + "memory_limit_in_mb": "1024", + "remaining_time_in_millis": "" + }, + "environment": { + "AWS_ACCESS_KEY_ID": "", + "AWS_DEFAULT_REGION": "", + "AWS_EXECUTION_ENV": "AWS_Lambda_ruby3.3", + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", + "AWS_LAMBDA_FUNCTION_NAME": "", + "AWS_LAMBDA_FUNCTION_VERSION": "$LATEST", + "AWS_LAMBDA_INITIALIZATION_TYPE": "on-demand", + "AWS_LAMBDA_LOG_GROUP_NAME": "/aws/lambda/", + "AWS_LAMBDA_LOG_STREAM_NAME": "", + "AWS_LAMBDA_RUNTIME_API": "127.0.0.1:9001", + "AWS_REGION": "", + "AWS_SECRET_ACCESS_KEY": "", + "AWS_SESSION_TOKEN": "", + "AWS_XRAY_CONTEXT_MISSING": "LOG_ERROR", + "AWS_XRAY_DAEMON_ADDRESS": "169.254.79.129:2000", + "GEM_HOME": "/var/runtime", + "GEM_PATH": "/var/task/vendor/bundle/ruby/3.3.0:/opt/ruby/gems/3.3.0:/var/runtime:/var/runtime/ruby/3.3.0", + "LAMBDA_RUNTIME_DIR": "/var/runtime", + "LAMBDA_TASK_ROOT": "/var/task", + "LANG": "en_US.UTF-8", + "LD_LIBRARY_PATH": "/var/lang/lib:/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib", + "PATH": "/var/lang/bin:/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin", + "PWD": "/var/task", + "RUBYLIB": "/var/runtime/gems/aws_lambda_ric-3.0.0/lib:/var/task:/var/runtime/lib:/opt/ruby/lib", + "SHLVL": "0", + "TEST_KEY": "TEST_VAL", + "TZ": ":UTC", + "_AWS_XRAY_DAEMON_ADDRESS": "169.254.79.129", + "_AWS_XRAY_DAEMON_PORT": "2000", + "_HANDLER": "function.handler", + "_X_AMZN_TRACE_ID": "" + }, + "packages": [] + }, + "invocation_result_payload_qualified": { + "ctx": { + "aws_request_id": "", + "function_name": "", + "function_version": "$LATEST", + "invoked_function_arn": "arn:aws:lambda::111111111111:function::$LATEST", + "log_group_name": "/aws/lambda/", + "log_stream_name": "", + "memory_limit_in_mb": "1024", + "remaining_time_in_millis": "" + }, + "environment": { + "AWS_ACCESS_KEY_ID": "", + "AWS_DEFAULT_REGION": "", + "AWS_EXECUTION_ENV": "AWS_Lambda_ruby3.3", + "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", + "AWS_LAMBDA_FUNCTION_NAME": "", + "AWS_LAMBDA_FUNCTION_VERSION": "$LATEST", + "AWS_LAMBDA_INITIALIZATION_TYPE": "on-demand", + "AWS_LAMBDA_LOG_GROUP_NAME": "/aws/lambda/", + "AWS_LAMBDA_LOG_STREAM_NAME": "", + "AWS_LAMBDA_RUNTIME_API": "127.0.0.1:9001", + "AWS_REGION": "", + "AWS_SECRET_ACCESS_KEY": "", + "AWS_SESSION_TOKEN": "", + "AWS_XRAY_CONTEXT_MISSING": "LOG_ERROR", + "AWS_XRAY_DAEMON_ADDRESS": "169.254.79.129:2000", + "GEM_HOME": "/var/runtime", + "GEM_PATH": "/var/task/vendor/bundle/ruby/3.3.0:/opt/ruby/gems/3.3.0:/var/runtime:/var/runtime/ruby/3.3.0", + "LAMBDA_RUNTIME_DIR": "/var/runtime", + "LAMBDA_TASK_ROOT": "/var/task", + "LANG": "en_US.UTF-8", + "LD_LIBRARY_PATH": "/var/lang/lib:/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib", + "PATH": "/var/lang/bin:/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin", + "PWD": "/var/task", + "RUBYLIB": "/var/runtime/gems/aws_lambda_ric-3.0.0/lib:/var/task:/var/runtime/lib:/opt/ruby/lib", + "SHLVL": "0", + "TEST_KEY": "TEST_VAL", + "TZ": ":UTC", + "_AWS_XRAY_DAEMON_ADDRESS": "169.254.79.129", + "_AWS_XRAY_DAEMON_PORT": "2000", + "_HANDLER": "function.handler", + "_X_AMZN_TRACE_ID": "" + }, + "packages": [] + } + } + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[ruby3.3]": { + "recorded-date": "22-04-2024, 10:11:09", + "recorded-content": { + "create_function_result": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "function.handler", + "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 1024, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "ruby3.3", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Pending", + "StateReason": "The function is being created.", + "StateReasonCode": "Creating", + "Timeout": 3, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + }, + "error_result": { + "ExecutedVersion": "$LATEST", + "FunctionError": "Unhandled", + "Payload": { + "errorMessage": "Error: some_error_msg", + "errorType": "Function", + "stackTrace": "" + }, + "StatusCode": 200, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[ruby3.3]": { + "recorded-date": "22-04-2024, 10:11:15", + "recorded-content": { + "create_function_result": { + "Architectures": [ + "x86_64" + ], + "CodeSha256": "", + "CodeSize": "", + "Description": "", + "Environment": { + "Variables": { + "AWS_LAMBDA_EXEC_WRAPPER": "/var/task/environment_wrapper" + } + }, + "EphemeralStorage": { + "Size": 512 + }, + "FunctionArn": "arn:aws:lambda::111111111111:function:", + "FunctionName": "", + "Handler": "function.handler", + "LastModified": "date", + "LoggingConfig": { + "LogFormat": "Text", + "LogGroup": "/aws/lambda/" + }, + "MemorySize": 1024, + "PackageType": "Zip", + "RevisionId": "", + "Role": "arn:aws:iam::111111111111:role/", + "Runtime": "ruby3.3", + "RuntimeVersionConfig": { + "RuntimeVersionArn": "arn:aws:lambda:::runtime:" + }, + "SnapStart": { + "ApplyOn": "None", + "OptimizationStatus": "Off" + }, + "State": "Pending", + "StateReason": "The function is being created.", + "StateReasonCode": "Creating", + "Timeout": 3, + "TracingConfig": { + "Mode": "PassThrough" + }, + "Version": "$LATEST", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 201 + } + } + } + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[ruby3.3]": { + "recorded-date": "22-04-2024, 10:11:23", + "recorded-content": {} } } diff --git a/tests/aws/services/lambda_/test_lambda_common.validation.json b/tests/aws/services/lambda_/test_lambda_common.validation.json index 10e001f5380d2..37dbda094bfad 100644 --- a/tests/aws/services/lambda_/test_lambda_common.validation.json +++ b/tests/aws/services/lambda_/test_lambda_common.validation.json @@ -42,7 +42,10 @@ "last_validated_date": "2024-03-20T21:26:46+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[ruby3.2]": { - "last_validated_date": "2024-03-20T21:26:43+00:00" + "last_validated_date": "2024-04-22T10:11:18+00:00" + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[ruby3.3]": { + "last_validated_date": "2024-04-22T10:11:22+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[dotnet6]": { "last_validated_date": "2024-03-20T21:20:34+00:00" @@ -93,7 +96,10 @@ "last_validated_date": "2024-03-20T21:19:50+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[ruby3.2]": { - "last_validated_date": "2024-03-20T21:20:26+00:00" + "last_validated_date": "2024-04-22T10:10:53+00:00" + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[ruby3.3]": { + "last_validated_date": "2024-04-22T10:10:58+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[dotnet6]": { "last_validated_date": "2024-03-20T21:21:47+00:00" @@ -144,7 +150,10 @@ "last_validated_date": "2024-03-20T21:21:16+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[ruby3.2]": { - "last_validated_date": "2024-03-20T21:21:40+00:00" + "last_validated_date": "2024-04-22T10:11:01+00:00" + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[ruby3.3]": { + "last_validated_date": "2024-04-22T10:11:04+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[dotnet6]": { "last_validated_date": "2024-03-20T21:24:38+00:00" @@ -189,7 +198,10 @@ "last_validated_date": "2024-03-20T21:24:52+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[ruby3.2]": { - "last_validated_date": "2024-03-20T21:24:49+00:00" + "last_validated_date": "2024-04-22T10:11:12+00:00" + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[ruby3.3]": { + "last_validated_date": "2024-04-22T10:11:14+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[dotnet6]": { "last_validated_date": "2024-03-20T21:23:28+00:00" @@ -240,6 +252,9 @@ "last_validated_date": "2024-03-20T21:23:00+00:00" }, "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[ruby3.2]": { - "last_validated_date": "2024-03-20T21:23:22+00:00" + "last_validated_date": "2024-04-22T10:11:06+00:00" + }, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[ruby3.3]": { + "last_validated_date": "2024-04-22T10:11:09+00:00" } } From c8602852f6381c25fb1c28e037e62d3831ec747d Mon Sep 17 00:00:00 2001 From: Aida Syoko <167442392+alongdate@users.noreply.github.com> Date: Tue, 23 Apr 2024 05:10:28 +0800 Subject: [PATCH 085/169] chore: fix some typos in comments (#10693) --- localstack/services/apigateway/helpers.py | 2 +- .../services/lambda_/invocation/docker_runtime_executor.py | 2 +- .../services/lambda_/invocation/execution_environment.py | 2 +- .../route53/resource_providers/aws_route53_recordset.py | 2 +- localstack/services/sns/provider.py | 4 ++-- .../services/stepfunctions/asl/component/state/state.py | 2 +- tests/unit/aws/protocol/test_serializer.py | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/localstack/services/apigateway/helpers.py b/localstack/services/apigateway/helpers.py index 3cadfec89870a..ad6bba5a233b6 100644 --- a/localstack/services/apigateway/helpers.py +++ b/localstack/services/apigateway/helpers.py @@ -1062,7 +1062,7 @@ def add_path_methods(rel_path: str, parts: List[str], parent_id=""): ] or not isinstance(field_schema, dict): LOG.warning("Ignoring unsupported field %s in path %s", field, rel_path) # TODO: check if we should skip parameters, those are global parameters applied to every routes but - # can be overriden at the operation level + # can be overridden at the operation level continue method_name = field.upper() diff --git a/localstack/services/lambda_/invocation/docker_runtime_executor.py b/localstack/services/lambda_/invocation/docker_runtime_executor.py index a0d8dc41b397d..715d8975bb11f 100644 --- a/localstack/services/lambda_/invocation/docker_runtime_executor.py +++ b/localstack/services/lambda_/invocation/docker_runtime_executor.py @@ -330,7 +330,7 @@ def start(self, env_vars: dict[str, str]) -> None: if config.LAMBDA_DOCKER_DNS: # Don't overwrite DNS container config if it is already set (e.g., using LAMBDA_DOCKER_DNS) LOG.warning( - "Container DNS overriden to %s, connection to names pointing to LocalStack, like 'localhost.localstack.cloud' will need additional configuration.", + "Container DNS overridden to %s, connection to names pointing to LocalStack, like 'localhost.localstack.cloud' will need additional configuration.", config.LAMBDA_DOCKER_DNS, ) container_config.dns = config.LAMBDA_DOCKER_DNS diff --git a/localstack/services/lambda_/invocation/execution_environment.py b/localstack/services/lambda_/invocation/execution_environment.py index 4c750aab806d0..a10d4ada2b00d 100644 --- a/localstack/services/lambda_/invocation/execution_environment.py +++ b/localstack/services/lambda_/invocation/execution_environment.py @@ -157,7 +157,7 @@ def get_environment_variables(self) -> Dict[str, str]: # config.handler is None for image lambdas and will be populated at runtime (e.g., by RIE) if self.function_version.config.handler: env_vars["_HANDLER"] = self.function_version.config.handler - # Will be overriden by the runtime itself unless it is a provided runtime + # Will be overridden by the runtime itself unless it is a provided runtime if self.function_version.config.runtime: env_vars["AWS_EXECUTION_ENV"] = "AWS_Lambda_rapid" if self.function_version.config.environment: diff --git a/localstack/services/route53/resource_providers/aws_route53_recordset.py b/localstack/services/route53/resource_providers/aws_route53_recordset.py index 91d48207756e3..c3d0e3866e14c 100644 --- a/localstack/services/route53/resource_providers/aws_route53_recordset.py +++ b/localstack/services/route53/resource_providers/aws_route53_recordset.py @@ -130,7 +130,7 @@ def create( ] }, ) - # TODO: not 100% sure this behaves the same betwen alias and non-alias records + # TODO: not 100% sure this behaves the same between alias and non-alias records model["Id"] = model["Name"] return ProgressEvent( diff --git a/localstack/services/sns/provider.py b/localstack/services/sns/provider.py index fc8a8c00ff051..c5ff62fa7b6a9 100644 --- a/localstack/services/sns/provider.py +++ b/localstack/services/sns/provider.py @@ -673,8 +673,8 @@ def subscribe( SubscriptionArn=subscription_arn, PendingConfirmation="true", Owner=context.account_id, - RawMessageDelivery="false", # default value, will be overriden if set - FilterPolicyScope="MessageAttributes", # default value, will be overriden if set + RawMessageDelivery="false", # default value, will be overridden if set + FilterPolicyScope="MessageAttributes", # default value, will be overridden if set SubscriptionPrincipal=principal, # dummy value, could be fetched with a call to STS? ) if attributes: diff --git a/localstack/services/stepfunctions/asl/component/state/state.py b/localstack/services/stepfunctions/asl/component/state/state.py index f89930fa93fa8..545052336d412 100644 --- a/localstack/services/stepfunctions/asl/component/state/state.py +++ b/localstack/services/stepfunctions/asl/component/state/state.py @@ -88,7 +88,7 @@ def from_state_props(self, state_props: StateProps) -> None: def _set_next(self, env: Environment) -> None: if env.next_state_name != self.name: - # Next was already overriden. + # Next was already overridden. return if isinstance(self.continue_with, ContinueWithNext): diff --git a/tests/unit/aws/protocol/test_serializer.py b/tests/unit/aws/protocol/test_serializer.py index 1fec106758959..1fefee1f67780 100644 --- a/tests/unit/aws/protocol/test_serializer.py +++ b/tests/unit/aws/protocol/test_serializer.py @@ -1496,7 +1496,7 @@ def event_generator() -> Iterator: def test_all_non_existing_key(): - """Tests the different protocols to allow non-existing keys in strucutres / dicts.""" + """Tests the different protocols to allow non-existing keys in structures / dicts.""" # query _botocore_serializer_integration_test( "cloudformation", From 720c4d7a8232b1a451faa1d084172d0f3cfe6516 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Tue, 23 Apr 2024 08:31:34 +0200 Subject: [PATCH 086/169] Upgrade pinned Python dependencies (#10708) --- .pre-commit-config.yaml | 2 +- requirements-base-runtime.txt | 30 +++++---------------- requirements-basic.txt | 50 +++-------------------------------- requirements-dev.txt | 20 +++++++------- requirements-runtime.txt | 10 +++---- requirements-test.txt | 14 +++++----- requirements-typehint.txt | 36 ++++++++++++------------- 7 files changed, 51 insertions(+), 111 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1a3437cac63dc..3e865ae550dd3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.3.7 + rev: v0.4.1 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index fd840bc64d927..5a0c1240b2e8c 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -15,14 +15,11 @@ blinker==1.7.0 # flask # quart boto3==1.34.84 - # via - # localstack-core (pyproject.toml) - # moto-ext + # via localstack-core (pyproject.toml) botocore==1.34.84 # via # boto3 # localstack-core (pyproject.toml) - # moto-ext # s3transfer build==1.2.1 # via localstack-core (pyproject.toml) @@ -46,7 +43,6 @@ constantly==23.10.4 cryptography==42.0.5 # via # localstack-core (pyproject.toml) - # moto-ext # pyopenssl dill==0.3.6 # via localstack-core (pyproject.toml) @@ -85,14 +81,13 @@ idna==3.7 # requests incremental==22.10.0 # via localstack-twisted -itsdangerous==2.1.2 +itsdangerous==2.2.0 # via # flask # quart jinja2==3.1.3 # via # flask - # moto-ext # quart jmespath==1.0.1 # via @@ -113,7 +108,6 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext==5.0.5.post1 packaging==24.0 # via # build @@ -139,15 +133,11 @@ pyopenssl==24.1.0 pyproject-hooks==1.0.0 # via build python-dateutil==2.9.0.post0 - # via - # botocore - # moto-ext + # via botocore python-dotenv==1.0.1 # via localstack-core (pyproject.toml) pyyaml==6.0.1 - # via - # localstack-core (pyproject.toml) - # responses + # via localstack-core (pyproject.toml) quart==0.19.5 # via localstack-core (pyproject.toml) readerwriterlock==1.0.9 @@ -156,17 +146,13 @@ requests==2.31.0 # via # docker # localstack-core (pyproject.toml) - # moto-ext # requests-aws4auth - # responses # rolo requests-aws4auth==1.2.3 # via localstack-core (pyproject.toml) -responses==0.25.0 - # via moto-ext rich==13.7.1 # via localstack-core (pyproject.toml) -rolo==0.4.0 +rolo==0.5.0 # via localstack-core (pyproject.toml) s3transfer==0.10.1 # via boto3 @@ -192,22 +178,18 @@ urllib3==2.2.1 # docker # localstack-core (pyproject.toml) # requests - # responses websocket-client==1.7.0 # via docker werkzeug==3.0.2 # via # flask # localstack-core (pyproject.toml) - # moto-ext # quart # rolo wsproto==1.2.0 # via hypercorn xmltodict==0.13.0 - # via - # localstack-core (pyproject.toml) - # moto-ext + # via localstack-core (pyproject.toml) zope-interface==6.3 # via localstack-twisted diff --git a/requirements-basic.txt b/requirements-basic.txt index 8f130af859c26..151aae1bc6cac 100644 --- a/requirements-basic.txt +++ b/requirements-basic.txt @@ -4,13 +4,6 @@ # # pip-compile --output-file=requirements-basic.txt --strip-extras --unsafe-package=distribute --unsafe-package=localstack-core --unsafe-package=pip --unsafe-package=setuptools pyproject.toml # -boto3==1.34.84 - # via moto-ext -botocore==1.34.84 - # via - # boto3 - # moto-ext - # s3transfer build==1.2.1 # via localstack-core (pyproject.toml) cachetools==5.3.3 @@ -24,9 +17,7 @@ charset-normalizer==3.3.2 click==8.1.7 # via localstack-core (pyproject.toml) cryptography==42.0.5 - # via - # localstack-core (pyproject.toml) - # moto-ext + # via localstack-core (pyproject.toml) dill==0.3.6 # via localstack-core (pyproject.toml) dnslib==0.9.24 @@ -35,21 +26,10 @@ dnspython==2.6.1 # via localstack-core (pyproject.toml) idna==3.7 # via requests -jinja2==3.1.3 - # via moto-ext -jmespath==1.0.1 - # via - # boto3 - # botocore markdown-it-py==3.0.0 # via rich -markupsafe==2.1.5 - # via - # jinja2 - # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext==5.0.5.post1 packaging==24.0 # via build pbr==6.0.0 @@ -64,31 +44,16 @@ pygments==2.17.2 # via rich pyproject-hooks==1.0.0 # via build -python-dateutil==2.9.0.post0 - # via - # botocore - # moto-ext python-dotenv==1.0.1 # via localstack-core (pyproject.toml) pyyaml==6.0.1 - # via - # localstack-core (pyproject.toml) - # responses + # via localstack-core (pyproject.toml) requests==2.31.0 - # via - # localstack-core (pyproject.toml) - # moto-ext - # responses -responses==0.25.0 - # via moto-ext + # via localstack-core (pyproject.toml) rich==13.7.1 # via localstack-core (pyproject.toml) -s3transfer==0.10.1 - # via boto3 semver==3.0.2 # via localstack-core (pyproject.toml) -six==1.16.0 - # via python-dateutil stevedore==5.2.0 # via # localstack-core (pyproject.toml) @@ -96,11 +61,4 @@ stevedore==5.2.0 tailer==0.4.1 # via localstack-core (pyproject.toml) urllib3==2.2.1 - # via - # botocore - # requests - # responses -werkzeug==3.0.2 - # via moto-ext -xmltodict==0.13.0 - # via moto-ext + # via requests diff --git a/requirements-dev.txt b/requirements-dev.txt index 10b47f7ac93f2..6578f3bc041c5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -18,7 +18,7 @@ antlr4-python3-runtime==4.13.1 # moto-ext anyio==4.3.0 # via httpx -apispec==6.6.0 +apispec==6.6.1 # via localstack-core argparse==1.4.0 # via amazon-kclpy @@ -37,7 +37,7 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.137.0 +aws-cdk-lib==2.138.0 # via localstack-core aws-sam-translator==1.87.0 # via @@ -92,7 +92,7 @@ cffi==1.16.0 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==0.86.3 +cfn-lint==0.86.4 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -186,7 +186,7 @@ hyperframe==6.0.1 # via h2 hyperlink==21.0.0 # via localstack-twisted -identify==2.5.35 +identify==2.5.36 # via pre-commit idna==3.7 # via @@ -201,7 +201,7 @@ incremental==22.10.0 # via localstack-twisted iniconfig==2.0.0 # via pytest -itsdangerous==2.1.2 +itsdangerous==2.2.0 # via # flask # quart @@ -314,7 +314,7 @@ pbr==6.0.0 # stevedore platformdirs==4.2.0 # via virtualenv -pluggy==1.4.0 +pluggy==1.5.0 # via # localstack-core # pytest @@ -414,7 +414,7 @@ referencing==0.31.1 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2023.12.25 +regex==2024.4.16 # via cfn-lint requests==2.31.0 # via @@ -439,7 +439,7 @@ rich==13.7.1 # via # localstack-core # localstack-core (pyproject.toml) -rolo==0.4.0 +rolo==0.5.0 # via localstack-core rpds-py==0.18.0 # via @@ -449,7 +449,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core (pyproject.toml) -ruff==0.3.7 +ruff==0.4.1 # via localstack-core (pyproject.toml) s3transfer==0.10.1 # via @@ -509,7 +509,7 @@ urllib3==2.2.1 # opensearch-py # requests # responses -virtualenv==20.25.1 +virtualenv==20.25.3 # via pre-commit websocket-client==1.7.0 # via diff --git a/requirements-runtime.txt b/requirements-runtime.txt index ad292ff736477..5641155033bd8 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -16,7 +16,7 @@ antlr4-python3-runtime==4.13.1 # via # localstack-core (pyproject.toml) # moto-ext -apispec==6.6.0 +apispec==6.6.1 # via localstack-core (pyproject.toml) argparse==1.4.0 # via amazon-kclpy @@ -73,7 +73,7 @@ certifi==2024.2.2 # requests cffi==1.16.0 # via cryptography -cfn-lint==0.86.3 +cfn-lint==0.86.4 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -148,7 +148,7 @@ idna==3.7 # requests incremental==22.10.0 # via localstack-twisted -itsdangerous==2.1.2 +itsdangerous==2.2.0 # via # flask # quart @@ -305,7 +305,7 @@ referencing==0.31.1 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2023.12.25 +regex==2024.4.16 # via cfn-lint requests==2.31.0 # via @@ -328,7 +328,7 @@ rich==13.7.1 # via # localstack-core # localstack-core (pyproject.toml) -rolo==0.4.0 +rolo==0.5.0 # via localstack-core rpds-py==0.18.0 # via diff --git a/requirements-test.txt b/requirements-test.txt index 216f47725eb4f..8ff9b61fc02ee 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -18,7 +18,7 @@ antlr4-python3-runtime==4.13.1 # moto-ext anyio==4.3.0 # via httpx -apispec==6.6.0 +apispec==6.6.1 # via localstack-core argparse==1.4.0 # via amazon-kclpy @@ -37,7 +37,7 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.137.0 +aws-cdk-lib==2.138.0 # via localstack-core (pyproject.toml) aws-sam-translator==1.87.0 # via @@ -90,7 +90,7 @@ certifi==2024.2.2 # requests cffi==1.16.0 # via cryptography -cfn-lint==0.86.3 +cfn-lint==0.86.4 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -185,7 +185,7 @@ incremental==22.10.0 # via localstack-twisted iniconfig==2.0.0 # via pytest -itsdangerous==2.1.2 +itsdangerous==2.2.0 # via # flask # quart @@ -290,7 +290,7 @@ pbr==6.0.0 # jschema-to-python # sarif-om # stevedore -pluggy==1.4.0 +pluggy==1.5.0 # via # localstack-core (pyproject.toml) # pytest @@ -382,7 +382,7 @@ referencing==0.31.1 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2023.12.25 +regex==2024.4.16 # via cfn-lint requests==2.31.0 # via @@ -406,7 +406,7 @@ rich==13.7.1 # via # localstack-core # localstack-core (pyproject.toml) -rolo==0.4.0 +rolo==0.5.0 # via localstack-core rpds-py==0.18.0 # via diff --git a/requirements-typehint.txt b/requirements-typehint.txt index eb138acd441ea..2b047a726a1cb 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -18,7 +18,7 @@ antlr4-python3-runtime==4.13.1 # moto-ext anyio==4.3.0 # via httpx -apispec==6.6.0 +apispec==6.6.1 # via localstack-core argparse==1.4.0 # via amazon-kclpy @@ -37,7 +37,7 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.137.0 +aws-cdk-lib==2.138.0 # via localstack-core aws-sam-translator==1.87.0 # via @@ -71,7 +71,7 @@ botocore==1.34.84 # localstack-snapshot # moto-ext # s3transfer -botocore-stubs==1.34.84 +botocore-stubs==1.34.89 # via boto3-stubs build==1.2.1 # via @@ -96,7 +96,7 @@ cffi==1.16.0 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==0.86.3 +cfn-lint==0.86.4 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -190,7 +190,7 @@ hyperframe==6.0.1 # via h2 hyperlink==21.0.0 # via localstack-twisted -identify==2.5.35 +identify==2.5.36 # via pre-commit idna==3.7 # via @@ -205,7 +205,7 @@ incremental==22.10.0 # via localstack-twisted iniconfig==2.0.0 # via pytest -itsdangerous==2.1.2 +itsdangerous==2.2.0 # via # flask # quart @@ -337,7 +337,7 @@ mypy-boto3-dynamodb==1.34.67 # via boto3-stubs mypy-boto3-dynamodbstreams==1.34.0 # via boto3-stubs -mypy-boto3-ec2==1.34.78 +mypy-boto3-ec2==1.34.86 # via boto3-stubs mypy-boto3-ecr==1.34.0 # via boto3-stubs @@ -355,7 +355,7 @@ mypy-boto3-elbv2==1.34.63 # via boto3-stubs mypy-boto3-emr==1.34.75 # via boto3-stubs -mypy-boto3-emr-serverless==1.34.0 +mypy-boto3-emr-serverless==1.34.87 # via boto3-stubs mypy-boto3-es==1.34.36 # via boto3-stubs @@ -367,7 +367,7 @@ mypy-boto3-fis==1.34.63 # via boto3-stubs mypy-boto3-glacier==1.34.0 # via boto3-stubs -mypy-boto3-glue==1.34.84 +mypy-boto3-glue==1.34.88 # via boto3-stubs mypy-boto3-iam==1.34.83 # via boto3-stubs @@ -379,7 +379,7 @@ mypy-boto3-iot-data==1.34.0 # via boto3-stubs mypy-boto3-iotanalytics==1.34.0 # via boto3-stubs -mypy-boto3-iotwireless==1.34.74 +mypy-boto3-iotwireless==1.34.85 # via boto3-stubs mypy-boto3-kafka==1.34.61 # via boto3-stubs @@ -391,7 +391,7 @@ mypy-boto3-kinesisanalyticsv2==1.34.64 # via boto3-stubs mypy-boto3-kms==1.34.84 # via boto3-stubs -mypy-boto3-lakeformation==1.34.7 +mypy-boto3-lakeformation==1.34.85 # via boto3-stubs mypy-boto3-lambda==1.34.77 # via boto3-stubs @@ -441,7 +441,7 @@ mypy-boto3-s3==1.34.65 # via boto3-stubs mypy-boto3-s3control==1.34.83 # via boto3-stubs -mypy-boto3-sagemaker==1.34.74 +mypy-boto3-sagemaker==1.34.89 # via boto3-stubs mypy-boto3-sagemaker-runtime==1.34.0 # via boto3-stubs @@ -449,7 +449,7 @@ mypy-boto3-secretsmanager==1.34.72 # via boto3-stubs mypy-boto3-serverlessrepo==1.34.0 # via boto3-stubs -mypy-boto3-servicediscovery==1.34.0 +mypy-boto3-servicediscovery==1.34.89 # via boto3-stubs mypy-boto3-ses==1.34.0 # via boto3-stubs @@ -510,7 +510,7 @@ pbr==6.0.0 # stevedore platformdirs==4.2.0 # via virtualenv -pluggy==1.4.0 +pluggy==1.5.0 # via # localstack-core # pytest @@ -610,7 +610,7 @@ referencing==0.31.1 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2023.12.25 +regex==2024.4.16 # via cfn-lint requests==2.31.0 # via @@ -635,7 +635,7 @@ rich==13.7.1 # via # localstack-core # localstack-core (pyproject.toml) -rolo==0.4.0 +rolo==0.5.0 # via localstack-core rpds-py==0.18.0 # via @@ -645,7 +645,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core -ruff==0.3.7 +ruff==0.4.1 # via localstack-core s3transfer==0.10.1 # via @@ -806,7 +806,7 @@ urllib3==2.2.1 # opensearch-py # requests # responses -virtualenv==20.25.1 +virtualenv==20.25.3 # via pre-commit websocket-client==1.7.0 # via From 536cc28da451d85378977e8d7f4223116ffef901 Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Tue, 23 Apr 2024 09:33:18 +0200 Subject: [PATCH 087/169] Bump Lambda runtime versions and increase sleep time (#10698) --- .../lambda_/test_lambda_developer_tools.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/tests/aws/services/lambda_/test_lambda_developer_tools.py b/tests/aws/services/lambda_/test_lambda_developer_tools.py index ffd6ef5c05957..8b8cca862fba3 100644 --- a/tests/aws/services/lambda_/test_lambda_developer_tools.py +++ b/tests/aws/services/lambda_/test_lambda_developer_tools.py @@ -28,10 +28,10 @@ class TestHotReloading: @pytest.mark.parametrize( "runtime,handler_file,handler_filename", [ - (Runtime.nodejs18_x, HOT_RELOADING_NODEJS_HANDLER, "handler.mjs"), - (Runtime.python3_9, HOT_RELOADING_PYTHON_HANDLER, "handler.py"), + (Runtime.nodejs20_x, HOT_RELOADING_NODEJS_HANDLER, "handler.mjs"), + (Runtime.python3_12, HOT_RELOADING_PYTHON_HANDLER, "handler.py"), ], - ids=["nodejs18.x", "python3.9"], + ids=["nodejs20.x", "python3.12"], ) @markers.aws.only_localstack def test_hot_reloading( @@ -45,6 +45,9 @@ def test_hot_reloading( aws_client, ): """Test hot reloading of lambda code""" + # Hot reloading is debounced with 500ms + # 0.6 works on Linux, but it takes slightly longer on macOS + sleep_time = 0.8 function_name = f"test-hot-reloading-{short_uid()}" hot_reloading_bucket = config.BUCKET_MARKER_LOCAL tmp_path = config.dirs.mounted_tmp @@ -74,7 +77,7 @@ def test_hot_reloading( with open(os.path.join(hot_reloading_dir_path, handler_filename), mode="wt") as f: f.write(function_content.replace("value1", "value2")) # we have to sleep here, since the hot reloading is debounced with 500ms - time.sleep(0.6) + time.sleep(sleep_time) response = aws_client.lambda_.invoke(FunctionName=function_name, Payload=b"{}") response_dict = json.load(response["Payload"]) assert response_dict["counter"] == 1 @@ -88,7 +91,7 @@ def test_hot_reloading( test_folder = os.path.join(hot_reloading_dir_path, "test-folder") mkdir(test_folder) # make sure the creation of the folder triggered reload - time.sleep(0.6) + time.sleep(sleep_time) response = aws_client.lambda_.invoke(FunctionName=function_name, Payload=b"{}") response_dict = json.load(response["Payload"]) assert response_dict["counter"] == 1 @@ -96,7 +99,7 @@ def test_hot_reloading( # now writing something in the new folder to check if it will reload with open(os.path.join(test_folder, "test-file"), mode="wt") as f: f.write("test-content") - time.sleep(0.6) + time.sleep(sleep_time) response = aws_client.lambda_.invoke(FunctionName=function_name, Payload=b"{}") response_dict = json.load(response["Payload"]) assert response_dict["counter"] == 1 @@ -128,7 +131,7 @@ def test_hot_reloading_publish_version( Handler="handler.handler", Code={"S3Bucket": hot_reloading_bucket, "S3Key": mount_path}, Role=lambda_su_role, - Runtime=Runtime.nodejs18_x, + Runtime=Runtime.nodejs20_x, ) aws_client.lambda_.publish_version(FunctionName=function_name, CodeSha256="zipfilehash") @@ -143,7 +146,7 @@ def test_additional_docker_flags(self, create_lambda_function, monkeypatch, aws_ create_lambda_function( handler_file=TEST_LAMBDA_ENV, func_name=function_name, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) aws_client.lambda_.get_waiter("function_active_v2").wait(FunctionName=function_name) result = aws_client.lambda_.invoke(FunctionName=function_name, Payload="{}") @@ -180,13 +183,13 @@ def _delete_network(): zip_file = create_lambda_archive( load_file(LAMBDA_NETWORKS_PYTHON_HANDLER), get_content=True, - runtime=Runtime.python3_9, + runtime=Runtime.python3_12, ) aws_client.lambda_.create_function( FunctionName=function_name, Code={"ZipFile": zip_file}, Handler="handler.handler", - Runtime=Runtime.python3_9, + Runtime=Runtime.python3_12, Role=lambda_su_role, ) cleanups.append(lambda: aws_client.lambda_.delete_function(FunctionName=function_name)) @@ -212,7 +215,7 @@ def test_lambda_localhost_localstack_cloud_connectivity( create_lambda_function( handler_file=LAMBDA_NETWORKS_PYTHON_HANDLER, func_name=function_name, - runtime=Runtime.python3_11, + runtime=Runtime.python3_12, ) result = aws_client.lambda_.invoke( From cc5f5703c01ee242bc89294a70d0996699ecb74b Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Tue, 23 Apr 2024 08:54:06 +0100 Subject: [PATCH 088/169] StepFunctions: Add Support for MaxConcurrencyPath (#10705) --- .../stepfunctions/asl/antlr/ASLLexer.g4 | 2 + .../stepfunctions/asl/antlr/ASLParser.g4 | 4 + .../asl/antlr/runtime/ASLLexer.py | 1899 +++++++-------- .../asl/antlr/runtime/ASLParser.py | 2150 +++++++++-------- .../asl/antlr/runtime/ASLParserListener.py | 9 + .../asl/antlr/runtime/ASLParserVisitor.py | 5 + .../distributed_iteration_component.py | 6 +- .../iteration/inline_iteration_component.py | 6 +- .../state_map/max_concurrency.py | 80 +- .../state_execution/state_map/state_map.py | 18 +- .../asl/component/state/state_props.py | 25 +- .../stepfunctions/asl/parse/preprocessor.py | 5 + .../scenarios/scenarios_templates.py | 3 + .../statemachines/max_concurrency_path.json5 | 28 + .../v2/scenarios/test_base_scenarios.py | 57 + .../test_base_scenarios.snapshot.json | 471 ++++ .../test_base_scenarios.validation.json | 15 + 17 files changed, 2777 insertions(+), 2006 deletions(-) create mode 100644 tests/aws/services/stepfunctions/templates/scenarios/statemachines/max_concurrency_path.json5 diff --git a/localstack/services/stepfunctions/asl/antlr/ASLLexer.g4 b/localstack/services/stepfunctions/asl/antlr/ASLLexer.g4 index 7a55eeb72c135..8cec3a352ef1e 100644 --- a/localstack/services/stepfunctions/asl/antlr/ASLLexer.g4 +++ b/localstack/services/stepfunctions/asl/antlr/ASLLexer.g4 @@ -179,6 +179,8 @@ ITERATOR: '"Iterator"'; ITEMSELECTOR: '"ItemSelector"'; +MAXCONCURRENCYPATH: '"MaxConcurrencyPath"'; + MAXCONCURRENCY: '"MaxConcurrency"'; RESOURCE: '"Resource"'; diff --git a/localstack/services/stepfunctions/asl/antlr/ASLParser.g4 b/localstack/services/stepfunctions/asl/antlr/ASLParser.g4 index 918353d8373b5..4a2ec8cf5df9e 100644 --- a/localstack/services/stepfunctions/asl/antlr/ASLParser.g4 +++ b/localstack/services/stepfunctions/asl/antlr/ASLParser.g4 @@ -46,6 +46,7 @@ state_stmt: | item_selector_decl | item_reader_decl | max_concurrency_decl + | max_concurrency_path_decl | timeout_seconds_decl | timeout_seconds_path_decl | heartbeat_seconds_decl @@ -110,6 +111,8 @@ items_path_decl: ITEMSPATH COLON keyword_or_string; max_concurrency_decl: MAXCONCURRENCY COLON INT; +max_concurrency_path_decl: MAXCONCURRENCYPATH COLON STRINGPATH; + parameters_decl: PARAMETERS COLON payload_tmpl_decl; timeout_seconds_decl: TIMEOUTSECONDS COLON INT; @@ -418,6 +421,7 @@ keyword_or_string: | ITERATOR | ITEMSELECTOR | MAXCONCURRENCY + | MAXCONCURRENCYPATH | RESOURCE | INPUTPATH | OUTPUTPATH diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLLexer.py b/localstack/services/stepfunctions/asl/antlr/runtime/ASLLexer.py index 4033681ec5367..f35bd83c2af17 100644 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLLexer.py +++ b/localstack/services/stepfunctions/asl/antlr/runtime/ASLLexer.py @@ -10,7 +10,7 @@ def serializedATN(): return [ - 4,0,140,2442,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7, + 4,0,141,2465,6,-1,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7, 5,2,6,7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12, 2,13,7,13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19, 7,19,2,20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25, @@ -33,183 +33,185 @@ def serializedATN(): 7,125,2,126,7,126,2,127,7,127,2,128,7,128,2,129,7,129,2,130,7,130, 2,131,7,131,2,132,7,132,2,133,7,133,2,134,7,134,2,135,7,135,2,136, 7,136,2,137,7,137,2,138,7,138,2,139,7,139,2,140,7,140,2,141,7,141, - 2,142,7,142,2,143,7,143,2,144,7,144,1,0,1,0,1,1,1,1,1,2,1,2,1,3, - 1,3,1,4,1,4,1,5,1,5,1,6,1,6,1,6,1,6,1,6,1,7,1,7,1,7,1,7,1,7,1,7, - 1,8,1,8,1,8,1,8,1,8,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,10, - 1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11, - 1,11,1,11,1,11,1,11,1,11,1,12,1,12,1,12,1,12,1,12,1,12,1,12,1,12, - 1,12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13,1,13, - 1,13,1,14,1,14,1,14,1,14,1,14,1,14,1,14,1,15,1,15,1,15,1,15,1,15, - 1,15,1,15,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,17,1,17, - 1,17,1,17,1,17,1,17,1,17,1,18,1,18,1,18,1,18,1,18,1,18,1,18,1,18, - 1,18,1,18,1,19,1,19,1,19,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20, - 1,20,1,20,1,20,1,21,1,21,1,21,1,21,1,21,1,21,1,21,1,21,1,21,1,21, - 1,21,1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,23,1,23,1,23, - 1,23,1,23,1,23,1,23,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24,1,24, - 1,24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,26, - 1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,27,1,27,1,27, - 1,27,1,27,1,27,1,28,1,28,1,28,1,28,1,28,1,28,1,28,1,28,1,28,1,28, - 1,28,1,28,1,28,1,28,1,28,1,28,1,29,1,29,1,29,1,29,1,29,1,29,1,29, - 1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,29, - 1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,31, - 1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,32,1,32,1,32,1,32,1,32, - 1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,33,1,33,1,33,1,33,1,33,1,33, - 1,33,1,33,1,33,1,33,1,33,1,33,1,34,1,34,1,34,1,34,1,34,1,34,1,34, - 1,34,1,34,1,34,1,34,1,35,1,35,1,35,1,35,1,35,1,35,1,35,1,35,1,35, - 1,35,1,35,1,35,1,35,1,35,1,36,1,36,1,36,1,36,1,36,1,36,1,37,1,37, - 1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37, - 1,37,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38, - 1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,39,1,39,1,39,1,39,1,39, - 1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39, - 1,39,1,39,1,39,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40, - 1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40, - 1,40,1,40,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41, - 1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41, - 1,41,1,41,1,41,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42, - 1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42, - 1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,43,1,43,1,43,1,43,1,43, - 1,43,1,43,1,43,1,43,1,43,1,43,1,43,1,43,1,43,1,43,1,43,1,43,1,43, - 1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44, - 1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,45,1,45,1,45,1,45, - 1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,45, - 1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,46,1,46,1,46,1,46,1,46,1,46, - 1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46, - 1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,47,1,47,1,47,1,47, - 1,47,1,48,1,48,1,48,1,48,1,48,1,48,1,48,1,48,1,48,1,48,1,48,1,48, - 1,48,1,48,1,48,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49, - 1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,50,1,50,1,50,1,50, - 1,50,1,50,1,50,1,50,1,50,1,50,1,50,1,50,1,50,1,50,1,50,1,50,1,50, - 1,50,1,50,1,50,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51, - 1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51, - 1,51,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52, - 1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52, - 1,52,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53, - 1,53,1,53,1,53,1,53,1,53,1,54,1,54,1,54,1,54,1,54,1,54,1,54,1,54, - 1,54,1,54,1,54,1,54,1,54,1,54,1,54,1,54,1,54,1,55,1,55,1,55,1,55, - 1,55,1,55,1,55,1,55,1,55,1,55,1,55,1,55,1,55,1,55,1,55,1,55,1,55, - 1,55,1,55,1,55,1,55,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56, - 1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56, - 1,56,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57, - 1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57, - 1,57,1,57,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58, - 1,58,1,58,1,58,1,58,1,58,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1,59, - 1,59,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1,60,1,60,1,60, - 1,60,1,60,1,60,1,60,1,60,1,60,1,60,1,60,1,60,1,60,1,60,1,60,1,60, - 1,60,1,60,1,60,1,60,1,60,1,60,1,61,1,61,1,61,1,61,1,61,1,61,1,61, - 1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,61, - 1,61,1,61,1,61,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62, - 1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62, - 1,62,1,62,1,62,1,62,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63, - 1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63, - 1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,64,1,64,1,64,1,64,1,64,1,64, - 1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64, - 1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64, - 1,64,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65, - 1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,66,1,66,1,66,1,66,1,66, - 1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66, - 1,66,1,66,1,66,1,66,1,66,1,66,1,67,1,67,1,67,1,67,1,67,1,67,1,67, - 1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67, - 1,67,1,67,1,67,1,67,1,67,1,67,1,68,1,68,1,68,1,68,1,68,1,68,1,68, - 1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68, - 1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,69,1,69,1,69, - 1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,70,1,70, - 1,70,1,70,1,70,1,70,1,70,1,70,1,70,1,70,1,71,1,71,1,71,1,71,1,71, - 1,71,1,71,1,71,1,71,1,71,1,71,1,71,1,71,1,71,1,71,1,71,1,72,1,72, - 1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,73,1,73,1,73, - 1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,73, - 1,73,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,74, - 1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,75,1,75,1,75,1,75, - 1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75, - 1,75,1,75,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76, - 1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,77, - 1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77, - 1,77,1,77,1,77,1,77,1,78,1,78,1,78,1,78,1,78,1,78,1,78,1,79,1,79, - 1,79,1,79,1,79,1,79,1,79,1,79,1,79,1,80,1,80,1,80,1,80,1,80,1,80, - 1,80,1,80,1,80,1,80,1,80,1,80,1,80,1,80,1,81,1,81,1,81,1,81,1,81, - 1,81,1,81,1,81,1,81,1,81,1,81,1,81,1,81,1,81,1,81,1,81,1,82,1,82, - 1,82,1,82,1,82,1,82,1,82,1,82,1,82,1,82,1,82,1,83,1,83,1,83,1,83, - 1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,84, - 1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,85,1,85,1,85, - 1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,86, - 1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86, - 1,86,1,86,1,86,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87, - 1,87,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88, - 1,89,1,89,1,89,1,89,1,89,1,89,1,89,1,89,1,89,1,89,1,89,1,89,1,89, - 1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,91, - 1,91,1,91,1,91,1,91,1,91,1,91,1,91,1,91,1,91,1,91,1,91,1,91,1,92, - 1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,93,1,93,1,93,1,93,1,93, - 1,93,1,93,1,93,1,93,1,93,1,93,1,93,1,93,1,94,1,94,1,94,1,94,1,94, - 1,94,1,94,1,94,1,94,1,94,1,94,1,94,1,94,1,94,1,94,1,94,1,94,1,95, - 1,95,1,95,1,95,1,95,1,95,1,95,1,95,1,95,1,95,1,95,1,95,1,95,1,96, - 1,96,1,96,1,96,1,96,1,96,1,96,1,96,1,96,1,96,1,96,1,96,1,96,1,96, - 1,96,1,97,1,97,1,97,1,97,1,97,1,97,1,97,1,97,1,97,1,97,1,97,1,97, - 1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,98, - 1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,99,1,99,1,99,1,99,1,99,1,99, - 1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,100,1,100,1,100,1,100,1,100, + 2,142,7,142,2,143,7,143,2,144,7,144,2,145,7,145,1,0,1,0,1,1,1,1, + 1,2,1,2,1,3,1,3,1,4,1,4,1,5,1,5,1,6,1,6,1,6,1,6,1,6,1,7,1,7,1,7, + 1,7,1,7,1,7,1,8,1,8,1,8,1,8,1,8,1,9,1,9,1,9,1,9,1,9,1,9,1,9,1,9, + 1,9,1,9,1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,10,1,11,1,11,1, + 11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,12,1,12,1,12,1,12,1,12,1, + 12,1,12,1,12,1,12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,1,13,1,13,1, + 13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,14,1,14,1,14,1,15,1,15,1, + 15,1,15,1,15,1,15,1,15,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1,16,1, + 16,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,18,1,18,1,18,1,18,1,18,1, + 18,1,18,1,18,1,18,1,18,1,19,1,19,1,19,1,19,1,19,1,19,1,19,1,20,1, + 20,1,20,1,20,1,20,1,20,1,20,1,21,1,21,1,21,1,21,1,21,1,21,1,21,1, + 21,1,21,1,21,1,21,1,22,1,22,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1, + 23,1,23,1,23,1,23,1,23,1,23,1,23,1,24,1,24,1,24,1,24,1,24,1,24,1, + 24,1,24,1,24,1,24,1,24,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1,25,1, + 25,1,25,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1, + 27,1,27,1,27,1,27,1,27,1,27,1,28,1,28,1,28,1,28,1,28,1,28,1,28,1, + 28,1,28,1,28,1,28,1,28,1,28,1,28,1,28,1,28,1,29,1,29,1,29,1,29,1, + 29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1,29,1, + 29,1,29,1,29,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,30,1, + 30,1,30,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,32,1,32,1, + 32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,33,1,33,1,33,1, + 33,1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,34,1,34,1,34,1,34,1, + 34,1,34,1,34,1,34,1,34,1,34,1,34,1,35,1,35,1,35,1,35,1,35,1,35,1, + 35,1,35,1,35,1,35,1,35,1,35,1,35,1,35,1,36,1,36,1,36,1,36,1,36,1, + 36,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1, + 37,1,37,1,37,1,37,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1, + 38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,39,1,39,1, + 39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1, + 39,1,39,1,39,1,39,1,39,1,39,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1, + 40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1,40,1, + 40,1,40,1,40,1,40,1,40,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1, + 41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,41,1, + 41,1,41,1,41,1,41,1,41,1,41,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1, + 42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1, + 42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,42,1,43,1,43,1, + 43,1,43,1,43,1,43,1,43,1,43,1,43,1,43,1,43,1,43,1,43,1,43,1,43,1, + 43,1,43,1,43,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1, + 44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,45,1, + 45,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1, + 45,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,45,1,46,1,46,1,46,1, + 46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1, + 46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,46,1,47,1, + 47,1,47,1,47,1,47,1,48,1,48,1,48,1,48,1,48,1,48,1,48,1,48,1,48,1, + 48,1,48,1,48,1,48,1,48,1,48,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1, + 49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,49,1,50,1, + 50,1,50,1,50,1,50,1,50,1,50,1,50,1,50,1,50,1,50,1,50,1,50,1,50,1, + 50,1,50,1,50,1,50,1,50,1,50,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1, + 51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1, + 51,1,51,1,51,1,51,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1, + 52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1,52,1, + 52,1,52,1,52,1,52,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1, + 53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1, + 53,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,54,1,54,1,54,1,54,1,54,1, + 54,1,54,1,54,1,54,1,54,1,54,1,54,1,54,1,54,1,54,1,54,1,54,1,55,1, + 55,1,55,1,55,1,55,1,55,1,55,1,55,1,55,1,55,1,55,1,55,1,55,1,55,1, + 55,1,55,1,55,1,55,1,55,1,55,1,55,1,56,1,56,1,56,1,56,1,56,1,56,1, + 56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1, + 56,1,56,1,56,1,56,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1, + 57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1, + 57,1,57,1,57,1,57,1,57,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1, + 58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,59,1,59,1,59,1,59,1,59,1, + 59,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1,59,1, + 60,1,60,1,60,1,60,1,60,1,60,1,60,1,60,1,60,1,60,1,60,1,60,1,60,1, + 60,1,60,1,60,1,60,1,60,1,60,1,60,1,60,1,60,1,61,1,61,1,61,1,61,1, + 61,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1,61,1, + 61,1,61,1,61,1,61,1,61,1,61,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1, + 62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1,62,1, + 62,1,62,1,62,1,62,1,62,1,62,1,62,1,63,1,63,1,63,1,63,1,63,1,63,1, + 63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1, + 63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,63,1,64,1,64,1,64,1, + 64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1, + 64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1,64,1, + 64,1,64,1,64,1,64,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1, + 65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,65,1,66,1,66,1, + 66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1, + 66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,67,1,67,1,67,1,67,1, + 67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1, + 67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,67,1,68,1,68,1,68,1,68,1, + 68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1, + 68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1,68,1, + 69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,1,69,1, + 69,1,70,1,70,1,70,1,70,1,70,1,70,1,70,1,70,1,70,1,70,1,71,1,71,1, + 71,1,71,1,71,1,71,1,71,1,71,1,71,1,71,1,71,1,71,1,71,1,71,1,71,1, + 71,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1, + 73,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1, + 73,1,73,1,73,1,73,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1, + 74,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,74,1,75,1, + 75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1, + 75,1,75,1,75,1,75,1,75,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1, + 76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1,76,1, + 76,1,76,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1, + 77,1,77,1,77,1,77,1,77,1,77,1,77,1,78,1,78,1,78,1,78,1,78,1,78,1, + 78,1,79,1,79,1,79,1,79,1,79,1,79,1,79,1,79,1,79,1,80,1,80,1,80,1, + 80,1,80,1,80,1,80,1,80,1,80,1,80,1,80,1,80,1,80,1,80,1,81,1,81,1, + 81,1,81,1,81,1,81,1,81,1,81,1,81,1,81,1,81,1,81,1,81,1,81,1,81,1, + 81,1,82,1,82,1,82,1,82,1,82,1,82,1,82,1,82,1,82,1,82,1,82,1,83,1, + 83,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1,83,1, + 83,1,83,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1, + 85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1,85,1, + 85,1,85,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1, + 86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,86,1,87,1,87,1,87,1, + 87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1,87,1, + 87,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,88,1,89,1, + 89,1,89,1,89,1,89,1,89,1,89,1,89,1,89,1,89,1,89,1,89,1,90,1,90,1, + 90,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,90,1,91,1,91,1, + 91,1,91,1,91,1,91,1,91,1,91,1,91,1,91,1,91,1,91,1,92,1,92,1,92,1, + 92,1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,92,1,93,1,93,1,93,1, + 93,1,93,1,93,1,93,1,93,1,93,1,94,1,94,1,94,1,94,1,94,1,94,1,94,1, + 94,1,94,1,94,1,94,1,94,1,94,1,95,1,95,1,95,1,95,1,95,1,95,1,95,1, + 95,1,95,1,95,1,95,1,95,1,95,1,95,1,95,1,95,1,95,1,96,1,96,1,96,1, + 96,1,96,1,96,1,96,1,96,1,96,1,96,1,96,1,96,1,96,1,97,1,97,1,97,1, + 97,1,97,1,97,1,97,1,97,1,97,1,97,1,97,1,97,1,97,1,97,1,97,1,98,1, + 98,1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,98,1,99,1,99,1, + 99,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1,99,1, + 99,1,99,1,99,1,99,1,99,1,100,1,100,1,100,1,100,1,100,1,100,1,100, 1,100,1,100,1,100,1,100,1,100,1,100,1,101,1,101,1,101,1,101,1,101, - 1,101,1,101,1,101,1,101,1,101,1,101,1,101,1,101,1,101,1,101,1,102, - 1,102,1,102,1,102,1,102,1,102,1,102,1,103,1,103,1,103,1,103,1,103, - 1,103,1,104,1,104,1,104,1,104,1,104,1,104,1,104,1,104,1,105,1,105, - 1,105,1,105,1,105,1,105,1,105,1,105,1,105,1,105,1,105,1,105,1,106, - 1,106,1,106,1,106,1,106,1,106,1,106,1,106,1,107,1,107,1,107,1,107, - 1,107,1,107,1,107,1,107,1,107,1,107,1,107,1,107,1,108,1,108,1,108, - 1,108,1,108,1,108,1,108,1,108,1,109,1,109,1,109,1,109,1,109,1,109, - 1,109,1,109,1,109,1,109,1,109,1,109,1,109,1,109,1,110,1,110,1,110, - 1,110,1,110,1,110,1,110,1,110,1,110,1,110,1,110,1,110,1,110,1,110, - 1,110,1,110,1,110,1,110,1,111,1,111,1,111,1,111,1,111,1,111,1,111, - 1,111,1,111,1,111,1,111,1,111,1,111,1,111,1,112,1,112,1,112,1,112, - 1,112,1,112,1,112,1,112,1,112,1,112,1,112,1,112,1,112,1,112,1,113, - 1,113,1,113,1,113,1,113,1,113,1,113,1,113,1,113,1,113,1,113,1,113, - 1,113,1,113,1,113,1,113,1,113,1,113,1,114,1,114,1,114,1,114,1,114, + 1,101,1,101,1,101,1,101,1,101,1,101,1,102,1,102,1,102,1,102,1,102, + 1,102,1,102,1,102,1,102,1,102,1,102,1,102,1,102,1,102,1,102,1,103, + 1,103,1,103,1,103,1,103,1,103,1,103,1,104,1,104,1,104,1,104,1,104, + 1,104,1,105,1,105,1,105,1,105,1,105,1,105,1,105,1,105,1,106,1,106, + 1,106,1,106,1,106,1,106,1,106,1,106,1,106,1,106,1,106,1,106,1,107, + 1,107,1,107,1,107,1,107,1,107,1,107,1,107,1,108,1,108,1,108,1,108, + 1,108,1,108,1,108,1,108,1,108,1,108,1,108,1,108,1,109,1,109,1,109, + 1,109,1,109,1,109,1,109,1,109,1,110,1,110,1,110,1,110,1,110,1,110, + 1,110,1,110,1,110,1,110,1,110,1,110,1,110,1,110,1,111,1,111,1,111, + 1,111,1,111,1,111,1,111,1,111,1,111,1,111,1,111,1,111,1,111,1,111, + 1,111,1,111,1,111,1,111,1,112,1,112,1,112,1,112,1,112,1,112,1,112, + 1,112,1,112,1,112,1,112,1,112,1,112,1,112,1,113,1,113,1,113,1,113, + 1,113,1,113,1,113,1,113,1,113,1,113,1,113,1,113,1,113,1,113,1,114, 1,114,1,114,1,114,1,114,1,114,1,114,1,114,1,114,1,114,1,114,1,114, - 1,114,1,115,1,115,1,115,1,115,1,115,1,115,1,115,1,116,1,116,1,116, - 1,116,1,116,1,116,1,116,1,117,1,117,1,117,1,117,1,117,1,117,1,117, - 1,117,1,118,1,118,1,118,1,118,1,118,1,118,1,118,1,118,1,118,1,118, - 1,118,1,118,1,118,1,119,1,119,1,119,1,119,1,119,1,119,1,119,1,119, - 1,119,1,119,1,119,1,119,1,119,1,119,1,119,1,119,1,119,1,119,1,119, - 1,119,1,119,1,119,1,119,1,119,1,119,1,119,1,119,1,120,1,120,1,120, + 1,114,1,114,1,114,1,114,1,114,1,114,1,115,1,115,1,115,1,115,1,115, + 1,115,1,115,1,115,1,115,1,115,1,115,1,115,1,115,1,115,1,115,1,115, + 1,115,1,116,1,116,1,116,1,116,1,116,1,116,1,116,1,117,1,117,1,117, + 1,117,1,117,1,117,1,117,1,118,1,118,1,118,1,118,1,118,1,118,1,118, + 1,118,1,119,1,119,1,119,1,119,1,119,1,119,1,119,1,119,1,119,1,119, + 1,119,1,119,1,119,1,120,1,120,1,120,1,120,1,120,1,120,1,120,1,120, 1,120,1,120,1,120,1,120,1,120,1,120,1,120,1,120,1,120,1,120,1,120, - 1,120,1,120,1,120,1,120,1,120,1,120,1,120,1,120,1,120,1,120,1,120, - 1,120,1,121,1,121,1,121,1,121,1,121,1,121,1,121,1,121,1,121,1,121, - 1,121,1,121,1,121,1,121,1,121,1,121,1,121,1,122,1,122,1,122,1,122, - 1,122,1,122,1,122,1,122,1,122,1,122,1,122,1,122,1,122,1,122,1,122, - 1,122,1,122,1,122,1,122,1,122,1,123,1,123,1,123,1,123,1,123,1,123, + 1,120,1,120,1,120,1,120,1,120,1,120,1,120,1,120,1,121,1,121,1,121, + 1,121,1,121,1,121,1,121,1,121,1,121,1,121,1,121,1,121,1,121,1,121, + 1,121,1,121,1,121,1,121,1,121,1,121,1,121,1,121,1,121,1,121,1,121, + 1,121,1,122,1,122,1,122,1,122,1,122,1,122,1,122,1,122,1,122,1,122, + 1,122,1,122,1,122,1,122,1,122,1,122,1,122,1,123,1,123,1,123,1,123, 1,123,1,123,1,123,1,123,1,123,1,123,1,123,1,123,1,123,1,123,1,123, - 1,123,1,123,1,123,1,123,1,124,1,124,1,124,1,124,1,124,1,124,1,124, - 1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124, + 1,123,1,123,1,123,1,123,1,123,1,124,1,124,1,124,1,124,1,124,1,124, 1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124,1,124, - 1,124,1,124,1,124,1,125,1,125,1,125,1,125,1,125,1,125,1,125,1,125, + 1,124,1,124,1,124,1,124,1,125,1,125,1,125,1,125,1,125,1,125,1,125, 1,125,1,125,1,125,1,125,1,125,1,125,1,125,1,125,1,125,1,125,1,125, 1,125,1,125,1,125,1,125,1,125,1,125,1,125,1,125,1,125,1,125,1,125, + 1,125,1,125,1,125,1,126,1,126,1,126,1,126,1,126,1,126,1,126,1,126, 1,126,1,126,1,126,1,126,1,126,1,126,1,126,1,126,1,126,1,126,1,126, 1,126,1,126,1,126,1,126,1,126,1,126,1,126,1,126,1,126,1,126,1,126, 1,127,1,127,1,127,1,127,1,127,1,127,1,127,1,127,1,127,1,127,1,127, 1,127,1,127,1,127,1,127,1,127,1,127,1,127,1,127,1,127,1,127,1,127, - 1,127,1,127,1,127,1,128,1,128,1,128,1,128,1,128,1,128,1,128,1,128, 1,128,1,128,1,128,1,128,1,128,1,128,1,128,1,128,1,128,1,128,1,128, - 1,128,1,128,1,128,1,128,1,128,1,128,1,128,1,129,1,129,1,129,1,129, - 1,129,1,129,1,129,1,129,1,129,1,129,1,129,1,129,1,129,1,129,1,129, - 1,129,1,129,1,129,1,129,1,129,1,129,1,129,1,129,1,129,1,129,1,129, + 1,128,1,128,1,128,1,128,1,128,1,128,1,128,1,128,1,128,1,128,1,128, + 1,128,1,128,1,128,1,129,1,129,1,129,1,129,1,129,1,129,1,129,1,129, 1,129,1,129,1,129,1,129,1,129,1,129,1,129,1,129,1,129,1,129,1,129, - 1,129,1,129,1,129,1,129,1,130,1,130,1,130,1,130,1,130,1,130,1,130, + 1,129,1,129,1,129,1,129,1,129,1,129,1,129,1,130,1,130,1,130,1,130, 1,130,1,130,1,130,1,130,1,130,1,130,1,130,1,130,1,130,1,130,1,130, - 1,130,1,130,1,130,1,130,1,130,1,130,1,130,1,130,1,131,1,131,1,131, - 1,131,1,131,1,131,1,131,1,131,1,131,1,131,1,131,1,131,1,131,1,131, + 1,130,1,130,1,130,1,130,1,130,1,130,1,130,1,130,1,130,1,130,1,130, + 1,130,1,130,1,130,1,130,1,130,1,130,1,130,1,130,1,130,1,130,1,130, + 1,130,1,130,1,130,1,130,1,131,1,131,1,131,1,131,1,131,1,131,1,131, 1,131,1,131,1,131,1,131,1,131,1,131,1,131,1,131,1,131,1,131,1,131, - 1,131,1,131,1,131,1,132,1,132,1,132,1,132,1,132,1,132,1,132,1,132, - 1,132,1,132,1,132,1,132,1,132,1,132,1,132,1,132,1,132,1,133,1,133, - 1,133,5,133,2346,8,133,10,133,12,133,2349,9,133,1,133,1,133,1,133, - 1,133,1,134,1,134,1,134,1,134,1,134,1,134,5,134,2361,8,134,10,134, - 12,134,2364,9,134,1,134,1,134,1,135,1,135,1,135,1,135,1,135,5,135, - 2373,8,135,10,135,12,135,2376,9,135,1,135,1,135,1,136,1,136,1,136, - 5,136,2383,8,136,10,136,12,136,2386,9,136,1,136,1,136,1,137,1,137, - 1,137,3,137,2393,8,137,1,138,1,138,1,138,1,138,1,138,1,138,1,139, - 1,139,1,140,1,140,1,141,1,141,1,141,5,141,2408,8,141,10,141,12,141, - 2411,9,141,3,141,2413,8,141,1,142,3,142,2416,8,142,1,142,1,142,1, - 142,4,142,2421,8,142,11,142,12,142,2422,3,142,2425,8,142,1,142,3, - 142,2428,8,142,1,143,1,143,3,143,2432,8,143,1,143,1,143,1,144,4, - 144,2437,8,144,11,144,12,144,2438,1,144,1,144,0,0,145,1,1,3,2,5, + 1,131,1,131,1,131,1,131,1,131,1,131,1,131,1,131,1,132,1,132,1,132, + 1,132,1,132,1,132,1,132,1,132,1,132,1,132,1,132,1,132,1,132,1,132, + 1,132,1,132,1,132,1,132,1,132,1,132,1,132,1,132,1,132,1,132,1,132, + 1,132,1,132,1,132,1,133,1,133,1,133,1,133,1,133,1,133,1,133,1,133, + 1,133,1,133,1,133,1,133,1,133,1,133,1,133,1,133,1,133,1,134,1,134, + 1,134,5,134,2369,8,134,10,134,12,134,2372,9,134,1,134,1,134,1,134, + 1,134,1,135,1,135,1,135,1,135,1,135,1,135,5,135,2384,8,135,10,135, + 12,135,2387,9,135,1,135,1,135,1,136,1,136,1,136,1,136,1,136,5,136, + 2396,8,136,10,136,12,136,2399,9,136,1,136,1,136,1,137,1,137,1,137, + 5,137,2406,8,137,10,137,12,137,2409,9,137,1,137,1,137,1,138,1,138, + 1,138,3,138,2416,8,138,1,139,1,139,1,139,1,139,1,139,1,139,1,140, + 1,140,1,141,1,141,1,142,1,142,1,142,5,142,2431,8,142,10,142,12,142, + 2434,9,142,3,142,2436,8,142,1,143,3,143,2439,8,143,1,143,1,143,1, + 143,4,143,2444,8,143,11,143,12,143,2445,3,143,2448,8,143,1,143,3, + 143,2451,8,143,1,144,1,144,3,144,2455,8,144,1,144,1,144,1,145,4, + 145,2460,8,145,11,145,12,145,2461,1,145,1,145,0,0,146,1,1,3,2,5, 3,7,4,9,5,11,6,13,7,15,8,17,9,19,10,21,11,23,12,25,13,27,14,29,15, 31,16,33,17,35,18,37,19,39,20,41,21,43,22,45,23,47,24,49,25,51,26, 53,27,55,28,57,29,59,30,61,31,63,32,65,33,67,34,69,35,71,36,73,37, @@ -224,699 +226,706 @@ def serializedATN(): 225,113,227,114,229,115,231,116,233,117,235,118,237,119,239,120, 241,121,243,122,245,123,247,124,249,125,251,126,253,127,255,128, 257,129,259,130,261,131,263,132,265,133,267,134,269,135,271,136, - 273,137,275,0,277,0,279,0,281,0,283,138,285,139,287,0,289,140,1, - 0,8,8,0,34,34,47,47,92,92,98,98,102,102,110,110,114,114,116,116, - 3,0,48,57,65,70,97,102,3,0,0,31,34,34,92,92,1,0,49,57,1,0,48,57, - 2,0,69,69,101,101,2,0,43,43,45,45,3,0,9,10,13,13,32,32,2453,0,1, - 1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0, - 0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1,0, - 0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29,1,0,0,0,0,31,1,0, - 0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,0,39,1,0,0,0,0,41,1,0, - 0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,1,0,0,0,0,49,1,0,0,0,0,51,1,0, - 0,0,0,53,1,0,0,0,0,55,1,0,0,0,0,57,1,0,0,0,0,59,1,0,0,0,0,61,1,0, - 0,0,0,63,1,0,0,0,0,65,1,0,0,0,0,67,1,0,0,0,0,69,1,0,0,0,0,71,1,0, - 0,0,0,73,1,0,0,0,0,75,1,0,0,0,0,77,1,0,0,0,0,79,1,0,0,0,0,81,1,0, - 0,0,0,83,1,0,0,0,0,85,1,0,0,0,0,87,1,0,0,0,0,89,1,0,0,0,0,91,1,0, - 0,0,0,93,1,0,0,0,0,95,1,0,0,0,0,97,1,0,0,0,0,99,1,0,0,0,0,101,1, - 0,0,0,0,103,1,0,0,0,0,105,1,0,0,0,0,107,1,0,0,0,0,109,1,0,0,0,0, - 111,1,0,0,0,0,113,1,0,0,0,0,115,1,0,0,0,0,117,1,0,0,0,0,119,1,0, - 0,0,0,121,1,0,0,0,0,123,1,0,0,0,0,125,1,0,0,0,0,127,1,0,0,0,0,129, - 1,0,0,0,0,131,1,0,0,0,0,133,1,0,0,0,0,135,1,0,0,0,0,137,1,0,0,0, - 0,139,1,0,0,0,0,141,1,0,0,0,0,143,1,0,0,0,0,145,1,0,0,0,0,147,1, - 0,0,0,0,149,1,0,0,0,0,151,1,0,0,0,0,153,1,0,0,0,0,155,1,0,0,0,0, - 157,1,0,0,0,0,159,1,0,0,0,0,161,1,0,0,0,0,163,1,0,0,0,0,165,1,0, - 0,0,0,167,1,0,0,0,0,169,1,0,0,0,0,171,1,0,0,0,0,173,1,0,0,0,0,175, - 1,0,0,0,0,177,1,0,0,0,0,179,1,0,0,0,0,181,1,0,0,0,0,183,1,0,0,0, - 0,185,1,0,0,0,0,187,1,0,0,0,0,189,1,0,0,0,0,191,1,0,0,0,0,193,1, - 0,0,0,0,195,1,0,0,0,0,197,1,0,0,0,0,199,1,0,0,0,0,201,1,0,0,0,0, - 203,1,0,0,0,0,205,1,0,0,0,0,207,1,0,0,0,0,209,1,0,0,0,0,211,1,0, - 0,0,0,213,1,0,0,0,0,215,1,0,0,0,0,217,1,0,0,0,0,219,1,0,0,0,0,221, - 1,0,0,0,0,223,1,0,0,0,0,225,1,0,0,0,0,227,1,0,0,0,0,229,1,0,0,0, - 0,231,1,0,0,0,0,233,1,0,0,0,0,235,1,0,0,0,0,237,1,0,0,0,0,239,1, - 0,0,0,0,241,1,0,0,0,0,243,1,0,0,0,0,245,1,0,0,0,0,247,1,0,0,0,0, - 249,1,0,0,0,0,251,1,0,0,0,0,253,1,0,0,0,0,255,1,0,0,0,0,257,1,0, - 0,0,0,259,1,0,0,0,0,261,1,0,0,0,0,263,1,0,0,0,0,265,1,0,0,0,0,267, - 1,0,0,0,0,269,1,0,0,0,0,271,1,0,0,0,0,273,1,0,0,0,0,283,1,0,0,0, - 0,285,1,0,0,0,0,289,1,0,0,0,1,291,1,0,0,0,3,293,1,0,0,0,5,295,1, - 0,0,0,7,297,1,0,0,0,9,299,1,0,0,0,11,301,1,0,0,0,13,303,1,0,0,0, - 15,308,1,0,0,0,17,314,1,0,0,0,19,319,1,0,0,0,21,329,1,0,0,0,23,338, - 1,0,0,0,25,348,1,0,0,0,27,360,1,0,0,0,29,370,1,0,0,0,31,377,1,0, - 0,0,33,384,1,0,0,0,35,393,1,0,0,0,37,400,1,0,0,0,39,410,1,0,0,0, - 41,417,1,0,0,0,43,424,1,0,0,0,45,435,1,0,0,0,47,441,1,0,0,0,49,451, - 1,0,0,0,51,462,1,0,0,0,53,472,1,0,0,0,55,483,1,0,0,0,57,489,1,0, - 0,0,59,505,1,0,0,0,61,525,1,0,0,0,63,537,1,0,0,0,65,546,1,0,0,0, - 67,558,1,0,0,0,69,570,1,0,0,0,71,581,1,0,0,0,73,595,1,0,0,0,75,601, - 1,0,0,0,77,617,1,0,0,0,79,637,1,0,0,0,81,658,1,0,0,0,83,683,1,0, - 0,0,85,710,1,0,0,0,87,741,1,0,0,0,89,759,1,0,0,0,91,781,1,0,0,0, - 93,805,1,0,0,0,95,833,1,0,0,0,97,838,1,0,0,0,99,853,1,0,0,0,101, - 872,1,0,0,0,103,892,1,0,0,0,105,916,1,0,0,0,107,942,1,0,0,0,109, - 972,1,0,0,0,111,989,1,0,0,0,113,1010,1,0,0,0,115,1033,1,0,0,0,117, - 1060,1,0,0,0,119,1076,1,0,0,0,121,1094,1,0,0,0,123,1116,1,0,0,0, - 125,1139,1,0,0,0,127,1166,1,0,0,0,129,1195,1,0,0,0,131,1228,1,0, - 0,0,133,1248,1,0,0,0,135,1272,1,0,0,0,137,1298,1,0,0,0,139,1328, - 1,0,0,0,141,1342,1,0,0,0,143,1352,1,0,0,0,145,1368,1,0,0,0,147,1380, - 1,0,0,0,149,1397,1,0,0,0,151,1418,1,0,0,0,153,1437,1,0,0,0,155,1460, - 1,0,0,0,157,1478,1,0,0,0,159,1485,1,0,0,0,161,1494,1,0,0,0,163,1508, - 1,0,0,0,165,1524,1,0,0,0,167,1535,1,0,0,0,169,1551,1,0,0,0,171,1562, - 1,0,0,0,173,1577,1,0,0,0,175,1594,1,0,0,0,177,1605,1,0,0,0,179,1617, - 1,0,0,0,181,1630,1,0,0,0,183,1642,1,0,0,0,185,1655,1,0,0,0,187,1664, - 1,0,0,0,189,1677,1,0,0,0,191,1694,1,0,0,0,193,1707,1,0,0,0,195,1722, - 1,0,0,0,197,1734,1,0,0,0,199,1754,1,0,0,0,201,1767,1,0,0,0,203,1778, - 1,0,0,0,205,1793,1,0,0,0,207,1800,1,0,0,0,209,1806,1,0,0,0,211,1814, - 1,0,0,0,213,1826,1,0,0,0,215,1834,1,0,0,0,217,1846,1,0,0,0,219,1854, - 1,0,0,0,221,1868,1,0,0,0,223,1886,1,0,0,0,225,1900,1,0,0,0,227,1914, - 1,0,0,0,229,1932,1,0,0,0,231,1949,1,0,0,0,233,1956,1,0,0,0,235,1963, - 1,0,0,0,237,1971,1,0,0,0,239,1984,1,0,0,0,241,2011,1,0,0,0,243,2037, - 1,0,0,0,245,2054,1,0,0,0,247,2074,1,0,0,0,249,2095,1,0,0,0,251,2127, - 1,0,0,0,253,2157,1,0,0,0,255,2179,1,0,0,0,257,2204,1,0,0,0,259,2230, - 1,0,0,0,261,2271,1,0,0,0,263,2297,1,0,0,0,265,2325,1,0,0,0,267,2342, - 1,0,0,0,269,2354,1,0,0,0,271,2367,1,0,0,0,273,2379,1,0,0,0,275,2389, - 1,0,0,0,277,2394,1,0,0,0,279,2400,1,0,0,0,281,2402,1,0,0,0,283,2412, - 1,0,0,0,285,2415,1,0,0,0,287,2429,1,0,0,0,289,2436,1,0,0,0,291,292, - 5,44,0,0,292,2,1,0,0,0,293,294,5,58,0,0,294,4,1,0,0,0,295,296,5, - 91,0,0,296,6,1,0,0,0,297,298,5,93,0,0,298,8,1,0,0,0,299,300,5,123, - 0,0,300,10,1,0,0,0,301,302,5,125,0,0,302,12,1,0,0,0,303,304,5,116, - 0,0,304,305,5,114,0,0,305,306,5,117,0,0,306,307,5,101,0,0,307,14, - 1,0,0,0,308,309,5,102,0,0,309,310,5,97,0,0,310,311,5,108,0,0,311, - 312,5,115,0,0,312,313,5,101,0,0,313,16,1,0,0,0,314,315,5,110,0,0, - 315,316,5,117,0,0,316,317,5,108,0,0,317,318,5,108,0,0,318,18,1,0, - 0,0,319,320,5,34,0,0,320,321,5,67,0,0,321,322,5,111,0,0,322,323, - 5,109,0,0,323,324,5,109,0,0,324,325,5,101,0,0,325,326,5,110,0,0, - 326,327,5,116,0,0,327,328,5,34,0,0,328,20,1,0,0,0,329,330,5,34,0, - 0,330,331,5,83,0,0,331,332,5,116,0,0,332,333,5,97,0,0,333,334,5, - 116,0,0,334,335,5,101,0,0,335,336,5,115,0,0,336,337,5,34,0,0,337, - 22,1,0,0,0,338,339,5,34,0,0,339,340,5,83,0,0,340,341,5,116,0,0,341, - 342,5,97,0,0,342,343,5,114,0,0,343,344,5,116,0,0,344,345,5,65,0, - 0,345,346,5,116,0,0,346,347,5,34,0,0,347,24,1,0,0,0,348,349,5,34, - 0,0,349,350,5,78,0,0,350,351,5,101,0,0,351,352,5,120,0,0,352,353, - 5,116,0,0,353,354,5,83,0,0,354,355,5,116,0,0,355,356,5,97,0,0,356, - 357,5,116,0,0,357,358,5,101,0,0,358,359,5,34,0,0,359,26,1,0,0,0, - 360,361,5,34,0,0,361,362,5,86,0,0,362,363,5,101,0,0,363,364,5,114, - 0,0,364,365,5,115,0,0,365,366,5,105,0,0,366,367,5,111,0,0,367,368, - 5,110,0,0,368,369,5,34,0,0,369,28,1,0,0,0,370,371,5,34,0,0,371,372, - 5,84,0,0,372,373,5,121,0,0,373,374,5,112,0,0,374,375,5,101,0,0,375, - 376,5,34,0,0,376,30,1,0,0,0,377,378,5,34,0,0,378,379,5,84,0,0,379, - 380,5,97,0,0,380,381,5,115,0,0,381,382,5,107,0,0,382,383,5,34,0, - 0,383,32,1,0,0,0,384,385,5,34,0,0,385,386,5,67,0,0,386,387,5,104, - 0,0,387,388,5,111,0,0,388,389,5,105,0,0,389,390,5,99,0,0,390,391, - 5,101,0,0,391,392,5,34,0,0,392,34,1,0,0,0,393,394,5,34,0,0,394,395, - 5,70,0,0,395,396,5,97,0,0,396,397,5,105,0,0,397,398,5,108,0,0,398, - 399,5,34,0,0,399,36,1,0,0,0,400,401,5,34,0,0,401,402,5,83,0,0,402, - 403,5,117,0,0,403,404,5,99,0,0,404,405,5,99,0,0,405,406,5,101,0, - 0,406,407,5,101,0,0,407,408,5,100,0,0,408,409,5,34,0,0,409,38,1, - 0,0,0,410,411,5,34,0,0,411,412,5,80,0,0,412,413,5,97,0,0,413,414, - 5,115,0,0,414,415,5,115,0,0,415,416,5,34,0,0,416,40,1,0,0,0,417, - 418,5,34,0,0,418,419,5,87,0,0,419,420,5,97,0,0,420,421,5,105,0,0, - 421,422,5,116,0,0,422,423,5,34,0,0,423,42,1,0,0,0,424,425,5,34,0, - 0,425,426,5,80,0,0,426,427,5,97,0,0,427,428,5,114,0,0,428,429,5, - 97,0,0,429,430,5,108,0,0,430,431,5,108,0,0,431,432,5,101,0,0,432, - 433,5,108,0,0,433,434,5,34,0,0,434,44,1,0,0,0,435,436,5,34,0,0,436, - 437,5,77,0,0,437,438,5,97,0,0,438,439,5,112,0,0,439,440,5,34,0,0, - 440,46,1,0,0,0,441,442,5,34,0,0,442,443,5,67,0,0,443,444,5,104,0, - 0,444,445,5,111,0,0,445,446,5,105,0,0,446,447,5,99,0,0,447,448,5, - 101,0,0,448,449,5,115,0,0,449,450,5,34,0,0,450,48,1,0,0,0,451,452, - 5,34,0,0,452,453,5,86,0,0,453,454,5,97,0,0,454,455,5,114,0,0,455, - 456,5,105,0,0,456,457,5,97,0,0,457,458,5,98,0,0,458,459,5,108,0, - 0,459,460,5,101,0,0,460,461,5,34,0,0,461,50,1,0,0,0,462,463,5,34, - 0,0,463,464,5,68,0,0,464,465,5,101,0,0,465,466,5,102,0,0,466,467, - 5,97,0,0,467,468,5,117,0,0,468,469,5,108,0,0,469,470,5,116,0,0,470, - 471,5,34,0,0,471,52,1,0,0,0,472,473,5,34,0,0,473,474,5,66,0,0,474, - 475,5,114,0,0,475,476,5,97,0,0,476,477,5,110,0,0,477,478,5,99,0, - 0,478,479,5,104,0,0,479,480,5,101,0,0,480,481,5,115,0,0,481,482, - 5,34,0,0,482,54,1,0,0,0,483,484,5,34,0,0,484,485,5,65,0,0,485,486, - 5,110,0,0,486,487,5,100,0,0,487,488,5,34,0,0,488,56,1,0,0,0,489, - 490,5,34,0,0,490,491,5,66,0,0,491,492,5,111,0,0,492,493,5,111,0, - 0,493,494,5,108,0,0,494,495,5,101,0,0,495,496,5,97,0,0,496,497,5, - 110,0,0,497,498,5,69,0,0,498,499,5,113,0,0,499,500,5,117,0,0,500, - 501,5,97,0,0,501,502,5,108,0,0,502,503,5,115,0,0,503,504,5,34,0, - 0,504,58,1,0,0,0,505,506,5,34,0,0,506,507,5,66,0,0,507,508,5,111, - 0,0,508,509,5,111,0,0,509,510,5,108,0,0,510,511,5,101,0,0,511,512, - 5,97,0,0,512,513,5,110,0,0,513,514,5,69,0,0,514,515,5,113,0,0,515, - 516,5,117,0,0,516,517,5,97,0,0,517,518,5,108,0,0,518,519,5,115,0, - 0,519,520,5,80,0,0,520,521,5,97,0,0,521,522,5,116,0,0,522,523,5, - 104,0,0,523,524,5,34,0,0,524,60,1,0,0,0,525,526,5,34,0,0,526,527, - 5,73,0,0,527,528,5,115,0,0,528,529,5,66,0,0,529,530,5,111,0,0,530, - 531,5,111,0,0,531,532,5,108,0,0,532,533,5,101,0,0,533,534,5,97,0, - 0,534,535,5,110,0,0,535,536,5,34,0,0,536,62,1,0,0,0,537,538,5,34, - 0,0,538,539,5,73,0,0,539,540,5,115,0,0,540,541,5,78,0,0,541,542, - 5,117,0,0,542,543,5,108,0,0,543,544,5,108,0,0,544,545,5,34,0,0,545, - 64,1,0,0,0,546,547,5,34,0,0,547,548,5,73,0,0,548,549,5,115,0,0,549, - 550,5,78,0,0,550,551,5,117,0,0,551,552,5,109,0,0,552,553,5,101,0, - 0,553,554,5,114,0,0,554,555,5,105,0,0,555,556,5,99,0,0,556,557,5, - 34,0,0,557,66,1,0,0,0,558,559,5,34,0,0,559,560,5,73,0,0,560,561, - 5,115,0,0,561,562,5,80,0,0,562,563,5,114,0,0,563,564,5,101,0,0,564, - 565,5,115,0,0,565,566,5,101,0,0,566,567,5,110,0,0,567,568,5,116, - 0,0,568,569,5,34,0,0,569,68,1,0,0,0,570,571,5,34,0,0,571,572,5,73, - 0,0,572,573,5,115,0,0,573,574,5,83,0,0,574,575,5,116,0,0,575,576, - 5,114,0,0,576,577,5,105,0,0,577,578,5,110,0,0,578,579,5,103,0,0, - 579,580,5,34,0,0,580,70,1,0,0,0,581,582,5,34,0,0,582,583,5,73,0, - 0,583,584,5,115,0,0,584,585,5,84,0,0,585,586,5,105,0,0,586,587,5, - 109,0,0,587,588,5,101,0,0,588,589,5,115,0,0,589,590,5,116,0,0,590, - 591,5,97,0,0,591,592,5,109,0,0,592,593,5,112,0,0,593,594,5,34,0, - 0,594,72,1,0,0,0,595,596,5,34,0,0,596,597,5,78,0,0,597,598,5,111, - 0,0,598,599,5,116,0,0,599,600,5,34,0,0,600,74,1,0,0,0,601,602,5, - 34,0,0,602,603,5,78,0,0,603,604,5,117,0,0,604,605,5,109,0,0,605, - 606,5,101,0,0,606,607,5,114,0,0,607,608,5,105,0,0,608,609,5,99,0, - 0,609,610,5,69,0,0,610,611,5,113,0,0,611,612,5,117,0,0,612,613,5, - 97,0,0,613,614,5,108,0,0,614,615,5,115,0,0,615,616,5,34,0,0,616, - 76,1,0,0,0,617,618,5,34,0,0,618,619,5,78,0,0,619,620,5,117,0,0,620, - 621,5,109,0,0,621,622,5,101,0,0,622,623,5,114,0,0,623,624,5,105, - 0,0,624,625,5,99,0,0,625,626,5,69,0,0,626,627,5,113,0,0,627,628, - 5,117,0,0,628,629,5,97,0,0,629,630,5,108,0,0,630,631,5,115,0,0,631, - 632,5,80,0,0,632,633,5,97,0,0,633,634,5,116,0,0,634,635,5,104,0, - 0,635,636,5,34,0,0,636,78,1,0,0,0,637,638,5,34,0,0,638,639,5,78, - 0,0,639,640,5,117,0,0,640,641,5,109,0,0,641,642,5,101,0,0,642,643, - 5,114,0,0,643,644,5,105,0,0,644,645,5,99,0,0,645,646,5,71,0,0,646, - 647,5,114,0,0,647,648,5,101,0,0,648,649,5,97,0,0,649,650,5,116,0, - 0,650,651,5,101,0,0,651,652,5,114,0,0,652,653,5,84,0,0,653,654,5, - 104,0,0,654,655,5,97,0,0,655,656,5,110,0,0,656,657,5,34,0,0,657, - 80,1,0,0,0,658,659,5,34,0,0,659,660,5,78,0,0,660,661,5,117,0,0,661, - 662,5,109,0,0,662,663,5,101,0,0,663,664,5,114,0,0,664,665,5,105, - 0,0,665,666,5,99,0,0,666,667,5,71,0,0,667,668,5,114,0,0,668,669, - 5,101,0,0,669,670,5,97,0,0,670,671,5,116,0,0,671,672,5,101,0,0,672, - 673,5,114,0,0,673,674,5,84,0,0,674,675,5,104,0,0,675,676,5,97,0, - 0,676,677,5,110,0,0,677,678,5,80,0,0,678,679,5,97,0,0,679,680,5, - 116,0,0,680,681,5,104,0,0,681,682,5,34,0,0,682,82,1,0,0,0,683,684, - 5,34,0,0,684,685,5,78,0,0,685,686,5,117,0,0,686,687,5,109,0,0,687, - 688,5,101,0,0,688,689,5,114,0,0,689,690,5,105,0,0,690,691,5,99,0, - 0,691,692,5,71,0,0,692,693,5,114,0,0,693,694,5,101,0,0,694,695,5, - 97,0,0,695,696,5,116,0,0,696,697,5,101,0,0,697,698,5,114,0,0,698, - 699,5,84,0,0,699,700,5,104,0,0,700,701,5,97,0,0,701,702,5,110,0, - 0,702,703,5,69,0,0,703,704,5,113,0,0,704,705,5,117,0,0,705,706,5, - 97,0,0,706,707,5,108,0,0,707,708,5,115,0,0,708,709,5,34,0,0,709, - 84,1,0,0,0,710,711,5,34,0,0,711,712,5,78,0,0,712,713,5,117,0,0,713, - 714,5,109,0,0,714,715,5,101,0,0,715,716,5,114,0,0,716,717,5,105, - 0,0,717,718,5,99,0,0,718,719,5,71,0,0,719,720,5,114,0,0,720,721, - 5,101,0,0,721,722,5,97,0,0,722,723,5,116,0,0,723,724,5,101,0,0,724, - 725,5,114,0,0,725,726,5,84,0,0,726,727,5,104,0,0,727,728,5,97,0, - 0,728,729,5,110,0,0,729,730,5,69,0,0,730,731,5,113,0,0,731,732,5, - 117,0,0,732,733,5,97,0,0,733,734,5,108,0,0,734,735,5,115,0,0,735, - 736,5,80,0,0,736,737,5,97,0,0,737,738,5,116,0,0,738,739,5,104,0, - 0,739,740,5,34,0,0,740,86,1,0,0,0,741,742,5,34,0,0,742,743,5,78, - 0,0,743,744,5,117,0,0,744,745,5,109,0,0,745,746,5,101,0,0,746,747, - 5,114,0,0,747,748,5,105,0,0,748,749,5,99,0,0,749,750,5,76,0,0,750, - 751,5,101,0,0,751,752,5,115,0,0,752,753,5,115,0,0,753,754,5,84,0, - 0,754,755,5,104,0,0,755,756,5,97,0,0,756,757,5,110,0,0,757,758,5, - 34,0,0,758,88,1,0,0,0,759,760,5,34,0,0,760,761,5,78,0,0,761,762, - 5,117,0,0,762,763,5,109,0,0,763,764,5,101,0,0,764,765,5,114,0,0, - 765,766,5,105,0,0,766,767,5,99,0,0,767,768,5,76,0,0,768,769,5,101, - 0,0,769,770,5,115,0,0,770,771,5,115,0,0,771,772,5,84,0,0,772,773, - 5,104,0,0,773,774,5,97,0,0,774,775,5,110,0,0,775,776,5,80,0,0,776, - 777,5,97,0,0,777,778,5,116,0,0,778,779,5,104,0,0,779,780,5,34,0, - 0,780,90,1,0,0,0,781,782,5,34,0,0,782,783,5,78,0,0,783,784,5,117, - 0,0,784,785,5,109,0,0,785,786,5,101,0,0,786,787,5,114,0,0,787,788, - 5,105,0,0,788,789,5,99,0,0,789,790,5,76,0,0,790,791,5,101,0,0,791, - 792,5,115,0,0,792,793,5,115,0,0,793,794,5,84,0,0,794,795,5,104,0, - 0,795,796,5,97,0,0,796,797,5,110,0,0,797,798,5,69,0,0,798,799,5, - 113,0,0,799,800,5,117,0,0,800,801,5,97,0,0,801,802,5,108,0,0,802, - 803,5,115,0,0,803,804,5,34,0,0,804,92,1,0,0,0,805,806,5,34,0,0,806, - 807,5,78,0,0,807,808,5,117,0,0,808,809,5,109,0,0,809,810,5,101,0, - 0,810,811,5,114,0,0,811,812,5,105,0,0,812,813,5,99,0,0,813,814,5, - 76,0,0,814,815,5,101,0,0,815,816,5,115,0,0,816,817,5,115,0,0,817, - 818,5,84,0,0,818,819,5,104,0,0,819,820,5,97,0,0,820,821,5,110,0, - 0,821,822,5,69,0,0,822,823,5,113,0,0,823,824,5,117,0,0,824,825,5, - 97,0,0,825,826,5,108,0,0,826,827,5,115,0,0,827,828,5,80,0,0,828, - 829,5,97,0,0,829,830,5,116,0,0,830,831,5,104,0,0,831,832,5,34,0, - 0,832,94,1,0,0,0,833,834,5,34,0,0,834,835,5,79,0,0,835,836,5,114, - 0,0,836,837,5,34,0,0,837,96,1,0,0,0,838,839,5,34,0,0,839,840,5,83, - 0,0,840,841,5,116,0,0,841,842,5,114,0,0,842,843,5,105,0,0,843,844, - 5,110,0,0,844,845,5,103,0,0,845,846,5,69,0,0,846,847,5,113,0,0,847, - 848,5,117,0,0,848,849,5,97,0,0,849,850,5,108,0,0,850,851,5,115,0, - 0,851,852,5,34,0,0,852,98,1,0,0,0,853,854,5,34,0,0,854,855,5,83, - 0,0,855,856,5,116,0,0,856,857,5,114,0,0,857,858,5,105,0,0,858,859, - 5,110,0,0,859,860,5,103,0,0,860,861,5,69,0,0,861,862,5,113,0,0,862, - 863,5,117,0,0,863,864,5,97,0,0,864,865,5,108,0,0,865,866,5,115,0, - 0,866,867,5,80,0,0,867,868,5,97,0,0,868,869,5,116,0,0,869,870,5, - 104,0,0,870,871,5,34,0,0,871,100,1,0,0,0,872,873,5,34,0,0,873,874, - 5,83,0,0,874,875,5,116,0,0,875,876,5,114,0,0,876,877,5,105,0,0,877, - 878,5,110,0,0,878,879,5,103,0,0,879,880,5,71,0,0,880,881,5,114,0, - 0,881,882,5,101,0,0,882,883,5,97,0,0,883,884,5,116,0,0,884,885,5, - 101,0,0,885,886,5,114,0,0,886,887,5,84,0,0,887,888,5,104,0,0,888, - 889,5,97,0,0,889,890,5,110,0,0,890,891,5,34,0,0,891,102,1,0,0,0, - 892,893,5,34,0,0,893,894,5,83,0,0,894,895,5,116,0,0,895,896,5,114, - 0,0,896,897,5,105,0,0,897,898,5,110,0,0,898,899,5,103,0,0,899,900, - 5,71,0,0,900,901,5,114,0,0,901,902,5,101,0,0,902,903,5,97,0,0,903, - 904,5,116,0,0,904,905,5,101,0,0,905,906,5,114,0,0,906,907,5,84,0, - 0,907,908,5,104,0,0,908,909,5,97,0,0,909,910,5,110,0,0,910,911,5, - 80,0,0,911,912,5,97,0,0,912,913,5,116,0,0,913,914,5,104,0,0,914, - 915,5,34,0,0,915,104,1,0,0,0,916,917,5,34,0,0,917,918,5,83,0,0,918, - 919,5,116,0,0,919,920,5,114,0,0,920,921,5,105,0,0,921,922,5,110, - 0,0,922,923,5,103,0,0,923,924,5,71,0,0,924,925,5,114,0,0,925,926, - 5,101,0,0,926,927,5,97,0,0,927,928,5,116,0,0,928,929,5,101,0,0,929, - 930,5,114,0,0,930,931,5,84,0,0,931,932,5,104,0,0,932,933,5,97,0, - 0,933,934,5,110,0,0,934,935,5,69,0,0,935,936,5,113,0,0,936,937,5, - 117,0,0,937,938,5,97,0,0,938,939,5,108,0,0,939,940,5,115,0,0,940, - 941,5,34,0,0,941,106,1,0,0,0,942,943,5,34,0,0,943,944,5,83,0,0,944, - 945,5,116,0,0,945,946,5,114,0,0,946,947,5,105,0,0,947,948,5,110, - 0,0,948,949,5,103,0,0,949,950,5,71,0,0,950,951,5,114,0,0,951,952, - 5,101,0,0,952,953,5,97,0,0,953,954,5,116,0,0,954,955,5,101,0,0,955, - 956,5,114,0,0,956,957,5,84,0,0,957,958,5,104,0,0,958,959,5,97,0, - 0,959,960,5,110,0,0,960,961,5,69,0,0,961,962,5,113,0,0,962,963,5, - 117,0,0,963,964,5,97,0,0,964,965,5,108,0,0,965,966,5,115,0,0,966, - 967,5,80,0,0,967,968,5,97,0,0,968,969,5,116,0,0,969,970,5,104,0, - 0,970,971,5,34,0,0,971,108,1,0,0,0,972,973,5,34,0,0,973,974,5,83, - 0,0,974,975,5,116,0,0,975,976,5,114,0,0,976,977,5,105,0,0,977,978, - 5,110,0,0,978,979,5,103,0,0,979,980,5,76,0,0,980,981,5,101,0,0,981, - 982,5,115,0,0,982,983,5,115,0,0,983,984,5,84,0,0,984,985,5,104,0, - 0,985,986,5,97,0,0,986,987,5,110,0,0,987,988,5,34,0,0,988,110,1, - 0,0,0,989,990,5,34,0,0,990,991,5,83,0,0,991,992,5,116,0,0,992,993, - 5,114,0,0,993,994,5,105,0,0,994,995,5,110,0,0,995,996,5,103,0,0, - 996,997,5,76,0,0,997,998,5,101,0,0,998,999,5,115,0,0,999,1000,5, - 115,0,0,1000,1001,5,84,0,0,1001,1002,5,104,0,0,1002,1003,5,97,0, - 0,1003,1004,5,110,0,0,1004,1005,5,80,0,0,1005,1006,5,97,0,0,1006, - 1007,5,116,0,0,1007,1008,5,104,0,0,1008,1009,5,34,0,0,1009,112,1, - 0,0,0,1010,1011,5,34,0,0,1011,1012,5,83,0,0,1012,1013,5,116,0,0, - 1013,1014,5,114,0,0,1014,1015,5,105,0,0,1015,1016,5,110,0,0,1016, - 1017,5,103,0,0,1017,1018,5,76,0,0,1018,1019,5,101,0,0,1019,1020, - 5,115,0,0,1020,1021,5,115,0,0,1021,1022,5,84,0,0,1022,1023,5,104, - 0,0,1023,1024,5,97,0,0,1024,1025,5,110,0,0,1025,1026,5,69,0,0,1026, - 1027,5,113,0,0,1027,1028,5,117,0,0,1028,1029,5,97,0,0,1029,1030, - 5,108,0,0,1030,1031,5,115,0,0,1031,1032,5,34,0,0,1032,114,1,0,0, - 0,1033,1034,5,34,0,0,1034,1035,5,83,0,0,1035,1036,5,116,0,0,1036, - 1037,5,114,0,0,1037,1038,5,105,0,0,1038,1039,5,110,0,0,1039,1040, - 5,103,0,0,1040,1041,5,76,0,0,1041,1042,5,101,0,0,1042,1043,5,115, - 0,0,1043,1044,5,115,0,0,1044,1045,5,84,0,0,1045,1046,5,104,0,0,1046, - 1047,5,97,0,0,1047,1048,5,110,0,0,1048,1049,5,69,0,0,1049,1050,5, - 113,0,0,1050,1051,5,117,0,0,1051,1052,5,97,0,0,1052,1053,5,108,0, - 0,1053,1054,5,115,0,0,1054,1055,5,80,0,0,1055,1056,5,97,0,0,1056, - 1057,5,116,0,0,1057,1058,5,104,0,0,1058,1059,5,34,0,0,1059,116,1, - 0,0,0,1060,1061,5,34,0,0,1061,1062,5,83,0,0,1062,1063,5,116,0,0, - 1063,1064,5,114,0,0,1064,1065,5,105,0,0,1065,1066,5,110,0,0,1066, - 1067,5,103,0,0,1067,1068,5,77,0,0,1068,1069,5,97,0,0,1069,1070,5, - 116,0,0,1070,1071,5,99,0,0,1071,1072,5,104,0,0,1072,1073,5,101,0, - 0,1073,1074,5,115,0,0,1074,1075,5,34,0,0,1075,118,1,0,0,0,1076,1077, - 5,34,0,0,1077,1078,5,84,0,0,1078,1079,5,105,0,0,1079,1080,5,109, - 0,0,1080,1081,5,101,0,0,1081,1082,5,115,0,0,1082,1083,5,116,0,0, - 1083,1084,5,97,0,0,1084,1085,5,109,0,0,1085,1086,5,112,0,0,1086, - 1087,5,69,0,0,1087,1088,5,113,0,0,1088,1089,5,117,0,0,1089,1090, - 5,97,0,0,1090,1091,5,108,0,0,1091,1092,5,115,0,0,1092,1093,5,34, - 0,0,1093,120,1,0,0,0,1094,1095,5,34,0,0,1095,1096,5,84,0,0,1096, - 1097,5,105,0,0,1097,1098,5,109,0,0,1098,1099,5,101,0,0,1099,1100, - 5,115,0,0,1100,1101,5,116,0,0,1101,1102,5,97,0,0,1102,1103,5,109, - 0,0,1103,1104,5,112,0,0,1104,1105,5,69,0,0,1105,1106,5,113,0,0,1106, - 1107,5,117,0,0,1107,1108,5,97,0,0,1108,1109,5,108,0,0,1109,1110, - 5,115,0,0,1110,1111,5,80,0,0,1111,1112,5,97,0,0,1112,1113,5,116, - 0,0,1113,1114,5,104,0,0,1114,1115,5,34,0,0,1115,122,1,0,0,0,1116, - 1117,5,34,0,0,1117,1118,5,84,0,0,1118,1119,5,105,0,0,1119,1120,5, - 109,0,0,1120,1121,5,101,0,0,1121,1122,5,115,0,0,1122,1123,5,116, - 0,0,1123,1124,5,97,0,0,1124,1125,5,109,0,0,1125,1126,5,112,0,0,1126, - 1127,5,71,0,0,1127,1128,5,114,0,0,1128,1129,5,101,0,0,1129,1130, - 5,97,0,0,1130,1131,5,116,0,0,1131,1132,5,101,0,0,1132,1133,5,114, - 0,0,1133,1134,5,84,0,0,1134,1135,5,104,0,0,1135,1136,5,97,0,0,1136, - 1137,5,110,0,0,1137,1138,5,34,0,0,1138,124,1,0,0,0,1139,1140,5,34, - 0,0,1140,1141,5,84,0,0,1141,1142,5,105,0,0,1142,1143,5,109,0,0,1143, - 1144,5,101,0,0,1144,1145,5,115,0,0,1145,1146,5,116,0,0,1146,1147, - 5,97,0,0,1147,1148,5,109,0,0,1148,1149,5,112,0,0,1149,1150,5,71, - 0,0,1150,1151,5,114,0,0,1151,1152,5,101,0,0,1152,1153,5,97,0,0,1153, - 1154,5,116,0,0,1154,1155,5,101,0,0,1155,1156,5,114,0,0,1156,1157, - 5,84,0,0,1157,1158,5,104,0,0,1158,1159,5,97,0,0,1159,1160,5,110, - 0,0,1160,1161,5,80,0,0,1161,1162,5,97,0,0,1162,1163,5,116,0,0,1163, - 1164,5,104,0,0,1164,1165,5,34,0,0,1165,126,1,0,0,0,1166,1167,5,34, - 0,0,1167,1168,5,84,0,0,1168,1169,5,105,0,0,1169,1170,5,109,0,0,1170, - 1171,5,101,0,0,1171,1172,5,115,0,0,1172,1173,5,116,0,0,1173,1174, - 5,97,0,0,1174,1175,5,109,0,0,1175,1176,5,112,0,0,1176,1177,5,71, - 0,0,1177,1178,5,114,0,0,1178,1179,5,101,0,0,1179,1180,5,97,0,0,1180, - 1181,5,116,0,0,1181,1182,5,101,0,0,1182,1183,5,114,0,0,1183,1184, - 5,84,0,0,1184,1185,5,104,0,0,1185,1186,5,97,0,0,1186,1187,5,110, - 0,0,1187,1188,5,69,0,0,1188,1189,5,113,0,0,1189,1190,5,117,0,0,1190, - 1191,5,97,0,0,1191,1192,5,108,0,0,1192,1193,5,115,0,0,1193,1194, - 5,34,0,0,1194,128,1,0,0,0,1195,1196,5,34,0,0,1196,1197,5,84,0,0, - 1197,1198,5,105,0,0,1198,1199,5,109,0,0,1199,1200,5,101,0,0,1200, - 1201,5,115,0,0,1201,1202,5,116,0,0,1202,1203,5,97,0,0,1203,1204, - 5,109,0,0,1204,1205,5,112,0,0,1205,1206,5,71,0,0,1206,1207,5,114, - 0,0,1207,1208,5,101,0,0,1208,1209,5,97,0,0,1209,1210,5,116,0,0,1210, - 1211,5,101,0,0,1211,1212,5,114,0,0,1212,1213,5,84,0,0,1213,1214, - 5,104,0,0,1214,1215,5,97,0,0,1215,1216,5,110,0,0,1216,1217,5,69, - 0,0,1217,1218,5,113,0,0,1218,1219,5,117,0,0,1219,1220,5,97,0,0,1220, - 1221,5,108,0,0,1221,1222,5,115,0,0,1222,1223,5,80,0,0,1223,1224, - 5,97,0,0,1224,1225,5,116,0,0,1225,1226,5,104,0,0,1226,1227,5,34, - 0,0,1227,130,1,0,0,0,1228,1229,5,34,0,0,1229,1230,5,84,0,0,1230, - 1231,5,105,0,0,1231,1232,5,109,0,0,1232,1233,5,101,0,0,1233,1234, - 5,115,0,0,1234,1235,5,116,0,0,1235,1236,5,97,0,0,1236,1237,5,109, - 0,0,1237,1238,5,112,0,0,1238,1239,5,76,0,0,1239,1240,5,101,0,0,1240, - 1241,5,115,0,0,1241,1242,5,115,0,0,1242,1243,5,84,0,0,1243,1244, - 5,104,0,0,1244,1245,5,97,0,0,1245,1246,5,110,0,0,1246,1247,5,34, - 0,0,1247,132,1,0,0,0,1248,1249,5,34,0,0,1249,1250,5,84,0,0,1250, - 1251,5,105,0,0,1251,1252,5,109,0,0,1252,1253,5,101,0,0,1253,1254, - 5,115,0,0,1254,1255,5,116,0,0,1255,1256,5,97,0,0,1256,1257,5,109, - 0,0,1257,1258,5,112,0,0,1258,1259,5,76,0,0,1259,1260,5,101,0,0,1260, - 1261,5,115,0,0,1261,1262,5,115,0,0,1262,1263,5,84,0,0,1263,1264, - 5,104,0,0,1264,1265,5,97,0,0,1265,1266,5,110,0,0,1266,1267,5,80, - 0,0,1267,1268,5,97,0,0,1268,1269,5,116,0,0,1269,1270,5,104,0,0,1270, - 1271,5,34,0,0,1271,134,1,0,0,0,1272,1273,5,34,0,0,1273,1274,5,84, - 0,0,1274,1275,5,105,0,0,1275,1276,5,109,0,0,1276,1277,5,101,0,0, - 1277,1278,5,115,0,0,1278,1279,5,116,0,0,1279,1280,5,97,0,0,1280, - 1281,5,109,0,0,1281,1282,5,112,0,0,1282,1283,5,76,0,0,1283,1284, - 5,101,0,0,1284,1285,5,115,0,0,1285,1286,5,115,0,0,1286,1287,5,84, - 0,0,1287,1288,5,104,0,0,1288,1289,5,97,0,0,1289,1290,5,110,0,0,1290, - 1291,5,69,0,0,1291,1292,5,113,0,0,1292,1293,5,117,0,0,1293,1294, - 5,97,0,0,1294,1295,5,108,0,0,1295,1296,5,115,0,0,1296,1297,5,34, - 0,0,1297,136,1,0,0,0,1298,1299,5,34,0,0,1299,1300,5,84,0,0,1300, - 1301,5,105,0,0,1301,1302,5,109,0,0,1302,1303,5,101,0,0,1303,1304, - 5,115,0,0,1304,1305,5,116,0,0,1305,1306,5,97,0,0,1306,1307,5,109, - 0,0,1307,1308,5,112,0,0,1308,1309,5,76,0,0,1309,1310,5,101,0,0,1310, - 1311,5,115,0,0,1311,1312,5,115,0,0,1312,1313,5,84,0,0,1313,1314, - 5,104,0,0,1314,1315,5,97,0,0,1315,1316,5,110,0,0,1316,1317,5,69, - 0,0,1317,1318,5,113,0,0,1318,1319,5,117,0,0,1319,1320,5,97,0,0,1320, - 1321,5,108,0,0,1321,1322,5,115,0,0,1322,1323,5,80,0,0,1323,1324, - 5,97,0,0,1324,1325,5,116,0,0,1325,1326,5,104,0,0,1326,1327,5,34, - 0,0,1327,138,1,0,0,0,1328,1329,5,34,0,0,1329,1330,5,83,0,0,1330, - 1331,5,101,0,0,1331,1332,5,99,0,0,1332,1333,5,111,0,0,1333,1334, - 5,110,0,0,1334,1335,5,100,0,0,1335,1336,5,115,0,0,1336,1337,5,80, - 0,0,1337,1338,5,97,0,0,1338,1339,5,116,0,0,1339,1340,5,104,0,0,1340, - 1341,5,34,0,0,1341,140,1,0,0,0,1342,1343,5,34,0,0,1343,1344,5,83, - 0,0,1344,1345,5,101,0,0,1345,1346,5,99,0,0,1346,1347,5,111,0,0,1347, - 1348,5,110,0,0,1348,1349,5,100,0,0,1349,1350,5,115,0,0,1350,1351, - 5,34,0,0,1351,142,1,0,0,0,1352,1353,5,34,0,0,1353,1354,5,84,0,0, - 1354,1355,5,105,0,0,1355,1356,5,109,0,0,1356,1357,5,101,0,0,1357, - 1358,5,115,0,0,1358,1359,5,116,0,0,1359,1360,5,97,0,0,1360,1361, - 5,109,0,0,1361,1362,5,112,0,0,1362,1363,5,80,0,0,1363,1364,5,97, - 0,0,1364,1365,5,116,0,0,1365,1366,5,104,0,0,1366,1367,5,34,0,0,1367, - 144,1,0,0,0,1368,1369,5,34,0,0,1369,1370,5,84,0,0,1370,1371,5,105, - 0,0,1371,1372,5,109,0,0,1372,1373,5,101,0,0,1373,1374,5,115,0,0, - 1374,1375,5,116,0,0,1375,1376,5,97,0,0,1376,1377,5,109,0,0,1377, - 1378,5,112,0,0,1378,1379,5,34,0,0,1379,146,1,0,0,0,1380,1381,5,34, - 0,0,1381,1382,5,84,0,0,1382,1383,5,105,0,0,1383,1384,5,109,0,0,1384, - 1385,5,101,0,0,1385,1386,5,111,0,0,1386,1387,5,117,0,0,1387,1388, - 5,116,0,0,1388,1389,5,83,0,0,1389,1390,5,101,0,0,1390,1391,5,99, - 0,0,1391,1392,5,111,0,0,1392,1393,5,110,0,0,1393,1394,5,100,0,0, - 1394,1395,5,115,0,0,1395,1396,5,34,0,0,1396,148,1,0,0,0,1397,1398, - 5,34,0,0,1398,1399,5,84,0,0,1399,1400,5,105,0,0,1400,1401,5,109, - 0,0,1401,1402,5,101,0,0,1402,1403,5,111,0,0,1403,1404,5,117,0,0, - 1404,1405,5,116,0,0,1405,1406,5,83,0,0,1406,1407,5,101,0,0,1407, - 1408,5,99,0,0,1408,1409,5,111,0,0,1409,1410,5,110,0,0,1410,1411, - 5,100,0,0,1411,1412,5,115,0,0,1412,1413,5,80,0,0,1413,1414,5,97, - 0,0,1414,1415,5,116,0,0,1415,1416,5,104,0,0,1416,1417,5,34,0,0,1417, - 150,1,0,0,0,1418,1419,5,34,0,0,1419,1420,5,72,0,0,1420,1421,5,101, - 0,0,1421,1422,5,97,0,0,1422,1423,5,114,0,0,1423,1424,5,116,0,0,1424, - 1425,5,98,0,0,1425,1426,5,101,0,0,1426,1427,5,97,0,0,1427,1428,5, - 116,0,0,1428,1429,5,83,0,0,1429,1430,5,101,0,0,1430,1431,5,99,0, - 0,1431,1432,5,111,0,0,1432,1433,5,110,0,0,1433,1434,5,100,0,0,1434, - 1435,5,115,0,0,1435,1436,5,34,0,0,1436,152,1,0,0,0,1437,1438,5,34, - 0,0,1438,1439,5,72,0,0,1439,1440,5,101,0,0,1440,1441,5,97,0,0,1441, - 1442,5,114,0,0,1442,1443,5,116,0,0,1443,1444,5,98,0,0,1444,1445, - 5,101,0,0,1445,1446,5,97,0,0,1446,1447,5,116,0,0,1447,1448,5,83, - 0,0,1448,1449,5,101,0,0,1449,1450,5,99,0,0,1450,1451,5,111,0,0,1451, - 1452,5,110,0,0,1452,1453,5,100,0,0,1453,1454,5,115,0,0,1454,1455, - 5,80,0,0,1455,1456,5,97,0,0,1456,1457,5,116,0,0,1457,1458,5,104, - 0,0,1458,1459,5,34,0,0,1459,154,1,0,0,0,1460,1461,5,34,0,0,1461, - 1462,5,80,0,0,1462,1463,5,114,0,0,1463,1464,5,111,0,0,1464,1465, - 5,99,0,0,1465,1466,5,101,0,0,1466,1467,5,115,0,0,1467,1468,5,115, - 0,0,1468,1469,5,111,0,0,1469,1470,5,114,0,0,1470,1471,5,67,0,0,1471, - 1472,5,111,0,0,1472,1473,5,110,0,0,1473,1474,5,102,0,0,1474,1475, - 5,105,0,0,1475,1476,5,103,0,0,1476,1477,5,34,0,0,1477,156,1,0,0, - 0,1478,1479,5,34,0,0,1479,1480,5,77,0,0,1480,1481,5,111,0,0,1481, - 1482,5,100,0,0,1482,1483,5,101,0,0,1483,1484,5,34,0,0,1484,158,1, - 0,0,0,1485,1486,5,34,0,0,1486,1487,5,73,0,0,1487,1488,5,78,0,0,1488, - 1489,5,76,0,0,1489,1490,5,73,0,0,1490,1491,5,78,0,0,1491,1492,5, - 69,0,0,1492,1493,5,34,0,0,1493,160,1,0,0,0,1494,1495,5,34,0,0,1495, - 1496,5,68,0,0,1496,1497,5,73,0,0,1497,1498,5,83,0,0,1498,1499,5, - 84,0,0,1499,1500,5,82,0,0,1500,1501,5,73,0,0,1501,1502,5,66,0,0, - 1502,1503,5,85,0,0,1503,1504,5,84,0,0,1504,1505,5,69,0,0,1505,1506, - 5,68,0,0,1506,1507,5,34,0,0,1507,162,1,0,0,0,1508,1509,5,34,0,0, - 1509,1510,5,69,0,0,1510,1511,5,120,0,0,1511,1512,5,101,0,0,1512, - 1513,5,99,0,0,1513,1514,5,117,0,0,1514,1515,5,116,0,0,1515,1516, - 5,105,0,0,1516,1517,5,111,0,0,1517,1518,5,110,0,0,1518,1519,5,84, - 0,0,1519,1520,5,121,0,0,1520,1521,5,112,0,0,1521,1522,5,101,0,0, - 1522,1523,5,34,0,0,1523,164,1,0,0,0,1524,1525,5,34,0,0,1525,1526, - 5,83,0,0,1526,1527,5,84,0,0,1527,1528,5,65,0,0,1528,1529,5,78,0, - 0,1529,1530,5,68,0,0,1530,1531,5,65,0,0,1531,1532,5,82,0,0,1532, - 1533,5,68,0,0,1533,1534,5,34,0,0,1534,166,1,0,0,0,1535,1536,5,34, - 0,0,1536,1537,5,73,0,0,1537,1538,5,116,0,0,1538,1539,5,101,0,0,1539, - 1540,5,109,0,0,1540,1541,5,80,0,0,1541,1542,5,114,0,0,1542,1543, - 5,111,0,0,1543,1544,5,99,0,0,1544,1545,5,101,0,0,1545,1546,5,115, - 0,0,1546,1547,5,115,0,0,1547,1548,5,111,0,0,1548,1549,5,114,0,0, - 1549,1550,5,34,0,0,1550,168,1,0,0,0,1551,1552,5,34,0,0,1552,1553, - 5,73,0,0,1553,1554,5,116,0,0,1554,1555,5,101,0,0,1555,1556,5,114, - 0,0,1556,1557,5,97,0,0,1557,1558,5,116,0,0,1558,1559,5,111,0,0,1559, - 1560,5,114,0,0,1560,1561,5,34,0,0,1561,170,1,0,0,0,1562,1563,5,34, - 0,0,1563,1564,5,73,0,0,1564,1565,5,116,0,0,1565,1566,5,101,0,0,1566, - 1567,5,109,0,0,1567,1568,5,83,0,0,1568,1569,5,101,0,0,1569,1570, - 5,108,0,0,1570,1571,5,101,0,0,1571,1572,5,99,0,0,1572,1573,5,116, - 0,0,1573,1574,5,111,0,0,1574,1575,5,114,0,0,1575,1576,5,34,0,0,1576, - 172,1,0,0,0,1577,1578,5,34,0,0,1578,1579,5,77,0,0,1579,1580,5,97, - 0,0,1580,1581,5,120,0,0,1581,1582,5,67,0,0,1582,1583,5,111,0,0,1583, - 1584,5,110,0,0,1584,1585,5,99,0,0,1585,1586,5,117,0,0,1586,1587, - 5,114,0,0,1587,1588,5,114,0,0,1588,1589,5,101,0,0,1589,1590,5,110, - 0,0,1590,1591,5,99,0,0,1591,1592,5,121,0,0,1592,1593,5,34,0,0,1593, - 174,1,0,0,0,1594,1595,5,34,0,0,1595,1596,5,82,0,0,1596,1597,5,101, - 0,0,1597,1598,5,115,0,0,1598,1599,5,111,0,0,1599,1600,5,117,0,0, - 1600,1601,5,114,0,0,1601,1602,5,99,0,0,1602,1603,5,101,0,0,1603, - 1604,5,34,0,0,1604,176,1,0,0,0,1605,1606,5,34,0,0,1606,1607,5,73, - 0,0,1607,1608,5,110,0,0,1608,1609,5,112,0,0,1609,1610,5,117,0,0, - 1610,1611,5,116,0,0,1611,1612,5,80,0,0,1612,1613,5,97,0,0,1613,1614, - 5,116,0,0,1614,1615,5,104,0,0,1615,1616,5,34,0,0,1616,178,1,0,0, - 0,1617,1618,5,34,0,0,1618,1619,5,79,0,0,1619,1620,5,117,0,0,1620, - 1621,5,116,0,0,1621,1622,5,112,0,0,1622,1623,5,117,0,0,1623,1624, - 5,116,0,0,1624,1625,5,80,0,0,1625,1626,5,97,0,0,1626,1627,5,116, - 0,0,1627,1628,5,104,0,0,1628,1629,5,34,0,0,1629,180,1,0,0,0,1630, - 1631,5,34,0,0,1631,1632,5,73,0,0,1632,1633,5,116,0,0,1633,1634,5, - 101,0,0,1634,1635,5,109,0,0,1635,1636,5,115,0,0,1636,1637,5,80,0, - 0,1637,1638,5,97,0,0,1638,1639,5,116,0,0,1639,1640,5,104,0,0,1640, - 1641,5,34,0,0,1641,182,1,0,0,0,1642,1643,5,34,0,0,1643,1644,5,82, - 0,0,1644,1645,5,101,0,0,1645,1646,5,115,0,0,1646,1647,5,117,0,0, - 1647,1648,5,108,0,0,1648,1649,5,116,0,0,1649,1650,5,80,0,0,1650, - 1651,5,97,0,0,1651,1652,5,116,0,0,1652,1653,5,104,0,0,1653,1654, - 5,34,0,0,1654,184,1,0,0,0,1655,1656,5,34,0,0,1656,1657,5,82,0,0, - 1657,1658,5,101,0,0,1658,1659,5,115,0,0,1659,1660,5,117,0,0,1660, - 1661,5,108,0,0,1661,1662,5,116,0,0,1662,1663,5,34,0,0,1663,186,1, - 0,0,0,1664,1665,5,34,0,0,1665,1666,5,80,0,0,1666,1667,5,97,0,0,1667, - 1668,5,114,0,0,1668,1669,5,97,0,0,1669,1670,5,109,0,0,1670,1671, - 5,101,0,0,1671,1672,5,116,0,0,1672,1673,5,101,0,0,1673,1674,5,114, - 0,0,1674,1675,5,115,0,0,1675,1676,5,34,0,0,1676,188,1,0,0,0,1677, - 1678,5,34,0,0,1678,1679,5,82,0,0,1679,1680,5,101,0,0,1680,1681,5, - 115,0,0,1681,1682,5,117,0,0,1682,1683,5,108,0,0,1683,1684,5,116, - 0,0,1684,1685,5,83,0,0,1685,1686,5,101,0,0,1686,1687,5,108,0,0,1687, - 1688,5,101,0,0,1688,1689,5,99,0,0,1689,1690,5,116,0,0,1690,1691, - 5,111,0,0,1691,1692,5,114,0,0,1692,1693,5,34,0,0,1693,190,1,0,0, - 0,1694,1695,5,34,0,0,1695,1696,5,73,0,0,1696,1697,5,116,0,0,1697, - 1698,5,101,0,0,1698,1699,5,109,0,0,1699,1700,5,82,0,0,1700,1701, - 5,101,0,0,1701,1702,5,97,0,0,1702,1703,5,100,0,0,1703,1704,5,101, - 0,0,1704,1705,5,114,0,0,1705,1706,5,34,0,0,1706,192,1,0,0,0,1707, - 1708,5,34,0,0,1708,1709,5,82,0,0,1709,1710,5,101,0,0,1710,1711,5, - 97,0,0,1711,1712,5,100,0,0,1712,1713,5,101,0,0,1713,1714,5,114,0, - 0,1714,1715,5,67,0,0,1715,1716,5,111,0,0,1716,1717,5,110,0,0,1717, - 1718,5,102,0,0,1718,1719,5,105,0,0,1719,1720,5,103,0,0,1720,1721, - 5,34,0,0,1721,194,1,0,0,0,1722,1723,5,34,0,0,1723,1724,5,73,0,0, - 1724,1725,5,110,0,0,1725,1726,5,112,0,0,1726,1727,5,117,0,0,1727, - 1728,5,116,0,0,1728,1729,5,84,0,0,1729,1730,5,121,0,0,1730,1731, - 5,112,0,0,1731,1732,5,101,0,0,1732,1733,5,34,0,0,1733,196,1,0,0, - 0,1734,1735,5,34,0,0,1735,1736,5,67,0,0,1736,1737,5,83,0,0,1737, - 1738,5,86,0,0,1738,1739,5,72,0,0,1739,1740,5,101,0,0,1740,1741,5, - 97,0,0,1741,1742,5,100,0,0,1742,1743,5,101,0,0,1743,1744,5,114,0, - 0,1744,1745,5,76,0,0,1745,1746,5,111,0,0,1746,1747,5,99,0,0,1747, - 1748,5,97,0,0,1748,1749,5,116,0,0,1749,1750,5,105,0,0,1750,1751, - 5,111,0,0,1751,1752,5,110,0,0,1752,1753,5,34,0,0,1753,198,1,0,0, - 0,1754,1755,5,34,0,0,1755,1756,5,67,0,0,1756,1757,5,83,0,0,1757, - 1758,5,86,0,0,1758,1759,5,72,0,0,1759,1760,5,101,0,0,1760,1761,5, - 97,0,0,1761,1762,5,100,0,0,1762,1763,5,101,0,0,1763,1764,5,114,0, - 0,1764,1765,5,115,0,0,1765,1766,5,34,0,0,1766,200,1,0,0,0,1767,1768, - 5,34,0,0,1768,1769,5,77,0,0,1769,1770,5,97,0,0,1770,1771,5,120,0, - 0,1771,1772,5,73,0,0,1772,1773,5,116,0,0,1773,1774,5,101,0,0,1774, - 1775,5,109,0,0,1775,1776,5,115,0,0,1776,1777,5,34,0,0,1777,202,1, - 0,0,0,1778,1779,5,34,0,0,1779,1780,5,77,0,0,1780,1781,5,97,0,0,1781, - 1782,5,120,0,0,1782,1783,5,73,0,0,1783,1784,5,116,0,0,1784,1785, - 5,101,0,0,1785,1786,5,109,0,0,1786,1787,5,115,0,0,1787,1788,5,80, - 0,0,1788,1789,5,97,0,0,1789,1790,5,116,0,0,1790,1791,5,104,0,0,1791, - 1792,5,34,0,0,1792,204,1,0,0,0,1793,1794,5,34,0,0,1794,1795,5,78, - 0,0,1795,1796,5,101,0,0,1796,1797,5,120,0,0,1797,1798,5,116,0,0, - 1798,1799,5,34,0,0,1799,206,1,0,0,0,1800,1801,5,34,0,0,1801,1802, - 5,69,0,0,1802,1803,5,110,0,0,1803,1804,5,100,0,0,1804,1805,5,34, - 0,0,1805,208,1,0,0,0,1806,1807,5,34,0,0,1807,1808,5,67,0,0,1808, - 1809,5,97,0,0,1809,1810,5,117,0,0,1810,1811,5,115,0,0,1811,1812, - 5,101,0,0,1812,1813,5,34,0,0,1813,210,1,0,0,0,1814,1815,5,34,0,0, - 1815,1816,5,67,0,0,1816,1817,5,97,0,0,1817,1818,5,117,0,0,1818,1819, - 5,115,0,0,1819,1820,5,101,0,0,1820,1821,5,80,0,0,1821,1822,5,97, - 0,0,1822,1823,5,116,0,0,1823,1824,5,104,0,0,1824,1825,5,34,0,0,1825, - 212,1,0,0,0,1826,1827,5,34,0,0,1827,1828,5,69,0,0,1828,1829,5,114, - 0,0,1829,1830,5,114,0,0,1830,1831,5,111,0,0,1831,1832,5,114,0,0, - 1832,1833,5,34,0,0,1833,214,1,0,0,0,1834,1835,5,34,0,0,1835,1836, - 5,69,0,0,1836,1837,5,114,0,0,1837,1838,5,114,0,0,1838,1839,5,111, - 0,0,1839,1840,5,114,0,0,1840,1841,5,80,0,0,1841,1842,5,97,0,0,1842, - 1843,5,116,0,0,1843,1844,5,104,0,0,1844,1845,5,34,0,0,1845,216,1, - 0,0,0,1846,1847,5,34,0,0,1847,1848,5,82,0,0,1848,1849,5,101,0,0, - 1849,1850,5,116,0,0,1850,1851,5,114,0,0,1851,1852,5,121,0,0,1852, - 1853,5,34,0,0,1853,218,1,0,0,0,1854,1855,5,34,0,0,1855,1856,5,69, - 0,0,1856,1857,5,114,0,0,1857,1858,5,114,0,0,1858,1859,5,111,0,0, - 1859,1860,5,114,0,0,1860,1861,5,69,0,0,1861,1862,5,113,0,0,1862, - 1863,5,117,0,0,1863,1864,5,97,0,0,1864,1865,5,108,0,0,1865,1866, - 5,115,0,0,1866,1867,5,34,0,0,1867,220,1,0,0,0,1868,1869,5,34,0,0, - 1869,1870,5,73,0,0,1870,1871,5,110,0,0,1871,1872,5,116,0,0,1872, - 1873,5,101,0,0,1873,1874,5,114,0,0,1874,1875,5,118,0,0,1875,1876, - 5,97,0,0,1876,1877,5,108,0,0,1877,1878,5,83,0,0,1878,1879,5,101, - 0,0,1879,1880,5,99,0,0,1880,1881,5,111,0,0,1881,1882,5,110,0,0,1882, - 1883,5,100,0,0,1883,1884,5,115,0,0,1884,1885,5,34,0,0,1885,222,1, - 0,0,0,1886,1887,5,34,0,0,1887,1888,5,77,0,0,1888,1889,5,97,0,0,1889, - 1890,5,120,0,0,1890,1891,5,65,0,0,1891,1892,5,116,0,0,1892,1893, - 5,116,0,0,1893,1894,5,101,0,0,1894,1895,5,109,0,0,1895,1896,5,112, - 0,0,1896,1897,5,116,0,0,1897,1898,5,115,0,0,1898,1899,5,34,0,0,1899, - 224,1,0,0,0,1900,1901,5,34,0,0,1901,1902,5,66,0,0,1902,1903,5,97, - 0,0,1903,1904,5,99,0,0,1904,1905,5,107,0,0,1905,1906,5,111,0,0,1906, - 1907,5,102,0,0,1907,1908,5,102,0,0,1908,1909,5,82,0,0,1909,1910, - 5,97,0,0,1910,1911,5,116,0,0,1911,1912,5,101,0,0,1912,1913,5,34, - 0,0,1913,226,1,0,0,0,1914,1915,5,34,0,0,1915,1916,5,77,0,0,1916, - 1917,5,97,0,0,1917,1918,5,120,0,0,1918,1919,5,68,0,0,1919,1920,5, - 101,0,0,1920,1921,5,108,0,0,1921,1922,5,97,0,0,1922,1923,5,121,0, - 0,1923,1924,5,83,0,0,1924,1925,5,101,0,0,1925,1926,5,99,0,0,1926, - 1927,5,111,0,0,1927,1928,5,110,0,0,1928,1929,5,100,0,0,1929,1930, - 5,115,0,0,1930,1931,5,34,0,0,1931,228,1,0,0,0,1932,1933,5,34,0,0, - 1933,1934,5,74,0,0,1934,1935,5,105,0,0,1935,1936,5,116,0,0,1936, - 1937,5,116,0,0,1937,1938,5,101,0,0,1938,1939,5,114,0,0,1939,1940, - 5,83,0,0,1940,1941,5,116,0,0,1941,1942,5,114,0,0,1942,1943,5,97, - 0,0,1943,1944,5,116,0,0,1944,1945,5,101,0,0,1945,1946,5,103,0,0, - 1946,1947,5,121,0,0,1947,1948,5,34,0,0,1948,230,1,0,0,0,1949,1950, - 5,34,0,0,1950,1951,5,70,0,0,1951,1952,5,85,0,0,1952,1953,5,76,0, - 0,1953,1954,5,76,0,0,1954,1955,5,34,0,0,1955,232,1,0,0,0,1956,1957, - 5,34,0,0,1957,1958,5,78,0,0,1958,1959,5,79,0,0,1959,1960,5,78,0, - 0,1960,1961,5,69,0,0,1961,1962,5,34,0,0,1962,234,1,0,0,0,1963,1964, - 5,34,0,0,1964,1965,5,67,0,0,1965,1966,5,97,0,0,1966,1967,5,116,0, - 0,1967,1968,5,99,0,0,1968,1969,5,104,0,0,1969,1970,5,34,0,0,1970, - 236,1,0,0,0,1971,1972,5,34,0,0,1972,1973,5,83,0,0,1973,1974,5,116, - 0,0,1974,1975,5,97,0,0,1975,1976,5,116,0,0,1976,1977,5,101,0,0,1977, - 1978,5,115,0,0,1978,1979,5,46,0,0,1979,1980,5,65,0,0,1980,1981,5, - 76,0,0,1981,1982,5,76,0,0,1982,1983,5,34,0,0,1983,238,1,0,0,0,1984, - 1985,5,34,0,0,1985,1986,5,83,0,0,1986,1987,5,116,0,0,1987,1988,5, - 97,0,0,1988,1989,5,116,0,0,1989,1990,5,101,0,0,1990,1991,5,115,0, - 0,1991,1992,5,46,0,0,1992,1993,5,68,0,0,1993,1994,5,97,0,0,1994, - 1995,5,116,0,0,1995,1996,5,97,0,0,1996,1997,5,76,0,0,1997,1998,5, - 105,0,0,1998,1999,5,109,0,0,1999,2000,5,105,0,0,2000,2001,5,116, - 0,0,2001,2002,5,69,0,0,2002,2003,5,120,0,0,2003,2004,5,99,0,0,2004, - 2005,5,101,0,0,2005,2006,5,101,0,0,2006,2007,5,100,0,0,2007,2008, - 5,101,0,0,2008,2009,5,100,0,0,2009,2010,5,34,0,0,2010,240,1,0,0, - 0,2011,2012,5,34,0,0,2012,2013,5,83,0,0,2013,2014,5,116,0,0,2014, - 2015,5,97,0,0,2015,2016,5,116,0,0,2016,2017,5,101,0,0,2017,2018, - 5,115,0,0,2018,2019,5,46,0,0,2019,2020,5,72,0,0,2020,2021,5,101, - 0,0,2021,2022,5,97,0,0,2022,2023,5,114,0,0,2023,2024,5,116,0,0,2024, - 2025,5,98,0,0,2025,2026,5,101,0,0,2026,2027,5,97,0,0,2027,2028,5, - 116,0,0,2028,2029,5,84,0,0,2029,2030,5,105,0,0,2030,2031,5,109,0, - 0,2031,2032,5,101,0,0,2032,2033,5,111,0,0,2033,2034,5,117,0,0,2034, - 2035,5,116,0,0,2035,2036,5,34,0,0,2036,242,1,0,0,0,2037,2038,5,34, - 0,0,2038,2039,5,83,0,0,2039,2040,5,116,0,0,2040,2041,5,97,0,0,2041, - 2042,5,116,0,0,2042,2043,5,101,0,0,2043,2044,5,115,0,0,2044,2045, - 5,46,0,0,2045,2046,5,84,0,0,2046,2047,5,105,0,0,2047,2048,5,109, - 0,0,2048,2049,5,101,0,0,2049,2050,5,111,0,0,2050,2051,5,117,0,0, - 2051,2052,5,116,0,0,2052,2053,5,34,0,0,2053,244,1,0,0,0,2054,2055, - 5,34,0,0,2055,2056,5,83,0,0,2056,2057,5,116,0,0,2057,2058,5,97,0, - 0,2058,2059,5,116,0,0,2059,2060,5,101,0,0,2060,2061,5,115,0,0,2061, - 2062,5,46,0,0,2062,2063,5,84,0,0,2063,2064,5,97,0,0,2064,2065,5, - 115,0,0,2065,2066,5,107,0,0,2066,2067,5,70,0,0,2067,2068,5,97,0, - 0,2068,2069,5,105,0,0,2069,2070,5,108,0,0,2070,2071,5,101,0,0,2071, - 2072,5,100,0,0,2072,2073,5,34,0,0,2073,246,1,0,0,0,2074,2075,5,34, - 0,0,2075,2076,5,83,0,0,2076,2077,5,116,0,0,2077,2078,5,97,0,0,2078, - 2079,5,116,0,0,2079,2080,5,101,0,0,2080,2081,5,115,0,0,2081,2082, - 5,46,0,0,2082,2083,5,80,0,0,2083,2084,5,101,0,0,2084,2085,5,114, - 0,0,2085,2086,5,109,0,0,2086,2087,5,105,0,0,2087,2088,5,115,0,0, - 2088,2089,5,115,0,0,2089,2090,5,105,0,0,2090,2091,5,111,0,0,2091, - 2092,5,110,0,0,2092,2093,5,115,0,0,2093,2094,5,34,0,0,2094,248,1, - 0,0,0,2095,2096,5,34,0,0,2096,2097,5,83,0,0,2097,2098,5,116,0,0, - 2098,2099,5,97,0,0,2099,2100,5,116,0,0,2100,2101,5,101,0,0,2101, - 2102,5,115,0,0,2102,2103,5,46,0,0,2103,2104,5,82,0,0,2104,2105,5, - 101,0,0,2105,2106,5,115,0,0,2106,2107,5,117,0,0,2107,2108,5,108, - 0,0,2108,2109,5,116,0,0,2109,2110,5,80,0,0,2110,2111,5,97,0,0,2111, - 2112,5,116,0,0,2112,2113,5,104,0,0,2113,2114,5,77,0,0,2114,2115, - 5,97,0,0,2115,2116,5,116,0,0,2116,2117,5,99,0,0,2117,2118,5,104, - 0,0,2118,2119,5,70,0,0,2119,2120,5,97,0,0,2120,2121,5,105,0,0,2121, - 2122,5,108,0,0,2122,2123,5,117,0,0,2123,2124,5,114,0,0,2124,2125, - 5,101,0,0,2125,2126,5,34,0,0,2126,250,1,0,0,0,2127,2128,5,34,0,0, - 2128,2129,5,83,0,0,2129,2130,5,116,0,0,2130,2131,5,97,0,0,2131,2132, - 5,116,0,0,2132,2133,5,101,0,0,2133,2134,5,115,0,0,2134,2135,5,46, - 0,0,2135,2136,5,80,0,0,2136,2137,5,97,0,0,2137,2138,5,114,0,0,2138, - 2139,5,97,0,0,2139,2140,5,109,0,0,2140,2141,5,101,0,0,2141,2142, - 5,116,0,0,2142,2143,5,101,0,0,2143,2144,5,114,0,0,2144,2145,5,80, - 0,0,2145,2146,5,97,0,0,2146,2147,5,116,0,0,2147,2148,5,104,0,0,2148, - 2149,5,70,0,0,2149,2150,5,97,0,0,2150,2151,5,105,0,0,2151,2152,5, - 108,0,0,2152,2153,5,117,0,0,2153,2154,5,114,0,0,2154,2155,5,101, - 0,0,2155,2156,5,34,0,0,2156,252,1,0,0,0,2157,2158,5,34,0,0,2158, - 2159,5,83,0,0,2159,2160,5,116,0,0,2160,2161,5,97,0,0,2161,2162,5, - 116,0,0,2162,2163,5,101,0,0,2163,2164,5,115,0,0,2164,2165,5,46,0, - 0,2165,2166,5,66,0,0,2166,2167,5,114,0,0,2167,2168,5,97,0,0,2168, - 2169,5,110,0,0,2169,2170,5,99,0,0,2170,2171,5,104,0,0,2171,2172, - 5,70,0,0,2172,2173,5,97,0,0,2173,2174,5,105,0,0,2174,2175,5,108, - 0,0,2175,2176,5,101,0,0,2176,2177,5,100,0,0,2177,2178,5,34,0,0,2178, - 254,1,0,0,0,2179,2180,5,34,0,0,2180,2181,5,83,0,0,2181,2182,5,116, - 0,0,2182,2183,5,97,0,0,2183,2184,5,116,0,0,2184,2185,5,101,0,0,2185, - 2186,5,115,0,0,2186,2187,5,46,0,0,2187,2188,5,78,0,0,2188,2189,5, - 111,0,0,2189,2190,5,67,0,0,2190,2191,5,104,0,0,2191,2192,5,111,0, - 0,2192,2193,5,105,0,0,2193,2194,5,99,0,0,2194,2195,5,101,0,0,2195, - 2196,5,77,0,0,2196,2197,5,97,0,0,2197,2198,5,116,0,0,2198,2199,5, - 99,0,0,2199,2200,5,104,0,0,2200,2201,5,101,0,0,2201,2202,5,100,0, - 0,2202,2203,5,34,0,0,2203,256,1,0,0,0,2204,2205,5,34,0,0,2205,2206, - 5,83,0,0,2206,2207,5,116,0,0,2207,2208,5,97,0,0,2208,2209,5,116, - 0,0,2209,2210,5,101,0,0,2210,2211,5,115,0,0,2211,2212,5,46,0,0,2212, - 2213,5,73,0,0,2213,2214,5,110,0,0,2214,2215,5,116,0,0,2215,2216, - 5,114,0,0,2216,2217,5,105,0,0,2217,2218,5,110,0,0,2218,2219,5,115, - 0,0,2219,2220,5,105,0,0,2220,2221,5,99,0,0,2221,2222,5,70,0,0,2222, - 2223,5,97,0,0,2223,2224,5,105,0,0,2224,2225,5,108,0,0,2225,2226, - 5,117,0,0,2226,2227,5,114,0,0,2227,2228,5,101,0,0,2228,2229,5,34, - 0,0,2229,258,1,0,0,0,2230,2231,5,34,0,0,2231,2232,5,83,0,0,2232, - 2233,5,116,0,0,2233,2234,5,97,0,0,2234,2235,5,116,0,0,2235,2236, - 5,101,0,0,2236,2237,5,115,0,0,2237,2238,5,46,0,0,2238,2239,5,69, - 0,0,2239,2240,5,120,0,0,2240,2241,5,99,0,0,2241,2242,5,101,0,0,2242, - 2243,5,101,0,0,2243,2244,5,100,0,0,2244,2245,5,84,0,0,2245,2246, - 5,111,0,0,2246,2247,5,108,0,0,2247,2248,5,101,0,0,2248,2249,5,114, - 0,0,2249,2250,5,97,0,0,2250,2251,5,116,0,0,2251,2252,5,101,0,0,2252, - 2253,5,100,0,0,2253,2254,5,70,0,0,2254,2255,5,97,0,0,2255,2256,5, - 105,0,0,2256,2257,5,108,0,0,2257,2258,5,117,0,0,2258,2259,5,114, - 0,0,2259,2260,5,101,0,0,2260,2261,5,84,0,0,2261,2262,5,104,0,0,2262, - 2263,5,114,0,0,2263,2264,5,101,0,0,2264,2265,5,115,0,0,2265,2266, - 5,104,0,0,2266,2267,5,111,0,0,2267,2268,5,108,0,0,2268,2269,5,100, - 0,0,2269,2270,5,34,0,0,2270,260,1,0,0,0,2271,2272,5,34,0,0,2272, - 2273,5,83,0,0,2273,2274,5,116,0,0,2274,2275,5,97,0,0,2275,2276,5, - 116,0,0,2276,2277,5,101,0,0,2277,2278,5,115,0,0,2278,2279,5,46,0, - 0,2279,2280,5,73,0,0,2280,2281,5,116,0,0,2281,2282,5,101,0,0,2282, - 2283,5,109,0,0,2283,2284,5,82,0,0,2284,2285,5,101,0,0,2285,2286, - 5,97,0,0,2286,2287,5,100,0,0,2287,2288,5,101,0,0,2288,2289,5,114, - 0,0,2289,2290,5,70,0,0,2290,2291,5,97,0,0,2291,2292,5,105,0,0,2292, - 2293,5,108,0,0,2293,2294,5,101,0,0,2294,2295,5,100,0,0,2295,2296, - 5,34,0,0,2296,262,1,0,0,0,2297,2298,5,34,0,0,2298,2299,5,83,0,0, - 2299,2300,5,116,0,0,2300,2301,5,97,0,0,2301,2302,5,116,0,0,2302, - 2303,5,101,0,0,2303,2304,5,115,0,0,2304,2305,5,46,0,0,2305,2306, - 5,82,0,0,2306,2307,5,101,0,0,2307,2308,5,115,0,0,2308,2309,5,117, - 0,0,2309,2310,5,108,0,0,2310,2311,5,116,0,0,2311,2312,5,87,0,0,2312, - 2313,5,114,0,0,2313,2314,5,105,0,0,2314,2315,5,116,0,0,2315,2316, - 5,101,0,0,2316,2317,5,114,0,0,2317,2318,5,70,0,0,2318,2319,5,97, - 0,0,2319,2320,5,105,0,0,2320,2321,5,108,0,0,2321,2322,5,101,0,0, - 2322,2323,5,100,0,0,2323,2324,5,34,0,0,2324,264,1,0,0,0,2325,2326, - 5,34,0,0,2326,2327,5,83,0,0,2327,2328,5,116,0,0,2328,2329,5,97,0, - 0,2329,2330,5,116,0,0,2330,2331,5,101,0,0,2331,2332,5,115,0,0,2332, - 2333,5,46,0,0,2333,2334,5,82,0,0,2334,2335,5,117,0,0,2335,2336,5, - 110,0,0,2336,2337,5,116,0,0,2337,2338,5,105,0,0,2338,2339,5,109, - 0,0,2339,2340,5,101,0,0,2340,2341,5,34,0,0,2341,266,1,0,0,0,2342, - 2347,5,34,0,0,2343,2346,3,275,137,0,2344,2346,3,281,140,0,2345,2343, - 1,0,0,0,2345,2344,1,0,0,0,2346,2349,1,0,0,0,2347,2345,1,0,0,0,2347, - 2348,1,0,0,0,2348,2350,1,0,0,0,2349,2347,1,0,0,0,2350,2351,5,46, - 0,0,2351,2352,5,36,0,0,2352,2353,5,34,0,0,2353,268,1,0,0,0,2354, - 2355,5,34,0,0,2355,2356,5,36,0,0,2356,2357,5,36,0,0,2357,2362,1, - 0,0,0,2358,2361,3,275,137,0,2359,2361,3,281,140,0,2360,2358,1,0, - 0,0,2360,2359,1,0,0,0,2361,2364,1,0,0,0,2362,2360,1,0,0,0,2362,2363, - 1,0,0,0,2363,2365,1,0,0,0,2364,2362,1,0,0,0,2365,2366,5,34,0,0,2366, - 270,1,0,0,0,2367,2368,5,34,0,0,2368,2369,5,36,0,0,2369,2374,1,0, - 0,0,2370,2373,3,275,137,0,2371,2373,3,281,140,0,2372,2370,1,0,0, - 0,2372,2371,1,0,0,0,2373,2376,1,0,0,0,2374,2372,1,0,0,0,2374,2375, - 1,0,0,0,2375,2377,1,0,0,0,2376,2374,1,0,0,0,2377,2378,5,34,0,0,2378, - 272,1,0,0,0,2379,2384,5,34,0,0,2380,2383,3,275,137,0,2381,2383,3, - 281,140,0,2382,2380,1,0,0,0,2382,2381,1,0,0,0,2383,2386,1,0,0,0, - 2384,2382,1,0,0,0,2384,2385,1,0,0,0,2385,2387,1,0,0,0,2386,2384, - 1,0,0,0,2387,2388,5,34,0,0,2388,274,1,0,0,0,2389,2392,5,92,0,0,2390, - 2393,7,0,0,0,2391,2393,3,277,138,0,2392,2390,1,0,0,0,2392,2391,1, - 0,0,0,2393,276,1,0,0,0,2394,2395,5,117,0,0,2395,2396,3,279,139,0, - 2396,2397,3,279,139,0,2397,2398,3,279,139,0,2398,2399,3,279,139, - 0,2399,278,1,0,0,0,2400,2401,7,1,0,0,2401,280,1,0,0,0,2402,2403, - 8,2,0,0,2403,282,1,0,0,0,2404,2413,5,48,0,0,2405,2409,7,3,0,0,2406, - 2408,7,4,0,0,2407,2406,1,0,0,0,2408,2411,1,0,0,0,2409,2407,1,0,0, - 0,2409,2410,1,0,0,0,2410,2413,1,0,0,0,2411,2409,1,0,0,0,2412,2404, - 1,0,0,0,2412,2405,1,0,0,0,2413,284,1,0,0,0,2414,2416,5,45,0,0,2415, - 2414,1,0,0,0,2415,2416,1,0,0,0,2416,2417,1,0,0,0,2417,2424,3,283, - 141,0,2418,2420,5,46,0,0,2419,2421,7,4,0,0,2420,2419,1,0,0,0,2421, - 2422,1,0,0,0,2422,2420,1,0,0,0,2422,2423,1,0,0,0,2423,2425,1,0,0, - 0,2424,2418,1,0,0,0,2424,2425,1,0,0,0,2425,2427,1,0,0,0,2426,2428, - 3,287,143,0,2427,2426,1,0,0,0,2427,2428,1,0,0,0,2428,286,1,0,0,0, - 2429,2431,7,5,0,0,2430,2432,7,6,0,0,2431,2430,1,0,0,0,2431,2432, - 1,0,0,0,2432,2433,1,0,0,0,2433,2434,3,283,141,0,2434,288,1,0,0,0, - 2435,2437,7,7,0,0,2436,2435,1,0,0,0,2437,2438,1,0,0,0,2438,2436, - 1,0,0,0,2438,2439,1,0,0,0,2439,2440,1,0,0,0,2440,2441,6,144,0,0, - 2441,290,1,0,0,0,18,0,2345,2347,2360,2362,2372,2374,2382,2384,2392, - 2409,2412,2415,2422,2424,2427,2431,2438,1,6,0,0 + 273,137,275,138,277,0,279,0,281,0,283,0,285,139,287,140,289,0,291, + 141,1,0,8,8,0,34,34,47,47,92,92,98,98,102,102,110,110,114,114,116, + 116,3,0,48,57,65,70,97,102,3,0,0,31,34,34,92,92,1,0,49,57,1,0,48, + 57,2,0,69,69,101,101,2,0,43,43,45,45,3,0,9,10,13,13,32,32,2476,0, + 1,1,0,0,0,0,3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1, + 0,0,0,0,13,1,0,0,0,0,15,1,0,0,0,0,17,1,0,0,0,0,19,1,0,0,0,0,21,1, + 0,0,0,0,23,1,0,0,0,0,25,1,0,0,0,0,27,1,0,0,0,0,29,1,0,0,0,0,31,1, + 0,0,0,0,33,1,0,0,0,0,35,1,0,0,0,0,37,1,0,0,0,0,39,1,0,0,0,0,41,1, + 0,0,0,0,43,1,0,0,0,0,45,1,0,0,0,0,47,1,0,0,0,0,49,1,0,0,0,0,51,1, + 0,0,0,0,53,1,0,0,0,0,55,1,0,0,0,0,57,1,0,0,0,0,59,1,0,0,0,0,61,1, + 0,0,0,0,63,1,0,0,0,0,65,1,0,0,0,0,67,1,0,0,0,0,69,1,0,0,0,0,71,1, + 0,0,0,0,73,1,0,0,0,0,75,1,0,0,0,0,77,1,0,0,0,0,79,1,0,0,0,0,81,1, + 0,0,0,0,83,1,0,0,0,0,85,1,0,0,0,0,87,1,0,0,0,0,89,1,0,0,0,0,91,1, + 0,0,0,0,93,1,0,0,0,0,95,1,0,0,0,0,97,1,0,0,0,0,99,1,0,0,0,0,101, + 1,0,0,0,0,103,1,0,0,0,0,105,1,0,0,0,0,107,1,0,0,0,0,109,1,0,0,0, + 0,111,1,0,0,0,0,113,1,0,0,0,0,115,1,0,0,0,0,117,1,0,0,0,0,119,1, + 0,0,0,0,121,1,0,0,0,0,123,1,0,0,0,0,125,1,0,0,0,0,127,1,0,0,0,0, + 129,1,0,0,0,0,131,1,0,0,0,0,133,1,0,0,0,0,135,1,0,0,0,0,137,1,0, + 0,0,0,139,1,0,0,0,0,141,1,0,0,0,0,143,1,0,0,0,0,145,1,0,0,0,0,147, + 1,0,0,0,0,149,1,0,0,0,0,151,1,0,0,0,0,153,1,0,0,0,0,155,1,0,0,0, + 0,157,1,0,0,0,0,159,1,0,0,0,0,161,1,0,0,0,0,163,1,0,0,0,0,165,1, + 0,0,0,0,167,1,0,0,0,0,169,1,0,0,0,0,171,1,0,0,0,0,173,1,0,0,0,0, + 175,1,0,0,0,0,177,1,0,0,0,0,179,1,0,0,0,0,181,1,0,0,0,0,183,1,0, + 0,0,0,185,1,0,0,0,0,187,1,0,0,0,0,189,1,0,0,0,0,191,1,0,0,0,0,193, + 1,0,0,0,0,195,1,0,0,0,0,197,1,0,0,0,0,199,1,0,0,0,0,201,1,0,0,0, + 0,203,1,0,0,0,0,205,1,0,0,0,0,207,1,0,0,0,0,209,1,0,0,0,0,211,1, + 0,0,0,0,213,1,0,0,0,0,215,1,0,0,0,0,217,1,0,0,0,0,219,1,0,0,0,0, + 221,1,0,0,0,0,223,1,0,0,0,0,225,1,0,0,0,0,227,1,0,0,0,0,229,1,0, + 0,0,0,231,1,0,0,0,0,233,1,0,0,0,0,235,1,0,0,0,0,237,1,0,0,0,0,239, + 1,0,0,0,0,241,1,0,0,0,0,243,1,0,0,0,0,245,1,0,0,0,0,247,1,0,0,0, + 0,249,1,0,0,0,0,251,1,0,0,0,0,253,1,0,0,0,0,255,1,0,0,0,0,257,1, + 0,0,0,0,259,1,0,0,0,0,261,1,0,0,0,0,263,1,0,0,0,0,265,1,0,0,0,0, + 267,1,0,0,0,0,269,1,0,0,0,0,271,1,0,0,0,0,273,1,0,0,0,0,275,1,0, + 0,0,0,285,1,0,0,0,0,287,1,0,0,0,0,291,1,0,0,0,1,293,1,0,0,0,3,295, + 1,0,0,0,5,297,1,0,0,0,7,299,1,0,0,0,9,301,1,0,0,0,11,303,1,0,0,0, + 13,305,1,0,0,0,15,310,1,0,0,0,17,316,1,0,0,0,19,321,1,0,0,0,21,331, + 1,0,0,0,23,340,1,0,0,0,25,350,1,0,0,0,27,362,1,0,0,0,29,372,1,0, + 0,0,31,379,1,0,0,0,33,386,1,0,0,0,35,395,1,0,0,0,37,402,1,0,0,0, + 39,412,1,0,0,0,41,419,1,0,0,0,43,426,1,0,0,0,45,437,1,0,0,0,47,443, + 1,0,0,0,49,453,1,0,0,0,51,464,1,0,0,0,53,474,1,0,0,0,55,485,1,0, + 0,0,57,491,1,0,0,0,59,507,1,0,0,0,61,527,1,0,0,0,63,539,1,0,0,0, + 65,548,1,0,0,0,67,560,1,0,0,0,69,572,1,0,0,0,71,583,1,0,0,0,73,597, + 1,0,0,0,75,603,1,0,0,0,77,619,1,0,0,0,79,639,1,0,0,0,81,660,1,0, + 0,0,83,685,1,0,0,0,85,712,1,0,0,0,87,743,1,0,0,0,89,761,1,0,0,0, + 91,783,1,0,0,0,93,807,1,0,0,0,95,835,1,0,0,0,97,840,1,0,0,0,99,855, + 1,0,0,0,101,874,1,0,0,0,103,894,1,0,0,0,105,918,1,0,0,0,107,944, + 1,0,0,0,109,974,1,0,0,0,111,991,1,0,0,0,113,1012,1,0,0,0,115,1035, + 1,0,0,0,117,1062,1,0,0,0,119,1078,1,0,0,0,121,1096,1,0,0,0,123,1118, + 1,0,0,0,125,1141,1,0,0,0,127,1168,1,0,0,0,129,1197,1,0,0,0,131,1230, + 1,0,0,0,133,1250,1,0,0,0,135,1274,1,0,0,0,137,1300,1,0,0,0,139,1330, + 1,0,0,0,141,1344,1,0,0,0,143,1354,1,0,0,0,145,1370,1,0,0,0,147,1382, + 1,0,0,0,149,1399,1,0,0,0,151,1420,1,0,0,0,153,1439,1,0,0,0,155,1462, + 1,0,0,0,157,1480,1,0,0,0,159,1487,1,0,0,0,161,1496,1,0,0,0,163,1510, + 1,0,0,0,165,1526,1,0,0,0,167,1537,1,0,0,0,169,1553,1,0,0,0,171,1564, + 1,0,0,0,173,1579,1,0,0,0,175,1600,1,0,0,0,177,1617,1,0,0,0,179,1628, + 1,0,0,0,181,1640,1,0,0,0,183,1653,1,0,0,0,185,1665,1,0,0,0,187,1678, + 1,0,0,0,189,1687,1,0,0,0,191,1700,1,0,0,0,193,1717,1,0,0,0,195,1730, + 1,0,0,0,197,1745,1,0,0,0,199,1757,1,0,0,0,201,1777,1,0,0,0,203,1790, + 1,0,0,0,205,1801,1,0,0,0,207,1816,1,0,0,0,209,1823,1,0,0,0,211,1829, + 1,0,0,0,213,1837,1,0,0,0,215,1849,1,0,0,0,217,1857,1,0,0,0,219,1869, + 1,0,0,0,221,1877,1,0,0,0,223,1891,1,0,0,0,225,1909,1,0,0,0,227,1923, + 1,0,0,0,229,1937,1,0,0,0,231,1955,1,0,0,0,233,1972,1,0,0,0,235,1979, + 1,0,0,0,237,1986,1,0,0,0,239,1994,1,0,0,0,241,2007,1,0,0,0,243,2034, + 1,0,0,0,245,2060,1,0,0,0,247,2077,1,0,0,0,249,2097,1,0,0,0,251,2118, + 1,0,0,0,253,2150,1,0,0,0,255,2180,1,0,0,0,257,2202,1,0,0,0,259,2227, + 1,0,0,0,261,2253,1,0,0,0,263,2294,1,0,0,0,265,2320,1,0,0,0,267,2348, + 1,0,0,0,269,2365,1,0,0,0,271,2377,1,0,0,0,273,2390,1,0,0,0,275,2402, + 1,0,0,0,277,2412,1,0,0,0,279,2417,1,0,0,0,281,2423,1,0,0,0,283,2425, + 1,0,0,0,285,2435,1,0,0,0,287,2438,1,0,0,0,289,2452,1,0,0,0,291,2459, + 1,0,0,0,293,294,5,44,0,0,294,2,1,0,0,0,295,296,5,58,0,0,296,4,1, + 0,0,0,297,298,5,91,0,0,298,6,1,0,0,0,299,300,5,93,0,0,300,8,1,0, + 0,0,301,302,5,123,0,0,302,10,1,0,0,0,303,304,5,125,0,0,304,12,1, + 0,0,0,305,306,5,116,0,0,306,307,5,114,0,0,307,308,5,117,0,0,308, + 309,5,101,0,0,309,14,1,0,0,0,310,311,5,102,0,0,311,312,5,97,0,0, + 312,313,5,108,0,0,313,314,5,115,0,0,314,315,5,101,0,0,315,16,1,0, + 0,0,316,317,5,110,0,0,317,318,5,117,0,0,318,319,5,108,0,0,319,320, + 5,108,0,0,320,18,1,0,0,0,321,322,5,34,0,0,322,323,5,67,0,0,323,324, + 5,111,0,0,324,325,5,109,0,0,325,326,5,109,0,0,326,327,5,101,0,0, + 327,328,5,110,0,0,328,329,5,116,0,0,329,330,5,34,0,0,330,20,1,0, + 0,0,331,332,5,34,0,0,332,333,5,83,0,0,333,334,5,116,0,0,334,335, + 5,97,0,0,335,336,5,116,0,0,336,337,5,101,0,0,337,338,5,115,0,0,338, + 339,5,34,0,0,339,22,1,0,0,0,340,341,5,34,0,0,341,342,5,83,0,0,342, + 343,5,116,0,0,343,344,5,97,0,0,344,345,5,114,0,0,345,346,5,116,0, + 0,346,347,5,65,0,0,347,348,5,116,0,0,348,349,5,34,0,0,349,24,1,0, + 0,0,350,351,5,34,0,0,351,352,5,78,0,0,352,353,5,101,0,0,353,354, + 5,120,0,0,354,355,5,116,0,0,355,356,5,83,0,0,356,357,5,116,0,0,357, + 358,5,97,0,0,358,359,5,116,0,0,359,360,5,101,0,0,360,361,5,34,0, + 0,361,26,1,0,0,0,362,363,5,34,0,0,363,364,5,86,0,0,364,365,5,101, + 0,0,365,366,5,114,0,0,366,367,5,115,0,0,367,368,5,105,0,0,368,369, + 5,111,0,0,369,370,5,110,0,0,370,371,5,34,0,0,371,28,1,0,0,0,372, + 373,5,34,0,0,373,374,5,84,0,0,374,375,5,121,0,0,375,376,5,112,0, + 0,376,377,5,101,0,0,377,378,5,34,0,0,378,30,1,0,0,0,379,380,5,34, + 0,0,380,381,5,84,0,0,381,382,5,97,0,0,382,383,5,115,0,0,383,384, + 5,107,0,0,384,385,5,34,0,0,385,32,1,0,0,0,386,387,5,34,0,0,387,388, + 5,67,0,0,388,389,5,104,0,0,389,390,5,111,0,0,390,391,5,105,0,0,391, + 392,5,99,0,0,392,393,5,101,0,0,393,394,5,34,0,0,394,34,1,0,0,0,395, + 396,5,34,0,0,396,397,5,70,0,0,397,398,5,97,0,0,398,399,5,105,0,0, + 399,400,5,108,0,0,400,401,5,34,0,0,401,36,1,0,0,0,402,403,5,34,0, + 0,403,404,5,83,0,0,404,405,5,117,0,0,405,406,5,99,0,0,406,407,5, + 99,0,0,407,408,5,101,0,0,408,409,5,101,0,0,409,410,5,100,0,0,410, + 411,5,34,0,0,411,38,1,0,0,0,412,413,5,34,0,0,413,414,5,80,0,0,414, + 415,5,97,0,0,415,416,5,115,0,0,416,417,5,115,0,0,417,418,5,34,0, + 0,418,40,1,0,0,0,419,420,5,34,0,0,420,421,5,87,0,0,421,422,5,97, + 0,0,422,423,5,105,0,0,423,424,5,116,0,0,424,425,5,34,0,0,425,42, + 1,0,0,0,426,427,5,34,0,0,427,428,5,80,0,0,428,429,5,97,0,0,429,430, + 5,114,0,0,430,431,5,97,0,0,431,432,5,108,0,0,432,433,5,108,0,0,433, + 434,5,101,0,0,434,435,5,108,0,0,435,436,5,34,0,0,436,44,1,0,0,0, + 437,438,5,34,0,0,438,439,5,77,0,0,439,440,5,97,0,0,440,441,5,112, + 0,0,441,442,5,34,0,0,442,46,1,0,0,0,443,444,5,34,0,0,444,445,5,67, + 0,0,445,446,5,104,0,0,446,447,5,111,0,0,447,448,5,105,0,0,448,449, + 5,99,0,0,449,450,5,101,0,0,450,451,5,115,0,0,451,452,5,34,0,0,452, + 48,1,0,0,0,453,454,5,34,0,0,454,455,5,86,0,0,455,456,5,97,0,0,456, + 457,5,114,0,0,457,458,5,105,0,0,458,459,5,97,0,0,459,460,5,98,0, + 0,460,461,5,108,0,0,461,462,5,101,0,0,462,463,5,34,0,0,463,50,1, + 0,0,0,464,465,5,34,0,0,465,466,5,68,0,0,466,467,5,101,0,0,467,468, + 5,102,0,0,468,469,5,97,0,0,469,470,5,117,0,0,470,471,5,108,0,0,471, + 472,5,116,0,0,472,473,5,34,0,0,473,52,1,0,0,0,474,475,5,34,0,0,475, + 476,5,66,0,0,476,477,5,114,0,0,477,478,5,97,0,0,478,479,5,110,0, + 0,479,480,5,99,0,0,480,481,5,104,0,0,481,482,5,101,0,0,482,483,5, + 115,0,0,483,484,5,34,0,0,484,54,1,0,0,0,485,486,5,34,0,0,486,487, + 5,65,0,0,487,488,5,110,0,0,488,489,5,100,0,0,489,490,5,34,0,0,490, + 56,1,0,0,0,491,492,5,34,0,0,492,493,5,66,0,0,493,494,5,111,0,0,494, + 495,5,111,0,0,495,496,5,108,0,0,496,497,5,101,0,0,497,498,5,97,0, + 0,498,499,5,110,0,0,499,500,5,69,0,0,500,501,5,113,0,0,501,502,5, + 117,0,0,502,503,5,97,0,0,503,504,5,108,0,0,504,505,5,115,0,0,505, + 506,5,34,0,0,506,58,1,0,0,0,507,508,5,34,0,0,508,509,5,66,0,0,509, + 510,5,111,0,0,510,511,5,111,0,0,511,512,5,108,0,0,512,513,5,101, + 0,0,513,514,5,97,0,0,514,515,5,110,0,0,515,516,5,69,0,0,516,517, + 5,113,0,0,517,518,5,117,0,0,518,519,5,97,0,0,519,520,5,108,0,0,520, + 521,5,115,0,0,521,522,5,80,0,0,522,523,5,97,0,0,523,524,5,116,0, + 0,524,525,5,104,0,0,525,526,5,34,0,0,526,60,1,0,0,0,527,528,5,34, + 0,0,528,529,5,73,0,0,529,530,5,115,0,0,530,531,5,66,0,0,531,532, + 5,111,0,0,532,533,5,111,0,0,533,534,5,108,0,0,534,535,5,101,0,0, + 535,536,5,97,0,0,536,537,5,110,0,0,537,538,5,34,0,0,538,62,1,0,0, + 0,539,540,5,34,0,0,540,541,5,73,0,0,541,542,5,115,0,0,542,543,5, + 78,0,0,543,544,5,117,0,0,544,545,5,108,0,0,545,546,5,108,0,0,546, + 547,5,34,0,0,547,64,1,0,0,0,548,549,5,34,0,0,549,550,5,73,0,0,550, + 551,5,115,0,0,551,552,5,78,0,0,552,553,5,117,0,0,553,554,5,109,0, + 0,554,555,5,101,0,0,555,556,5,114,0,0,556,557,5,105,0,0,557,558, + 5,99,0,0,558,559,5,34,0,0,559,66,1,0,0,0,560,561,5,34,0,0,561,562, + 5,73,0,0,562,563,5,115,0,0,563,564,5,80,0,0,564,565,5,114,0,0,565, + 566,5,101,0,0,566,567,5,115,0,0,567,568,5,101,0,0,568,569,5,110, + 0,0,569,570,5,116,0,0,570,571,5,34,0,0,571,68,1,0,0,0,572,573,5, + 34,0,0,573,574,5,73,0,0,574,575,5,115,0,0,575,576,5,83,0,0,576,577, + 5,116,0,0,577,578,5,114,0,0,578,579,5,105,0,0,579,580,5,110,0,0, + 580,581,5,103,0,0,581,582,5,34,0,0,582,70,1,0,0,0,583,584,5,34,0, + 0,584,585,5,73,0,0,585,586,5,115,0,0,586,587,5,84,0,0,587,588,5, + 105,0,0,588,589,5,109,0,0,589,590,5,101,0,0,590,591,5,115,0,0,591, + 592,5,116,0,0,592,593,5,97,0,0,593,594,5,109,0,0,594,595,5,112,0, + 0,595,596,5,34,0,0,596,72,1,0,0,0,597,598,5,34,0,0,598,599,5,78, + 0,0,599,600,5,111,0,0,600,601,5,116,0,0,601,602,5,34,0,0,602,74, + 1,0,0,0,603,604,5,34,0,0,604,605,5,78,0,0,605,606,5,117,0,0,606, + 607,5,109,0,0,607,608,5,101,0,0,608,609,5,114,0,0,609,610,5,105, + 0,0,610,611,5,99,0,0,611,612,5,69,0,0,612,613,5,113,0,0,613,614, + 5,117,0,0,614,615,5,97,0,0,615,616,5,108,0,0,616,617,5,115,0,0,617, + 618,5,34,0,0,618,76,1,0,0,0,619,620,5,34,0,0,620,621,5,78,0,0,621, + 622,5,117,0,0,622,623,5,109,0,0,623,624,5,101,0,0,624,625,5,114, + 0,0,625,626,5,105,0,0,626,627,5,99,0,0,627,628,5,69,0,0,628,629, + 5,113,0,0,629,630,5,117,0,0,630,631,5,97,0,0,631,632,5,108,0,0,632, + 633,5,115,0,0,633,634,5,80,0,0,634,635,5,97,0,0,635,636,5,116,0, + 0,636,637,5,104,0,0,637,638,5,34,0,0,638,78,1,0,0,0,639,640,5,34, + 0,0,640,641,5,78,0,0,641,642,5,117,0,0,642,643,5,109,0,0,643,644, + 5,101,0,0,644,645,5,114,0,0,645,646,5,105,0,0,646,647,5,99,0,0,647, + 648,5,71,0,0,648,649,5,114,0,0,649,650,5,101,0,0,650,651,5,97,0, + 0,651,652,5,116,0,0,652,653,5,101,0,0,653,654,5,114,0,0,654,655, + 5,84,0,0,655,656,5,104,0,0,656,657,5,97,0,0,657,658,5,110,0,0,658, + 659,5,34,0,0,659,80,1,0,0,0,660,661,5,34,0,0,661,662,5,78,0,0,662, + 663,5,117,0,0,663,664,5,109,0,0,664,665,5,101,0,0,665,666,5,114, + 0,0,666,667,5,105,0,0,667,668,5,99,0,0,668,669,5,71,0,0,669,670, + 5,114,0,0,670,671,5,101,0,0,671,672,5,97,0,0,672,673,5,116,0,0,673, + 674,5,101,0,0,674,675,5,114,0,0,675,676,5,84,0,0,676,677,5,104,0, + 0,677,678,5,97,0,0,678,679,5,110,0,0,679,680,5,80,0,0,680,681,5, + 97,0,0,681,682,5,116,0,0,682,683,5,104,0,0,683,684,5,34,0,0,684, + 82,1,0,0,0,685,686,5,34,0,0,686,687,5,78,0,0,687,688,5,117,0,0,688, + 689,5,109,0,0,689,690,5,101,0,0,690,691,5,114,0,0,691,692,5,105, + 0,0,692,693,5,99,0,0,693,694,5,71,0,0,694,695,5,114,0,0,695,696, + 5,101,0,0,696,697,5,97,0,0,697,698,5,116,0,0,698,699,5,101,0,0,699, + 700,5,114,0,0,700,701,5,84,0,0,701,702,5,104,0,0,702,703,5,97,0, + 0,703,704,5,110,0,0,704,705,5,69,0,0,705,706,5,113,0,0,706,707,5, + 117,0,0,707,708,5,97,0,0,708,709,5,108,0,0,709,710,5,115,0,0,710, + 711,5,34,0,0,711,84,1,0,0,0,712,713,5,34,0,0,713,714,5,78,0,0,714, + 715,5,117,0,0,715,716,5,109,0,0,716,717,5,101,0,0,717,718,5,114, + 0,0,718,719,5,105,0,0,719,720,5,99,0,0,720,721,5,71,0,0,721,722, + 5,114,0,0,722,723,5,101,0,0,723,724,5,97,0,0,724,725,5,116,0,0,725, + 726,5,101,0,0,726,727,5,114,0,0,727,728,5,84,0,0,728,729,5,104,0, + 0,729,730,5,97,0,0,730,731,5,110,0,0,731,732,5,69,0,0,732,733,5, + 113,0,0,733,734,5,117,0,0,734,735,5,97,0,0,735,736,5,108,0,0,736, + 737,5,115,0,0,737,738,5,80,0,0,738,739,5,97,0,0,739,740,5,116,0, + 0,740,741,5,104,0,0,741,742,5,34,0,0,742,86,1,0,0,0,743,744,5,34, + 0,0,744,745,5,78,0,0,745,746,5,117,0,0,746,747,5,109,0,0,747,748, + 5,101,0,0,748,749,5,114,0,0,749,750,5,105,0,0,750,751,5,99,0,0,751, + 752,5,76,0,0,752,753,5,101,0,0,753,754,5,115,0,0,754,755,5,115,0, + 0,755,756,5,84,0,0,756,757,5,104,0,0,757,758,5,97,0,0,758,759,5, + 110,0,0,759,760,5,34,0,0,760,88,1,0,0,0,761,762,5,34,0,0,762,763, + 5,78,0,0,763,764,5,117,0,0,764,765,5,109,0,0,765,766,5,101,0,0,766, + 767,5,114,0,0,767,768,5,105,0,0,768,769,5,99,0,0,769,770,5,76,0, + 0,770,771,5,101,0,0,771,772,5,115,0,0,772,773,5,115,0,0,773,774, + 5,84,0,0,774,775,5,104,0,0,775,776,5,97,0,0,776,777,5,110,0,0,777, + 778,5,80,0,0,778,779,5,97,0,0,779,780,5,116,0,0,780,781,5,104,0, + 0,781,782,5,34,0,0,782,90,1,0,0,0,783,784,5,34,0,0,784,785,5,78, + 0,0,785,786,5,117,0,0,786,787,5,109,0,0,787,788,5,101,0,0,788,789, + 5,114,0,0,789,790,5,105,0,0,790,791,5,99,0,0,791,792,5,76,0,0,792, + 793,5,101,0,0,793,794,5,115,0,0,794,795,5,115,0,0,795,796,5,84,0, + 0,796,797,5,104,0,0,797,798,5,97,0,0,798,799,5,110,0,0,799,800,5, + 69,0,0,800,801,5,113,0,0,801,802,5,117,0,0,802,803,5,97,0,0,803, + 804,5,108,0,0,804,805,5,115,0,0,805,806,5,34,0,0,806,92,1,0,0,0, + 807,808,5,34,0,0,808,809,5,78,0,0,809,810,5,117,0,0,810,811,5,109, + 0,0,811,812,5,101,0,0,812,813,5,114,0,0,813,814,5,105,0,0,814,815, + 5,99,0,0,815,816,5,76,0,0,816,817,5,101,0,0,817,818,5,115,0,0,818, + 819,5,115,0,0,819,820,5,84,0,0,820,821,5,104,0,0,821,822,5,97,0, + 0,822,823,5,110,0,0,823,824,5,69,0,0,824,825,5,113,0,0,825,826,5, + 117,0,0,826,827,5,97,0,0,827,828,5,108,0,0,828,829,5,115,0,0,829, + 830,5,80,0,0,830,831,5,97,0,0,831,832,5,116,0,0,832,833,5,104,0, + 0,833,834,5,34,0,0,834,94,1,0,0,0,835,836,5,34,0,0,836,837,5,79, + 0,0,837,838,5,114,0,0,838,839,5,34,0,0,839,96,1,0,0,0,840,841,5, + 34,0,0,841,842,5,83,0,0,842,843,5,116,0,0,843,844,5,114,0,0,844, + 845,5,105,0,0,845,846,5,110,0,0,846,847,5,103,0,0,847,848,5,69,0, + 0,848,849,5,113,0,0,849,850,5,117,0,0,850,851,5,97,0,0,851,852,5, + 108,0,0,852,853,5,115,0,0,853,854,5,34,0,0,854,98,1,0,0,0,855,856, + 5,34,0,0,856,857,5,83,0,0,857,858,5,116,0,0,858,859,5,114,0,0,859, + 860,5,105,0,0,860,861,5,110,0,0,861,862,5,103,0,0,862,863,5,69,0, + 0,863,864,5,113,0,0,864,865,5,117,0,0,865,866,5,97,0,0,866,867,5, + 108,0,0,867,868,5,115,0,0,868,869,5,80,0,0,869,870,5,97,0,0,870, + 871,5,116,0,0,871,872,5,104,0,0,872,873,5,34,0,0,873,100,1,0,0,0, + 874,875,5,34,0,0,875,876,5,83,0,0,876,877,5,116,0,0,877,878,5,114, + 0,0,878,879,5,105,0,0,879,880,5,110,0,0,880,881,5,103,0,0,881,882, + 5,71,0,0,882,883,5,114,0,0,883,884,5,101,0,0,884,885,5,97,0,0,885, + 886,5,116,0,0,886,887,5,101,0,0,887,888,5,114,0,0,888,889,5,84,0, + 0,889,890,5,104,0,0,890,891,5,97,0,0,891,892,5,110,0,0,892,893,5, + 34,0,0,893,102,1,0,0,0,894,895,5,34,0,0,895,896,5,83,0,0,896,897, + 5,116,0,0,897,898,5,114,0,0,898,899,5,105,0,0,899,900,5,110,0,0, + 900,901,5,103,0,0,901,902,5,71,0,0,902,903,5,114,0,0,903,904,5,101, + 0,0,904,905,5,97,0,0,905,906,5,116,0,0,906,907,5,101,0,0,907,908, + 5,114,0,0,908,909,5,84,0,0,909,910,5,104,0,0,910,911,5,97,0,0,911, + 912,5,110,0,0,912,913,5,80,0,0,913,914,5,97,0,0,914,915,5,116,0, + 0,915,916,5,104,0,0,916,917,5,34,0,0,917,104,1,0,0,0,918,919,5,34, + 0,0,919,920,5,83,0,0,920,921,5,116,0,0,921,922,5,114,0,0,922,923, + 5,105,0,0,923,924,5,110,0,0,924,925,5,103,0,0,925,926,5,71,0,0,926, + 927,5,114,0,0,927,928,5,101,0,0,928,929,5,97,0,0,929,930,5,116,0, + 0,930,931,5,101,0,0,931,932,5,114,0,0,932,933,5,84,0,0,933,934,5, + 104,0,0,934,935,5,97,0,0,935,936,5,110,0,0,936,937,5,69,0,0,937, + 938,5,113,0,0,938,939,5,117,0,0,939,940,5,97,0,0,940,941,5,108,0, + 0,941,942,5,115,0,0,942,943,5,34,0,0,943,106,1,0,0,0,944,945,5,34, + 0,0,945,946,5,83,0,0,946,947,5,116,0,0,947,948,5,114,0,0,948,949, + 5,105,0,0,949,950,5,110,0,0,950,951,5,103,0,0,951,952,5,71,0,0,952, + 953,5,114,0,0,953,954,5,101,0,0,954,955,5,97,0,0,955,956,5,116,0, + 0,956,957,5,101,0,0,957,958,5,114,0,0,958,959,5,84,0,0,959,960,5, + 104,0,0,960,961,5,97,0,0,961,962,5,110,0,0,962,963,5,69,0,0,963, + 964,5,113,0,0,964,965,5,117,0,0,965,966,5,97,0,0,966,967,5,108,0, + 0,967,968,5,115,0,0,968,969,5,80,0,0,969,970,5,97,0,0,970,971,5, + 116,0,0,971,972,5,104,0,0,972,973,5,34,0,0,973,108,1,0,0,0,974,975, + 5,34,0,0,975,976,5,83,0,0,976,977,5,116,0,0,977,978,5,114,0,0,978, + 979,5,105,0,0,979,980,5,110,0,0,980,981,5,103,0,0,981,982,5,76,0, + 0,982,983,5,101,0,0,983,984,5,115,0,0,984,985,5,115,0,0,985,986, + 5,84,0,0,986,987,5,104,0,0,987,988,5,97,0,0,988,989,5,110,0,0,989, + 990,5,34,0,0,990,110,1,0,0,0,991,992,5,34,0,0,992,993,5,83,0,0,993, + 994,5,116,0,0,994,995,5,114,0,0,995,996,5,105,0,0,996,997,5,110, + 0,0,997,998,5,103,0,0,998,999,5,76,0,0,999,1000,5,101,0,0,1000,1001, + 5,115,0,0,1001,1002,5,115,0,0,1002,1003,5,84,0,0,1003,1004,5,104, + 0,0,1004,1005,5,97,0,0,1005,1006,5,110,0,0,1006,1007,5,80,0,0,1007, + 1008,5,97,0,0,1008,1009,5,116,0,0,1009,1010,5,104,0,0,1010,1011, + 5,34,0,0,1011,112,1,0,0,0,1012,1013,5,34,0,0,1013,1014,5,83,0,0, + 1014,1015,5,116,0,0,1015,1016,5,114,0,0,1016,1017,5,105,0,0,1017, + 1018,5,110,0,0,1018,1019,5,103,0,0,1019,1020,5,76,0,0,1020,1021, + 5,101,0,0,1021,1022,5,115,0,0,1022,1023,5,115,0,0,1023,1024,5,84, + 0,0,1024,1025,5,104,0,0,1025,1026,5,97,0,0,1026,1027,5,110,0,0,1027, + 1028,5,69,0,0,1028,1029,5,113,0,0,1029,1030,5,117,0,0,1030,1031, + 5,97,0,0,1031,1032,5,108,0,0,1032,1033,5,115,0,0,1033,1034,5,34, + 0,0,1034,114,1,0,0,0,1035,1036,5,34,0,0,1036,1037,5,83,0,0,1037, + 1038,5,116,0,0,1038,1039,5,114,0,0,1039,1040,5,105,0,0,1040,1041, + 5,110,0,0,1041,1042,5,103,0,0,1042,1043,5,76,0,0,1043,1044,5,101, + 0,0,1044,1045,5,115,0,0,1045,1046,5,115,0,0,1046,1047,5,84,0,0,1047, + 1048,5,104,0,0,1048,1049,5,97,0,0,1049,1050,5,110,0,0,1050,1051, + 5,69,0,0,1051,1052,5,113,0,0,1052,1053,5,117,0,0,1053,1054,5,97, + 0,0,1054,1055,5,108,0,0,1055,1056,5,115,0,0,1056,1057,5,80,0,0,1057, + 1058,5,97,0,0,1058,1059,5,116,0,0,1059,1060,5,104,0,0,1060,1061, + 5,34,0,0,1061,116,1,0,0,0,1062,1063,5,34,0,0,1063,1064,5,83,0,0, + 1064,1065,5,116,0,0,1065,1066,5,114,0,0,1066,1067,5,105,0,0,1067, + 1068,5,110,0,0,1068,1069,5,103,0,0,1069,1070,5,77,0,0,1070,1071, + 5,97,0,0,1071,1072,5,116,0,0,1072,1073,5,99,0,0,1073,1074,5,104, + 0,0,1074,1075,5,101,0,0,1075,1076,5,115,0,0,1076,1077,5,34,0,0,1077, + 118,1,0,0,0,1078,1079,5,34,0,0,1079,1080,5,84,0,0,1080,1081,5,105, + 0,0,1081,1082,5,109,0,0,1082,1083,5,101,0,0,1083,1084,5,115,0,0, + 1084,1085,5,116,0,0,1085,1086,5,97,0,0,1086,1087,5,109,0,0,1087, + 1088,5,112,0,0,1088,1089,5,69,0,0,1089,1090,5,113,0,0,1090,1091, + 5,117,0,0,1091,1092,5,97,0,0,1092,1093,5,108,0,0,1093,1094,5,115, + 0,0,1094,1095,5,34,0,0,1095,120,1,0,0,0,1096,1097,5,34,0,0,1097, + 1098,5,84,0,0,1098,1099,5,105,0,0,1099,1100,5,109,0,0,1100,1101, + 5,101,0,0,1101,1102,5,115,0,0,1102,1103,5,116,0,0,1103,1104,5,97, + 0,0,1104,1105,5,109,0,0,1105,1106,5,112,0,0,1106,1107,5,69,0,0,1107, + 1108,5,113,0,0,1108,1109,5,117,0,0,1109,1110,5,97,0,0,1110,1111, + 5,108,0,0,1111,1112,5,115,0,0,1112,1113,5,80,0,0,1113,1114,5,97, + 0,0,1114,1115,5,116,0,0,1115,1116,5,104,0,0,1116,1117,5,34,0,0,1117, + 122,1,0,0,0,1118,1119,5,34,0,0,1119,1120,5,84,0,0,1120,1121,5,105, + 0,0,1121,1122,5,109,0,0,1122,1123,5,101,0,0,1123,1124,5,115,0,0, + 1124,1125,5,116,0,0,1125,1126,5,97,0,0,1126,1127,5,109,0,0,1127, + 1128,5,112,0,0,1128,1129,5,71,0,0,1129,1130,5,114,0,0,1130,1131, + 5,101,0,0,1131,1132,5,97,0,0,1132,1133,5,116,0,0,1133,1134,5,101, + 0,0,1134,1135,5,114,0,0,1135,1136,5,84,0,0,1136,1137,5,104,0,0,1137, + 1138,5,97,0,0,1138,1139,5,110,0,0,1139,1140,5,34,0,0,1140,124,1, + 0,0,0,1141,1142,5,34,0,0,1142,1143,5,84,0,0,1143,1144,5,105,0,0, + 1144,1145,5,109,0,0,1145,1146,5,101,0,0,1146,1147,5,115,0,0,1147, + 1148,5,116,0,0,1148,1149,5,97,0,0,1149,1150,5,109,0,0,1150,1151, + 5,112,0,0,1151,1152,5,71,0,0,1152,1153,5,114,0,0,1153,1154,5,101, + 0,0,1154,1155,5,97,0,0,1155,1156,5,116,0,0,1156,1157,5,101,0,0,1157, + 1158,5,114,0,0,1158,1159,5,84,0,0,1159,1160,5,104,0,0,1160,1161, + 5,97,0,0,1161,1162,5,110,0,0,1162,1163,5,80,0,0,1163,1164,5,97,0, + 0,1164,1165,5,116,0,0,1165,1166,5,104,0,0,1166,1167,5,34,0,0,1167, + 126,1,0,0,0,1168,1169,5,34,0,0,1169,1170,5,84,0,0,1170,1171,5,105, + 0,0,1171,1172,5,109,0,0,1172,1173,5,101,0,0,1173,1174,5,115,0,0, + 1174,1175,5,116,0,0,1175,1176,5,97,0,0,1176,1177,5,109,0,0,1177, + 1178,5,112,0,0,1178,1179,5,71,0,0,1179,1180,5,114,0,0,1180,1181, + 5,101,0,0,1181,1182,5,97,0,0,1182,1183,5,116,0,0,1183,1184,5,101, + 0,0,1184,1185,5,114,0,0,1185,1186,5,84,0,0,1186,1187,5,104,0,0,1187, + 1188,5,97,0,0,1188,1189,5,110,0,0,1189,1190,5,69,0,0,1190,1191,5, + 113,0,0,1191,1192,5,117,0,0,1192,1193,5,97,0,0,1193,1194,5,108,0, + 0,1194,1195,5,115,0,0,1195,1196,5,34,0,0,1196,128,1,0,0,0,1197,1198, + 5,34,0,0,1198,1199,5,84,0,0,1199,1200,5,105,0,0,1200,1201,5,109, + 0,0,1201,1202,5,101,0,0,1202,1203,5,115,0,0,1203,1204,5,116,0,0, + 1204,1205,5,97,0,0,1205,1206,5,109,0,0,1206,1207,5,112,0,0,1207, + 1208,5,71,0,0,1208,1209,5,114,0,0,1209,1210,5,101,0,0,1210,1211, + 5,97,0,0,1211,1212,5,116,0,0,1212,1213,5,101,0,0,1213,1214,5,114, + 0,0,1214,1215,5,84,0,0,1215,1216,5,104,0,0,1216,1217,5,97,0,0,1217, + 1218,5,110,0,0,1218,1219,5,69,0,0,1219,1220,5,113,0,0,1220,1221, + 5,117,0,0,1221,1222,5,97,0,0,1222,1223,5,108,0,0,1223,1224,5,115, + 0,0,1224,1225,5,80,0,0,1225,1226,5,97,0,0,1226,1227,5,116,0,0,1227, + 1228,5,104,0,0,1228,1229,5,34,0,0,1229,130,1,0,0,0,1230,1231,5,34, + 0,0,1231,1232,5,84,0,0,1232,1233,5,105,0,0,1233,1234,5,109,0,0,1234, + 1235,5,101,0,0,1235,1236,5,115,0,0,1236,1237,5,116,0,0,1237,1238, + 5,97,0,0,1238,1239,5,109,0,0,1239,1240,5,112,0,0,1240,1241,5,76, + 0,0,1241,1242,5,101,0,0,1242,1243,5,115,0,0,1243,1244,5,115,0,0, + 1244,1245,5,84,0,0,1245,1246,5,104,0,0,1246,1247,5,97,0,0,1247,1248, + 5,110,0,0,1248,1249,5,34,0,0,1249,132,1,0,0,0,1250,1251,5,34,0,0, + 1251,1252,5,84,0,0,1252,1253,5,105,0,0,1253,1254,5,109,0,0,1254, + 1255,5,101,0,0,1255,1256,5,115,0,0,1256,1257,5,116,0,0,1257,1258, + 5,97,0,0,1258,1259,5,109,0,0,1259,1260,5,112,0,0,1260,1261,5,76, + 0,0,1261,1262,5,101,0,0,1262,1263,5,115,0,0,1263,1264,5,115,0,0, + 1264,1265,5,84,0,0,1265,1266,5,104,0,0,1266,1267,5,97,0,0,1267,1268, + 5,110,0,0,1268,1269,5,80,0,0,1269,1270,5,97,0,0,1270,1271,5,116, + 0,0,1271,1272,5,104,0,0,1272,1273,5,34,0,0,1273,134,1,0,0,0,1274, + 1275,5,34,0,0,1275,1276,5,84,0,0,1276,1277,5,105,0,0,1277,1278,5, + 109,0,0,1278,1279,5,101,0,0,1279,1280,5,115,0,0,1280,1281,5,116, + 0,0,1281,1282,5,97,0,0,1282,1283,5,109,0,0,1283,1284,5,112,0,0,1284, + 1285,5,76,0,0,1285,1286,5,101,0,0,1286,1287,5,115,0,0,1287,1288, + 5,115,0,0,1288,1289,5,84,0,0,1289,1290,5,104,0,0,1290,1291,5,97, + 0,0,1291,1292,5,110,0,0,1292,1293,5,69,0,0,1293,1294,5,113,0,0,1294, + 1295,5,117,0,0,1295,1296,5,97,0,0,1296,1297,5,108,0,0,1297,1298, + 5,115,0,0,1298,1299,5,34,0,0,1299,136,1,0,0,0,1300,1301,5,34,0,0, + 1301,1302,5,84,0,0,1302,1303,5,105,0,0,1303,1304,5,109,0,0,1304, + 1305,5,101,0,0,1305,1306,5,115,0,0,1306,1307,5,116,0,0,1307,1308, + 5,97,0,0,1308,1309,5,109,0,0,1309,1310,5,112,0,0,1310,1311,5,76, + 0,0,1311,1312,5,101,0,0,1312,1313,5,115,0,0,1313,1314,5,115,0,0, + 1314,1315,5,84,0,0,1315,1316,5,104,0,0,1316,1317,5,97,0,0,1317,1318, + 5,110,0,0,1318,1319,5,69,0,0,1319,1320,5,113,0,0,1320,1321,5,117, + 0,0,1321,1322,5,97,0,0,1322,1323,5,108,0,0,1323,1324,5,115,0,0,1324, + 1325,5,80,0,0,1325,1326,5,97,0,0,1326,1327,5,116,0,0,1327,1328,5, + 104,0,0,1328,1329,5,34,0,0,1329,138,1,0,0,0,1330,1331,5,34,0,0,1331, + 1332,5,83,0,0,1332,1333,5,101,0,0,1333,1334,5,99,0,0,1334,1335,5, + 111,0,0,1335,1336,5,110,0,0,1336,1337,5,100,0,0,1337,1338,5,115, + 0,0,1338,1339,5,80,0,0,1339,1340,5,97,0,0,1340,1341,5,116,0,0,1341, + 1342,5,104,0,0,1342,1343,5,34,0,0,1343,140,1,0,0,0,1344,1345,5,34, + 0,0,1345,1346,5,83,0,0,1346,1347,5,101,0,0,1347,1348,5,99,0,0,1348, + 1349,5,111,0,0,1349,1350,5,110,0,0,1350,1351,5,100,0,0,1351,1352, + 5,115,0,0,1352,1353,5,34,0,0,1353,142,1,0,0,0,1354,1355,5,34,0,0, + 1355,1356,5,84,0,0,1356,1357,5,105,0,0,1357,1358,5,109,0,0,1358, + 1359,5,101,0,0,1359,1360,5,115,0,0,1360,1361,5,116,0,0,1361,1362, + 5,97,0,0,1362,1363,5,109,0,0,1363,1364,5,112,0,0,1364,1365,5,80, + 0,0,1365,1366,5,97,0,0,1366,1367,5,116,0,0,1367,1368,5,104,0,0,1368, + 1369,5,34,0,0,1369,144,1,0,0,0,1370,1371,5,34,0,0,1371,1372,5,84, + 0,0,1372,1373,5,105,0,0,1373,1374,5,109,0,0,1374,1375,5,101,0,0, + 1375,1376,5,115,0,0,1376,1377,5,116,0,0,1377,1378,5,97,0,0,1378, + 1379,5,109,0,0,1379,1380,5,112,0,0,1380,1381,5,34,0,0,1381,146,1, + 0,0,0,1382,1383,5,34,0,0,1383,1384,5,84,0,0,1384,1385,5,105,0,0, + 1385,1386,5,109,0,0,1386,1387,5,101,0,0,1387,1388,5,111,0,0,1388, + 1389,5,117,0,0,1389,1390,5,116,0,0,1390,1391,5,83,0,0,1391,1392, + 5,101,0,0,1392,1393,5,99,0,0,1393,1394,5,111,0,0,1394,1395,5,110, + 0,0,1395,1396,5,100,0,0,1396,1397,5,115,0,0,1397,1398,5,34,0,0,1398, + 148,1,0,0,0,1399,1400,5,34,0,0,1400,1401,5,84,0,0,1401,1402,5,105, + 0,0,1402,1403,5,109,0,0,1403,1404,5,101,0,0,1404,1405,5,111,0,0, + 1405,1406,5,117,0,0,1406,1407,5,116,0,0,1407,1408,5,83,0,0,1408, + 1409,5,101,0,0,1409,1410,5,99,0,0,1410,1411,5,111,0,0,1411,1412, + 5,110,0,0,1412,1413,5,100,0,0,1413,1414,5,115,0,0,1414,1415,5,80, + 0,0,1415,1416,5,97,0,0,1416,1417,5,116,0,0,1417,1418,5,104,0,0,1418, + 1419,5,34,0,0,1419,150,1,0,0,0,1420,1421,5,34,0,0,1421,1422,5,72, + 0,0,1422,1423,5,101,0,0,1423,1424,5,97,0,0,1424,1425,5,114,0,0,1425, + 1426,5,116,0,0,1426,1427,5,98,0,0,1427,1428,5,101,0,0,1428,1429, + 5,97,0,0,1429,1430,5,116,0,0,1430,1431,5,83,0,0,1431,1432,5,101, + 0,0,1432,1433,5,99,0,0,1433,1434,5,111,0,0,1434,1435,5,110,0,0,1435, + 1436,5,100,0,0,1436,1437,5,115,0,0,1437,1438,5,34,0,0,1438,152,1, + 0,0,0,1439,1440,5,34,0,0,1440,1441,5,72,0,0,1441,1442,5,101,0,0, + 1442,1443,5,97,0,0,1443,1444,5,114,0,0,1444,1445,5,116,0,0,1445, + 1446,5,98,0,0,1446,1447,5,101,0,0,1447,1448,5,97,0,0,1448,1449,5, + 116,0,0,1449,1450,5,83,0,0,1450,1451,5,101,0,0,1451,1452,5,99,0, + 0,1452,1453,5,111,0,0,1453,1454,5,110,0,0,1454,1455,5,100,0,0,1455, + 1456,5,115,0,0,1456,1457,5,80,0,0,1457,1458,5,97,0,0,1458,1459,5, + 116,0,0,1459,1460,5,104,0,0,1460,1461,5,34,0,0,1461,154,1,0,0,0, + 1462,1463,5,34,0,0,1463,1464,5,80,0,0,1464,1465,5,114,0,0,1465,1466, + 5,111,0,0,1466,1467,5,99,0,0,1467,1468,5,101,0,0,1468,1469,5,115, + 0,0,1469,1470,5,115,0,0,1470,1471,5,111,0,0,1471,1472,5,114,0,0, + 1472,1473,5,67,0,0,1473,1474,5,111,0,0,1474,1475,5,110,0,0,1475, + 1476,5,102,0,0,1476,1477,5,105,0,0,1477,1478,5,103,0,0,1478,1479, + 5,34,0,0,1479,156,1,0,0,0,1480,1481,5,34,0,0,1481,1482,5,77,0,0, + 1482,1483,5,111,0,0,1483,1484,5,100,0,0,1484,1485,5,101,0,0,1485, + 1486,5,34,0,0,1486,158,1,0,0,0,1487,1488,5,34,0,0,1488,1489,5,73, + 0,0,1489,1490,5,78,0,0,1490,1491,5,76,0,0,1491,1492,5,73,0,0,1492, + 1493,5,78,0,0,1493,1494,5,69,0,0,1494,1495,5,34,0,0,1495,160,1,0, + 0,0,1496,1497,5,34,0,0,1497,1498,5,68,0,0,1498,1499,5,73,0,0,1499, + 1500,5,83,0,0,1500,1501,5,84,0,0,1501,1502,5,82,0,0,1502,1503,5, + 73,0,0,1503,1504,5,66,0,0,1504,1505,5,85,0,0,1505,1506,5,84,0,0, + 1506,1507,5,69,0,0,1507,1508,5,68,0,0,1508,1509,5,34,0,0,1509,162, + 1,0,0,0,1510,1511,5,34,0,0,1511,1512,5,69,0,0,1512,1513,5,120,0, + 0,1513,1514,5,101,0,0,1514,1515,5,99,0,0,1515,1516,5,117,0,0,1516, + 1517,5,116,0,0,1517,1518,5,105,0,0,1518,1519,5,111,0,0,1519,1520, + 5,110,0,0,1520,1521,5,84,0,0,1521,1522,5,121,0,0,1522,1523,5,112, + 0,0,1523,1524,5,101,0,0,1524,1525,5,34,0,0,1525,164,1,0,0,0,1526, + 1527,5,34,0,0,1527,1528,5,83,0,0,1528,1529,5,84,0,0,1529,1530,5, + 65,0,0,1530,1531,5,78,0,0,1531,1532,5,68,0,0,1532,1533,5,65,0,0, + 1533,1534,5,82,0,0,1534,1535,5,68,0,0,1535,1536,5,34,0,0,1536,166, + 1,0,0,0,1537,1538,5,34,0,0,1538,1539,5,73,0,0,1539,1540,5,116,0, + 0,1540,1541,5,101,0,0,1541,1542,5,109,0,0,1542,1543,5,80,0,0,1543, + 1544,5,114,0,0,1544,1545,5,111,0,0,1545,1546,5,99,0,0,1546,1547, + 5,101,0,0,1547,1548,5,115,0,0,1548,1549,5,115,0,0,1549,1550,5,111, + 0,0,1550,1551,5,114,0,0,1551,1552,5,34,0,0,1552,168,1,0,0,0,1553, + 1554,5,34,0,0,1554,1555,5,73,0,0,1555,1556,5,116,0,0,1556,1557,5, + 101,0,0,1557,1558,5,114,0,0,1558,1559,5,97,0,0,1559,1560,5,116,0, + 0,1560,1561,5,111,0,0,1561,1562,5,114,0,0,1562,1563,5,34,0,0,1563, + 170,1,0,0,0,1564,1565,5,34,0,0,1565,1566,5,73,0,0,1566,1567,5,116, + 0,0,1567,1568,5,101,0,0,1568,1569,5,109,0,0,1569,1570,5,83,0,0,1570, + 1571,5,101,0,0,1571,1572,5,108,0,0,1572,1573,5,101,0,0,1573,1574, + 5,99,0,0,1574,1575,5,116,0,0,1575,1576,5,111,0,0,1576,1577,5,114, + 0,0,1577,1578,5,34,0,0,1578,172,1,0,0,0,1579,1580,5,34,0,0,1580, + 1581,5,77,0,0,1581,1582,5,97,0,0,1582,1583,5,120,0,0,1583,1584,5, + 67,0,0,1584,1585,5,111,0,0,1585,1586,5,110,0,0,1586,1587,5,99,0, + 0,1587,1588,5,117,0,0,1588,1589,5,114,0,0,1589,1590,5,114,0,0,1590, + 1591,5,101,0,0,1591,1592,5,110,0,0,1592,1593,5,99,0,0,1593,1594, + 5,121,0,0,1594,1595,5,80,0,0,1595,1596,5,97,0,0,1596,1597,5,116, + 0,0,1597,1598,5,104,0,0,1598,1599,5,34,0,0,1599,174,1,0,0,0,1600, + 1601,5,34,0,0,1601,1602,5,77,0,0,1602,1603,5,97,0,0,1603,1604,5, + 120,0,0,1604,1605,5,67,0,0,1605,1606,5,111,0,0,1606,1607,5,110,0, + 0,1607,1608,5,99,0,0,1608,1609,5,117,0,0,1609,1610,5,114,0,0,1610, + 1611,5,114,0,0,1611,1612,5,101,0,0,1612,1613,5,110,0,0,1613,1614, + 5,99,0,0,1614,1615,5,121,0,0,1615,1616,5,34,0,0,1616,176,1,0,0,0, + 1617,1618,5,34,0,0,1618,1619,5,82,0,0,1619,1620,5,101,0,0,1620,1621, + 5,115,0,0,1621,1622,5,111,0,0,1622,1623,5,117,0,0,1623,1624,5,114, + 0,0,1624,1625,5,99,0,0,1625,1626,5,101,0,0,1626,1627,5,34,0,0,1627, + 178,1,0,0,0,1628,1629,5,34,0,0,1629,1630,5,73,0,0,1630,1631,5,110, + 0,0,1631,1632,5,112,0,0,1632,1633,5,117,0,0,1633,1634,5,116,0,0, + 1634,1635,5,80,0,0,1635,1636,5,97,0,0,1636,1637,5,116,0,0,1637,1638, + 5,104,0,0,1638,1639,5,34,0,0,1639,180,1,0,0,0,1640,1641,5,34,0,0, + 1641,1642,5,79,0,0,1642,1643,5,117,0,0,1643,1644,5,116,0,0,1644, + 1645,5,112,0,0,1645,1646,5,117,0,0,1646,1647,5,116,0,0,1647,1648, + 5,80,0,0,1648,1649,5,97,0,0,1649,1650,5,116,0,0,1650,1651,5,104, + 0,0,1651,1652,5,34,0,0,1652,182,1,0,0,0,1653,1654,5,34,0,0,1654, + 1655,5,73,0,0,1655,1656,5,116,0,0,1656,1657,5,101,0,0,1657,1658, + 5,109,0,0,1658,1659,5,115,0,0,1659,1660,5,80,0,0,1660,1661,5,97, + 0,0,1661,1662,5,116,0,0,1662,1663,5,104,0,0,1663,1664,5,34,0,0,1664, + 184,1,0,0,0,1665,1666,5,34,0,0,1666,1667,5,82,0,0,1667,1668,5,101, + 0,0,1668,1669,5,115,0,0,1669,1670,5,117,0,0,1670,1671,5,108,0,0, + 1671,1672,5,116,0,0,1672,1673,5,80,0,0,1673,1674,5,97,0,0,1674,1675, + 5,116,0,0,1675,1676,5,104,0,0,1676,1677,5,34,0,0,1677,186,1,0,0, + 0,1678,1679,5,34,0,0,1679,1680,5,82,0,0,1680,1681,5,101,0,0,1681, + 1682,5,115,0,0,1682,1683,5,117,0,0,1683,1684,5,108,0,0,1684,1685, + 5,116,0,0,1685,1686,5,34,0,0,1686,188,1,0,0,0,1687,1688,5,34,0,0, + 1688,1689,5,80,0,0,1689,1690,5,97,0,0,1690,1691,5,114,0,0,1691,1692, + 5,97,0,0,1692,1693,5,109,0,0,1693,1694,5,101,0,0,1694,1695,5,116, + 0,0,1695,1696,5,101,0,0,1696,1697,5,114,0,0,1697,1698,5,115,0,0, + 1698,1699,5,34,0,0,1699,190,1,0,0,0,1700,1701,5,34,0,0,1701,1702, + 5,82,0,0,1702,1703,5,101,0,0,1703,1704,5,115,0,0,1704,1705,5,117, + 0,0,1705,1706,5,108,0,0,1706,1707,5,116,0,0,1707,1708,5,83,0,0,1708, + 1709,5,101,0,0,1709,1710,5,108,0,0,1710,1711,5,101,0,0,1711,1712, + 5,99,0,0,1712,1713,5,116,0,0,1713,1714,5,111,0,0,1714,1715,5,114, + 0,0,1715,1716,5,34,0,0,1716,192,1,0,0,0,1717,1718,5,34,0,0,1718, + 1719,5,73,0,0,1719,1720,5,116,0,0,1720,1721,5,101,0,0,1721,1722, + 5,109,0,0,1722,1723,5,82,0,0,1723,1724,5,101,0,0,1724,1725,5,97, + 0,0,1725,1726,5,100,0,0,1726,1727,5,101,0,0,1727,1728,5,114,0,0, + 1728,1729,5,34,0,0,1729,194,1,0,0,0,1730,1731,5,34,0,0,1731,1732, + 5,82,0,0,1732,1733,5,101,0,0,1733,1734,5,97,0,0,1734,1735,5,100, + 0,0,1735,1736,5,101,0,0,1736,1737,5,114,0,0,1737,1738,5,67,0,0,1738, + 1739,5,111,0,0,1739,1740,5,110,0,0,1740,1741,5,102,0,0,1741,1742, + 5,105,0,0,1742,1743,5,103,0,0,1743,1744,5,34,0,0,1744,196,1,0,0, + 0,1745,1746,5,34,0,0,1746,1747,5,73,0,0,1747,1748,5,110,0,0,1748, + 1749,5,112,0,0,1749,1750,5,117,0,0,1750,1751,5,116,0,0,1751,1752, + 5,84,0,0,1752,1753,5,121,0,0,1753,1754,5,112,0,0,1754,1755,5,101, + 0,0,1755,1756,5,34,0,0,1756,198,1,0,0,0,1757,1758,5,34,0,0,1758, + 1759,5,67,0,0,1759,1760,5,83,0,0,1760,1761,5,86,0,0,1761,1762,5, + 72,0,0,1762,1763,5,101,0,0,1763,1764,5,97,0,0,1764,1765,5,100,0, + 0,1765,1766,5,101,0,0,1766,1767,5,114,0,0,1767,1768,5,76,0,0,1768, + 1769,5,111,0,0,1769,1770,5,99,0,0,1770,1771,5,97,0,0,1771,1772,5, + 116,0,0,1772,1773,5,105,0,0,1773,1774,5,111,0,0,1774,1775,5,110, + 0,0,1775,1776,5,34,0,0,1776,200,1,0,0,0,1777,1778,5,34,0,0,1778, + 1779,5,67,0,0,1779,1780,5,83,0,0,1780,1781,5,86,0,0,1781,1782,5, + 72,0,0,1782,1783,5,101,0,0,1783,1784,5,97,0,0,1784,1785,5,100,0, + 0,1785,1786,5,101,0,0,1786,1787,5,114,0,0,1787,1788,5,115,0,0,1788, + 1789,5,34,0,0,1789,202,1,0,0,0,1790,1791,5,34,0,0,1791,1792,5,77, + 0,0,1792,1793,5,97,0,0,1793,1794,5,120,0,0,1794,1795,5,73,0,0,1795, + 1796,5,116,0,0,1796,1797,5,101,0,0,1797,1798,5,109,0,0,1798,1799, + 5,115,0,0,1799,1800,5,34,0,0,1800,204,1,0,0,0,1801,1802,5,34,0,0, + 1802,1803,5,77,0,0,1803,1804,5,97,0,0,1804,1805,5,120,0,0,1805,1806, + 5,73,0,0,1806,1807,5,116,0,0,1807,1808,5,101,0,0,1808,1809,5,109, + 0,0,1809,1810,5,115,0,0,1810,1811,5,80,0,0,1811,1812,5,97,0,0,1812, + 1813,5,116,0,0,1813,1814,5,104,0,0,1814,1815,5,34,0,0,1815,206,1, + 0,0,0,1816,1817,5,34,0,0,1817,1818,5,78,0,0,1818,1819,5,101,0,0, + 1819,1820,5,120,0,0,1820,1821,5,116,0,0,1821,1822,5,34,0,0,1822, + 208,1,0,0,0,1823,1824,5,34,0,0,1824,1825,5,69,0,0,1825,1826,5,110, + 0,0,1826,1827,5,100,0,0,1827,1828,5,34,0,0,1828,210,1,0,0,0,1829, + 1830,5,34,0,0,1830,1831,5,67,0,0,1831,1832,5,97,0,0,1832,1833,5, + 117,0,0,1833,1834,5,115,0,0,1834,1835,5,101,0,0,1835,1836,5,34,0, + 0,1836,212,1,0,0,0,1837,1838,5,34,0,0,1838,1839,5,67,0,0,1839,1840, + 5,97,0,0,1840,1841,5,117,0,0,1841,1842,5,115,0,0,1842,1843,5,101, + 0,0,1843,1844,5,80,0,0,1844,1845,5,97,0,0,1845,1846,5,116,0,0,1846, + 1847,5,104,0,0,1847,1848,5,34,0,0,1848,214,1,0,0,0,1849,1850,5,34, + 0,0,1850,1851,5,69,0,0,1851,1852,5,114,0,0,1852,1853,5,114,0,0,1853, + 1854,5,111,0,0,1854,1855,5,114,0,0,1855,1856,5,34,0,0,1856,216,1, + 0,0,0,1857,1858,5,34,0,0,1858,1859,5,69,0,0,1859,1860,5,114,0,0, + 1860,1861,5,114,0,0,1861,1862,5,111,0,0,1862,1863,5,114,0,0,1863, + 1864,5,80,0,0,1864,1865,5,97,0,0,1865,1866,5,116,0,0,1866,1867,5, + 104,0,0,1867,1868,5,34,0,0,1868,218,1,0,0,0,1869,1870,5,34,0,0,1870, + 1871,5,82,0,0,1871,1872,5,101,0,0,1872,1873,5,116,0,0,1873,1874, + 5,114,0,0,1874,1875,5,121,0,0,1875,1876,5,34,0,0,1876,220,1,0,0, + 0,1877,1878,5,34,0,0,1878,1879,5,69,0,0,1879,1880,5,114,0,0,1880, + 1881,5,114,0,0,1881,1882,5,111,0,0,1882,1883,5,114,0,0,1883,1884, + 5,69,0,0,1884,1885,5,113,0,0,1885,1886,5,117,0,0,1886,1887,5,97, + 0,0,1887,1888,5,108,0,0,1888,1889,5,115,0,0,1889,1890,5,34,0,0,1890, + 222,1,0,0,0,1891,1892,5,34,0,0,1892,1893,5,73,0,0,1893,1894,5,110, + 0,0,1894,1895,5,116,0,0,1895,1896,5,101,0,0,1896,1897,5,114,0,0, + 1897,1898,5,118,0,0,1898,1899,5,97,0,0,1899,1900,5,108,0,0,1900, + 1901,5,83,0,0,1901,1902,5,101,0,0,1902,1903,5,99,0,0,1903,1904,5, + 111,0,0,1904,1905,5,110,0,0,1905,1906,5,100,0,0,1906,1907,5,115, + 0,0,1907,1908,5,34,0,0,1908,224,1,0,0,0,1909,1910,5,34,0,0,1910, + 1911,5,77,0,0,1911,1912,5,97,0,0,1912,1913,5,120,0,0,1913,1914,5, + 65,0,0,1914,1915,5,116,0,0,1915,1916,5,116,0,0,1916,1917,5,101,0, + 0,1917,1918,5,109,0,0,1918,1919,5,112,0,0,1919,1920,5,116,0,0,1920, + 1921,5,115,0,0,1921,1922,5,34,0,0,1922,226,1,0,0,0,1923,1924,5,34, + 0,0,1924,1925,5,66,0,0,1925,1926,5,97,0,0,1926,1927,5,99,0,0,1927, + 1928,5,107,0,0,1928,1929,5,111,0,0,1929,1930,5,102,0,0,1930,1931, + 5,102,0,0,1931,1932,5,82,0,0,1932,1933,5,97,0,0,1933,1934,5,116, + 0,0,1934,1935,5,101,0,0,1935,1936,5,34,0,0,1936,228,1,0,0,0,1937, + 1938,5,34,0,0,1938,1939,5,77,0,0,1939,1940,5,97,0,0,1940,1941,5, + 120,0,0,1941,1942,5,68,0,0,1942,1943,5,101,0,0,1943,1944,5,108,0, + 0,1944,1945,5,97,0,0,1945,1946,5,121,0,0,1946,1947,5,83,0,0,1947, + 1948,5,101,0,0,1948,1949,5,99,0,0,1949,1950,5,111,0,0,1950,1951, + 5,110,0,0,1951,1952,5,100,0,0,1952,1953,5,115,0,0,1953,1954,5,34, + 0,0,1954,230,1,0,0,0,1955,1956,5,34,0,0,1956,1957,5,74,0,0,1957, + 1958,5,105,0,0,1958,1959,5,116,0,0,1959,1960,5,116,0,0,1960,1961, + 5,101,0,0,1961,1962,5,114,0,0,1962,1963,5,83,0,0,1963,1964,5,116, + 0,0,1964,1965,5,114,0,0,1965,1966,5,97,0,0,1966,1967,5,116,0,0,1967, + 1968,5,101,0,0,1968,1969,5,103,0,0,1969,1970,5,121,0,0,1970,1971, + 5,34,0,0,1971,232,1,0,0,0,1972,1973,5,34,0,0,1973,1974,5,70,0,0, + 1974,1975,5,85,0,0,1975,1976,5,76,0,0,1976,1977,5,76,0,0,1977,1978, + 5,34,0,0,1978,234,1,0,0,0,1979,1980,5,34,0,0,1980,1981,5,78,0,0, + 1981,1982,5,79,0,0,1982,1983,5,78,0,0,1983,1984,5,69,0,0,1984,1985, + 5,34,0,0,1985,236,1,0,0,0,1986,1987,5,34,0,0,1987,1988,5,67,0,0, + 1988,1989,5,97,0,0,1989,1990,5,116,0,0,1990,1991,5,99,0,0,1991,1992, + 5,104,0,0,1992,1993,5,34,0,0,1993,238,1,0,0,0,1994,1995,5,34,0,0, + 1995,1996,5,83,0,0,1996,1997,5,116,0,0,1997,1998,5,97,0,0,1998,1999, + 5,116,0,0,1999,2000,5,101,0,0,2000,2001,5,115,0,0,2001,2002,5,46, + 0,0,2002,2003,5,65,0,0,2003,2004,5,76,0,0,2004,2005,5,76,0,0,2005, + 2006,5,34,0,0,2006,240,1,0,0,0,2007,2008,5,34,0,0,2008,2009,5,83, + 0,0,2009,2010,5,116,0,0,2010,2011,5,97,0,0,2011,2012,5,116,0,0,2012, + 2013,5,101,0,0,2013,2014,5,115,0,0,2014,2015,5,46,0,0,2015,2016, + 5,68,0,0,2016,2017,5,97,0,0,2017,2018,5,116,0,0,2018,2019,5,97,0, + 0,2019,2020,5,76,0,0,2020,2021,5,105,0,0,2021,2022,5,109,0,0,2022, + 2023,5,105,0,0,2023,2024,5,116,0,0,2024,2025,5,69,0,0,2025,2026, + 5,120,0,0,2026,2027,5,99,0,0,2027,2028,5,101,0,0,2028,2029,5,101, + 0,0,2029,2030,5,100,0,0,2030,2031,5,101,0,0,2031,2032,5,100,0,0, + 2032,2033,5,34,0,0,2033,242,1,0,0,0,2034,2035,5,34,0,0,2035,2036, + 5,83,0,0,2036,2037,5,116,0,0,2037,2038,5,97,0,0,2038,2039,5,116, + 0,0,2039,2040,5,101,0,0,2040,2041,5,115,0,0,2041,2042,5,46,0,0,2042, + 2043,5,72,0,0,2043,2044,5,101,0,0,2044,2045,5,97,0,0,2045,2046,5, + 114,0,0,2046,2047,5,116,0,0,2047,2048,5,98,0,0,2048,2049,5,101,0, + 0,2049,2050,5,97,0,0,2050,2051,5,116,0,0,2051,2052,5,84,0,0,2052, + 2053,5,105,0,0,2053,2054,5,109,0,0,2054,2055,5,101,0,0,2055,2056, + 5,111,0,0,2056,2057,5,117,0,0,2057,2058,5,116,0,0,2058,2059,5,34, + 0,0,2059,244,1,0,0,0,2060,2061,5,34,0,0,2061,2062,5,83,0,0,2062, + 2063,5,116,0,0,2063,2064,5,97,0,0,2064,2065,5,116,0,0,2065,2066, + 5,101,0,0,2066,2067,5,115,0,0,2067,2068,5,46,0,0,2068,2069,5,84, + 0,0,2069,2070,5,105,0,0,2070,2071,5,109,0,0,2071,2072,5,101,0,0, + 2072,2073,5,111,0,0,2073,2074,5,117,0,0,2074,2075,5,116,0,0,2075, + 2076,5,34,0,0,2076,246,1,0,0,0,2077,2078,5,34,0,0,2078,2079,5,83, + 0,0,2079,2080,5,116,0,0,2080,2081,5,97,0,0,2081,2082,5,116,0,0,2082, + 2083,5,101,0,0,2083,2084,5,115,0,0,2084,2085,5,46,0,0,2085,2086, + 5,84,0,0,2086,2087,5,97,0,0,2087,2088,5,115,0,0,2088,2089,5,107, + 0,0,2089,2090,5,70,0,0,2090,2091,5,97,0,0,2091,2092,5,105,0,0,2092, + 2093,5,108,0,0,2093,2094,5,101,0,0,2094,2095,5,100,0,0,2095,2096, + 5,34,0,0,2096,248,1,0,0,0,2097,2098,5,34,0,0,2098,2099,5,83,0,0, + 2099,2100,5,116,0,0,2100,2101,5,97,0,0,2101,2102,5,116,0,0,2102, + 2103,5,101,0,0,2103,2104,5,115,0,0,2104,2105,5,46,0,0,2105,2106, + 5,80,0,0,2106,2107,5,101,0,0,2107,2108,5,114,0,0,2108,2109,5,109, + 0,0,2109,2110,5,105,0,0,2110,2111,5,115,0,0,2111,2112,5,115,0,0, + 2112,2113,5,105,0,0,2113,2114,5,111,0,0,2114,2115,5,110,0,0,2115, + 2116,5,115,0,0,2116,2117,5,34,0,0,2117,250,1,0,0,0,2118,2119,5,34, + 0,0,2119,2120,5,83,0,0,2120,2121,5,116,0,0,2121,2122,5,97,0,0,2122, + 2123,5,116,0,0,2123,2124,5,101,0,0,2124,2125,5,115,0,0,2125,2126, + 5,46,0,0,2126,2127,5,82,0,0,2127,2128,5,101,0,0,2128,2129,5,115, + 0,0,2129,2130,5,117,0,0,2130,2131,5,108,0,0,2131,2132,5,116,0,0, + 2132,2133,5,80,0,0,2133,2134,5,97,0,0,2134,2135,5,116,0,0,2135,2136, + 5,104,0,0,2136,2137,5,77,0,0,2137,2138,5,97,0,0,2138,2139,5,116, + 0,0,2139,2140,5,99,0,0,2140,2141,5,104,0,0,2141,2142,5,70,0,0,2142, + 2143,5,97,0,0,2143,2144,5,105,0,0,2144,2145,5,108,0,0,2145,2146, + 5,117,0,0,2146,2147,5,114,0,0,2147,2148,5,101,0,0,2148,2149,5,34, + 0,0,2149,252,1,0,0,0,2150,2151,5,34,0,0,2151,2152,5,83,0,0,2152, + 2153,5,116,0,0,2153,2154,5,97,0,0,2154,2155,5,116,0,0,2155,2156, + 5,101,0,0,2156,2157,5,115,0,0,2157,2158,5,46,0,0,2158,2159,5,80, + 0,0,2159,2160,5,97,0,0,2160,2161,5,114,0,0,2161,2162,5,97,0,0,2162, + 2163,5,109,0,0,2163,2164,5,101,0,0,2164,2165,5,116,0,0,2165,2166, + 5,101,0,0,2166,2167,5,114,0,0,2167,2168,5,80,0,0,2168,2169,5,97, + 0,0,2169,2170,5,116,0,0,2170,2171,5,104,0,0,2171,2172,5,70,0,0,2172, + 2173,5,97,0,0,2173,2174,5,105,0,0,2174,2175,5,108,0,0,2175,2176, + 5,117,0,0,2176,2177,5,114,0,0,2177,2178,5,101,0,0,2178,2179,5,34, + 0,0,2179,254,1,0,0,0,2180,2181,5,34,0,0,2181,2182,5,83,0,0,2182, + 2183,5,116,0,0,2183,2184,5,97,0,0,2184,2185,5,116,0,0,2185,2186, + 5,101,0,0,2186,2187,5,115,0,0,2187,2188,5,46,0,0,2188,2189,5,66, + 0,0,2189,2190,5,114,0,0,2190,2191,5,97,0,0,2191,2192,5,110,0,0,2192, + 2193,5,99,0,0,2193,2194,5,104,0,0,2194,2195,5,70,0,0,2195,2196,5, + 97,0,0,2196,2197,5,105,0,0,2197,2198,5,108,0,0,2198,2199,5,101,0, + 0,2199,2200,5,100,0,0,2200,2201,5,34,0,0,2201,256,1,0,0,0,2202,2203, + 5,34,0,0,2203,2204,5,83,0,0,2204,2205,5,116,0,0,2205,2206,5,97,0, + 0,2206,2207,5,116,0,0,2207,2208,5,101,0,0,2208,2209,5,115,0,0,2209, + 2210,5,46,0,0,2210,2211,5,78,0,0,2211,2212,5,111,0,0,2212,2213,5, + 67,0,0,2213,2214,5,104,0,0,2214,2215,5,111,0,0,2215,2216,5,105,0, + 0,2216,2217,5,99,0,0,2217,2218,5,101,0,0,2218,2219,5,77,0,0,2219, + 2220,5,97,0,0,2220,2221,5,116,0,0,2221,2222,5,99,0,0,2222,2223,5, + 104,0,0,2223,2224,5,101,0,0,2224,2225,5,100,0,0,2225,2226,5,34,0, + 0,2226,258,1,0,0,0,2227,2228,5,34,0,0,2228,2229,5,83,0,0,2229,2230, + 5,116,0,0,2230,2231,5,97,0,0,2231,2232,5,116,0,0,2232,2233,5,101, + 0,0,2233,2234,5,115,0,0,2234,2235,5,46,0,0,2235,2236,5,73,0,0,2236, + 2237,5,110,0,0,2237,2238,5,116,0,0,2238,2239,5,114,0,0,2239,2240, + 5,105,0,0,2240,2241,5,110,0,0,2241,2242,5,115,0,0,2242,2243,5,105, + 0,0,2243,2244,5,99,0,0,2244,2245,5,70,0,0,2245,2246,5,97,0,0,2246, + 2247,5,105,0,0,2247,2248,5,108,0,0,2248,2249,5,117,0,0,2249,2250, + 5,114,0,0,2250,2251,5,101,0,0,2251,2252,5,34,0,0,2252,260,1,0,0, + 0,2253,2254,5,34,0,0,2254,2255,5,83,0,0,2255,2256,5,116,0,0,2256, + 2257,5,97,0,0,2257,2258,5,116,0,0,2258,2259,5,101,0,0,2259,2260, + 5,115,0,0,2260,2261,5,46,0,0,2261,2262,5,69,0,0,2262,2263,5,120, + 0,0,2263,2264,5,99,0,0,2264,2265,5,101,0,0,2265,2266,5,101,0,0,2266, + 2267,5,100,0,0,2267,2268,5,84,0,0,2268,2269,5,111,0,0,2269,2270, + 5,108,0,0,2270,2271,5,101,0,0,2271,2272,5,114,0,0,2272,2273,5,97, + 0,0,2273,2274,5,116,0,0,2274,2275,5,101,0,0,2275,2276,5,100,0,0, + 2276,2277,5,70,0,0,2277,2278,5,97,0,0,2278,2279,5,105,0,0,2279,2280, + 5,108,0,0,2280,2281,5,117,0,0,2281,2282,5,114,0,0,2282,2283,5,101, + 0,0,2283,2284,5,84,0,0,2284,2285,5,104,0,0,2285,2286,5,114,0,0,2286, + 2287,5,101,0,0,2287,2288,5,115,0,0,2288,2289,5,104,0,0,2289,2290, + 5,111,0,0,2290,2291,5,108,0,0,2291,2292,5,100,0,0,2292,2293,5,34, + 0,0,2293,262,1,0,0,0,2294,2295,5,34,0,0,2295,2296,5,83,0,0,2296, + 2297,5,116,0,0,2297,2298,5,97,0,0,2298,2299,5,116,0,0,2299,2300, + 5,101,0,0,2300,2301,5,115,0,0,2301,2302,5,46,0,0,2302,2303,5,73, + 0,0,2303,2304,5,116,0,0,2304,2305,5,101,0,0,2305,2306,5,109,0,0, + 2306,2307,5,82,0,0,2307,2308,5,101,0,0,2308,2309,5,97,0,0,2309,2310, + 5,100,0,0,2310,2311,5,101,0,0,2311,2312,5,114,0,0,2312,2313,5,70, + 0,0,2313,2314,5,97,0,0,2314,2315,5,105,0,0,2315,2316,5,108,0,0,2316, + 2317,5,101,0,0,2317,2318,5,100,0,0,2318,2319,5,34,0,0,2319,264,1, + 0,0,0,2320,2321,5,34,0,0,2321,2322,5,83,0,0,2322,2323,5,116,0,0, + 2323,2324,5,97,0,0,2324,2325,5,116,0,0,2325,2326,5,101,0,0,2326, + 2327,5,115,0,0,2327,2328,5,46,0,0,2328,2329,5,82,0,0,2329,2330,5, + 101,0,0,2330,2331,5,115,0,0,2331,2332,5,117,0,0,2332,2333,5,108, + 0,0,2333,2334,5,116,0,0,2334,2335,5,87,0,0,2335,2336,5,114,0,0,2336, + 2337,5,105,0,0,2337,2338,5,116,0,0,2338,2339,5,101,0,0,2339,2340, + 5,114,0,0,2340,2341,5,70,0,0,2341,2342,5,97,0,0,2342,2343,5,105, + 0,0,2343,2344,5,108,0,0,2344,2345,5,101,0,0,2345,2346,5,100,0,0, + 2346,2347,5,34,0,0,2347,266,1,0,0,0,2348,2349,5,34,0,0,2349,2350, + 5,83,0,0,2350,2351,5,116,0,0,2351,2352,5,97,0,0,2352,2353,5,116, + 0,0,2353,2354,5,101,0,0,2354,2355,5,115,0,0,2355,2356,5,46,0,0,2356, + 2357,5,82,0,0,2357,2358,5,117,0,0,2358,2359,5,110,0,0,2359,2360, + 5,116,0,0,2360,2361,5,105,0,0,2361,2362,5,109,0,0,2362,2363,5,101, + 0,0,2363,2364,5,34,0,0,2364,268,1,0,0,0,2365,2370,5,34,0,0,2366, + 2369,3,277,138,0,2367,2369,3,283,141,0,2368,2366,1,0,0,0,2368,2367, + 1,0,0,0,2369,2372,1,0,0,0,2370,2368,1,0,0,0,2370,2371,1,0,0,0,2371, + 2373,1,0,0,0,2372,2370,1,0,0,0,2373,2374,5,46,0,0,2374,2375,5,36, + 0,0,2375,2376,5,34,0,0,2376,270,1,0,0,0,2377,2378,5,34,0,0,2378, + 2379,5,36,0,0,2379,2380,5,36,0,0,2380,2385,1,0,0,0,2381,2384,3,277, + 138,0,2382,2384,3,283,141,0,2383,2381,1,0,0,0,2383,2382,1,0,0,0, + 2384,2387,1,0,0,0,2385,2383,1,0,0,0,2385,2386,1,0,0,0,2386,2388, + 1,0,0,0,2387,2385,1,0,0,0,2388,2389,5,34,0,0,2389,272,1,0,0,0,2390, + 2391,5,34,0,0,2391,2392,5,36,0,0,2392,2397,1,0,0,0,2393,2396,3,277, + 138,0,2394,2396,3,283,141,0,2395,2393,1,0,0,0,2395,2394,1,0,0,0, + 2396,2399,1,0,0,0,2397,2395,1,0,0,0,2397,2398,1,0,0,0,2398,2400, + 1,0,0,0,2399,2397,1,0,0,0,2400,2401,5,34,0,0,2401,274,1,0,0,0,2402, + 2407,5,34,0,0,2403,2406,3,277,138,0,2404,2406,3,283,141,0,2405,2403, + 1,0,0,0,2405,2404,1,0,0,0,2406,2409,1,0,0,0,2407,2405,1,0,0,0,2407, + 2408,1,0,0,0,2408,2410,1,0,0,0,2409,2407,1,0,0,0,2410,2411,5,34, + 0,0,2411,276,1,0,0,0,2412,2415,5,92,0,0,2413,2416,7,0,0,0,2414,2416, + 3,279,139,0,2415,2413,1,0,0,0,2415,2414,1,0,0,0,2416,278,1,0,0,0, + 2417,2418,5,117,0,0,2418,2419,3,281,140,0,2419,2420,3,281,140,0, + 2420,2421,3,281,140,0,2421,2422,3,281,140,0,2422,280,1,0,0,0,2423, + 2424,7,1,0,0,2424,282,1,0,0,0,2425,2426,8,2,0,0,2426,284,1,0,0,0, + 2427,2436,5,48,0,0,2428,2432,7,3,0,0,2429,2431,7,4,0,0,2430,2429, + 1,0,0,0,2431,2434,1,0,0,0,2432,2430,1,0,0,0,2432,2433,1,0,0,0,2433, + 2436,1,0,0,0,2434,2432,1,0,0,0,2435,2427,1,0,0,0,2435,2428,1,0,0, + 0,2436,286,1,0,0,0,2437,2439,5,45,0,0,2438,2437,1,0,0,0,2438,2439, + 1,0,0,0,2439,2440,1,0,0,0,2440,2447,3,285,142,0,2441,2443,5,46,0, + 0,2442,2444,7,4,0,0,2443,2442,1,0,0,0,2444,2445,1,0,0,0,2445,2443, + 1,0,0,0,2445,2446,1,0,0,0,2446,2448,1,0,0,0,2447,2441,1,0,0,0,2447, + 2448,1,0,0,0,2448,2450,1,0,0,0,2449,2451,3,289,144,0,2450,2449,1, + 0,0,0,2450,2451,1,0,0,0,2451,288,1,0,0,0,2452,2454,7,5,0,0,2453, + 2455,7,6,0,0,2454,2453,1,0,0,0,2454,2455,1,0,0,0,2455,2456,1,0,0, + 0,2456,2457,3,285,142,0,2457,290,1,0,0,0,2458,2460,7,7,0,0,2459, + 2458,1,0,0,0,2460,2461,1,0,0,0,2461,2459,1,0,0,0,2461,2462,1,0,0, + 0,2462,2463,1,0,0,0,2463,2464,6,145,0,0,2464,292,1,0,0,0,18,0,2368, + 2370,2383,2385,2395,2397,2405,2407,2415,2432,2435,2438,2445,2447, + 2450,2454,2461,1,6,0,0 ] class ASLLexer(Lexer): @@ -1011,60 +1020,61 @@ class ASLLexer(Lexer): ITEMPROCESSOR = 84 ITERATOR = 85 ITEMSELECTOR = 86 - MAXCONCURRENCY = 87 - RESOURCE = 88 - INPUTPATH = 89 - OUTPUTPATH = 90 - ITEMSPATH = 91 - RESULTPATH = 92 - RESULT = 93 - PARAMETERS = 94 - RESULTSELECTOR = 95 - ITEMREADER = 96 - READERCONFIG = 97 - INPUTTYPE = 98 - CSVHEADERLOCATION = 99 - CSVHEADERS = 100 - MAXITEMS = 101 - MAXITEMSPATH = 102 - NEXT = 103 - END = 104 - CAUSE = 105 - CAUSEPATH = 106 - ERROR = 107 - ERRORPATH = 108 - RETRY = 109 - ERROREQUALS = 110 - INTERVALSECONDS = 111 - MAXATTEMPTS = 112 - BACKOFFRATE = 113 - MAXDELAYSECONDS = 114 - JITTERSTRATEGY = 115 - FULL = 116 - NONE = 117 - CATCH = 118 - ERRORNAMEStatesALL = 119 - ERRORNAMEStatesDataLimitExceeded = 120 - ERRORNAMEStatesHeartbeatTimeout = 121 - ERRORNAMEStatesTimeout = 122 - ERRORNAMEStatesTaskFailed = 123 - ERRORNAMEStatesPermissions = 124 - ERRORNAMEStatesResultPathMatchFailure = 125 - ERRORNAMEStatesParameterPathFailure = 126 - ERRORNAMEStatesBranchFailed = 127 - ERRORNAMEStatesNoChoiceMatched = 128 - ERRORNAMEStatesIntrinsicFailure = 129 - ERRORNAMEStatesExceedToleratedFailureThreshold = 130 - ERRORNAMEStatesItemReaderFailed = 131 - ERRORNAMEStatesResultWriterFailed = 132 - ERRORNAMEStatesRuntime = 133 - STRINGDOLLAR = 134 - STRINGPATHCONTEXTOBJ = 135 - STRINGPATH = 136 - STRING = 137 - INT = 138 - NUMBER = 139 - WS = 140 + MAXCONCURRENCYPATH = 87 + MAXCONCURRENCY = 88 + RESOURCE = 89 + INPUTPATH = 90 + OUTPUTPATH = 91 + ITEMSPATH = 92 + RESULTPATH = 93 + RESULT = 94 + PARAMETERS = 95 + RESULTSELECTOR = 96 + ITEMREADER = 97 + READERCONFIG = 98 + INPUTTYPE = 99 + CSVHEADERLOCATION = 100 + CSVHEADERS = 101 + MAXITEMS = 102 + MAXITEMSPATH = 103 + NEXT = 104 + END = 105 + CAUSE = 106 + CAUSEPATH = 107 + ERROR = 108 + ERRORPATH = 109 + RETRY = 110 + ERROREQUALS = 111 + INTERVALSECONDS = 112 + MAXATTEMPTS = 113 + BACKOFFRATE = 114 + MAXDELAYSECONDS = 115 + JITTERSTRATEGY = 116 + FULL = 117 + NONE = 118 + CATCH = 119 + ERRORNAMEStatesALL = 120 + ERRORNAMEStatesDataLimitExceeded = 121 + ERRORNAMEStatesHeartbeatTimeout = 122 + ERRORNAMEStatesTimeout = 123 + ERRORNAMEStatesTaskFailed = 124 + ERRORNAMEStatesPermissions = 125 + ERRORNAMEStatesResultPathMatchFailure = 126 + ERRORNAMEStatesParameterPathFailure = 127 + ERRORNAMEStatesBranchFailed = 128 + ERRORNAMEStatesNoChoiceMatched = 129 + ERRORNAMEStatesIntrinsicFailure = 130 + ERRORNAMEStatesExceedToleratedFailureThreshold = 131 + ERRORNAMEStatesItemReaderFailed = 132 + ERRORNAMEStatesResultWriterFailed = 133 + ERRORNAMEStatesRuntime = 134 + STRINGDOLLAR = 135 + STRINGPATHCONTEXTOBJ = 136 + STRINGPATH = 137 + STRING = 138 + INT = 139 + NUMBER = 140 + WS = 141 channelNames = [ u"DEFAULT_TOKEN_CHANNEL", u"HIDDEN" ] @@ -1096,23 +1106,23 @@ class ASLLexer(Lexer): "'\"TimeoutSecondsPath\"'", "'\"HeartbeatSeconds\"'", "'\"HeartbeatSecondsPath\"'", "'\"ProcessorConfig\"'", "'\"Mode\"'", "'\"INLINE\"'", "'\"DISTRIBUTED\"'", "'\"ExecutionType\"'", "'\"STANDARD\"'", "'\"ItemProcessor\"'", - "'\"Iterator\"'", "'\"ItemSelector\"'", "'\"MaxConcurrency\"'", - "'\"Resource\"'", "'\"InputPath\"'", "'\"OutputPath\"'", "'\"ItemsPath\"'", - "'\"ResultPath\"'", "'\"Result\"'", "'\"Parameters\"'", "'\"ResultSelector\"'", - "'\"ItemReader\"'", "'\"ReaderConfig\"'", "'\"InputType\"'", - "'\"CSVHeaderLocation\"'", "'\"CSVHeaders\"'", "'\"MaxItems\"'", - "'\"MaxItemsPath\"'", "'\"Next\"'", "'\"End\"'", "'\"Cause\"'", - "'\"CausePath\"'", "'\"Error\"'", "'\"ErrorPath\"'", "'\"Retry\"'", - "'\"ErrorEquals\"'", "'\"IntervalSeconds\"'", "'\"MaxAttempts\"'", - "'\"BackoffRate\"'", "'\"MaxDelaySeconds\"'", "'\"JitterStrategy\"'", - "'\"FULL\"'", "'\"NONE\"'", "'\"Catch\"'", "'\"States.ALL\"'", - "'\"States.DataLimitExceeded\"'", "'\"States.HeartbeatTimeout\"'", - "'\"States.Timeout\"'", "'\"States.TaskFailed\"'", "'\"States.Permissions\"'", - "'\"States.ResultPathMatchFailure\"'", "'\"States.ParameterPathFailure\"'", - "'\"States.BranchFailed\"'", "'\"States.NoChoiceMatched\"'", - "'\"States.IntrinsicFailure\"'", "'\"States.ExceedToleratedFailureThreshold\"'", - "'\"States.ItemReaderFailed\"'", "'\"States.ResultWriterFailed\"'", - "'\"States.Runtime\"'" ] + "'\"Iterator\"'", "'\"ItemSelector\"'", "'\"MaxConcurrencyPath\"'", + "'\"MaxConcurrency\"'", "'\"Resource\"'", "'\"InputPath\"'", + "'\"OutputPath\"'", "'\"ItemsPath\"'", "'\"ResultPath\"'", "'\"Result\"'", + "'\"Parameters\"'", "'\"ResultSelector\"'", "'\"ItemReader\"'", + "'\"ReaderConfig\"'", "'\"InputType\"'", "'\"CSVHeaderLocation\"'", + "'\"CSVHeaders\"'", "'\"MaxItems\"'", "'\"MaxItemsPath\"'", + "'\"Next\"'", "'\"End\"'", "'\"Cause\"'", "'\"CausePath\"'", + "'\"Error\"'", "'\"ErrorPath\"'", "'\"Retry\"'", "'\"ErrorEquals\"'", + "'\"IntervalSeconds\"'", "'\"MaxAttempts\"'", "'\"BackoffRate\"'", + "'\"MaxDelaySeconds\"'", "'\"JitterStrategy\"'", "'\"FULL\"'", + "'\"NONE\"'", "'\"Catch\"'", "'\"States.ALL\"'", "'\"States.DataLimitExceeded\"'", + "'\"States.HeartbeatTimeout\"'", "'\"States.Timeout\"'", "'\"States.TaskFailed\"'", + "'\"States.Permissions\"'", "'\"States.ResultPathMatchFailure\"'", + "'\"States.ParameterPathFailure\"'", "'\"States.BranchFailed\"'", + "'\"States.NoChoiceMatched\"'", "'\"States.IntrinsicFailure\"'", + "'\"States.ExceedToleratedFailureThreshold\"'", "'\"States.ItemReaderFailed\"'", + "'\"States.ResultWriterFailed\"'", "'\"States.Runtime\"'" ] symbolicNames = [ "", "COMMA", "COLON", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "TRUE", @@ -1135,21 +1145,22 @@ class ASLLexer(Lexer): "TIMESTAMP", "TIMEOUTSECONDS", "TIMEOUTSECONDSPATH", "HEARTBEATSECONDS", "HEARTBEATSECONDSPATH", "PROCESSORCONFIG", "MODE", "INLINE", "DISTRIBUTED", "EXECUTIONTYPE", "STANDARD", "ITEMPROCESSOR", - "ITERATOR", "ITEMSELECTOR", "MAXCONCURRENCY", "RESOURCE", "INPUTPATH", - "OUTPUTPATH", "ITEMSPATH", "RESULTPATH", "RESULT", "PARAMETERS", - "RESULTSELECTOR", "ITEMREADER", "READERCONFIG", "INPUTTYPE", - "CSVHEADERLOCATION", "CSVHEADERS", "MAXITEMS", "MAXITEMSPATH", - "NEXT", "END", "CAUSE", "CAUSEPATH", "ERROR", "ERRORPATH", "RETRY", - "ERROREQUALS", "INTERVALSECONDS", "MAXATTEMPTS", "BACKOFFRATE", - "MAXDELAYSECONDS", "JITTERSTRATEGY", "FULL", "NONE", "CATCH", - "ERRORNAMEStatesALL", "ERRORNAMEStatesDataLimitExceeded", "ERRORNAMEStatesHeartbeatTimeout", - "ERRORNAMEStatesTimeout", "ERRORNAMEStatesTaskFailed", "ERRORNAMEStatesPermissions", - "ERRORNAMEStatesResultPathMatchFailure", "ERRORNAMEStatesParameterPathFailure", - "ERRORNAMEStatesBranchFailed", "ERRORNAMEStatesNoChoiceMatched", - "ERRORNAMEStatesIntrinsicFailure", "ERRORNAMEStatesExceedToleratedFailureThreshold", - "ERRORNAMEStatesItemReaderFailed", "ERRORNAMEStatesResultWriterFailed", - "ERRORNAMEStatesRuntime", "STRINGDOLLAR", "STRINGPATHCONTEXTOBJ", - "STRINGPATH", "STRING", "INT", "NUMBER", "WS" ] + "ITERATOR", "ITEMSELECTOR", "MAXCONCURRENCYPATH", "MAXCONCURRENCY", + "RESOURCE", "INPUTPATH", "OUTPUTPATH", "ITEMSPATH", "RESULTPATH", + "RESULT", "PARAMETERS", "RESULTSELECTOR", "ITEMREADER", "READERCONFIG", + "INPUTTYPE", "CSVHEADERLOCATION", "CSVHEADERS", "MAXITEMS", + "MAXITEMSPATH", "NEXT", "END", "CAUSE", "CAUSEPATH", "ERROR", + "ERRORPATH", "RETRY", "ERROREQUALS", "INTERVALSECONDS", "MAXATTEMPTS", + "BACKOFFRATE", "MAXDELAYSECONDS", "JITTERSTRATEGY", "FULL", + "NONE", "CATCH", "ERRORNAMEStatesALL", "ERRORNAMEStatesDataLimitExceeded", + "ERRORNAMEStatesHeartbeatTimeout", "ERRORNAMEStatesTimeout", + "ERRORNAMEStatesTaskFailed", "ERRORNAMEStatesPermissions", "ERRORNAMEStatesResultPathMatchFailure", + "ERRORNAMEStatesParameterPathFailure", "ERRORNAMEStatesBranchFailed", + "ERRORNAMEStatesNoChoiceMatched", "ERRORNAMEStatesIntrinsicFailure", + "ERRORNAMEStatesExceedToleratedFailureThreshold", "ERRORNAMEStatesItemReaderFailed", + "ERRORNAMEStatesResultWriterFailed", "ERRORNAMEStatesRuntime", + "STRINGDOLLAR", "STRINGPATHCONTEXTOBJ", "STRINGPATH", "STRING", + "INT", "NUMBER", "WS" ] ruleNames = [ "COMMA", "COLON", "LBRACK", "RBRACK", "LBRACE", "RBRACE", "TRUE", "FALSE", "NULL", "COMMENT", "STATES", "STARTAT", @@ -1173,8 +1184,8 @@ class ASLLexer(Lexer): "TIMEOUTSECONDS", "TIMEOUTSECONDSPATH", "HEARTBEATSECONDS", "HEARTBEATSECONDSPATH", "PROCESSORCONFIG", "MODE", "INLINE", "DISTRIBUTED", "EXECUTIONTYPE", "STANDARD", "ITEMPROCESSOR", - "ITERATOR", "ITEMSELECTOR", "MAXCONCURRENCY", "RESOURCE", - "INPUTPATH", "OUTPUTPATH", "ITEMSPATH", "RESULTPATH", + "ITERATOR", "ITEMSELECTOR", "MAXCONCURRENCYPATH", "MAXCONCURRENCY", + "RESOURCE", "INPUTPATH", "OUTPUTPATH", "ITEMSPATH", "RESULTPATH", "RESULT", "PARAMETERS", "RESULTSELECTOR", "ITEMREADER", "READERCONFIG", "INPUTTYPE", "CSVHEADERLOCATION", "CSVHEADERS", "MAXITEMS", "MAXITEMSPATH", "NEXT", "END", "CAUSE", "CAUSEPATH", diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLParser.py b/localstack/services/stepfunctions/asl/antlr/runtime/ASLParser.py index 6630597378de2..965e3f1ae477b 100644 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLParser.py +++ b/localstack/services/stepfunctions/asl/antlr/runtime/ASLParser.py @@ -10,7 +10,7 @@ def serializedATN(): return [ - 4,1,140,838,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6, + 4,1,141,845,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6, 7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7, 13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,19,2, 20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2,26,7, @@ -24,290 +24,293 @@ def serializedATN(): 72,7,72,2,73,7,73,2,74,7,74,2,75,7,75,2,76,7,76,2,77,7,77,2,78,7, 78,2,79,7,79,2,80,7,80,2,81,7,81,2,82,7,82,2,83,7,83,2,84,7,84,2, 85,7,85,2,86,7,86,2,87,7,87,2,88,7,88,2,89,7,89,2,90,7,90,2,91,7, - 91,1,0,1,0,1,0,1,1,1,1,1,1,1,1,5,1,192,8,1,10,1,12,1,195,9,1,1,1, - 1,1,1,2,1,2,1,2,1,2,1,2,3,2,204,8,2,1,3,1,3,1,3,1,3,1,4,1,4,1,4, - 1,4,1,5,1,5,1,5,1,5,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6, + 91,2,92,7,92,1,0,1,0,1,0,1,1,1,1,1,1,1,1,5,1,194,8,1,10,1,12,1,197, + 9,1,1,1,1,1,1,2,1,2,1,2,1,2,1,2,3,2,206,8,2,1,3,1,3,1,3,1,3,1,4, + 1,4,1,4,1,4,1,5,1,5,1,5,1,5,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6, 1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6, - 1,6,1,6,1,6,1,6,1,6,1,6,1,6,3,6,252,8,6,1,7,1,7,1,7,1,7,1,7,1,7, - 5,7,260,8,7,10,7,12,7,263,9,7,1,7,1,7,1,8,1,8,1,9,1,9,1,9,1,9,1, - 10,1,10,1,10,1,10,5,10,277,8,10,10,10,12,10,280,9,10,1,10,1,10,1, - 11,1,11,1,11,1,11,1,12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,1,14,1, - 14,1,14,1,14,3,14,300,8,14,1,15,1,15,1,15,1,15,1,16,1,16,1,16,1, - 16,3,16,310,8,16,1,17,1,17,1,17,1,17,3,17,316,8,17,1,18,1,18,1,18, - 1,18,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,21,1,21,1,21,1,21, - 1,21,1,21,3,21,336,8,21,1,22,1,22,1,22,1,22,1,23,1,23,1,23,1,23, - 1,23,1,23,3,23,348,8,23,1,24,1,24,1,24,1,24,1,25,1,25,1,25,1,25, - 1,26,1,26,1,26,1,26,1,27,1,27,1,27,1,27,1,28,1,28,1,28,1,28,1,29, - 1,29,1,29,1,29,1,30,1,30,1,30,1,30,1,31,1,31,1,31,1,31,1,32,1,32, - 1,32,1,32,1,33,1,33,1,33,1,33,1,34,1,34,1,34,1,34,1,35,1,35,1,35, - 1,35,5,35,398,8,35,10,35,12,35,401,9,35,1,35,1,35,1,35,1,35,3,35, - 407,8,35,1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,36, - 1,36,1,36,3,36,422,8,36,1,37,1,37,1,38,1,38,1,38,1,38,5,38,430,8, - 38,10,38,12,38,433,9,38,1,38,1,38,1,38,1,38,3,38,439,8,38,1,39,1, - 39,1,39,1,39,3,39,445,8,39,1,40,1,40,1,40,1,40,1,40,3,40,452,8,40, - 1,41,1,41,1,41,1,41,1,42,1,42,1,43,1,43,1,43,1,43,1,43,1,43,5,43, - 466,8,43,10,43,12,43,469,9,43,1,43,1,43,1,44,1,44,1,44,1,44,4,44, - 477,8,44,11,44,12,44,478,1,44,1,44,1,44,1,44,1,44,1,44,5,44,487, - 8,44,10,44,12,44,490,9,44,1,44,1,44,3,44,494,8,44,1,45,1,45,1,45, - 1,45,3,45,500,8,45,1,46,1,46,3,46,504,8,46,1,47,1,47,1,47,1,47,1, - 47,1,47,1,47,5,47,513,8,47,10,47,12,47,516,9,47,1,47,1,47,3,47,520, - 8,47,1,48,1,48,1,48,1,48,1,49,1,49,1,49,1,49,1,50,1,50,1,50,1,50, - 1,50,1,50,5,50,536,8,50,10,50,12,50,539,9,50,1,50,1,50,1,51,1,51, - 1,51,1,51,1,51,1,51,5,51,549,8,51,10,51,12,51,552,9,51,1,51,1,51, - 1,52,1,52,1,52,1,52,3,52,560,8,52,1,53,1,53,1,53,1,53,1,53,1,53, - 5,53,568,8,53,10,53,12,53,571,9,53,1,53,1,53,1,54,1,54,3,54,577, - 8,54,1,55,1,55,1,55,1,55,1,56,1,56,1,57,1,57,1,57,1,57,1,58,1,58, - 1,59,1,59,1,59,1,59,1,59,1,59,5,59,597,8,59,10,59,12,59,600,9,59, - 1,59,1,59,1,60,1,60,1,60,1,60,3,60,608,8,60,1,61,1,61,1,61,1,61, - 1,62,1,62,1,62,1,62,1,62,1,62,5,62,620,8,62,10,62,12,62,623,9,62, - 1,62,1,62,1,63,1,63,1,63,3,63,630,8,63,1,64,1,64,1,64,1,64,1,64, - 1,64,5,64,638,8,64,10,64,12,64,641,9,64,1,64,1,64,1,65,1,65,1,65, - 1,65,1,65,3,65,650,8,65,1,66,1,66,1,66,1,66,1,67,1,67,1,67,1,67, - 1,68,1,68,1,68,1,68,1,68,1,68,5,68,666,8,68,10,68,12,68,669,9,68, - 1,68,1,68,1,69,1,69,1,69,1,69,1,70,1,70,1,70,1,70,1,71,1,71,1,71, - 1,71,1,71,1,71,5,71,687,8,71,10,71,12,71,690,9,71,3,71,692,8,71, - 1,71,1,71,1,72,1,72,1,72,1,72,5,72,700,8,72,10,72,12,72,703,9,72, - 1,72,1,72,1,73,1,73,1,73,1,73,1,73,1,73,1,73,3,73,714,8,73,1,74, - 1,74,1,74,1,74,1,74,1,74,5,74,722,8,74,10,74,12,74,725,9,74,1,74, - 1,74,1,75,1,75,1,75,1,75,1,76,1,76,1,76,1,76,1,77,1,77,1,77,1,77, - 1,78,1,78,1,78,1,78,1,79,1,79,1,79,1,79,1,80,1,80,1,80,1,80,1,80, - 1,80,5,80,755,8,80,10,80,12,80,758,9,80,3,80,760,8,80,1,80,1,80, - 1,81,1,81,1,81,1,81,5,81,768,8,81,10,81,12,81,771,9,81,1,81,1,81, - 1,82,1,82,1,82,1,82,3,82,779,8,82,1,83,1,83,1,84,1,84,1,85,1,85, - 1,86,1,86,3,86,789,8,86,1,87,1,87,1,87,1,87,5,87,795,8,87,10,87, - 12,87,798,9,87,1,87,1,87,1,87,1,87,3,87,804,8,87,1,88,1,88,1,88, - 1,88,1,89,1,89,1,89,1,89,5,89,814,8,89,10,89,12,89,817,9,89,1,89, - 1,89,1,89,1,89,3,89,823,8,89,1,90,1,90,1,90,1,90,1,90,1,90,1,90, - 1,90,1,90,3,90,834,8,90,1,91,1,91,1,91,0,0,92,0,2,4,6,8,10,12,14, - 16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58, - 60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100, - 102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132, - 134,136,138,140,142,144,146,148,150,152,154,156,158,160,162,164, - 166,168,170,172,174,176,178,180,182,0,9,1,0,7,8,1,0,16,23,1,0,80, - 81,1,0,138,139,1,0,116,117,3,0,29,36,38,47,49,69,3,0,28,28,37,37, - 48,48,1,0,119,133,5,0,10,13,15,105,107,107,109,119,121,137,863,0, - 184,1,0,0,0,2,187,1,0,0,0,4,203,1,0,0,0,6,205,1,0,0,0,8,209,1,0, - 0,0,10,213,1,0,0,0,12,251,1,0,0,0,14,253,1,0,0,0,16,266,1,0,0,0, - 18,268,1,0,0,0,20,272,1,0,0,0,22,283,1,0,0,0,24,287,1,0,0,0,26,291, - 1,0,0,0,28,295,1,0,0,0,30,301,1,0,0,0,32,305,1,0,0,0,34,311,1,0, - 0,0,36,317,1,0,0,0,38,321,1,0,0,0,40,325,1,0,0,0,42,335,1,0,0,0, - 44,337,1,0,0,0,46,347,1,0,0,0,48,349,1,0,0,0,50,353,1,0,0,0,52,357, - 1,0,0,0,54,361,1,0,0,0,56,365,1,0,0,0,58,369,1,0,0,0,60,373,1,0, - 0,0,62,377,1,0,0,0,64,381,1,0,0,0,66,385,1,0,0,0,68,389,1,0,0,0, - 70,406,1,0,0,0,72,421,1,0,0,0,74,423,1,0,0,0,76,438,1,0,0,0,78,444, - 1,0,0,0,80,451,1,0,0,0,82,453,1,0,0,0,84,457,1,0,0,0,86,459,1,0, - 0,0,88,493,1,0,0,0,90,499,1,0,0,0,92,503,1,0,0,0,94,505,1,0,0,0, - 96,521,1,0,0,0,98,525,1,0,0,0,100,529,1,0,0,0,102,542,1,0,0,0,104, - 559,1,0,0,0,106,561,1,0,0,0,108,576,1,0,0,0,110,578,1,0,0,0,112, - 582,1,0,0,0,114,584,1,0,0,0,116,588,1,0,0,0,118,590,1,0,0,0,120, - 607,1,0,0,0,122,609,1,0,0,0,124,613,1,0,0,0,126,629,1,0,0,0,128, - 631,1,0,0,0,130,649,1,0,0,0,132,651,1,0,0,0,134,655,1,0,0,0,136, - 659,1,0,0,0,138,672,1,0,0,0,140,676,1,0,0,0,142,680,1,0,0,0,144, - 695,1,0,0,0,146,713,1,0,0,0,148,715,1,0,0,0,150,728,1,0,0,0,152, - 732,1,0,0,0,154,736,1,0,0,0,156,740,1,0,0,0,158,744,1,0,0,0,160, - 748,1,0,0,0,162,763,1,0,0,0,164,778,1,0,0,0,166,780,1,0,0,0,168, - 782,1,0,0,0,170,784,1,0,0,0,172,788,1,0,0,0,174,803,1,0,0,0,176, - 805,1,0,0,0,178,822,1,0,0,0,180,833,1,0,0,0,182,835,1,0,0,0,184, - 185,3,2,1,0,185,186,5,0,0,1,186,1,1,0,0,0,187,188,5,5,0,0,188,193, - 3,4,2,0,189,190,5,1,0,0,190,192,3,4,2,0,191,189,1,0,0,0,192,195, - 1,0,0,0,193,191,1,0,0,0,193,194,1,0,0,0,194,196,1,0,0,0,195,193, - 1,0,0,0,196,197,5,6,0,0,197,3,1,0,0,0,198,204,3,8,4,0,199,204,3, - 10,5,0,200,204,3,6,3,0,201,204,3,14,7,0,202,204,3,62,31,0,203,198, - 1,0,0,0,203,199,1,0,0,0,203,200,1,0,0,0,203,201,1,0,0,0,203,202, - 1,0,0,0,204,5,1,0,0,0,205,206,5,12,0,0,206,207,5,2,0,0,207,208,3, - 182,91,0,208,7,1,0,0,0,209,210,5,10,0,0,210,211,5,2,0,0,211,212, - 3,182,91,0,212,9,1,0,0,0,213,214,5,14,0,0,214,215,5,2,0,0,215,216, - 3,182,91,0,216,11,1,0,0,0,217,252,3,8,4,0,218,252,3,22,11,0,219, - 252,3,28,14,0,220,252,3,26,13,0,221,252,3,24,12,0,222,252,3,30,15, - 0,223,252,3,32,16,0,224,252,3,34,17,0,225,252,3,36,18,0,226,252, - 3,38,19,0,227,252,3,86,43,0,228,252,3,40,20,0,229,252,3,42,21,0, - 230,252,3,44,22,0,231,252,3,46,23,0,232,252,3,48,24,0,233,252,3, - 50,25,0,234,252,3,52,26,0,235,252,3,54,27,0,236,252,3,56,28,0,237, - 252,3,102,51,0,238,252,3,118,59,0,239,252,3,122,61,0,240,252,3,124, - 62,0,241,252,3,58,29,0,242,252,3,62,31,0,243,252,3,64,32,0,244,252, - 3,66,33,0,245,252,3,68,34,0,246,252,3,100,50,0,247,252,3,60,30,0, - 248,252,3,142,71,0,249,252,3,160,80,0,250,252,3,82,41,0,251,217, - 1,0,0,0,251,218,1,0,0,0,251,219,1,0,0,0,251,220,1,0,0,0,251,221, - 1,0,0,0,251,222,1,0,0,0,251,223,1,0,0,0,251,224,1,0,0,0,251,225, - 1,0,0,0,251,226,1,0,0,0,251,227,1,0,0,0,251,228,1,0,0,0,251,229, - 1,0,0,0,251,230,1,0,0,0,251,231,1,0,0,0,251,232,1,0,0,0,251,233, - 1,0,0,0,251,234,1,0,0,0,251,235,1,0,0,0,251,236,1,0,0,0,251,237, - 1,0,0,0,251,238,1,0,0,0,251,239,1,0,0,0,251,240,1,0,0,0,251,241, - 1,0,0,0,251,242,1,0,0,0,251,243,1,0,0,0,251,244,1,0,0,0,251,245, - 1,0,0,0,251,246,1,0,0,0,251,247,1,0,0,0,251,248,1,0,0,0,251,249, - 1,0,0,0,251,250,1,0,0,0,252,13,1,0,0,0,253,254,5,11,0,0,254,255, - 5,2,0,0,255,256,5,5,0,0,256,261,3,18,9,0,257,258,5,1,0,0,258,260, - 3,18,9,0,259,257,1,0,0,0,260,263,1,0,0,0,261,259,1,0,0,0,261,262, - 1,0,0,0,262,264,1,0,0,0,263,261,1,0,0,0,264,265,5,6,0,0,265,15,1, - 0,0,0,266,267,3,182,91,0,267,17,1,0,0,0,268,269,3,16,8,0,269,270, - 5,2,0,0,270,271,3,20,10,0,271,19,1,0,0,0,272,273,5,5,0,0,273,278, - 3,12,6,0,274,275,5,1,0,0,275,277,3,12,6,0,276,274,1,0,0,0,277,280, - 1,0,0,0,278,276,1,0,0,0,278,279,1,0,0,0,279,281,1,0,0,0,280,278, - 1,0,0,0,281,282,5,6,0,0,282,21,1,0,0,0,283,284,5,15,0,0,284,285, - 5,2,0,0,285,286,3,84,42,0,286,23,1,0,0,0,287,288,5,103,0,0,288,289, - 5,2,0,0,289,290,3,182,91,0,290,25,1,0,0,0,291,292,5,88,0,0,292,293, - 5,2,0,0,293,294,3,182,91,0,294,27,1,0,0,0,295,296,5,89,0,0,296,299, - 5,2,0,0,297,300,5,9,0,0,298,300,3,182,91,0,299,297,1,0,0,0,299,298, - 1,0,0,0,300,29,1,0,0,0,301,302,5,93,0,0,302,303,5,2,0,0,303,304, - 3,180,90,0,304,31,1,0,0,0,305,306,5,92,0,0,306,309,5,2,0,0,307,310, - 5,9,0,0,308,310,3,182,91,0,309,307,1,0,0,0,309,308,1,0,0,0,310,33, - 1,0,0,0,311,312,5,90,0,0,312,315,5,2,0,0,313,316,5,9,0,0,314,316, - 3,182,91,0,315,313,1,0,0,0,315,314,1,0,0,0,316,35,1,0,0,0,317,318, - 5,104,0,0,318,319,5,2,0,0,319,320,7,0,0,0,320,37,1,0,0,0,321,322, - 5,26,0,0,322,323,5,2,0,0,323,324,3,182,91,0,324,39,1,0,0,0,325,326, - 5,107,0,0,326,327,5,2,0,0,327,328,3,182,91,0,328,41,1,0,0,0,329, - 330,5,108,0,0,330,331,5,2,0,0,331,336,5,136,0,0,332,333,5,108,0, - 0,333,334,5,2,0,0,334,336,3,74,37,0,335,329,1,0,0,0,335,332,1,0, - 0,0,336,43,1,0,0,0,337,338,5,105,0,0,338,339,5,2,0,0,339,340,3,182, - 91,0,340,45,1,0,0,0,341,342,5,106,0,0,342,343,5,2,0,0,343,348,5, - 136,0,0,344,345,5,106,0,0,345,346,5,2,0,0,346,348,3,74,37,0,347, - 341,1,0,0,0,347,344,1,0,0,0,348,47,1,0,0,0,349,350,5,71,0,0,350, - 351,5,2,0,0,351,352,5,138,0,0,352,49,1,0,0,0,353,354,5,70,0,0,354, - 355,5,2,0,0,355,356,3,182,91,0,356,51,1,0,0,0,357,358,5,73,0,0,358, - 359,5,2,0,0,359,360,3,182,91,0,360,53,1,0,0,0,361,362,5,72,0,0,362, - 363,5,2,0,0,363,364,3,182,91,0,364,55,1,0,0,0,365,366,5,91,0,0,366, - 367,5,2,0,0,367,368,3,182,91,0,368,57,1,0,0,0,369,370,5,87,0,0,370, - 371,5,2,0,0,371,372,5,138,0,0,372,59,1,0,0,0,373,374,5,94,0,0,374, - 375,5,2,0,0,375,376,3,70,35,0,376,61,1,0,0,0,377,378,5,74,0,0,378, - 379,5,2,0,0,379,380,5,138,0,0,380,63,1,0,0,0,381,382,5,75,0,0,382, - 383,5,2,0,0,383,384,5,136,0,0,384,65,1,0,0,0,385,386,5,76,0,0,386, - 387,5,2,0,0,387,388,5,138,0,0,388,67,1,0,0,0,389,390,5,77,0,0,390, - 391,5,2,0,0,391,392,5,136,0,0,392,69,1,0,0,0,393,394,5,5,0,0,394, - 399,3,72,36,0,395,396,5,1,0,0,396,398,3,72,36,0,397,395,1,0,0,0, - 398,401,1,0,0,0,399,397,1,0,0,0,399,400,1,0,0,0,400,402,1,0,0,0, - 401,399,1,0,0,0,402,403,5,6,0,0,403,407,1,0,0,0,404,405,5,5,0,0, - 405,407,5,6,0,0,406,393,1,0,0,0,406,404,1,0,0,0,407,71,1,0,0,0,408, - 409,5,134,0,0,409,410,5,2,0,0,410,422,5,136,0,0,411,412,5,134,0, - 0,412,413,5,2,0,0,413,422,5,135,0,0,414,415,5,134,0,0,415,416,5, - 2,0,0,416,422,3,74,37,0,417,418,3,182,91,0,418,419,5,2,0,0,419,420, - 3,78,39,0,420,422,1,0,0,0,421,408,1,0,0,0,421,411,1,0,0,0,421,414, - 1,0,0,0,421,417,1,0,0,0,422,73,1,0,0,0,423,424,5,137,0,0,424,75, - 1,0,0,0,425,426,5,3,0,0,426,431,3,78,39,0,427,428,5,1,0,0,428,430, - 3,78,39,0,429,427,1,0,0,0,430,433,1,0,0,0,431,429,1,0,0,0,431,432, - 1,0,0,0,432,434,1,0,0,0,433,431,1,0,0,0,434,435,5,4,0,0,435,439, - 1,0,0,0,436,437,5,3,0,0,437,439,5,4,0,0,438,425,1,0,0,0,438,436, - 1,0,0,0,439,77,1,0,0,0,440,445,3,72,36,0,441,445,3,76,38,0,442,445, - 3,70,35,0,443,445,3,80,40,0,444,440,1,0,0,0,444,441,1,0,0,0,444, - 442,1,0,0,0,444,443,1,0,0,0,445,79,1,0,0,0,446,452,5,139,0,0,447, - 452,5,138,0,0,448,452,7,0,0,0,449,452,5,9,0,0,450,452,3,182,91,0, - 451,446,1,0,0,0,451,447,1,0,0,0,451,448,1,0,0,0,451,449,1,0,0,0, - 451,450,1,0,0,0,452,81,1,0,0,0,453,454,5,95,0,0,454,455,5,2,0,0, - 455,456,3,70,35,0,456,83,1,0,0,0,457,458,7,1,0,0,458,85,1,0,0,0, - 459,460,5,24,0,0,460,461,5,2,0,0,461,462,5,3,0,0,462,467,3,88,44, - 0,463,464,5,1,0,0,464,466,3,88,44,0,465,463,1,0,0,0,466,469,1,0, - 0,0,467,465,1,0,0,0,467,468,1,0,0,0,468,470,1,0,0,0,469,467,1,0, - 0,0,470,471,5,4,0,0,471,87,1,0,0,0,472,473,5,5,0,0,473,476,3,90, - 45,0,474,475,5,1,0,0,475,477,3,90,45,0,476,474,1,0,0,0,477,478,1, - 0,0,0,478,476,1,0,0,0,478,479,1,0,0,0,479,480,1,0,0,0,480,481,5, - 6,0,0,481,494,1,0,0,0,482,483,5,5,0,0,483,488,3,92,46,0,484,485, - 5,1,0,0,485,487,3,92,46,0,486,484,1,0,0,0,487,490,1,0,0,0,488,486, - 1,0,0,0,488,489,1,0,0,0,489,491,1,0,0,0,490,488,1,0,0,0,491,492, - 5,6,0,0,492,494,1,0,0,0,493,472,1,0,0,0,493,482,1,0,0,0,494,89,1, - 0,0,0,495,500,3,96,48,0,496,500,3,98,49,0,497,500,3,24,12,0,498, - 500,3,8,4,0,499,495,1,0,0,0,499,496,1,0,0,0,499,497,1,0,0,0,499, - 498,1,0,0,0,500,91,1,0,0,0,501,504,3,94,47,0,502,504,3,24,12,0,503, - 501,1,0,0,0,503,502,1,0,0,0,504,93,1,0,0,0,505,506,3,168,84,0,506, - 519,5,2,0,0,507,520,3,88,44,0,508,509,5,3,0,0,509,514,3,88,44,0, - 510,511,5,1,0,0,511,513,3,88,44,0,512,510,1,0,0,0,513,516,1,0,0, - 0,514,512,1,0,0,0,514,515,1,0,0,0,515,517,1,0,0,0,516,514,1,0,0, - 0,517,518,5,4,0,0,518,520,1,0,0,0,519,507,1,0,0,0,519,508,1,0,0, - 0,520,95,1,0,0,0,521,522,5,25,0,0,522,523,5,2,0,0,523,524,3,182, - 91,0,524,97,1,0,0,0,525,526,3,166,83,0,526,527,5,2,0,0,527,528,3, - 180,90,0,528,99,1,0,0,0,529,530,5,27,0,0,530,531,5,2,0,0,531,532, - 5,3,0,0,532,537,3,2,1,0,533,534,5,1,0,0,534,536,3,2,1,0,535,533, - 1,0,0,0,536,539,1,0,0,0,537,535,1,0,0,0,537,538,1,0,0,0,538,540, - 1,0,0,0,539,537,1,0,0,0,540,541,5,4,0,0,541,101,1,0,0,0,542,543, - 5,84,0,0,543,544,5,2,0,0,544,545,5,5,0,0,545,550,3,104,52,0,546, - 547,5,1,0,0,547,549,3,104,52,0,548,546,1,0,0,0,549,552,1,0,0,0,550, - 548,1,0,0,0,550,551,1,0,0,0,551,553,1,0,0,0,552,550,1,0,0,0,553, - 554,5,6,0,0,554,103,1,0,0,0,555,560,3,106,53,0,556,560,3,6,3,0,557, - 560,3,14,7,0,558,560,3,8,4,0,559,555,1,0,0,0,559,556,1,0,0,0,559, - 557,1,0,0,0,559,558,1,0,0,0,560,105,1,0,0,0,561,562,5,78,0,0,562, - 563,5,2,0,0,563,564,5,5,0,0,564,569,3,108,54,0,565,566,5,1,0,0,566, - 568,3,108,54,0,567,565,1,0,0,0,568,571,1,0,0,0,569,567,1,0,0,0,569, - 570,1,0,0,0,570,572,1,0,0,0,571,569,1,0,0,0,572,573,5,6,0,0,573, - 107,1,0,0,0,574,577,3,110,55,0,575,577,3,114,57,0,576,574,1,0,0, - 0,576,575,1,0,0,0,577,109,1,0,0,0,578,579,5,79,0,0,579,580,5,2,0, - 0,580,581,3,112,56,0,581,111,1,0,0,0,582,583,7,2,0,0,583,113,1,0, - 0,0,584,585,5,82,0,0,585,586,5,2,0,0,586,587,3,116,58,0,587,115, - 1,0,0,0,588,589,5,83,0,0,589,117,1,0,0,0,590,591,5,85,0,0,591,592, - 5,2,0,0,592,593,5,5,0,0,593,598,3,120,60,0,594,595,5,1,0,0,595,597, - 3,120,60,0,596,594,1,0,0,0,597,600,1,0,0,0,598,596,1,0,0,0,598,599, - 1,0,0,0,599,601,1,0,0,0,600,598,1,0,0,0,601,602,5,6,0,0,602,119, - 1,0,0,0,603,608,3,6,3,0,604,608,3,14,7,0,605,608,3,8,4,0,606,608, - 3,106,53,0,607,603,1,0,0,0,607,604,1,0,0,0,607,605,1,0,0,0,607,606, - 1,0,0,0,608,121,1,0,0,0,609,610,5,86,0,0,610,611,5,2,0,0,611,612, - 3,70,35,0,612,123,1,0,0,0,613,614,5,96,0,0,614,615,5,2,0,0,615,616, - 5,5,0,0,616,621,3,126,63,0,617,618,5,1,0,0,618,620,3,126,63,0,619, - 617,1,0,0,0,620,623,1,0,0,0,621,619,1,0,0,0,621,622,1,0,0,0,622, - 624,1,0,0,0,623,621,1,0,0,0,624,625,5,6,0,0,625,125,1,0,0,0,626, - 630,3,26,13,0,627,630,3,60,30,0,628,630,3,128,64,0,629,626,1,0,0, - 0,629,627,1,0,0,0,629,628,1,0,0,0,630,127,1,0,0,0,631,632,5,97,0, - 0,632,633,5,2,0,0,633,634,5,5,0,0,634,639,3,130,65,0,635,636,5,1, - 0,0,636,638,3,130,65,0,637,635,1,0,0,0,638,641,1,0,0,0,639,637,1, - 0,0,0,639,640,1,0,0,0,640,642,1,0,0,0,641,639,1,0,0,0,642,643,5, - 6,0,0,643,129,1,0,0,0,644,650,3,132,66,0,645,650,3,134,67,0,646, - 650,3,136,68,0,647,650,3,138,69,0,648,650,3,140,70,0,649,644,1,0, - 0,0,649,645,1,0,0,0,649,646,1,0,0,0,649,647,1,0,0,0,649,648,1,0, - 0,0,650,131,1,0,0,0,651,652,5,98,0,0,652,653,5,2,0,0,653,654,3,182, - 91,0,654,133,1,0,0,0,655,656,5,99,0,0,656,657,5,2,0,0,657,658,3, - 182,91,0,658,135,1,0,0,0,659,660,5,100,0,0,660,661,5,2,0,0,661,662, - 5,3,0,0,662,667,3,182,91,0,663,664,5,1,0,0,664,666,3,182,91,0,665, - 663,1,0,0,0,666,669,1,0,0,0,667,665,1,0,0,0,667,668,1,0,0,0,668, - 670,1,0,0,0,669,667,1,0,0,0,670,671,5,4,0,0,671,137,1,0,0,0,672, - 673,5,101,0,0,673,674,5,2,0,0,674,675,5,138,0,0,675,139,1,0,0,0, - 676,677,5,102,0,0,677,678,5,2,0,0,678,679,5,136,0,0,679,141,1,0, - 0,0,680,681,5,109,0,0,681,682,5,2,0,0,682,691,5,3,0,0,683,688,3, - 144,72,0,684,685,5,1,0,0,685,687,3,144,72,0,686,684,1,0,0,0,687, - 690,1,0,0,0,688,686,1,0,0,0,688,689,1,0,0,0,689,692,1,0,0,0,690, - 688,1,0,0,0,691,683,1,0,0,0,691,692,1,0,0,0,692,693,1,0,0,0,693, - 694,5,4,0,0,694,143,1,0,0,0,695,696,5,5,0,0,696,701,3,146,73,0,697, - 698,5,1,0,0,698,700,3,146,73,0,699,697,1,0,0,0,700,703,1,0,0,0,701, - 699,1,0,0,0,701,702,1,0,0,0,702,704,1,0,0,0,703,701,1,0,0,0,704, - 705,5,6,0,0,705,145,1,0,0,0,706,714,3,148,74,0,707,714,3,150,75, - 0,708,714,3,152,76,0,709,714,3,154,77,0,710,714,3,156,78,0,711,714, - 3,158,79,0,712,714,3,8,4,0,713,706,1,0,0,0,713,707,1,0,0,0,713,708, - 1,0,0,0,713,709,1,0,0,0,713,710,1,0,0,0,713,711,1,0,0,0,713,712, - 1,0,0,0,714,147,1,0,0,0,715,716,5,110,0,0,716,717,5,2,0,0,717,718, - 5,3,0,0,718,723,3,172,86,0,719,720,5,1,0,0,720,722,3,172,86,0,721, - 719,1,0,0,0,722,725,1,0,0,0,723,721,1,0,0,0,723,724,1,0,0,0,724, - 726,1,0,0,0,725,723,1,0,0,0,726,727,5,4,0,0,727,149,1,0,0,0,728, - 729,5,111,0,0,729,730,5,2,0,0,730,731,5,138,0,0,731,151,1,0,0,0, - 732,733,5,112,0,0,733,734,5,2,0,0,734,735,5,138,0,0,735,153,1,0, - 0,0,736,737,5,113,0,0,737,738,5,2,0,0,738,739,7,3,0,0,739,155,1, - 0,0,0,740,741,5,114,0,0,741,742,5,2,0,0,742,743,5,138,0,0,743,157, - 1,0,0,0,744,745,5,115,0,0,745,746,5,2,0,0,746,747,7,4,0,0,747,159, - 1,0,0,0,748,749,5,118,0,0,749,750,5,2,0,0,750,759,5,3,0,0,751,756, - 3,162,81,0,752,753,5,1,0,0,753,755,3,162,81,0,754,752,1,0,0,0,755, - 758,1,0,0,0,756,754,1,0,0,0,756,757,1,0,0,0,757,760,1,0,0,0,758, - 756,1,0,0,0,759,751,1,0,0,0,759,760,1,0,0,0,760,761,1,0,0,0,761, - 762,5,4,0,0,762,161,1,0,0,0,763,764,5,5,0,0,764,769,3,164,82,0,765, - 766,5,1,0,0,766,768,3,164,82,0,767,765,1,0,0,0,768,771,1,0,0,0,769, - 767,1,0,0,0,769,770,1,0,0,0,770,772,1,0,0,0,771,769,1,0,0,0,772, - 773,5,6,0,0,773,163,1,0,0,0,774,779,3,148,74,0,775,779,3,32,16,0, - 776,779,3,24,12,0,777,779,3,8,4,0,778,774,1,0,0,0,778,775,1,0,0, - 0,778,776,1,0,0,0,778,777,1,0,0,0,779,165,1,0,0,0,780,781,7,5,0, - 0,781,167,1,0,0,0,782,783,7,6,0,0,783,169,1,0,0,0,784,785,7,7,0, - 0,785,171,1,0,0,0,786,789,3,170,85,0,787,789,3,182,91,0,788,786, - 1,0,0,0,788,787,1,0,0,0,789,173,1,0,0,0,790,791,5,5,0,0,791,796, - 3,176,88,0,792,793,5,1,0,0,793,795,3,176,88,0,794,792,1,0,0,0,795, - 798,1,0,0,0,796,794,1,0,0,0,796,797,1,0,0,0,797,799,1,0,0,0,798, - 796,1,0,0,0,799,800,5,6,0,0,800,804,1,0,0,0,801,802,5,5,0,0,802, - 804,5,6,0,0,803,790,1,0,0,0,803,801,1,0,0,0,804,175,1,0,0,0,805, - 806,3,182,91,0,806,807,5,2,0,0,807,808,3,180,90,0,808,177,1,0,0, - 0,809,810,5,3,0,0,810,815,3,180,90,0,811,812,5,1,0,0,812,814,3,180, - 90,0,813,811,1,0,0,0,814,817,1,0,0,0,815,813,1,0,0,0,815,816,1,0, - 0,0,816,818,1,0,0,0,817,815,1,0,0,0,818,819,5,4,0,0,819,823,1,0, - 0,0,820,821,5,3,0,0,821,823,5,4,0,0,822,809,1,0,0,0,822,820,1,0, - 0,0,823,179,1,0,0,0,824,834,5,139,0,0,825,834,5,138,0,0,826,834, - 5,7,0,0,827,834,5,8,0,0,828,834,5,9,0,0,829,834,3,176,88,0,830,834, - 3,178,89,0,831,834,3,174,87,0,832,834,3,182,91,0,833,824,1,0,0,0, - 833,825,1,0,0,0,833,826,1,0,0,0,833,827,1,0,0,0,833,828,1,0,0,0, - 833,829,1,0,0,0,833,830,1,0,0,0,833,831,1,0,0,0,833,832,1,0,0,0, - 834,181,1,0,0,0,835,836,7,8,0,0,836,183,1,0,0,0,52,193,203,251,261, - 278,299,309,315,335,347,399,406,421,431,438,444,451,467,478,488, - 493,499,503,514,519,537,550,559,569,576,598,607,621,629,639,649, - 667,688,691,701,713,723,756,759,769,778,788,796,803,815,822,833 + 1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,1,6,3,6,255,8,6,1,7,1,7,1,7, + 1,7,1,7,1,7,5,7,263,8,7,10,7,12,7,266,9,7,1,7,1,7,1,8,1,8,1,9,1, + 9,1,9,1,9,1,10,1,10,1,10,1,10,5,10,280,8,10,10,10,12,10,283,9,10, + 1,10,1,10,1,11,1,11,1,11,1,11,1,12,1,12,1,12,1,12,1,13,1,13,1,13, + 1,13,1,14,1,14,1,14,1,14,3,14,303,8,14,1,15,1,15,1,15,1,15,1,16, + 1,16,1,16,1,16,3,16,313,8,16,1,17,1,17,1,17,1,17,3,17,319,8,17,1, + 18,1,18,1,18,1,18,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,20,1,21,1, + 21,1,21,1,21,1,21,1,21,3,21,339,8,21,1,22,1,22,1,22,1,22,1,23,1, + 23,1,23,1,23,1,23,1,23,3,23,351,8,23,1,24,1,24,1,24,1,24,1,25,1, + 25,1,25,1,25,1,26,1,26,1,26,1,26,1,27,1,27,1,27,1,27,1,28,1,28,1, + 28,1,28,1,29,1,29,1,29,1,29,1,30,1,30,1,30,1,30,1,31,1,31,1,31,1, + 31,1,32,1,32,1,32,1,32,1,33,1,33,1,33,1,33,1,34,1,34,1,34,1,34,1, + 35,1,35,1,35,1,35,1,36,1,36,1,36,1,36,5,36,405,8,36,10,36,12,36, + 408,9,36,1,36,1,36,1,36,1,36,3,36,414,8,36,1,37,1,37,1,37,1,37,1, + 37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,1,37,3,37,429,8,37,1,38,1, + 38,1,39,1,39,1,39,1,39,5,39,437,8,39,10,39,12,39,440,9,39,1,39,1, + 39,1,39,1,39,3,39,446,8,39,1,40,1,40,1,40,1,40,3,40,452,8,40,1,41, + 1,41,1,41,1,41,1,41,3,41,459,8,41,1,42,1,42,1,42,1,42,1,43,1,43, + 1,44,1,44,1,44,1,44,1,44,1,44,5,44,473,8,44,10,44,12,44,476,9,44, + 1,44,1,44,1,45,1,45,1,45,1,45,4,45,484,8,45,11,45,12,45,485,1,45, + 1,45,1,45,1,45,1,45,1,45,5,45,494,8,45,10,45,12,45,497,9,45,1,45, + 1,45,3,45,501,8,45,1,46,1,46,1,46,1,46,3,46,507,8,46,1,47,1,47,3, + 47,511,8,47,1,48,1,48,1,48,1,48,1,48,1,48,1,48,5,48,520,8,48,10, + 48,12,48,523,9,48,1,48,1,48,3,48,527,8,48,1,49,1,49,1,49,1,49,1, + 50,1,50,1,50,1,50,1,51,1,51,1,51,1,51,1,51,1,51,5,51,543,8,51,10, + 51,12,51,546,9,51,1,51,1,51,1,52,1,52,1,52,1,52,1,52,1,52,5,52,556, + 8,52,10,52,12,52,559,9,52,1,52,1,52,1,53,1,53,1,53,1,53,3,53,567, + 8,53,1,54,1,54,1,54,1,54,1,54,1,54,5,54,575,8,54,10,54,12,54,578, + 9,54,1,54,1,54,1,55,1,55,3,55,584,8,55,1,56,1,56,1,56,1,56,1,57, + 1,57,1,58,1,58,1,58,1,58,1,59,1,59,1,60,1,60,1,60,1,60,1,60,1,60, + 5,60,604,8,60,10,60,12,60,607,9,60,1,60,1,60,1,61,1,61,1,61,1,61, + 3,61,615,8,61,1,62,1,62,1,62,1,62,1,63,1,63,1,63,1,63,1,63,1,63, + 5,63,627,8,63,10,63,12,63,630,9,63,1,63,1,63,1,64,1,64,1,64,3,64, + 637,8,64,1,65,1,65,1,65,1,65,1,65,1,65,5,65,645,8,65,10,65,12,65, + 648,9,65,1,65,1,65,1,66,1,66,1,66,1,66,1,66,3,66,657,8,66,1,67,1, + 67,1,67,1,67,1,68,1,68,1,68,1,68,1,69,1,69,1,69,1,69,1,69,1,69,5, + 69,673,8,69,10,69,12,69,676,9,69,1,69,1,69,1,70,1,70,1,70,1,70,1, + 71,1,71,1,71,1,71,1,72,1,72,1,72,1,72,1,72,1,72,5,72,694,8,72,10, + 72,12,72,697,9,72,3,72,699,8,72,1,72,1,72,1,73,1,73,1,73,1,73,5, + 73,707,8,73,10,73,12,73,710,9,73,1,73,1,73,1,74,1,74,1,74,1,74,1, + 74,1,74,1,74,3,74,721,8,74,1,75,1,75,1,75,1,75,1,75,1,75,5,75,729, + 8,75,10,75,12,75,732,9,75,1,75,1,75,1,76,1,76,1,76,1,76,1,77,1,77, + 1,77,1,77,1,78,1,78,1,78,1,78,1,79,1,79,1,79,1,79,1,80,1,80,1,80, + 1,80,1,81,1,81,1,81,1,81,1,81,1,81,5,81,762,8,81,10,81,12,81,765, + 9,81,3,81,767,8,81,1,81,1,81,1,82,1,82,1,82,1,82,5,82,775,8,82,10, + 82,12,82,778,9,82,1,82,1,82,1,83,1,83,1,83,1,83,3,83,786,8,83,1, + 84,1,84,1,85,1,85,1,86,1,86,1,87,1,87,3,87,796,8,87,1,88,1,88,1, + 88,1,88,5,88,802,8,88,10,88,12,88,805,9,88,1,88,1,88,1,88,1,88,3, + 88,811,8,88,1,89,1,89,1,89,1,89,1,90,1,90,1,90,1,90,5,90,821,8,90, + 10,90,12,90,824,9,90,1,90,1,90,1,90,1,90,3,90,830,8,90,1,91,1,91, + 1,91,1,91,1,91,1,91,1,91,1,91,1,91,3,91,841,8,91,1,92,1,92,1,92, + 0,0,93,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40, + 42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84, + 86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,120, + 122,124,126,128,130,132,134,136,138,140,142,144,146,148,150,152, + 154,156,158,160,162,164,166,168,170,172,174,176,178,180,182,184, + 0,9,1,0,7,8,1,0,16,23,1,0,80,81,1,0,139,140,1,0,117,118,3,0,29,36, + 38,47,49,69,3,0,28,28,37,37,48,48,1,0,120,134,5,0,10,13,15,106,108, + 108,110,120,122,138,870,0,186,1,0,0,0,2,189,1,0,0,0,4,205,1,0,0, + 0,6,207,1,0,0,0,8,211,1,0,0,0,10,215,1,0,0,0,12,254,1,0,0,0,14,256, + 1,0,0,0,16,269,1,0,0,0,18,271,1,0,0,0,20,275,1,0,0,0,22,286,1,0, + 0,0,24,290,1,0,0,0,26,294,1,0,0,0,28,298,1,0,0,0,30,304,1,0,0,0, + 32,308,1,0,0,0,34,314,1,0,0,0,36,320,1,0,0,0,38,324,1,0,0,0,40,328, + 1,0,0,0,42,338,1,0,0,0,44,340,1,0,0,0,46,350,1,0,0,0,48,352,1,0, + 0,0,50,356,1,0,0,0,52,360,1,0,0,0,54,364,1,0,0,0,56,368,1,0,0,0, + 58,372,1,0,0,0,60,376,1,0,0,0,62,380,1,0,0,0,64,384,1,0,0,0,66,388, + 1,0,0,0,68,392,1,0,0,0,70,396,1,0,0,0,72,413,1,0,0,0,74,428,1,0, + 0,0,76,430,1,0,0,0,78,445,1,0,0,0,80,451,1,0,0,0,82,458,1,0,0,0, + 84,460,1,0,0,0,86,464,1,0,0,0,88,466,1,0,0,0,90,500,1,0,0,0,92,506, + 1,0,0,0,94,510,1,0,0,0,96,512,1,0,0,0,98,528,1,0,0,0,100,532,1,0, + 0,0,102,536,1,0,0,0,104,549,1,0,0,0,106,566,1,0,0,0,108,568,1,0, + 0,0,110,583,1,0,0,0,112,585,1,0,0,0,114,589,1,0,0,0,116,591,1,0, + 0,0,118,595,1,0,0,0,120,597,1,0,0,0,122,614,1,0,0,0,124,616,1,0, + 0,0,126,620,1,0,0,0,128,636,1,0,0,0,130,638,1,0,0,0,132,656,1,0, + 0,0,134,658,1,0,0,0,136,662,1,0,0,0,138,666,1,0,0,0,140,679,1,0, + 0,0,142,683,1,0,0,0,144,687,1,0,0,0,146,702,1,0,0,0,148,720,1,0, + 0,0,150,722,1,0,0,0,152,735,1,0,0,0,154,739,1,0,0,0,156,743,1,0, + 0,0,158,747,1,0,0,0,160,751,1,0,0,0,162,755,1,0,0,0,164,770,1,0, + 0,0,166,785,1,0,0,0,168,787,1,0,0,0,170,789,1,0,0,0,172,791,1,0, + 0,0,174,795,1,0,0,0,176,810,1,0,0,0,178,812,1,0,0,0,180,829,1,0, + 0,0,182,840,1,0,0,0,184,842,1,0,0,0,186,187,3,2,1,0,187,188,5,0, + 0,1,188,1,1,0,0,0,189,190,5,5,0,0,190,195,3,4,2,0,191,192,5,1,0, + 0,192,194,3,4,2,0,193,191,1,0,0,0,194,197,1,0,0,0,195,193,1,0,0, + 0,195,196,1,0,0,0,196,198,1,0,0,0,197,195,1,0,0,0,198,199,5,6,0, + 0,199,3,1,0,0,0,200,206,3,8,4,0,201,206,3,10,5,0,202,206,3,6,3,0, + 203,206,3,14,7,0,204,206,3,64,32,0,205,200,1,0,0,0,205,201,1,0,0, + 0,205,202,1,0,0,0,205,203,1,0,0,0,205,204,1,0,0,0,206,5,1,0,0,0, + 207,208,5,12,0,0,208,209,5,2,0,0,209,210,3,184,92,0,210,7,1,0,0, + 0,211,212,5,10,0,0,212,213,5,2,0,0,213,214,3,184,92,0,214,9,1,0, + 0,0,215,216,5,14,0,0,216,217,5,2,0,0,217,218,3,184,92,0,218,11,1, + 0,0,0,219,255,3,8,4,0,220,255,3,22,11,0,221,255,3,28,14,0,222,255, + 3,26,13,0,223,255,3,24,12,0,224,255,3,30,15,0,225,255,3,32,16,0, + 226,255,3,34,17,0,227,255,3,36,18,0,228,255,3,38,19,0,229,255,3, + 88,44,0,230,255,3,40,20,0,231,255,3,42,21,0,232,255,3,44,22,0,233, + 255,3,46,23,0,234,255,3,48,24,0,235,255,3,50,25,0,236,255,3,52,26, + 0,237,255,3,54,27,0,238,255,3,56,28,0,239,255,3,104,52,0,240,255, + 3,120,60,0,241,255,3,124,62,0,242,255,3,126,63,0,243,255,3,58,29, + 0,244,255,3,60,30,0,245,255,3,64,32,0,246,255,3,66,33,0,247,255, + 3,68,34,0,248,255,3,70,35,0,249,255,3,102,51,0,250,255,3,62,31,0, + 251,255,3,144,72,0,252,255,3,162,81,0,253,255,3,84,42,0,254,219, + 1,0,0,0,254,220,1,0,0,0,254,221,1,0,0,0,254,222,1,0,0,0,254,223, + 1,0,0,0,254,224,1,0,0,0,254,225,1,0,0,0,254,226,1,0,0,0,254,227, + 1,0,0,0,254,228,1,0,0,0,254,229,1,0,0,0,254,230,1,0,0,0,254,231, + 1,0,0,0,254,232,1,0,0,0,254,233,1,0,0,0,254,234,1,0,0,0,254,235, + 1,0,0,0,254,236,1,0,0,0,254,237,1,0,0,0,254,238,1,0,0,0,254,239, + 1,0,0,0,254,240,1,0,0,0,254,241,1,0,0,0,254,242,1,0,0,0,254,243, + 1,0,0,0,254,244,1,0,0,0,254,245,1,0,0,0,254,246,1,0,0,0,254,247, + 1,0,0,0,254,248,1,0,0,0,254,249,1,0,0,0,254,250,1,0,0,0,254,251, + 1,0,0,0,254,252,1,0,0,0,254,253,1,0,0,0,255,13,1,0,0,0,256,257,5, + 11,0,0,257,258,5,2,0,0,258,259,5,5,0,0,259,264,3,18,9,0,260,261, + 5,1,0,0,261,263,3,18,9,0,262,260,1,0,0,0,263,266,1,0,0,0,264,262, + 1,0,0,0,264,265,1,0,0,0,265,267,1,0,0,0,266,264,1,0,0,0,267,268, + 5,6,0,0,268,15,1,0,0,0,269,270,3,184,92,0,270,17,1,0,0,0,271,272, + 3,16,8,0,272,273,5,2,0,0,273,274,3,20,10,0,274,19,1,0,0,0,275,276, + 5,5,0,0,276,281,3,12,6,0,277,278,5,1,0,0,278,280,3,12,6,0,279,277, + 1,0,0,0,280,283,1,0,0,0,281,279,1,0,0,0,281,282,1,0,0,0,282,284, + 1,0,0,0,283,281,1,0,0,0,284,285,5,6,0,0,285,21,1,0,0,0,286,287,5, + 15,0,0,287,288,5,2,0,0,288,289,3,86,43,0,289,23,1,0,0,0,290,291, + 5,104,0,0,291,292,5,2,0,0,292,293,3,184,92,0,293,25,1,0,0,0,294, + 295,5,89,0,0,295,296,5,2,0,0,296,297,3,184,92,0,297,27,1,0,0,0,298, + 299,5,90,0,0,299,302,5,2,0,0,300,303,5,9,0,0,301,303,3,184,92,0, + 302,300,1,0,0,0,302,301,1,0,0,0,303,29,1,0,0,0,304,305,5,94,0,0, + 305,306,5,2,0,0,306,307,3,182,91,0,307,31,1,0,0,0,308,309,5,93,0, + 0,309,312,5,2,0,0,310,313,5,9,0,0,311,313,3,184,92,0,312,310,1,0, + 0,0,312,311,1,0,0,0,313,33,1,0,0,0,314,315,5,91,0,0,315,318,5,2, + 0,0,316,319,5,9,0,0,317,319,3,184,92,0,318,316,1,0,0,0,318,317,1, + 0,0,0,319,35,1,0,0,0,320,321,5,105,0,0,321,322,5,2,0,0,322,323,7, + 0,0,0,323,37,1,0,0,0,324,325,5,26,0,0,325,326,5,2,0,0,326,327,3, + 184,92,0,327,39,1,0,0,0,328,329,5,108,0,0,329,330,5,2,0,0,330,331, + 3,184,92,0,331,41,1,0,0,0,332,333,5,109,0,0,333,334,5,2,0,0,334, + 339,5,137,0,0,335,336,5,109,0,0,336,337,5,2,0,0,337,339,3,76,38, + 0,338,332,1,0,0,0,338,335,1,0,0,0,339,43,1,0,0,0,340,341,5,106,0, + 0,341,342,5,2,0,0,342,343,3,184,92,0,343,45,1,0,0,0,344,345,5,107, + 0,0,345,346,5,2,0,0,346,351,5,137,0,0,347,348,5,107,0,0,348,349, + 5,2,0,0,349,351,3,76,38,0,350,344,1,0,0,0,350,347,1,0,0,0,351,47, + 1,0,0,0,352,353,5,71,0,0,353,354,5,2,0,0,354,355,5,139,0,0,355,49, + 1,0,0,0,356,357,5,70,0,0,357,358,5,2,0,0,358,359,3,184,92,0,359, + 51,1,0,0,0,360,361,5,73,0,0,361,362,5,2,0,0,362,363,3,184,92,0,363, + 53,1,0,0,0,364,365,5,72,0,0,365,366,5,2,0,0,366,367,3,184,92,0,367, + 55,1,0,0,0,368,369,5,92,0,0,369,370,5,2,0,0,370,371,3,184,92,0,371, + 57,1,0,0,0,372,373,5,88,0,0,373,374,5,2,0,0,374,375,5,139,0,0,375, + 59,1,0,0,0,376,377,5,87,0,0,377,378,5,2,0,0,378,379,5,137,0,0,379, + 61,1,0,0,0,380,381,5,95,0,0,381,382,5,2,0,0,382,383,3,72,36,0,383, + 63,1,0,0,0,384,385,5,74,0,0,385,386,5,2,0,0,386,387,5,139,0,0,387, + 65,1,0,0,0,388,389,5,75,0,0,389,390,5,2,0,0,390,391,5,137,0,0,391, + 67,1,0,0,0,392,393,5,76,0,0,393,394,5,2,0,0,394,395,5,139,0,0,395, + 69,1,0,0,0,396,397,5,77,0,0,397,398,5,2,0,0,398,399,5,137,0,0,399, + 71,1,0,0,0,400,401,5,5,0,0,401,406,3,74,37,0,402,403,5,1,0,0,403, + 405,3,74,37,0,404,402,1,0,0,0,405,408,1,0,0,0,406,404,1,0,0,0,406, + 407,1,0,0,0,407,409,1,0,0,0,408,406,1,0,0,0,409,410,5,6,0,0,410, + 414,1,0,0,0,411,412,5,5,0,0,412,414,5,6,0,0,413,400,1,0,0,0,413, + 411,1,0,0,0,414,73,1,0,0,0,415,416,5,135,0,0,416,417,5,2,0,0,417, + 429,5,137,0,0,418,419,5,135,0,0,419,420,5,2,0,0,420,429,5,136,0, + 0,421,422,5,135,0,0,422,423,5,2,0,0,423,429,3,76,38,0,424,425,3, + 184,92,0,425,426,5,2,0,0,426,427,3,80,40,0,427,429,1,0,0,0,428,415, + 1,0,0,0,428,418,1,0,0,0,428,421,1,0,0,0,428,424,1,0,0,0,429,75,1, + 0,0,0,430,431,5,138,0,0,431,77,1,0,0,0,432,433,5,3,0,0,433,438,3, + 80,40,0,434,435,5,1,0,0,435,437,3,80,40,0,436,434,1,0,0,0,437,440, + 1,0,0,0,438,436,1,0,0,0,438,439,1,0,0,0,439,441,1,0,0,0,440,438, + 1,0,0,0,441,442,5,4,0,0,442,446,1,0,0,0,443,444,5,3,0,0,444,446, + 5,4,0,0,445,432,1,0,0,0,445,443,1,0,0,0,446,79,1,0,0,0,447,452,3, + 74,37,0,448,452,3,78,39,0,449,452,3,72,36,0,450,452,3,82,41,0,451, + 447,1,0,0,0,451,448,1,0,0,0,451,449,1,0,0,0,451,450,1,0,0,0,452, + 81,1,0,0,0,453,459,5,140,0,0,454,459,5,139,0,0,455,459,7,0,0,0,456, + 459,5,9,0,0,457,459,3,184,92,0,458,453,1,0,0,0,458,454,1,0,0,0,458, + 455,1,0,0,0,458,456,1,0,0,0,458,457,1,0,0,0,459,83,1,0,0,0,460,461, + 5,96,0,0,461,462,5,2,0,0,462,463,3,72,36,0,463,85,1,0,0,0,464,465, + 7,1,0,0,465,87,1,0,0,0,466,467,5,24,0,0,467,468,5,2,0,0,468,469, + 5,3,0,0,469,474,3,90,45,0,470,471,5,1,0,0,471,473,3,90,45,0,472, + 470,1,0,0,0,473,476,1,0,0,0,474,472,1,0,0,0,474,475,1,0,0,0,475, + 477,1,0,0,0,476,474,1,0,0,0,477,478,5,4,0,0,478,89,1,0,0,0,479,480, + 5,5,0,0,480,483,3,92,46,0,481,482,5,1,0,0,482,484,3,92,46,0,483, + 481,1,0,0,0,484,485,1,0,0,0,485,483,1,0,0,0,485,486,1,0,0,0,486, + 487,1,0,0,0,487,488,5,6,0,0,488,501,1,0,0,0,489,490,5,5,0,0,490, + 495,3,94,47,0,491,492,5,1,0,0,492,494,3,94,47,0,493,491,1,0,0,0, + 494,497,1,0,0,0,495,493,1,0,0,0,495,496,1,0,0,0,496,498,1,0,0,0, + 497,495,1,0,0,0,498,499,5,6,0,0,499,501,1,0,0,0,500,479,1,0,0,0, + 500,489,1,0,0,0,501,91,1,0,0,0,502,507,3,98,49,0,503,507,3,100,50, + 0,504,507,3,24,12,0,505,507,3,8,4,0,506,502,1,0,0,0,506,503,1,0, + 0,0,506,504,1,0,0,0,506,505,1,0,0,0,507,93,1,0,0,0,508,511,3,96, + 48,0,509,511,3,24,12,0,510,508,1,0,0,0,510,509,1,0,0,0,511,95,1, + 0,0,0,512,513,3,170,85,0,513,526,5,2,0,0,514,527,3,90,45,0,515,516, + 5,3,0,0,516,521,3,90,45,0,517,518,5,1,0,0,518,520,3,90,45,0,519, + 517,1,0,0,0,520,523,1,0,0,0,521,519,1,0,0,0,521,522,1,0,0,0,522, + 524,1,0,0,0,523,521,1,0,0,0,524,525,5,4,0,0,525,527,1,0,0,0,526, + 514,1,0,0,0,526,515,1,0,0,0,527,97,1,0,0,0,528,529,5,25,0,0,529, + 530,5,2,0,0,530,531,3,184,92,0,531,99,1,0,0,0,532,533,3,168,84,0, + 533,534,5,2,0,0,534,535,3,182,91,0,535,101,1,0,0,0,536,537,5,27, + 0,0,537,538,5,2,0,0,538,539,5,3,0,0,539,544,3,2,1,0,540,541,5,1, + 0,0,541,543,3,2,1,0,542,540,1,0,0,0,543,546,1,0,0,0,544,542,1,0, + 0,0,544,545,1,0,0,0,545,547,1,0,0,0,546,544,1,0,0,0,547,548,5,4, + 0,0,548,103,1,0,0,0,549,550,5,84,0,0,550,551,5,2,0,0,551,552,5,5, + 0,0,552,557,3,106,53,0,553,554,5,1,0,0,554,556,3,106,53,0,555,553, + 1,0,0,0,556,559,1,0,0,0,557,555,1,0,0,0,557,558,1,0,0,0,558,560, + 1,0,0,0,559,557,1,0,0,0,560,561,5,6,0,0,561,105,1,0,0,0,562,567, + 3,108,54,0,563,567,3,6,3,0,564,567,3,14,7,0,565,567,3,8,4,0,566, + 562,1,0,0,0,566,563,1,0,0,0,566,564,1,0,0,0,566,565,1,0,0,0,567, + 107,1,0,0,0,568,569,5,78,0,0,569,570,5,2,0,0,570,571,5,5,0,0,571, + 576,3,110,55,0,572,573,5,1,0,0,573,575,3,110,55,0,574,572,1,0,0, + 0,575,578,1,0,0,0,576,574,1,0,0,0,576,577,1,0,0,0,577,579,1,0,0, + 0,578,576,1,0,0,0,579,580,5,6,0,0,580,109,1,0,0,0,581,584,3,112, + 56,0,582,584,3,116,58,0,583,581,1,0,0,0,583,582,1,0,0,0,584,111, + 1,0,0,0,585,586,5,79,0,0,586,587,5,2,0,0,587,588,3,114,57,0,588, + 113,1,0,0,0,589,590,7,2,0,0,590,115,1,0,0,0,591,592,5,82,0,0,592, + 593,5,2,0,0,593,594,3,118,59,0,594,117,1,0,0,0,595,596,5,83,0,0, + 596,119,1,0,0,0,597,598,5,85,0,0,598,599,5,2,0,0,599,600,5,5,0,0, + 600,605,3,122,61,0,601,602,5,1,0,0,602,604,3,122,61,0,603,601,1, + 0,0,0,604,607,1,0,0,0,605,603,1,0,0,0,605,606,1,0,0,0,606,608,1, + 0,0,0,607,605,1,0,0,0,608,609,5,6,0,0,609,121,1,0,0,0,610,615,3, + 6,3,0,611,615,3,14,7,0,612,615,3,8,4,0,613,615,3,108,54,0,614,610, + 1,0,0,0,614,611,1,0,0,0,614,612,1,0,0,0,614,613,1,0,0,0,615,123, + 1,0,0,0,616,617,5,86,0,0,617,618,5,2,0,0,618,619,3,72,36,0,619,125, + 1,0,0,0,620,621,5,97,0,0,621,622,5,2,0,0,622,623,5,5,0,0,623,628, + 3,128,64,0,624,625,5,1,0,0,625,627,3,128,64,0,626,624,1,0,0,0,627, + 630,1,0,0,0,628,626,1,0,0,0,628,629,1,0,0,0,629,631,1,0,0,0,630, + 628,1,0,0,0,631,632,5,6,0,0,632,127,1,0,0,0,633,637,3,26,13,0,634, + 637,3,62,31,0,635,637,3,130,65,0,636,633,1,0,0,0,636,634,1,0,0,0, + 636,635,1,0,0,0,637,129,1,0,0,0,638,639,5,98,0,0,639,640,5,2,0,0, + 640,641,5,5,0,0,641,646,3,132,66,0,642,643,5,1,0,0,643,645,3,132, + 66,0,644,642,1,0,0,0,645,648,1,0,0,0,646,644,1,0,0,0,646,647,1,0, + 0,0,647,649,1,0,0,0,648,646,1,0,0,0,649,650,5,6,0,0,650,131,1,0, + 0,0,651,657,3,134,67,0,652,657,3,136,68,0,653,657,3,138,69,0,654, + 657,3,140,70,0,655,657,3,142,71,0,656,651,1,0,0,0,656,652,1,0,0, + 0,656,653,1,0,0,0,656,654,1,0,0,0,656,655,1,0,0,0,657,133,1,0,0, + 0,658,659,5,99,0,0,659,660,5,2,0,0,660,661,3,184,92,0,661,135,1, + 0,0,0,662,663,5,100,0,0,663,664,5,2,0,0,664,665,3,184,92,0,665,137, + 1,0,0,0,666,667,5,101,0,0,667,668,5,2,0,0,668,669,5,3,0,0,669,674, + 3,184,92,0,670,671,5,1,0,0,671,673,3,184,92,0,672,670,1,0,0,0,673, + 676,1,0,0,0,674,672,1,0,0,0,674,675,1,0,0,0,675,677,1,0,0,0,676, + 674,1,0,0,0,677,678,5,4,0,0,678,139,1,0,0,0,679,680,5,102,0,0,680, + 681,5,2,0,0,681,682,5,139,0,0,682,141,1,0,0,0,683,684,5,103,0,0, + 684,685,5,2,0,0,685,686,5,137,0,0,686,143,1,0,0,0,687,688,5,110, + 0,0,688,689,5,2,0,0,689,698,5,3,0,0,690,695,3,146,73,0,691,692,5, + 1,0,0,692,694,3,146,73,0,693,691,1,0,0,0,694,697,1,0,0,0,695,693, + 1,0,0,0,695,696,1,0,0,0,696,699,1,0,0,0,697,695,1,0,0,0,698,690, + 1,0,0,0,698,699,1,0,0,0,699,700,1,0,0,0,700,701,5,4,0,0,701,145, + 1,0,0,0,702,703,5,5,0,0,703,708,3,148,74,0,704,705,5,1,0,0,705,707, + 3,148,74,0,706,704,1,0,0,0,707,710,1,0,0,0,708,706,1,0,0,0,708,709, + 1,0,0,0,709,711,1,0,0,0,710,708,1,0,0,0,711,712,5,6,0,0,712,147, + 1,0,0,0,713,721,3,150,75,0,714,721,3,152,76,0,715,721,3,154,77,0, + 716,721,3,156,78,0,717,721,3,158,79,0,718,721,3,160,80,0,719,721, + 3,8,4,0,720,713,1,0,0,0,720,714,1,0,0,0,720,715,1,0,0,0,720,716, + 1,0,0,0,720,717,1,0,0,0,720,718,1,0,0,0,720,719,1,0,0,0,721,149, + 1,0,0,0,722,723,5,111,0,0,723,724,5,2,0,0,724,725,5,3,0,0,725,730, + 3,174,87,0,726,727,5,1,0,0,727,729,3,174,87,0,728,726,1,0,0,0,729, + 732,1,0,0,0,730,728,1,0,0,0,730,731,1,0,0,0,731,733,1,0,0,0,732, + 730,1,0,0,0,733,734,5,4,0,0,734,151,1,0,0,0,735,736,5,112,0,0,736, + 737,5,2,0,0,737,738,5,139,0,0,738,153,1,0,0,0,739,740,5,113,0,0, + 740,741,5,2,0,0,741,742,5,139,0,0,742,155,1,0,0,0,743,744,5,114, + 0,0,744,745,5,2,0,0,745,746,7,3,0,0,746,157,1,0,0,0,747,748,5,115, + 0,0,748,749,5,2,0,0,749,750,5,139,0,0,750,159,1,0,0,0,751,752,5, + 116,0,0,752,753,5,2,0,0,753,754,7,4,0,0,754,161,1,0,0,0,755,756, + 5,119,0,0,756,757,5,2,0,0,757,766,5,3,0,0,758,763,3,164,82,0,759, + 760,5,1,0,0,760,762,3,164,82,0,761,759,1,0,0,0,762,765,1,0,0,0,763, + 761,1,0,0,0,763,764,1,0,0,0,764,767,1,0,0,0,765,763,1,0,0,0,766, + 758,1,0,0,0,766,767,1,0,0,0,767,768,1,0,0,0,768,769,5,4,0,0,769, + 163,1,0,0,0,770,771,5,5,0,0,771,776,3,166,83,0,772,773,5,1,0,0,773, + 775,3,166,83,0,774,772,1,0,0,0,775,778,1,0,0,0,776,774,1,0,0,0,776, + 777,1,0,0,0,777,779,1,0,0,0,778,776,1,0,0,0,779,780,5,6,0,0,780, + 165,1,0,0,0,781,786,3,150,75,0,782,786,3,32,16,0,783,786,3,24,12, + 0,784,786,3,8,4,0,785,781,1,0,0,0,785,782,1,0,0,0,785,783,1,0,0, + 0,785,784,1,0,0,0,786,167,1,0,0,0,787,788,7,5,0,0,788,169,1,0,0, + 0,789,790,7,6,0,0,790,171,1,0,0,0,791,792,7,7,0,0,792,173,1,0,0, + 0,793,796,3,172,86,0,794,796,3,184,92,0,795,793,1,0,0,0,795,794, + 1,0,0,0,796,175,1,0,0,0,797,798,5,5,0,0,798,803,3,178,89,0,799,800, + 5,1,0,0,800,802,3,178,89,0,801,799,1,0,0,0,802,805,1,0,0,0,803,801, + 1,0,0,0,803,804,1,0,0,0,804,806,1,0,0,0,805,803,1,0,0,0,806,807, + 5,6,0,0,807,811,1,0,0,0,808,809,5,5,0,0,809,811,5,6,0,0,810,797, + 1,0,0,0,810,808,1,0,0,0,811,177,1,0,0,0,812,813,3,184,92,0,813,814, + 5,2,0,0,814,815,3,182,91,0,815,179,1,0,0,0,816,817,5,3,0,0,817,822, + 3,182,91,0,818,819,5,1,0,0,819,821,3,182,91,0,820,818,1,0,0,0,821, + 824,1,0,0,0,822,820,1,0,0,0,822,823,1,0,0,0,823,825,1,0,0,0,824, + 822,1,0,0,0,825,826,5,4,0,0,826,830,1,0,0,0,827,828,5,3,0,0,828, + 830,5,4,0,0,829,816,1,0,0,0,829,827,1,0,0,0,830,181,1,0,0,0,831, + 841,5,140,0,0,832,841,5,139,0,0,833,841,5,7,0,0,834,841,5,8,0,0, + 835,841,5,9,0,0,836,841,3,178,89,0,837,841,3,180,90,0,838,841,3, + 176,88,0,839,841,3,184,92,0,840,831,1,0,0,0,840,832,1,0,0,0,840, + 833,1,0,0,0,840,834,1,0,0,0,840,835,1,0,0,0,840,836,1,0,0,0,840, + 837,1,0,0,0,840,838,1,0,0,0,840,839,1,0,0,0,841,183,1,0,0,0,842, + 843,7,8,0,0,843,185,1,0,0,0,52,195,205,254,264,281,302,312,318,338, + 350,406,413,428,438,445,451,458,474,485,495,500,506,510,521,526, + 544,557,566,576,583,605,614,628,636,646,656,674,695,698,708,720, + 730,763,766,776,785,795,803,810,822,829,840 ] class ASLParser ( Parser ): @@ -351,17 +354,17 @@ class ASLParser ( Parser ): "'\"ProcessorConfig\"'", "'\"Mode\"'", "'\"INLINE\"'", "'\"DISTRIBUTED\"'", "'\"ExecutionType\"'", "'\"STANDARD\"'", "'\"ItemProcessor\"'", "'\"Iterator\"'", "'\"ItemSelector\"'", - "'\"MaxConcurrency\"'", "'\"Resource\"'", "'\"InputPath\"'", - "'\"OutputPath\"'", "'\"ItemsPath\"'", "'\"ResultPath\"'", - "'\"Result\"'", "'\"Parameters\"'", "'\"ResultSelector\"'", - "'\"ItemReader\"'", "'\"ReaderConfig\"'", "'\"InputType\"'", - "'\"CSVHeaderLocation\"'", "'\"CSVHeaders\"'", "'\"MaxItems\"'", - "'\"MaxItemsPath\"'", "'\"Next\"'", "'\"End\"'", "'\"Cause\"'", - "'\"CausePath\"'", "'\"Error\"'", "'\"ErrorPath\"'", - "'\"Retry\"'", "'\"ErrorEquals\"'", "'\"IntervalSeconds\"'", - "'\"MaxAttempts\"'", "'\"BackoffRate\"'", "'\"MaxDelaySeconds\"'", - "'\"JitterStrategy\"'", "'\"FULL\"'", "'\"NONE\"'", - "'\"Catch\"'", "'\"States.ALL\"'", "'\"States.DataLimitExceeded\"'", + "'\"MaxConcurrencyPath\"'", "'\"MaxConcurrency\"'", + "'\"Resource\"'", "'\"InputPath\"'", "'\"OutputPath\"'", + "'\"ItemsPath\"'", "'\"ResultPath\"'", "'\"Result\"'", + "'\"Parameters\"'", "'\"ResultSelector\"'", "'\"ItemReader\"'", + "'\"ReaderConfig\"'", "'\"InputType\"'", "'\"CSVHeaderLocation\"'", + "'\"CSVHeaders\"'", "'\"MaxItems\"'", "'\"MaxItemsPath\"'", + "'\"Next\"'", "'\"End\"'", "'\"Cause\"'", "'\"CausePath\"'", + "'\"Error\"'", "'\"ErrorPath\"'", "'\"Retry\"'", "'\"ErrorEquals\"'", + "'\"IntervalSeconds\"'", "'\"MaxAttempts\"'", "'\"BackoffRate\"'", + "'\"MaxDelaySeconds\"'", "'\"JitterStrategy\"'", "'\"FULL\"'", + "'\"NONE\"'", "'\"Catch\"'", "'\"States.ALL\"'", "'\"States.DataLimitExceeded\"'", "'\"States.HeartbeatTimeout\"'", "'\"States.Timeout\"'", "'\"States.TaskFailed\"'", "'\"States.Permissions\"'", "'\"States.ResultPathMatchFailure\"'", "'\"States.ParameterPathFailure\"'", @@ -394,11 +397,12 @@ class ASLParser ( Parser ): "HEARTBEATSECONDS", "HEARTBEATSECONDSPATH", "PROCESSORCONFIG", "MODE", "INLINE", "DISTRIBUTED", "EXECUTIONTYPE", "STANDARD", "ITEMPROCESSOR", "ITERATOR", "ITEMSELECTOR", - "MAXCONCURRENCY", "RESOURCE", "INPUTPATH", "OUTPUTPATH", - "ITEMSPATH", "RESULTPATH", "RESULT", "PARAMETERS", - "RESULTSELECTOR", "ITEMREADER", "READERCONFIG", "INPUTTYPE", - "CSVHEADERLOCATION", "CSVHEADERS", "MAXITEMS", "MAXITEMSPATH", - "NEXT", "END", "CAUSE", "CAUSEPATH", "ERROR", "ERRORPATH", + "MAXCONCURRENCYPATH", "MAXCONCURRENCY", "RESOURCE", + "INPUTPATH", "OUTPUTPATH", "ITEMSPATH", "RESULTPATH", + "RESULT", "PARAMETERS", "RESULTSELECTOR", "ITEMREADER", + "READERCONFIG", "INPUTTYPE", "CSVHEADERLOCATION", + "CSVHEADERS", "MAXITEMS", "MAXITEMSPATH", "NEXT", + "END", "CAUSE", "CAUSEPATH", "ERROR", "ERRORPATH", "RETRY", "ERROREQUALS", "INTERVALSECONDS", "MAXATTEMPTS", "BACKOFFRATE", "MAXDELAYSECONDS", "JITTERSTRATEGY", "FULL", "NONE", "CATCH", "ERRORNAMEStatesALL", "ERRORNAMEStatesDataLimitExceeded", @@ -441,68 +445,69 @@ class ASLParser ( Parser ): RULE_timestamp_path_decl = 27 RULE_items_path_decl = 28 RULE_max_concurrency_decl = 29 - RULE_parameters_decl = 30 - RULE_timeout_seconds_decl = 31 - RULE_timeout_seconds_path_decl = 32 - RULE_heartbeat_seconds_decl = 33 - RULE_heartbeat_seconds_path_decl = 34 - RULE_payload_tmpl_decl = 35 - RULE_payload_binding = 36 - RULE_intrinsic_func = 37 - RULE_payload_arr_decl = 38 - RULE_payload_value_decl = 39 - RULE_payload_value_lit = 40 - RULE_result_selector_decl = 41 - RULE_state_type = 42 - RULE_choices_decl = 43 - RULE_choice_rule = 44 - RULE_comparison_variable_stmt = 45 - RULE_comparison_composite_stmt = 46 - RULE_comparison_composite = 47 - RULE_variable_decl = 48 - RULE_comparison_func = 49 - RULE_branches_decl = 50 - RULE_item_processor_decl = 51 - RULE_item_processor_item = 52 - RULE_processor_config_decl = 53 - RULE_processor_config_field = 54 - RULE_mode_decl = 55 - RULE_mode_type = 56 - RULE_execution_decl = 57 - RULE_execution_type = 58 - RULE_iterator_decl = 59 - RULE_iterator_decl_item = 60 - RULE_item_selector_decl = 61 - RULE_item_reader_decl = 62 - RULE_items_reader_field = 63 - RULE_reader_config_decl = 64 - RULE_reader_config_field = 65 - RULE_input_type_decl = 66 - RULE_csv_header_location_decl = 67 - RULE_csv_headers_decl = 68 - RULE_max_items_decl = 69 - RULE_max_items_path_decl = 70 - RULE_retry_decl = 71 - RULE_retrier_decl = 72 - RULE_retrier_stmt = 73 - RULE_error_equals_decl = 74 - RULE_interval_seconds_decl = 75 - RULE_max_attempts_decl = 76 - RULE_backoff_rate_decl = 77 - RULE_max_delay_seconds_decl = 78 - RULE_jitter_strategy_decl = 79 - RULE_catch_decl = 80 - RULE_catcher_decl = 81 - RULE_catcher_stmt = 82 - RULE_comparison_op = 83 - RULE_choice_operator = 84 - RULE_states_error_name = 85 - RULE_error_name = 86 - RULE_json_obj_decl = 87 - RULE_json_binding = 88 - RULE_json_arr_decl = 89 - RULE_json_value_decl = 90 - RULE_keyword_or_string = 91 + RULE_max_concurrency_path_decl = 30 + RULE_parameters_decl = 31 + RULE_timeout_seconds_decl = 32 + RULE_timeout_seconds_path_decl = 33 + RULE_heartbeat_seconds_decl = 34 + RULE_heartbeat_seconds_path_decl = 35 + RULE_payload_tmpl_decl = 36 + RULE_payload_binding = 37 + RULE_intrinsic_func = 38 + RULE_payload_arr_decl = 39 + RULE_payload_value_decl = 40 + RULE_payload_value_lit = 41 + RULE_result_selector_decl = 42 + RULE_state_type = 43 + RULE_choices_decl = 44 + RULE_choice_rule = 45 + RULE_comparison_variable_stmt = 46 + RULE_comparison_composite_stmt = 47 + RULE_comparison_composite = 48 + RULE_variable_decl = 49 + RULE_comparison_func = 50 + RULE_branches_decl = 51 + RULE_item_processor_decl = 52 + RULE_item_processor_item = 53 + RULE_processor_config_decl = 54 + RULE_processor_config_field = 55 + RULE_mode_decl = 56 + RULE_mode_type = 57 + RULE_execution_decl = 58 + RULE_execution_type = 59 + RULE_iterator_decl = 60 + RULE_iterator_decl_item = 61 + RULE_item_selector_decl = 62 + RULE_item_reader_decl = 63 + RULE_items_reader_field = 64 + RULE_reader_config_decl = 65 + RULE_reader_config_field = 66 + RULE_input_type_decl = 67 + RULE_csv_header_location_decl = 68 + RULE_csv_headers_decl = 69 + RULE_max_items_decl = 70 + RULE_max_items_path_decl = 71 + RULE_retry_decl = 72 + RULE_retrier_decl = 73 + RULE_retrier_stmt = 74 + RULE_error_equals_decl = 75 + RULE_interval_seconds_decl = 76 + RULE_max_attempts_decl = 77 + RULE_backoff_rate_decl = 78 + RULE_max_delay_seconds_decl = 79 + RULE_jitter_strategy_decl = 80 + RULE_catch_decl = 81 + RULE_catcher_decl = 82 + RULE_catcher_stmt = 83 + RULE_comparison_op = 84 + RULE_choice_operator = 85 + RULE_states_error_name = 86 + RULE_error_name = 87 + RULE_json_obj_decl = 88 + RULE_json_binding = 89 + RULE_json_arr_decl = 90 + RULE_json_value_decl = 91 + RULE_keyword_or_string = 92 ruleNames = [ "state_machine", "program_decl", "top_layer_stmt", "startat_decl", "comment_decl", "version_decl", "state_stmt", "states_decl", @@ -512,26 +517,27 @@ class ASLParser ( Parser ): "error_decl", "error_path_decl", "cause_decl", "cause_path_decl", "seconds_decl", "seconds_path_decl", "timestamp_decl", "timestamp_path_decl", "items_path_decl", "max_concurrency_decl", - "parameters_decl", "timeout_seconds_decl", "timeout_seconds_path_decl", - "heartbeat_seconds_decl", "heartbeat_seconds_path_decl", - "payload_tmpl_decl", "payload_binding", "intrinsic_func", - "payload_arr_decl", "payload_value_decl", "payload_value_lit", - "result_selector_decl", "state_type", "choices_decl", - "choice_rule", "comparison_variable_stmt", "comparison_composite_stmt", - "comparison_composite", "variable_decl", "comparison_func", - "branches_decl", "item_processor_decl", "item_processor_item", - "processor_config_decl", "processor_config_field", "mode_decl", - "mode_type", "execution_decl", "execution_type", "iterator_decl", - "iterator_decl_item", "item_selector_decl", "item_reader_decl", - "items_reader_field", "reader_config_decl", "reader_config_field", - "input_type_decl", "csv_header_location_decl", "csv_headers_decl", - "max_items_decl", "max_items_path_decl", "retry_decl", - "retrier_decl", "retrier_stmt", "error_equals_decl", - "interval_seconds_decl", "max_attempts_decl", "backoff_rate_decl", - "max_delay_seconds_decl", "jitter_strategy_decl", "catch_decl", - "catcher_decl", "catcher_stmt", "comparison_op", "choice_operator", - "states_error_name", "error_name", "json_obj_decl", "json_binding", - "json_arr_decl", "json_value_decl", "keyword_or_string" ] + "max_concurrency_path_decl", "parameters_decl", "timeout_seconds_decl", + "timeout_seconds_path_decl", "heartbeat_seconds_decl", + "heartbeat_seconds_path_decl", "payload_tmpl_decl", "payload_binding", + "intrinsic_func", "payload_arr_decl", "payload_value_decl", + "payload_value_lit", "result_selector_decl", "state_type", + "choices_decl", "choice_rule", "comparison_variable_stmt", + "comparison_composite_stmt", "comparison_composite", + "variable_decl", "comparison_func", "branches_decl", + "item_processor_decl", "item_processor_item", "processor_config_decl", + "processor_config_field", "mode_decl", "mode_type", "execution_decl", + "execution_type", "iterator_decl", "iterator_decl_item", + "item_selector_decl", "item_reader_decl", "items_reader_field", + "reader_config_decl", "reader_config_field", "input_type_decl", + "csv_header_location_decl", "csv_headers_decl", "max_items_decl", + "max_items_path_decl", "retry_decl", "retrier_decl", + "retrier_stmt", "error_equals_decl", "interval_seconds_decl", + "max_attempts_decl", "backoff_rate_decl", "max_delay_seconds_decl", + "jitter_strategy_decl", "catch_decl", "catcher_decl", + "catcher_stmt", "comparison_op", "choice_operator", "states_error_name", + "error_name", "json_obj_decl", "json_binding", "json_arr_decl", + "json_value_decl", "keyword_or_string" ] EOF = Token.EOF COMMA=1 @@ -620,60 +626,61 @@ class ASLParser ( Parser ): ITEMPROCESSOR=84 ITERATOR=85 ITEMSELECTOR=86 - MAXCONCURRENCY=87 - RESOURCE=88 - INPUTPATH=89 - OUTPUTPATH=90 - ITEMSPATH=91 - RESULTPATH=92 - RESULT=93 - PARAMETERS=94 - RESULTSELECTOR=95 - ITEMREADER=96 - READERCONFIG=97 - INPUTTYPE=98 - CSVHEADERLOCATION=99 - CSVHEADERS=100 - MAXITEMS=101 - MAXITEMSPATH=102 - NEXT=103 - END=104 - CAUSE=105 - CAUSEPATH=106 - ERROR=107 - ERRORPATH=108 - RETRY=109 - ERROREQUALS=110 - INTERVALSECONDS=111 - MAXATTEMPTS=112 - BACKOFFRATE=113 - MAXDELAYSECONDS=114 - JITTERSTRATEGY=115 - FULL=116 - NONE=117 - CATCH=118 - ERRORNAMEStatesALL=119 - ERRORNAMEStatesDataLimitExceeded=120 - ERRORNAMEStatesHeartbeatTimeout=121 - ERRORNAMEStatesTimeout=122 - ERRORNAMEStatesTaskFailed=123 - ERRORNAMEStatesPermissions=124 - ERRORNAMEStatesResultPathMatchFailure=125 - ERRORNAMEStatesParameterPathFailure=126 - ERRORNAMEStatesBranchFailed=127 - ERRORNAMEStatesNoChoiceMatched=128 - ERRORNAMEStatesIntrinsicFailure=129 - ERRORNAMEStatesExceedToleratedFailureThreshold=130 - ERRORNAMEStatesItemReaderFailed=131 - ERRORNAMEStatesResultWriterFailed=132 - ERRORNAMEStatesRuntime=133 - STRINGDOLLAR=134 - STRINGPATHCONTEXTOBJ=135 - STRINGPATH=136 - STRING=137 - INT=138 - NUMBER=139 - WS=140 + MAXCONCURRENCYPATH=87 + MAXCONCURRENCY=88 + RESOURCE=89 + INPUTPATH=90 + OUTPUTPATH=91 + ITEMSPATH=92 + RESULTPATH=93 + RESULT=94 + PARAMETERS=95 + RESULTSELECTOR=96 + ITEMREADER=97 + READERCONFIG=98 + INPUTTYPE=99 + CSVHEADERLOCATION=100 + CSVHEADERS=101 + MAXITEMS=102 + MAXITEMSPATH=103 + NEXT=104 + END=105 + CAUSE=106 + CAUSEPATH=107 + ERROR=108 + ERRORPATH=109 + RETRY=110 + ERROREQUALS=111 + INTERVALSECONDS=112 + MAXATTEMPTS=113 + BACKOFFRATE=114 + MAXDELAYSECONDS=115 + JITTERSTRATEGY=116 + FULL=117 + NONE=118 + CATCH=119 + ERRORNAMEStatesALL=120 + ERRORNAMEStatesDataLimitExceeded=121 + ERRORNAMEStatesHeartbeatTimeout=122 + ERRORNAMEStatesTimeout=123 + ERRORNAMEStatesTaskFailed=124 + ERRORNAMEStatesPermissions=125 + ERRORNAMEStatesResultPathMatchFailure=126 + ERRORNAMEStatesParameterPathFailure=127 + ERRORNAMEStatesBranchFailed=128 + ERRORNAMEStatesNoChoiceMatched=129 + ERRORNAMEStatesIntrinsicFailure=130 + ERRORNAMEStatesExceedToleratedFailureThreshold=131 + ERRORNAMEStatesItemReaderFailed=132 + ERRORNAMEStatesResultWriterFailed=133 + ERRORNAMEStatesRuntime=134 + STRINGDOLLAR=135 + STRINGPATHCONTEXTOBJ=136 + STRINGPATH=137 + STRING=138 + INT=139 + NUMBER=140 + WS=141 def __init__(self, input:TokenStream, output:TextIO = sys.stdout): super().__init__(input, output) @@ -724,9 +731,9 @@ def state_machine(self): self.enterRule(localctx, 0, self.RULE_state_machine) try: self.enterOuterAlt(localctx, 1) - self.state = 184 + self.state = 186 self.program_decl() - self.state = 185 + self.state = 187 self.match(ASLParser.EOF) except RecognitionException as re: localctx.exception = re @@ -790,23 +797,23 @@ def program_decl(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 187 + self.state = 189 self.match(ASLParser.LBRACE) - self.state = 188 + self.state = 190 self.top_layer_stmt() - self.state = 193 + self.state = 195 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 189 + self.state = 191 self.match(ASLParser.COMMA) - self.state = 190 + self.state = 192 self.top_layer_stmt() - self.state = 195 + self.state = 197 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 196 + self.state = 198 self.match(ASLParser.RBRACE) except RecognitionException as re: localctx.exception = re @@ -869,32 +876,32 @@ def top_layer_stmt(self): localctx = ASLParser.Top_layer_stmtContext(self, self._ctx, self.state) self.enterRule(localctx, 4, self.RULE_top_layer_stmt) try: - self.state = 203 + self.state = 205 self._errHandler.sync(self) token = self._input.LA(1) if token in [10]: self.enterOuterAlt(localctx, 1) - self.state = 198 + self.state = 200 self.comment_decl() pass elif token in [14]: self.enterOuterAlt(localctx, 2) - self.state = 199 + self.state = 201 self.version_decl() pass elif token in [12]: self.enterOuterAlt(localctx, 3) - self.state = 200 + self.state = 202 self.startat_decl() pass elif token in [11]: self.enterOuterAlt(localctx, 4) - self.state = 201 + self.state = 203 self.states_decl() pass elif token in [74]: self.enterOuterAlt(localctx, 5) - self.state = 202 + self.state = 204 self.timeout_seconds_decl() pass else: @@ -952,11 +959,11 @@ def startat_decl(self): self.enterRule(localctx, 6, self.RULE_startat_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 205 + self.state = 207 self.match(ASLParser.STARTAT) - self.state = 206 + self.state = 208 self.match(ASLParser.COLON) - self.state = 207 + self.state = 209 self.keyword_or_string() except RecognitionException as re: localctx.exception = re @@ -1010,11 +1017,11 @@ def comment_decl(self): self.enterRule(localctx, 8, self.RULE_comment_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 209 + self.state = 211 self.match(ASLParser.COMMENT) - self.state = 210 + self.state = 212 self.match(ASLParser.COLON) - self.state = 211 + self.state = 213 self.keyword_or_string() except RecognitionException as re: localctx.exception = re @@ -1068,11 +1075,11 @@ def version_decl(self): self.enterRule(localctx, 10, self.RULE_version_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 213 + self.state = 215 self.match(ASLParser.VERSION) - self.state = 214 + self.state = 216 self.match(ASLParser.COLON) - self.state = 215 + self.state = 217 self.keyword_or_string() except RecognitionException as re: localctx.exception = re @@ -1190,6 +1197,10 @@ def max_concurrency_decl(self): return self.getTypedRuleContext(ASLParser.Max_concurrency_declContext,0) + def max_concurrency_path_decl(self): + return self.getTypedRuleContext(ASLParser.Max_concurrency_path_declContext,0) + + def timeout_seconds_decl(self): return self.getTypedRuleContext(ASLParser.Timeout_seconds_declContext,0) @@ -1251,177 +1262,182 @@ def state_stmt(self): localctx = ASLParser.State_stmtContext(self, self._ctx, self.state) self.enterRule(localctx, 12, self.RULE_state_stmt) try: - self.state = 251 + self.state = 254 self._errHandler.sync(self) token = self._input.LA(1) if token in [10]: self.enterOuterAlt(localctx, 1) - self.state = 217 + self.state = 219 self.comment_decl() pass elif token in [15]: self.enterOuterAlt(localctx, 2) - self.state = 218 + self.state = 220 self.type_decl() pass - elif token in [89]: + elif token in [90]: self.enterOuterAlt(localctx, 3) - self.state = 219 + self.state = 221 self.input_path_decl() pass - elif token in [88]: + elif token in [89]: self.enterOuterAlt(localctx, 4) - self.state = 220 + self.state = 222 self.resource_decl() pass - elif token in [103]: + elif token in [104]: self.enterOuterAlt(localctx, 5) - self.state = 221 + self.state = 223 self.next_decl() pass - elif token in [93]: + elif token in [94]: self.enterOuterAlt(localctx, 6) - self.state = 222 + self.state = 224 self.result_decl() pass - elif token in [92]: + elif token in [93]: self.enterOuterAlt(localctx, 7) - self.state = 223 + self.state = 225 self.result_path_decl() pass - elif token in [90]: + elif token in [91]: self.enterOuterAlt(localctx, 8) - self.state = 224 + self.state = 226 self.output_path_decl() pass - elif token in [104]: + elif token in [105]: self.enterOuterAlt(localctx, 9) - self.state = 225 + self.state = 227 self.end_decl() pass elif token in [26]: self.enterOuterAlt(localctx, 10) - self.state = 226 + self.state = 228 self.default_decl() pass elif token in [24]: self.enterOuterAlt(localctx, 11) - self.state = 227 + self.state = 229 self.choices_decl() pass - elif token in [107]: + elif token in [108]: self.enterOuterAlt(localctx, 12) - self.state = 228 + self.state = 230 self.error_decl() pass - elif token in [108]: + elif token in [109]: self.enterOuterAlt(localctx, 13) - self.state = 229 + self.state = 231 self.error_path_decl() pass - elif token in [105]: + elif token in [106]: self.enterOuterAlt(localctx, 14) - self.state = 230 + self.state = 232 self.cause_decl() pass - elif token in [106]: + elif token in [107]: self.enterOuterAlt(localctx, 15) - self.state = 231 + self.state = 233 self.cause_path_decl() pass elif token in [71]: self.enterOuterAlt(localctx, 16) - self.state = 232 + self.state = 234 self.seconds_decl() pass elif token in [70]: self.enterOuterAlt(localctx, 17) - self.state = 233 + self.state = 235 self.seconds_path_decl() pass elif token in [73]: self.enterOuterAlt(localctx, 18) - self.state = 234 + self.state = 236 self.timestamp_decl() pass elif token in [72]: self.enterOuterAlt(localctx, 19) - self.state = 235 + self.state = 237 self.timestamp_path_decl() pass - elif token in [91]: + elif token in [92]: self.enterOuterAlt(localctx, 20) - self.state = 236 + self.state = 238 self.items_path_decl() pass elif token in [84]: self.enterOuterAlt(localctx, 21) - self.state = 237 + self.state = 239 self.item_processor_decl() pass elif token in [85]: self.enterOuterAlt(localctx, 22) - self.state = 238 + self.state = 240 self.iterator_decl() pass elif token in [86]: self.enterOuterAlt(localctx, 23) - self.state = 239 + self.state = 241 self.item_selector_decl() pass - elif token in [96]: + elif token in [97]: self.enterOuterAlt(localctx, 24) - self.state = 240 + self.state = 242 self.item_reader_decl() pass - elif token in [87]: + elif token in [88]: self.enterOuterAlt(localctx, 25) - self.state = 241 + self.state = 243 self.max_concurrency_decl() pass - elif token in [74]: + elif token in [87]: self.enterOuterAlt(localctx, 26) - self.state = 242 + self.state = 244 + self.max_concurrency_path_decl() + pass + elif token in [74]: + self.enterOuterAlt(localctx, 27) + self.state = 245 self.timeout_seconds_decl() pass elif token in [75]: - self.enterOuterAlt(localctx, 27) - self.state = 243 + self.enterOuterAlt(localctx, 28) + self.state = 246 self.timeout_seconds_path_decl() pass elif token in [76]: - self.enterOuterAlt(localctx, 28) - self.state = 244 + self.enterOuterAlt(localctx, 29) + self.state = 247 self.heartbeat_seconds_decl() pass elif token in [77]: - self.enterOuterAlt(localctx, 29) - self.state = 245 + self.enterOuterAlt(localctx, 30) + self.state = 248 self.heartbeat_seconds_path_decl() pass elif token in [27]: - self.enterOuterAlt(localctx, 30) - self.state = 246 + self.enterOuterAlt(localctx, 31) + self.state = 249 self.branches_decl() pass - elif token in [94]: - self.enterOuterAlt(localctx, 31) - self.state = 247 + elif token in [95]: + self.enterOuterAlt(localctx, 32) + self.state = 250 self.parameters_decl() pass - elif token in [109]: - self.enterOuterAlt(localctx, 32) - self.state = 248 + elif token in [110]: + self.enterOuterAlt(localctx, 33) + self.state = 251 self.retry_decl() pass - elif token in [118]: - self.enterOuterAlt(localctx, 33) - self.state = 249 + elif token in [119]: + self.enterOuterAlt(localctx, 34) + self.state = 252 self.catch_decl() pass - elif token in [95]: - self.enterOuterAlt(localctx, 34) - self.state = 250 + elif token in [96]: + self.enterOuterAlt(localctx, 35) + self.state = 253 self.result_selector_decl() pass else: @@ -1495,27 +1511,27 @@ def states_decl(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 253 + self.state = 256 self.match(ASLParser.STATES) - self.state = 254 + self.state = 257 self.match(ASLParser.COLON) - self.state = 255 + self.state = 258 self.match(ASLParser.LBRACE) - self.state = 256 + self.state = 259 self.state_decl() - self.state = 261 + self.state = 264 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 257 + self.state = 260 self.match(ASLParser.COMMA) - self.state = 258 + self.state = 261 self.state_decl() - self.state = 263 + self.state = 266 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 264 + self.state = 267 self.match(ASLParser.RBRACE) except RecognitionException as re: localctx.exception = re @@ -1563,7 +1579,7 @@ def state_name(self): self.enterRule(localctx, 16, self.RULE_state_name) try: self.enterOuterAlt(localctx, 1) - self.state = 266 + self.state = 269 self.keyword_or_string() except RecognitionException as re: localctx.exception = re @@ -1618,11 +1634,11 @@ def state_decl(self): self.enterRule(localctx, 18, self.RULE_state_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 268 + self.state = 271 self.state_name() - self.state = 269 + self.state = 272 self.match(ASLParser.COLON) - self.state = 270 + self.state = 273 self.state_decl_body() except RecognitionException as re: localctx.exception = re @@ -1686,23 +1702,23 @@ def state_decl_body(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 272 + self.state = 275 self.match(ASLParser.LBRACE) - self.state = 273 + self.state = 276 self.state_stmt() - self.state = 278 + self.state = 281 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 274 + self.state = 277 self.match(ASLParser.COMMA) - self.state = 275 + self.state = 278 self.state_stmt() - self.state = 280 + self.state = 283 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 281 + self.state = 284 self.match(ASLParser.RBRACE) except RecognitionException as re: localctx.exception = re @@ -1756,11 +1772,11 @@ def type_decl(self): self.enterRule(localctx, 22, self.RULE_type_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 283 + self.state = 286 self.match(ASLParser.TYPE) - self.state = 284 + self.state = 287 self.match(ASLParser.COLON) - self.state = 285 + self.state = 288 self.state_type() except RecognitionException as re: localctx.exception = re @@ -1814,11 +1830,11 @@ def next_decl(self): self.enterRule(localctx, 24, self.RULE_next_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 287 + self.state = 290 self.match(ASLParser.NEXT) - self.state = 288 + self.state = 291 self.match(ASLParser.COLON) - self.state = 289 + self.state = 292 self.keyword_or_string() except RecognitionException as re: localctx.exception = re @@ -1872,11 +1888,11 @@ def resource_decl(self): self.enterRule(localctx, 26, self.RULE_resource_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 291 + self.state = 294 self.match(ASLParser.RESOURCE) - self.state = 292 + self.state = 295 self.match(ASLParser.COLON) - self.state = 293 + self.state = 296 self.keyword_or_string() except RecognitionException as re: localctx.exception = re @@ -1933,19 +1949,19 @@ def input_path_decl(self): self.enterRule(localctx, 28, self.RULE_input_path_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 295 + self.state = 298 self.match(ASLParser.INPUTPATH) - self.state = 296 - self.match(ASLParser.COLON) self.state = 299 + self.match(ASLParser.COLON) + self.state = 302 self._errHandler.sync(self) token = self._input.LA(1) if token in [9]: - self.state = 297 + self.state = 300 self.match(ASLParser.NULL) pass - elif token in [10, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 107, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137]: - self.state = 298 + elif token in [10, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138]: + self.state = 301 self.keyword_or_string() pass else: @@ -2003,11 +2019,11 @@ def result_decl(self): self.enterRule(localctx, 30, self.RULE_result_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 301 + self.state = 304 self.match(ASLParser.RESULT) - self.state = 302 + self.state = 305 self.match(ASLParser.COLON) - self.state = 303 + self.state = 306 self.json_value_decl() except RecognitionException as re: localctx.exception = re @@ -2064,19 +2080,19 @@ def result_path_decl(self): self.enterRule(localctx, 32, self.RULE_result_path_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 305 + self.state = 308 self.match(ASLParser.RESULTPATH) - self.state = 306 - self.match(ASLParser.COLON) self.state = 309 + self.match(ASLParser.COLON) + self.state = 312 self._errHandler.sync(self) token = self._input.LA(1) if token in [9]: - self.state = 307 + self.state = 310 self.match(ASLParser.NULL) pass - elif token in [10, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 107, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137]: - self.state = 308 + elif token in [10, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138]: + self.state = 311 self.keyword_or_string() pass else: @@ -2137,19 +2153,19 @@ def output_path_decl(self): self.enterRule(localctx, 34, self.RULE_output_path_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 311 + self.state = 314 self.match(ASLParser.OUTPUTPATH) - self.state = 312 - self.match(ASLParser.COLON) self.state = 315 + self.match(ASLParser.COLON) + self.state = 318 self._errHandler.sync(self) token = self._input.LA(1) if token in [9]: - self.state = 313 + self.state = 316 self.match(ASLParser.NULL) pass - elif token in [10, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 107, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137]: - self.state = 314 + elif token in [10, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138]: + self.state = 317 self.keyword_or_string() pass else: @@ -2210,11 +2226,11 @@ def end_decl(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 317 + self.state = 320 self.match(ASLParser.END) - self.state = 318 + self.state = 321 self.match(ASLParser.COLON) - self.state = 319 + self.state = 322 _la = self._input.LA(1) if not(_la==7 or _la==8): self._errHandler.recoverInline(self) @@ -2273,11 +2289,11 @@ def default_decl(self): self.enterRule(localctx, 38, self.RULE_default_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 321 + self.state = 324 self.match(ASLParser.DEFAULT) - self.state = 322 + self.state = 325 self.match(ASLParser.COLON) - self.state = 323 + self.state = 326 self.keyword_or_string() except RecognitionException as re: localctx.exception = re @@ -2331,11 +2347,11 @@ def error_decl(self): self.enterRule(localctx, 40, self.RULE_error_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 325 + self.state = 328 self.match(ASLParser.ERROR) - self.state = 326 + self.state = 329 self.match(ASLParser.COLON) - self.state = 327 + self.state = 330 self.keyword_or_string() except RecognitionException as re: localctx.exception = re @@ -2426,28 +2442,28 @@ def error_path_decl(self): localctx = ASLParser.Error_path_declContext(self, self._ctx, self.state) self.enterRule(localctx, 42, self.RULE_error_path_decl) try: - self.state = 335 + self.state = 338 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input,8,self._ctx) if la_ == 1: localctx = ASLParser.Error_path_decl_pathContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 329 + self.state = 332 self.match(ASLParser.ERRORPATH) - self.state = 330 + self.state = 333 self.match(ASLParser.COLON) - self.state = 331 + self.state = 334 self.match(ASLParser.STRINGPATH) pass elif la_ == 2: localctx = ASLParser.Error_path_decl_intrinsicContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 332 + self.state = 335 self.match(ASLParser.ERRORPATH) - self.state = 333 + self.state = 336 self.match(ASLParser.COLON) - self.state = 334 + self.state = 337 self.intrinsic_func() pass @@ -2504,11 +2520,11 @@ def cause_decl(self): self.enterRule(localctx, 44, self.RULE_cause_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 337 + self.state = 340 self.match(ASLParser.CAUSE) - self.state = 338 + self.state = 341 self.match(ASLParser.COLON) - self.state = 339 + self.state = 342 self.keyword_or_string() except RecognitionException as re: localctx.exception = re @@ -2599,28 +2615,28 @@ def cause_path_decl(self): localctx = ASLParser.Cause_path_declContext(self, self._ctx, self.state) self.enterRule(localctx, 46, self.RULE_cause_path_decl) try: - self.state = 347 + self.state = 350 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input,9,self._ctx) if la_ == 1: localctx = ASLParser.Cause_path_decl_pathContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 341 + self.state = 344 self.match(ASLParser.CAUSEPATH) - self.state = 342 + self.state = 345 self.match(ASLParser.COLON) - self.state = 343 + self.state = 346 self.match(ASLParser.STRINGPATH) pass elif la_ == 2: localctx = ASLParser.Cause_path_decl_intrinsicContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 344 + self.state = 347 self.match(ASLParser.CAUSEPATH) - self.state = 345 + self.state = 348 self.match(ASLParser.COLON) - self.state = 346 + self.state = 349 self.intrinsic_func() pass @@ -2676,11 +2692,11 @@ def seconds_decl(self): self.enterRule(localctx, 48, self.RULE_seconds_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 349 + self.state = 352 self.match(ASLParser.SECONDS) - self.state = 350 + self.state = 353 self.match(ASLParser.COLON) - self.state = 351 + self.state = 354 self.match(ASLParser.INT) except RecognitionException as re: localctx.exception = re @@ -2734,11 +2750,11 @@ def seconds_path_decl(self): self.enterRule(localctx, 50, self.RULE_seconds_path_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 353 + self.state = 356 self.match(ASLParser.SECONDSPATH) - self.state = 354 + self.state = 357 self.match(ASLParser.COLON) - self.state = 355 + self.state = 358 self.keyword_or_string() except RecognitionException as re: localctx.exception = re @@ -2792,11 +2808,11 @@ def timestamp_decl(self): self.enterRule(localctx, 52, self.RULE_timestamp_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 357 + self.state = 360 self.match(ASLParser.TIMESTAMP) - self.state = 358 + self.state = 361 self.match(ASLParser.COLON) - self.state = 359 + self.state = 362 self.keyword_or_string() except RecognitionException as re: localctx.exception = re @@ -2850,11 +2866,11 @@ def timestamp_path_decl(self): self.enterRule(localctx, 54, self.RULE_timestamp_path_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 361 + self.state = 364 self.match(ASLParser.TIMESTAMPPATH) - self.state = 362 + self.state = 365 self.match(ASLParser.COLON) - self.state = 363 + self.state = 366 self.keyword_or_string() except RecognitionException as re: localctx.exception = re @@ -2908,11 +2924,11 @@ def items_path_decl(self): self.enterRule(localctx, 56, self.RULE_items_path_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 365 + self.state = 368 self.match(ASLParser.ITEMSPATH) - self.state = 366 + self.state = 369 self.match(ASLParser.COLON) - self.state = 367 + self.state = 370 self.keyword_or_string() except RecognitionException as re: localctx.exception = re @@ -2965,11 +2981,11 @@ def max_concurrency_decl(self): self.enterRule(localctx, 58, self.RULE_max_concurrency_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 369 + self.state = 372 self.match(ASLParser.MAXCONCURRENCY) - self.state = 370 + self.state = 373 self.match(ASLParser.COLON) - self.state = 371 + self.state = 374 self.match(ASLParser.INT) except RecognitionException as re: localctx.exception = re @@ -2980,6 +2996,63 @@ def max_concurrency_decl(self): return localctx + class Max_concurrency_path_declContext(ParserRuleContext): + __slots__ = 'parser' + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def MAXCONCURRENCYPATH(self): + return self.getToken(ASLParser.MAXCONCURRENCYPATH, 0) + + def COLON(self): + return self.getToken(ASLParser.COLON, 0) + + def STRINGPATH(self): + return self.getToken(ASLParser.STRINGPATH, 0) + + def getRuleIndex(self): + return ASLParser.RULE_max_concurrency_path_decl + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterMax_concurrency_path_decl" ): + listener.enterMax_concurrency_path_decl(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitMax_concurrency_path_decl" ): + listener.exitMax_concurrency_path_decl(self) + + def accept(self, visitor:ParseTreeVisitor): + if hasattr( visitor, "visitMax_concurrency_path_decl" ): + return visitor.visitMax_concurrency_path_decl(self) + else: + return visitor.visitChildren(self) + + + + + def max_concurrency_path_decl(self): + + localctx = ASLParser.Max_concurrency_path_declContext(self, self._ctx, self.state) + self.enterRule(localctx, 60, self.RULE_max_concurrency_path_decl) + try: + self.enterOuterAlt(localctx, 1) + self.state = 376 + self.match(ASLParser.MAXCONCURRENCYPATH) + self.state = 377 + self.match(ASLParser.COLON) + self.state = 378 + self.match(ASLParser.STRINGPATH) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class Parameters_declContext(ParserRuleContext): __slots__ = 'parser' @@ -3020,14 +3093,14 @@ def accept(self, visitor:ParseTreeVisitor): def parameters_decl(self): localctx = ASLParser.Parameters_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 60, self.RULE_parameters_decl) + self.enterRule(localctx, 62, self.RULE_parameters_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 373 + self.state = 380 self.match(ASLParser.PARAMETERS) - self.state = 374 + self.state = 381 self.match(ASLParser.COLON) - self.state = 375 + self.state = 382 self.payload_tmpl_decl() except RecognitionException as re: localctx.exception = re @@ -3077,14 +3150,14 @@ def accept(self, visitor:ParseTreeVisitor): def timeout_seconds_decl(self): localctx = ASLParser.Timeout_seconds_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 62, self.RULE_timeout_seconds_decl) + self.enterRule(localctx, 64, self.RULE_timeout_seconds_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 377 + self.state = 384 self.match(ASLParser.TIMEOUTSECONDS) - self.state = 378 + self.state = 385 self.match(ASLParser.COLON) - self.state = 379 + self.state = 386 self.match(ASLParser.INT) except RecognitionException as re: localctx.exception = re @@ -3134,14 +3207,14 @@ def accept(self, visitor:ParseTreeVisitor): def timeout_seconds_path_decl(self): localctx = ASLParser.Timeout_seconds_path_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 64, self.RULE_timeout_seconds_path_decl) + self.enterRule(localctx, 66, self.RULE_timeout_seconds_path_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 381 + self.state = 388 self.match(ASLParser.TIMEOUTSECONDSPATH) - self.state = 382 + self.state = 389 self.match(ASLParser.COLON) - self.state = 383 + self.state = 390 self.match(ASLParser.STRINGPATH) except RecognitionException as re: localctx.exception = re @@ -3191,14 +3264,14 @@ def accept(self, visitor:ParseTreeVisitor): def heartbeat_seconds_decl(self): localctx = ASLParser.Heartbeat_seconds_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 66, self.RULE_heartbeat_seconds_decl) + self.enterRule(localctx, 68, self.RULE_heartbeat_seconds_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 385 + self.state = 392 self.match(ASLParser.HEARTBEATSECONDS) - self.state = 386 + self.state = 393 self.match(ASLParser.COLON) - self.state = 387 + self.state = 394 self.match(ASLParser.INT) except RecognitionException as re: localctx.exception = re @@ -3248,14 +3321,14 @@ def accept(self, visitor:ParseTreeVisitor): def heartbeat_seconds_path_decl(self): localctx = ASLParser.Heartbeat_seconds_path_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 68, self.RULE_heartbeat_seconds_path_decl) + self.enterRule(localctx, 70, self.RULE_heartbeat_seconds_path_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 389 + self.state = 396 self.match(ASLParser.HEARTBEATSECONDSPATH) - self.state = 390 + self.state = 397 self.match(ASLParser.COLON) - self.state = 391 + self.state = 398 self.match(ASLParser.STRINGPATH) except RecognitionException as re: localctx.exception = re @@ -3315,39 +3388,39 @@ def accept(self, visitor:ParseTreeVisitor): def payload_tmpl_decl(self): localctx = ASLParser.Payload_tmpl_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 70, self.RULE_payload_tmpl_decl) + self.enterRule(localctx, 72, self.RULE_payload_tmpl_decl) self._la = 0 # Token type try: - self.state = 406 + self.state = 413 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input,11,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 393 + self.state = 400 self.match(ASLParser.LBRACE) - self.state = 394 + self.state = 401 self.payload_binding() - self.state = 399 + self.state = 406 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 395 + self.state = 402 self.match(ASLParser.COMMA) - self.state = 396 + self.state = 403 self.payload_binding() - self.state = 401 + self.state = 408 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 402 + self.state = 409 self.match(ASLParser.RBRACE) pass elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 404 + self.state = 411 self.match(ASLParser.LBRACE) - self.state = 405 + self.state = 412 self.match(ASLParser.RBRACE) pass @@ -3497,52 +3570,52 @@ def accept(self, visitor:ParseTreeVisitor): def payload_binding(self): localctx = ASLParser.Payload_bindingContext(self, self._ctx, self.state) - self.enterRule(localctx, 72, self.RULE_payload_binding) + self.enterRule(localctx, 74, self.RULE_payload_binding) try: - self.state = 421 + self.state = 428 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input,12,self._ctx) if la_ == 1: localctx = ASLParser.Payload_binding_pathContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 408 + self.state = 415 self.match(ASLParser.STRINGDOLLAR) - self.state = 409 + self.state = 416 self.match(ASLParser.COLON) - self.state = 410 + self.state = 417 self.match(ASLParser.STRINGPATH) pass elif la_ == 2: localctx = ASLParser.Payload_binding_path_context_objContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 411 + self.state = 418 self.match(ASLParser.STRINGDOLLAR) - self.state = 412 + self.state = 419 self.match(ASLParser.COLON) - self.state = 413 + self.state = 420 self.match(ASLParser.STRINGPATHCONTEXTOBJ) pass elif la_ == 3: localctx = ASLParser.Payload_binding_intrinsic_funcContext(self, localctx) self.enterOuterAlt(localctx, 3) - self.state = 414 + self.state = 421 self.match(ASLParser.STRINGDOLLAR) - self.state = 415 + self.state = 422 self.match(ASLParser.COLON) - self.state = 416 + self.state = 423 self.intrinsic_func() pass elif la_ == 4: localctx = ASLParser.Payload_binding_valueContext(self, localctx) self.enterOuterAlt(localctx, 4) - self.state = 417 + self.state = 424 self.keyword_or_string() - self.state = 418 + self.state = 425 self.match(ASLParser.COLON) - self.state = 419 + self.state = 426 self.payload_value_decl() pass @@ -3589,10 +3662,10 @@ def accept(self, visitor:ParseTreeVisitor): def intrinsic_func(self): localctx = ASLParser.Intrinsic_funcContext(self, self._ctx, self.state) - self.enterRule(localctx, 74, self.RULE_intrinsic_func) + self.enterRule(localctx, 76, self.RULE_intrinsic_func) try: self.enterOuterAlt(localctx, 1) - self.state = 423 + self.state = 430 self.match(ASLParser.STRING) except RecognitionException as re: localctx.exception = re @@ -3652,39 +3725,39 @@ def accept(self, visitor:ParseTreeVisitor): def payload_arr_decl(self): localctx = ASLParser.Payload_arr_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 76, self.RULE_payload_arr_decl) + self.enterRule(localctx, 78, self.RULE_payload_arr_decl) self._la = 0 # Token type try: - self.state = 438 + self.state = 445 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input,14,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 425 + self.state = 432 self.match(ASLParser.LBRACK) - self.state = 426 + self.state = 433 self.payload_value_decl() - self.state = 431 + self.state = 438 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 427 + self.state = 434 self.match(ASLParser.COMMA) - self.state = 428 + self.state = 435 self.payload_value_decl() - self.state = 433 + self.state = 440 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 434 + self.state = 441 self.match(ASLParser.RBRACK) pass elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 436 + self.state = 443 self.match(ASLParser.LBRACK) - self.state = 437 + self.state = 444 self.match(ASLParser.RBRACK) pass @@ -3744,32 +3817,32 @@ def accept(self, visitor:ParseTreeVisitor): def payload_value_decl(self): localctx = ASLParser.Payload_value_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 78, self.RULE_payload_value_decl) + self.enterRule(localctx, 80, self.RULE_payload_value_decl) try: - self.state = 444 + self.state = 451 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input,15,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 440 + self.state = 447 self.payload_binding() pass elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 441 + self.state = 448 self.payload_arr_decl() pass elif la_ == 3: self.enterOuterAlt(localctx, 3) - self.state = 442 + self.state = 449 self.payload_tmpl_decl() pass elif la_ == 4: self.enterOuterAlt(localctx, 4) - self.state = 443 + self.state = 450 self.payload_value_lit() pass @@ -3927,28 +4000,28 @@ def accept(self, visitor:ParseTreeVisitor): def payload_value_lit(self): localctx = ASLParser.Payload_value_litContext(self, self._ctx, self.state) - self.enterRule(localctx, 80, self.RULE_payload_value_lit) + self.enterRule(localctx, 82, self.RULE_payload_value_lit) self._la = 0 # Token type try: - self.state = 451 + self.state = 458 self._errHandler.sync(self) token = self._input.LA(1) - if token in [139]: + if token in [140]: localctx = ASLParser.Payload_value_floatContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 446 + self.state = 453 self.match(ASLParser.NUMBER) pass - elif token in [138]: + elif token in [139]: localctx = ASLParser.Payload_value_intContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 447 + self.state = 454 self.match(ASLParser.INT) pass elif token in [7, 8]: localctx = ASLParser.Payload_value_boolContext(self, localctx) self.enterOuterAlt(localctx, 3) - self.state = 448 + self.state = 455 _la = self._input.LA(1) if not(_la==7 or _la==8): self._errHandler.recoverInline(self) @@ -3959,13 +4032,13 @@ def payload_value_lit(self): elif token in [9]: localctx = ASLParser.Payload_value_nullContext(self, localctx) self.enterOuterAlt(localctx, 4) - self.state = 449 + self.state = 456 self.match(ASLParser.NULL) pass - elif token in [10, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 107, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137]: + elif token in [10, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 108, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138]: localctx = ASLParser.Payload_value_strContext(self, localctx) self.enterOuterAlt(localctx, 5) - self.state = 450 + self.state = 457 self.keyword_or_string() pass else: @@ -4020,14 +4093,14 @@ def accept(self, visitor:ParseTreeVisitor): def result_selector_decl(self): localctx = ASLParser.Result_selector_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 82, self.RULE_result_selector_decl) + self.enterRule(localctx, 84, self.RULE_result_selector_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 453 + self.state = 460 self.match(ASLParser.RESULTSELECTOR) - self.state = 454 + self.state = 461 self.match(ASLParser.COLON) - self.state = 455 + self.state = 462 self.payload_tmpl_decl() except RecognitionException as re: localctx.exception = re @@ -4092,11 +4165,11 @@ def accept(self, visitor:ParseTreeVisitor): def state_type(self): localctx = ASLParser.State_typeContext(self, self._ctx, self.state) - self.enterRule(localctx, 84, self.RULE_state_type) + self.enterRule(localctx, 86, self.RULE_state_type) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 457 + self.state = 464 _la = self._input.LA(1) if not((((_la) & ~0x3f) == 0 and ((1 << _la) & 16711680) != 0)): self._errHandler.recoverInline(self) @@ -4167,31 +4240,31 @@ def accept(self, visitor:ParseTreeVisitor): def choices_decl(self): localctx = ASLParser.Choices_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 86, self.RULE_choices_decl) + self.enterRule(localctx, 88, self.RULE_choices_decl) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 459 + self.state = 466 self.match(ASLParser.CHOICES) - self.state = 460 + self.state = 467 self.match(ASLParser.COLON) - self.state = 461 + self.state = 468 self.match(ASLParser.LBRACK) - self.state = 462 + self.state = 469 self.choice_rule() - self.state = 467 + self.state = 474 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 463 + self.state = 470 self.match(ASLParser.COMMA) - self.state = 464 + self.state = 471 self.choice_rule() - self.state = 469 + self.state = 476 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 470 + self.state = 477 self.match(ASLParser.RBRACK) except RecognitionException as re: localctx.exception = re @@ -4297,57 +4370,57 @@ def accept(self, visitor:ParseTreeVisitor): def choice_rule(self): localctx = ASLParser.Choice_ruleContext(self, self._ctx, self.state) - self.enterRule(localctx, 88, self.RULE_choice_rule) + self.enterRule(localctx, 90, self.RULE_choice_rule) self._la = 0 # Token type try: - self.state = 493 + self.state = 500 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input,20,self._ctx) if la_ == 1: localctx = ASLParser.Choice_rule_comparison_variableContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 472 + self.state = 479 self.match(ASLParser.LBRACE) - self.state = 473 + self.state = 480 self.comparison_variable_stmt() - self.state = 476 + self.state = 483 self._errHandler.sync(self) _la = self._input.LA(1) while True: - self.state = 474 + self.state = 481 self.match(ASLParser.COMMA) - self.state = 475 + self.state = 482 self.comparison_variable_stmt() - self.state = 478 + self.state = 485 self._errHandler.sync(self) _la = self._input.LA(1) if not (_la==1): break - self.state = 480 + self.state = 487 self.match(ASLParser.RBRACE) pass elif la_ == 2: localctx = ASLParser.Choice_rule_comparison_compositeContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 482 + self.state = 489 self.match(ASLParser.LBRACE) - self.state = 483 + self.state = 490 self.comparison_composite_stmt() - self.state = 488 + self.state = 495 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 484 + self.state = 491 self.match(ASLParser.COMMA) - self.state = 485 + self.state = 492 self.comparison_composite_stmt() - self.state = 490 + self.state = 497 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 491 + self.state = 498 self.match(ASLParser.RBRACE) pass @@ -4407,29 +4480,29 @@ def accept(self, visitor:ParseTreeVisitor): def comparison_variable_stmt(self): localctx = ASLParser.Comparison_variable_stmtContext(self, self._ctx, self.state) - self.enterRule(localctx, 90, self.RULE_comparison_variable_stmt) + self.enterRule(localctx, 92, self.RULE_comparison_variable_stmt) try: - self.state = 499 + self.state = 506 self._errHandler.sync(self) token = self._input.LA(1) if token in [25]: self.enterOuterAlt(localctx, 1) - self.state = 495 + self.state = 502 self.variable_decl() pass elif token in [29, 30, 31, 32, 33, 34, 35, 36, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69]: self.enterOuterAlt(localctx, 2) - self.state = 496 + self.state = 503 self.comparison_func() pass - elif token in [103]: + elif token in [104]: self.enterOuterAlt(localctx, 3) - self.state = 497 + self.state = 504 self.next_decl() pass elif token in [10]: self.enterOuterAlt(localctx, 4) - self.state = 498 + self.state = 505 self.comment_decl() pass else: @@ -4482,19 +4555,19 @@ def accept(self, visitor:ParseTreeVisitor): def comparison_composite_stmt(self): localctx = ASLParser.Comparison_composite_stmtContext(self, self._ctx, self.state) - self.enterRule(localctx, 92, self.RULE_comparison_composite_stmt) + self.enterRule(localctx, 94, self.RULE_comparison_composite_stmt) try: - self.state = 503 + self.state = 510 self._errHandler.sync(self) token = self._input.LA(1) if token in [28, 37, 48]: self.enterOuterAlt(localctx, 1) - self.state = 501 + self.state = 508 self.comparison_composite() pass - elif token in [103]: + elif token in [104]: self.enterOuterAlt(localctx, 2) - self.state = 502 + self.state = 509 self.next_decl() pass else: @@ -4565,39 +4638,39 @@ def accept(self, visitor:ParseTreeVisitor): def comparison_composite(self): localctx = ASLParser.Comparison_compositeContext(self, self._ctx, self.state) - self.enterRule(localctx, 94, self.RULE_comparison_composite) + self.enterRule(localctx, 96, self.RULE_comparison_composite) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 505 + self.state = 512 self.choice_operator() - self.state = 506 + self.state = 513 self.match(ASLParser.COLON) - self.state = 519 + self.state = 526 self._errHandler.sync(self) token = self._input.LA(1) if token in [5]: - self.state = 507 + self.state = 514 self.choice_rule() pass elif token in [3]: - self.state = 508 + self.state = 515 self.match(ASLParser.LBRACK) - self.state = 509 + self.state = 516 self.choice_rule() - self.state = 514 + self.state = 521 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 510 + self.state = 517 self.match(ASLParser.COMMA) - self.state = 511 + self.state = 518 self.choice_rule() - self.state = 516 + self.state = 523 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 517 + self.state = 524 self.match(ASLParser.RBRACK) pass else: @@ -4652,14 +4725,14 @@ def accept(self, visitor:ParseTreeVisitor): def variable_decl(self): localctx = ASLParser.Variable_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 96, self.RULE_variable_decl) + self.enterRule(localctx, 98, self.RULE_variable_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 521 + self.state = 528 self.match(ASLParser.VARIABLE) - self.state = 522 + self.state = 529 self.match(ASLParser.COLON) - self.state = 523 + self.state = 530 self.keyword_or_string() except RecognitionException as re: localctx.exception = re @@ -4711,14 +4784,14 @@ def accept(self, visitor:ParseTreeVisitor): def comparison_func(self): localctx = ASLParser.Comparison_funcContext(self, self._ctx, self.state) - self.enterRule(localctx, 98, self.RULE_comparison_func) + self.enterRule(localctx, 100, self.RULE_comparison_func) try: self.enterOuterAlt(localctx, 1) - self.state = 525 + self.state = 532 self.comparison_op() - self.state = 526 + self.state = 533 self.match(ASLParser.COLON) - self.state = 527 + self.state = 534 self.json_value_decl() except RecognitionException as re: localctx.exception = re @@ -4784,31 +4857,31 @@ def accept(self, visitor:ParseTreeVisitor): def branches_decl(self): localctx = ASLParser.Branches_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 100, self.RULE_branches_decl) + self.enterRule(localctx, 102, self.RULE_branches_decl) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 529 + self.state = 536 self.match(ASLParser.BRANCHES) - self.state = 530 + self.state = 537 self.match(ASLParser.COLON) - self.state = 531 + self.state = 538 self.match(ASLParser.LBRACK) - self.state = 532 + self.state = 539 self.program_decl() - self.state = 537 + self.state = 544 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 533 + self.state = 540 self.match(ASLParser.COMMA) - self.state = 534 + self.state = 541 self.program_decl() - self.state = 539 + self.state = 546 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 540 + self.state = 547 self.match(ASLParser.RBRACK) except RecognitionException as re: localctx.exception = re @@ -4874,31 +4947,31 @@ def accept(self, visitor:ParseTreeVisitor): def item_processor_decl(self): localctx = ASLParser.Item_processor_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 102, self.RULE_item_processor_decl) + self.enterRule(localctx, 104, self.RULE_item_processor_decl) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 542 + self.state = 549 self.match(ASLParser.ITEMPROCESSOR) - self.state = 543 + self.state = 550 self.match(ASLParser.COLON) - self.state = 544 + self.state = 551 self.match(ASLParser.LBRACE) - self.state = 545 + self.state = 552 self.item_processor_item() - self.state = 550 + self.state = 557 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 546 + self.state = 553 self.match(ASLParser.COMMA) - self.state = 547 + self.state = 554 self.item_processor_item() - self.state = 552 + self.state = 559 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 553 + self.state = 560 self.match(ASLParser.RBRACE) except RecognitionException as re: localctx.exception = re @@ -4955,29 +5028,29 @@ def accept(self, visitor:ParseTreeVisitor): def item_processor_item(self): localctx = ASLParser.Item_processor_itemContext(self, self._ctx, self.state) - self.enterRule(localctx, 104, self.RULE_item_processor_item) + self.enterRule(localctx, 106, self.RULE_item_processor_item) try: - self.state = 559 + self.state = 566 self._errHandler.sync(self) token = self._input.LA(1) if token in [78]: self.enterOuterAlt(localctx, 1) - self.state = 555 + self.state = 562 self.processor_config_decl() pass elif token in [12]: self.enterOuterAlt(localctx, 2) - self.state = 556 + self.state = 563 self.startat_decl() pass elif token in [11]: self.enterOuterAlt(localctx, 3) - self.state = 557 + self.state = 564 self.states_decl() pass elif token in [10]: self.enterOuterAlt(localctx, 4) - self.state = 558 + self.state = 565 self.comment_decl() pass else: @@ -5047,31 +5120,31 @@ def accept(self, visitor:ParseTreeVisitor): def processor_config_decl(self): localctx = ASLParser.Processor_config_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 106, self.RULE_processor_config_decl) + self.enterRule(localctx, 108, self.RULE_processor_config_decl) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 561 + self.state = 568 self.match(ASLParser.PROCESSORCONFIG) - self.state = 562 + self.state = 569 self.match(ASLParser.COLON) - self.state = 563 + self.state = 570 self.match(ASLParser.LBRACE) - self.state = 564 + self.state = 571 self.processor_config_field() - self.state = 569 + self.state = 576 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 565 + self.state = 572 self.match(ASLParser.COMMA) - self.state = 566 + self.state = 573 self.processor_config_field() - self.state = 571 + self.state = 578 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 572 + self.state = 579 self.match(ASLParser.RBRACE) except RecognitionException as re: localctx.exception = re @@ -5120,19 +5193,19 @@ def accept(self, visitor:ParseTreeVisitor): def processor_config_field(self): localctx = ASLParser.Processor_config_fieldContext(self, self._ctx, self.state) - self.enterRule(localctx, 108, self.RULE_processor_config_field) + self.enterRule(localctx, 110, self.RULE_processor_config_field) try: - self.state = 576 + self.state = 583 self._errHandler.sync(self) token = self._input.LA(1) if token in [79]: self.enterOuterAlt(localctx, 1) - self.state = 574 + self.state = 581 self.mode_decl() pass elif token in [82]: self.enterOuterAlt(localctx, 2) - self.state = 575 + self.state = 582 self.execution_decl() pass else: @@ -5187,14 +5260,14 @@ def accept(self, visitor:ParseTreeVisitor): def mode_decl(self): localctx = ASLParser.Mode_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 110, self.RULE_mode_decl) + self.enterRule(localctx, 112, self.RULE_mode_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 578 + self.state = 585 self.match(ASLParser.MODE) - self.state = 579 + self.state = 586 self.match(ASLParser.COLON) - self.state = 580 + self.state = 587 self.mode_type() except RecognitionException as re: localctx.exception = re @@ -5241,11 +5314,11 @@ def accept(self, visitor:ParseTreeVisitor): def mode_type(self): localctx = ASLParser.Mode_typeContext(self, self._ctx, self.state) - self.enterRule(localctx, 112, self.RULE_mode_type) + self.enterRule(localctx, 114, self.RULE_mode_type) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 582 + self.state = 589 _la = self._input.LA(1) if not(_la==80 or _la==81): self._errHandler.recoverInline(self) @@ -5301,14 +5374,14 @@ def accept(self, visitor:ParseTreeVisitor): def execution_decl(self): localctx = ASLParser.Execution_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 114, self.RULE_execution_decl) + self.enterRule(localctx, 116, self.RULE_execution_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 584 + self.state = 591 self.match(ASLParser.EXECUTIONTYPE) - self.state = 585 + self.state = 592 self.match(ASLParser.COLON) - self.state = 586 + self.state = 593 self.execution_type() except RecognitionException as re: localctx.exception = re @@ -5352,10 +5425,10 @@ def accept(self, visitor:ParseTreeVisitor): def execution_type(self): localctx = ASLParser.Execution_typeContext(self, self._ctx, self.state) - self.enterRule(localctx, 116, self.RULE_execution_type) + self.enterRule(localctx, 118, self.RULE_execution_type) try: self.enterOuterAlt(localctx, 1) - self.state = 588 + self.state = 595 self.match(ASLParser.STANDARD) except RecognitionException as re: localctx.exception = re @@ -5421,31 +5494,31 @@ def accept(self, visitor:ParseTreeVisitor): def iterator_decl(self): localctx = ASLParser.Iterator_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 118, self.RULE_iterator_decl) + self.enterRule(localctx, 120, self.RULE_iterator_decl) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 590 + self.state = 597 self.match(ASLParser.ITERATOR) - self.state = 591 + self.state = 598 self.match(ASLParser.COLON) - self.state = 592 + self.state = 599 self.match(ASLParser.LBRACE) - self.state = 593 + self.state = 600 self.iterator_decl_item() - self.state = 598 + self.state = 605 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 594 + self.state = 601 self.match(ASLParser.COMMA) - self.state = 595 + self.state = 602 self.iterator_decl_item() - self.state = 600 + self.state = 607 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 601 + self.state = 608 self.match(ASLParser.RBRACE) except RecognitionException as re: localctx.exception = re @@ -5502,29 +5575,29 @@ def accept(self, visitor:ParseTreeVisitor): def iterator_decl_item(self): localctx = ASLParser.Iterator_decl_itemContext(self, self._ctx, self.state) - self.enterRule(localctx, 120, self.RULE_iterator_decl_item) + self.enterRule(localctx, 122, self.RULE_iterator_decl_item) try: - self.state = 607 + self.state = 614 self._errHandler.sync(self) token = self._input.LA(1) if token in [12]: self.enterOuterAlt(localctx, 1) - self.state = 603 + self.state = 610 self.startat_decl() pass elif token in [11]: self.enterOuterAlt(localctx, 2) - self.state = 604 + self.state = 611 self.states_decl() pass elif token in [10]: self.enterOuterAlt(localctx, 3) - self.state = 605 + self.state = 612 self.comment_decl() pass elif token in [78]: self.enterOuterAlt(localctx, 4) - self.state = 606 + self.state = 613 self.processor_config_decl() pass else: @@ -5579,14 +5652,14 @@ def accept(self, visitor:ParseTreeVisitor): def item_selector_decl(self): localctx = ASLParser.Item_selector_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 122, self.RULE_item_selector_decl) + self.enterRule(localctx, 124, self.RULE_item_selector_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 609 + self.state = 616 self.match(ASLParser.ITEMSELECTOR) - self.state = 610 + self.state = 617 self.match(ASLParser.COLON) - self.state = 611 + self.state = 618 self.payload_tmpl_decl() except RecognitionException as re: localctx.exception = re @@ -5652,31 +5725,31 @@ def accept(self, visitor:ParseTreeVisitor): def item_reader_decl(self): localctx = ASLParser.Item_reader_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 124, self.RULE_item_reader_decl) + self.enterRule(localctx, 126, self.RULE_item_reader_decl) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 613 + self.state = 620 self.match(ASLParser.ITEMREADER) - self.state = 614 + self.state = 621 self.match(ASLParser.COLON) - self.state = 615 + self.state = 622 self.match(ASLParser.LBRACE) - self.state = 616 + self.state = 623 self.items_reader_field() - self.state = 621 + self.state = 628 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 617 + self.state = 624 self.match(ASLParser.COMMA) - self.state = 618 + self.state = 625 self.items_reader_field() - self.state = 623 + self.state = 630 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 624 + self.state = 631 self.match(ASLParser.RBRACE) except RecognitionException as re: localctx.exception = re @@ -5729,24 +5802,24 @@ def accept(self, visitor:ParseTreeVisitor): def items_reader_field(self): localctx = ASLParser.Items_reader_fieldContext(self, self._ctx, self.state) - self.enterRule(localctx, 126, self.RULE_items_reader_field) + self.enterRule(localctx, 128, self.RULE_items_reader_field) try: - self.state = 629 + self.state = 636 self._errHandler.sync(self) token = self._input.LA(1) - if token in [88]: + if token in [89]: self.enterOuterAlt(localctx, 1) - self.state = 626 + self.state = 633 self.resource_decl() pass - elif token in [94]: + elif token in [95]: self.enterOuterAlt(localctx, 2) - self.state = 627 + self.state = 634 self.parameters_decl() pass - elif token in [97]: + elif token in [98]: self.enterOuterAlt(localctx, 3) - self.state = 628 + self.state = 635 self.reader_config_decl() pass else: @@ -5816,31 +5889,31 @@ def accept(self, visitor:ParseTreeVisitor): def reader_config_decl(self): localctx = ASLParser.Reader_config_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 128, self.RULE_reader_config_decl) + self.enterRule(localctx, 130, self.RULE_reader_config_decl) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 631 + self.state = 638 self.match(ASLParser.READERCONFIG) - self.state = 632 + self.state = 639 self.match(ASLParser.COLON) - self.state = 633 + self.state = 640 self.match(ASLParser.LBRACE) - self.state = 634 + self.state = 641 self.reader_config_field() - self.state = 639 + self.state = 646 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 635 + self.state = 642 self.match(ASLParser.COMMA) - self.state = 636 + self.state = 643 self.reader_config_field() - self.state = 641 + self.state = 648 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 642 + self.state = 649 self.match(ASLParser.RBRACE) except RecognitionException as re: localctx.exception = re @@ -5901,34 +5974,34 @@ def accept(self, visitor:ParseTreeVisitor): def reader_config_field(self): localctx = ASLParser.Reader_config_fieldContext(self, self._ctx, self.state) - self.enterRule(localctx, 130, self.RULE_reader_config_field) + self.enterRule(localctx, 132, self.RULE_reader_config_field) try: - self.state = 649 + self.state = 656 self._errHandler.sync(self) token = self._input.LA(1) - if token in [98]: + if token in [99]: self.enterOuterAlt(localctx, 1) - self.state = 644 + self.state = 651 self.input_type_decl() pass - elif token in [99]: + elif token in [100]: self.enterOuterAlt(localctx, 2) - self.state = 645 + self.state = 652 self.csv_header_location_decl() pass - elif token in [100]: + elif token in [101]: self.enterOuterAlt(localctx, 3) - self.state = 646 + self.state = 653 self.csv_headers_decl() pass - elif token in [101]: + elif token in [102]: self.enterOuterAlt(localctx, 4) - self.state = 647 + self.state = 654 self.max_items_decl() pass - elif token in [102]: + elif token in [103]: self.enterOuterAlt(localctx, 5) - self.state = 648 + self.state = 655 self.max_items_path_decl() pass else: @@ -5983,14 +6056,14 @@ def accept(self, visitor:ParseTreeVisitor): def input_type_decl(self): localctx = ASLParser.Input_type_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 132, self.RULE_input_type_decl) + self.enterRule(localctx, 134, self.RULE_input_type_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 651 + self.state = 658 self.match(ASLParser.INPUTTYPE) - self.state = 652 + self.state = 659 self.match(ASLParser.COLON) - self.state = 653 + self.state = 660 self.keyword_or_string() except RecognitionException as re: localctx.exception = re @@ -6041,14 +6114,14 @@ def accept(self, visitor:ParseTreeVisitor): def csv_header_location_decl(self): localctx = ASLParser.Csv_header_location_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 134, self.RULE_csv_header_location_decl) + self.enterRule(localctx, 136, self.RULE_csv_header_location_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 655 + self.state = 662 self.match(ASLParser.CSVHEADERLOCATION) - self.state = 656 + self.state = 663 self.match(ASLParser.COLON) - self.state = 657 + self.state = 664 self.keyword_or_string() except RecognitionException as re: localctx.exception = re @@ -6114,31 +6187,31 @@ def accept(self, visitor:ParseTreeVisitor): def csv_headers_decl(self): localctx = ASLParser.Csv_headers_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 136, self.RULE_csv_headers_decl) + self.enterRule(localctx, 138, self.RULE_csv_headers_decl) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 659 + self.state = 666 self.match(ASLParser.CSVHEADERS) - self.state = 660 + self.state = 667 self.match(ASLParser.COLON) - self.state = 661 + self.state = 668 self.match(ASLParser.LBRACK) - self.state = 662 + self.state = 669 self.keyword_or_string() - self.state = 667 + self.state = 674 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 663 + self.state = 670 self.match(ASLParser.COMMA) - self.state = 664 + self.state = 671 self.keyword_or_string() - self.state = 669 + self.state = 676 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 670 + self.state = 677 self.match(ASLParser.RBRACK) except RecognitionException as re: localctx.exception = re @@ -6188,14 +6261,14 @@ def accept(self, visitor:ParseTreeVisitor): def max_items_decl(self): localctx = ASLParser.Max_items_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 138, self.RULE_max_items_decl) + self.enterRule(localctx, 140, self.RULE_max_items_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 672 + self.state = 679 self.match(ASLParser.MAXITEMS) - self.state = 673 + self.state = 680 self.match(ASLParser.COLON) - self.state = 674 + self.state = 681 self.match(ASLParser.INT) except RecognitionException as re: localctx.exception = re @@ -6245,14 +6318,14 @@ def accept(self, visitor:ParseTreeVisitor): def max_items_path_decl(self): localctx = ASLParser.Max_items_path_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 140, self.RULE_max_items_path_decl) + self.enterRule(localctx, 142, self.RULE_max_items_path_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 676 + self.state = 683 self.match(ASLParser.MAXITEMSPATH) - self.state = 677 + self.state = 684 self.match(ASLParser.COLON) - self.state = 678 + self.state = 685 self.match(ASLParser.STRINGPATH) except RecognitionException as re: localctx.exception = re @@ -6318,37 +6391,37 @@ def accept(self, visitor:ParseTreeVisitor): def retry_decl(self): localctx = ASLParser.Retry_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 142, self.RULE_retry_decl) + self.enterRule(localctx, 144, self.RULE_retry_decl) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 680 + self.state = 687 self.match(ASLParser.RETRY) - self.state = 681 + self.state = 688 self.match(ASLParser.COLON) - self.state = 682 + self.state = 689 self.match(ASLParser.LBRACK) - self.state = 691 + self.state = 698 self._errHandler.sync(self) _la = self._input.LA(1) if _la==5: - self.state = 683 + self.state = 690 self.retrier_decl() - self.state = 688 + self.state = 695 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 684 + self.state = 691 self.match(ASLParser.COMMA) - self.state = 685 + self.state = 692 self.retrier_decl() - self.state = 690 + self.state = 697 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 693 + self.state = 700 self.match(ASLParser.RBRACK) except RecognitionException as re: localctx.exception = re @@ -6408,27 +6481,27 @@ def accept(self, visitor:ParseTreeVisitor): def retrier_decl(self): localctx = ASLParser.Retrier_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 144, self.RULE_retrier_decl) + self.enterRule(localctx, 146, self.RULE_retrier_decl) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 695 + self.state = 702 self.match(ASLParser.LBRACE) - self.state = 696 + self.state = 703 self.retrier_stmt() - self.state = 701 + self.state = 708 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 697 + self.state = 704 self.match(ASLParser.COMMA) - self.state = 698 + self.state = 705 self.retrier_stmt() - self.state = 703 + self.state = 710 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 704 + self.state = 711 self.match(ASLParser.RBRACE) except RecognitionException as re: localctx.exception = re @@ -6497,44 +6570,44 @@ def accept(self, visitor:ParseTreeVisitor): def retrier_stmt(self): localctx = ASLParser.Retrier_stmtContext(self, self._ctx, self.state) - self.enterRule(localctx, 146, self.RULE_retrier_stmt) + self.enterRule(localctx, 148, self.RULE_retrier_stmt) try: - self.state = 713 + self.state = 720 self._errHandler.sync(self) token = self._input.LA(1) - if token in [110]: + if token in [111]: self.enterOuterAlt(localctx, 1) - self.state = 706 + self.state = 713 self.error_equals_decl() pass - elif token in [111]: + elif token in [112]: self.enterOuterAlt(localctx, 2) - self.state = 707 + self.state = 714 self.interval_seconds_decl() pass - elif token in [112]: + elif token in [113]: self.enterOuterAlt(localctx, 3) - self.state = 708 + self.state = 715 self.max_attempts_decl() pass - elif token in [113]: + elif token in [114]: self.enterOuterAlt(localctx, 4) - self.state = 709 + self.state = 716 self.backoff_rate_decl() pass - elif token in [114]: + elif token in [115]: self.enterOuterAlt(localctx, 5) - self.state = 710 + self.state = 717 self.max_delay_seconds_decl() pass - elif token in [115]: + elif token in [116]: self.enterOuterAlt(localctx, 6) - self.state = 711 + self.state = 718 self.jitter_strategy_decl() pass elif token in [10]: self.enterOuterAlt(localctx, 7) - self.state = 712 + self.state = 719 self.comment_decl() pass else: @@ -6604,31 +6677,31 @@ def accept(self, visitor:ParseTreeVisitor): def error_equals_decl(self): localctx = ASLParser.Error_equals_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 148, self.RULE_error_equals_decl) + self.enterRule(localctx, 150, self.RULE_error_equals_decl) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 715 + self.state = 722 self.match(ASLParser.ERROREQUALS) - self.state = 716 + self.state = 723 self.match(ASLParser.COLON) - self.state = 717 + self.state = 724 self.match(ASLParser.LBRACK) - self.state = 718 + self.state = 725 self.error_name() - self.state = 723 + self.state = 730 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 719 + self.state = 726 self.match(ASLParser.COMMA) - self.state = 720 + self.state = 727 self.error_name() - self.state = 725 + self.state = 732 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 726 + self.state = 733 self.match(ASLParser.RBRACK) except RecognitionException as re: localctx.exception = re @@ -6678,14 +6751,14 @@ def accept(self, visitor:ParseTreeVisitor): def interval_seconds_decl(self): localctx = ASLParser.Interval_seconds_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 150, self.RULE_interval_seconds_decl) + self.enterRule(localctx, 152, self.RULE_interval_seconds_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 728 + self.state = 735 self.match(ASLParser.INTERVALSECONDS) - self.state = 729 + self.state = 736 self.match(ASLParser.COLON) - self.state = 730 + self.state = 737 self.match(ASLParser.INT) except RecognitionException as re: localctx.exception = re @@ -6735,14 +6808,14 @@ def accept(self, visitor:ParseTreeVisitor): def max_attempts_decl(self): localctx = ASLParser.Max_attempts_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 152, self.RULE_max_attempts_decl) + self.enterRule(localctx, 154, self.RULE_max_attempts_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 732 + self.state = 739 self.match(ASLParser.MAXATTEMPTS) - self.state = 733 + self.state = 740 self.match(ASLParser.COLON) - self.state = 734 + self.state = 741 self.match(ASLParser.INT) except RecognitionException as re: localctx.exception = re @@ -6795,17 +6868,17 @@ def accept(self, visitor:ParseTreeVisitor): def backoff_rate_decl(self): localctx = ASLParser.Backoff_rate_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 154, self.RULE_backoff_rate_decl) + self.enterRule(localctx, 156, self.RULE_backoff_rate_decl) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 736 + self.state = 743 self.match(ASLParser.BACKOFFRATE) - self.state = 737 + self.state = 744 self.match(ASLParser.COLON) - self.state = 738 + self.state = 745 _la = self._input.LA(1) - if not(_la==138 or _la==139): + if not(_la==139 or _la==140): self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) @@ -6858,14 +6931,14 @@ def accept(self, visitor:ParseTreeVisitor): def max_delay_seconds_decl(self): localctx = ASLParser.Max_delay_seconds_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 156, self.RULE_max_delay_seconds_decl) + self.enterRule(localctx, 158, self.RULE_max_delay_seconds_decl) try: self.enterOuterAlt(localctx, 1) - self.state = 740 + self.state = 747 self.match(ASLParser.MAXDELAYSECONDS) - self.state = 741 + self.state = 748 self.match(ASLParser.COLON) - self.state = 742 + self.state = 749 self.match(ASLParser.INT) except RecognitionException as re: localctx.exception = re @@ -6918,17 +6991,17 @@ def accept(self, visitor:ParseTreeVisitor): def jitter_strategy_decl(self): localctx = ASLParser.Jitter_strategy_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 158, self.RULE_jitter_strategy_decl) + self.enterRule(localctx, 160, self.RULE_jitter_strategy_decl) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 744 + self.state = 751 self.match(ASLParser.JITTERSTRATEGY) - self.state = 745 + self.state = 752 self.match(ASLParser.COLON) - self.state = 746 + self.state = 753 _la = self._input.LA(1) - if not(_la==116 or _la==117): + if not(_la==117 or _la==118): self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) @@ -6997,37 +7070,37 @@ def accept(self, visitor:ParseTreeVisitor): def catch_decl(self): localctx = ASLParser.Catch_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 160, self.RULE_catch_decl) + self.enterRule(localctx, 162, self.RULE_catch_decl) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 748 + self.state = 755 self.match(ASLParser.CATCH) - self.state = 749 + self.state = 756 self.match(ASLParser.COLON) - self.state = 750 + self.state = 757 self.match(ASLParser.LBRACK) - self.state = 759 + self.state = 766 self._errHandler.sync(self) _la = self._input.LA(1) if _la==5: - self.state = 751 + self.state = 758 self.catcher_decl() - self.state = 756 + self.state = 763 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 752 + self.state = 759 self.match(ASLParser.COMMA) - self.state = 753 + self.state = 760 self.catcher_decl() - self.state = 758 + self.state = 765 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 761 + self.state = 768 self.match(ASLParser.RBRACK) except RecognitionException as re: localctx.exception = re @@ -7087,27 +7160,27 @@ def accept(self, visitor:ParseTreeVisitor): def catcher_decl(self): localctx = ASLParser.Catcher_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 162, self.RULE_catcher_decl) + self.enterRule(localctx, 164, self.RULE_catcher_decl) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 763 + self.state = 770 self.match(ASLParser.LBRACE) - self.state = 764 + self.state = 771 self.catcher_stmt() - self.state = 769 + self.state = 776 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 765 + self.state = 772 self.match(ASLParser.COMMA) - self.state = 766 + self.state = 773 self.catcher_stmt() - self.state = 771 + self.state = 778 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 772 + self.state = 779 self.match(ASLParser.RBRACE) except RecognitionException as re: localctx.exception = re @@ -7164,29 +7237,29 @@ def accept(self, visitor:ParseTreeVisitor): def catcher_stmt(self): localctx = ASLParser.Catcher_stmtContext(self, self._ctx, self.state) - self.enterRule(localctx, 164, self.RULE_catcher_stmt) + self.enterRule(localctx, 166, self.RULE_catcher_stmt) try: - self.state = 778 + self.state = 785 self._errHandler.sync(self) token = self._input.LA(1) - if token in [110]: + if token in [111]: self.enterOuterAlt(localctx, 1) - self.state = 774 + self.state = 781 self.error_equals_decl() pass - elif token in [92]: + elif token in [93]: self.enterOuterAlt(localctx, 2) - self.state = 775 + self.state = 782 self.result_path_decl() pass - elif token in [103]: + elif token in [104]: self.enterOuterAlt(localctx, 3) - self.state = 776 + self.state = 783 self.next_decl() pass elif token in [10]: self.enterOuterAlt(localctx, 4) - self.state = 777 + self.state = 784 self.comment_decl() pass else: @@ -7348,11 +7421,11 @@ def accept(self, visitor:ParseTreeVisitor): def comparison_op(self): localctx = ASLParser.Comparison_opContext(self, self._ctx, self.state) - self.enterRule(localctx, 166, self.RULE_comparison_op) + self.enterRule(localctx, 168, self.RULE_comparison_op) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 780 + self.state = 787 _la = self._input.LA(1) if not(((((_la - 29)) & ~0x3f) == 0 and ((1 << (_la - 29)) & 2199022731007) != 0)): self._errHandler.recoverInline(self) @@ -7407,11 +7480,11 @@ def accept(self, visitor:ParseTreeVisitor): def choice_operator(self): localctx = ASLParser.Choice_operatorContext(self, self._ctx, self.state) - self.enterRule(localctx, 168, self.RULE_choice_operator) + self.enterRule(localctx, 170, self.RULE_choice_operator) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 782 + self.state = 789 _la = self._input.LA(1) if not((((_la) & ~0x3f) == 0 and ((1 << _la) & 281612684099584) != 0)): self._errHandler.recoverInline(self) @@ -7502,13 +7575,13 @@ def accept(self, visitor:ParseTreeVisitor): def states_error_name(self): localctx = ASLParser.States_error_nameContext(self, self._ctx, self.state) - self.enterRule(localctx, 170, self.RULE_states_error_name) + self.enterRule(localctx, 172, self.RULE_states_error_name) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 784 + self.state = 791 _la = self._input.LA(1) - if not(((((_la - 119)) & ~0x3f) == 0 and ((1 << (_la - 119)) & 32767) != 0)): + if not(((((_la - 120)) & ~0x3f) == 0 and ((1 << (_la - 120)) & 32767) != 0)): self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) @@ -7560,20 +7633,20 @@ def accept(self, visitor:ParseTreeVisitor): def error_name(self): localctx = ASLParser.Error_nameContext(self, self._ctx, self.state) - self.enterRule(localctx, 172, self.RULE_error_name) + self.enterRule(localctx, 174, self.RULE_error_name) try: - self.state = 788 + self.state = 795 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input,46,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 786 + self.state = 793 self.states_error_name() pass elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 787 + self.state = 794 self.keyword_or_string() pass @@ -7636,39 +7709,39 @@ def accept(self, visitor:ParseTreeVisitor): def json_obj_decl(self): localctx = ASLParser.Json_obj_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 174, self.RULE_json_obj_decl) + self.enterRule(localctx, 176, self.RULE_json_obj_decl) self._la = 0 # Token type try: - self.state = 803 + self.state = 810 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input,48,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 790 + self.state = 797 self.match(ASLParser.LBRACE) - self.state = 791 + self.state = 798 self.json_binding() - self.state = 796 + self.state = 803 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 792 + self.state = 799 self.match(ASLParser.COMMA) - self.state = 793 + self.state = 800 self.json_binding() - self.state = 798 + self.state = 805 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 799 + self.state = 806 self.match(ASLParser.RBRACE) pass elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 801 + self.state = 808 self.match(ASLParser.LBRACE) - self.state = 802 + self.state = 809 self.match(ASLParser.RBRACE) pass @@ -7723,14 +7796,14 @@ def accept(self, visitor:ParseTreeVisitor): def json_binding(self): localctx = ASLParser.Json_bindingContext(self, self._ctx, self.state) - self.enterRule(localctx, 176, self.RULE_json_binding) + self.enterRule(localctx, 178, self.RULE_json_binding) try: self.enterOuterAlt(localctx, 1) - self.state = 805 + self.state = 812 self.keyword_or_string() - self.state = 806 + self.state = 813 self.match(ASLParser.COLON) - self.state = 807 + self.state = 814 self.json_value_decl() except RecognitionException as re: localctx.exception = re @@ -7790,39 +7863,39 @@ def accept(self, visitor:ParseTreeVisitor): def json_arr_decl(self): localctx = ASLParser.Json_arr_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 178, self.RULE_json_arr_decl) + self.enterRule(localctx, 180, self.RULE_json_arr_decl) self._la = 0 # Token type try: - self.state = 822 + self.state = 829 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input,50,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 809 + self.state = 816 self.match(ASLParser.LBRACK) - self.state = 810 + self.state = 817 self.json_value_decl() - self.state = 815 + self.state = 822 self._errHandler.sync(self) _la = self._input.LA(1) while _la==1: - self.state = 811 + self.state = 818 self.match(ASLParser.COMMA) - self.state = 812 + self.state = 819 self.json_value_decl() - self.state = 817 + self.state = 824 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 818 + self.state = 825 self.match(ASLParser.RBRACK) pass elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 820 + self.state = 827 self.match(ASLParser.LBRACK) - self.state = 821 + self.state = 828 self.match(ASLParser.RBRACK) pass @@ -7897,62 +7970,62 @@ def accept(self, visitor:ParseTreeVisitor): def json_value_decl(self): localctx = ASLParser.Json_value_declContext(self, self._ctx, self.state) - self.enterRule(localctx, 180, self.RULE_json_value_decl) + self.enterRule(localctx, 182, self.RULE_json_value_decl) try: - self.state = 833 + self.state = 840 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input,51,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 824 + self.state = 831 self.match(ASLParser.NUMBER) pass elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 825 + self.state = 832 self.match(ASLParser.INT) pass elif la_ == 3: self.enterOuterAlt(localctx, 3) - self.state = 826 + self.state = 833 self.match(ASLParser.TRUE) pass elif la_ == 4: self.enterOuterAlt(localctx, 4) - self.state = 827 + self.state = 834 self.match(ASLParser.FALSE) pass elif la_ == 5: self.enterOuterAlt(localctx, 5) - self.state = 828 + self.state = 835 self.match(ASLParser.NULL) pass elif la_ == 6: self.enterOuterAlt(localctx, 6) - self.state = 829 + self.state = 836 self.json_binding() pass elif la_ == 7: self.enterOuterAlt(localctx, 7) - self.state = 830 + self.state = 837 self.json_arr_decl() pass elif la_ == 8: self.enterOuterAlt(localctx, 8) - self.state = 831 + self.state = 838 self.json_obj_decl() pass elif la_ == 9: self.enterOuterAlt(localctx, 9) - self.state = 832 + self.state = 839 self.keyword_or_string() pass @@ -8216,6 +8289,9 @@ def ITEMSELECTOR(self): def MAXCONCURRENCY(self): return self.getToken(ASLParser.MAXCONCURRENCY, 0) + def MAXCONCURRENCYPATH(self): + return self.getToken(ASLParser.MAXCONCURRENCYPATH, 0) + def RESOURCE(self): return self.getToken(ASLParser.RESOURCE, 0) @@ -8368,13 +8444,13 @@ def accept(self, visitor:ParseTreeVisitor): def keyword_or_string(self): localctx = ASLParser.Keyword_or_stringContext(self, self._ctx, self.state) - self.enterRule(localctx, 182, self.RULE_keyword_or_string) + self.enterRule(localctx, 184, self.RULE_keyword_or_string) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 835 + self.state = 842 _la = self._input.LA(1) - if not(((((_la - 10)) & ~0x3f) == 0 and ((1 << (_la - 10)) & -17) != 0) or ((((_la - 74)) & ~0x3f) == 0 and ((1 << (_la - 74)) & -70390219014145) != 0)): + if not((((_la) & ~0x3f) == 0 and ((1 << _la) & -17408) != 0) or ((((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -144159168540966913) != 0) or ((((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & 2047) != 0)): self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLParserListener.py b/localstack/services/stepfunctions/asl/antlr/runtime/ASLParserListener.py index 71098ac4228d2..e2abd81bff23e 100644 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLParserListener.py +++ b/localstack/services/stepfunctions/asl/antlr/runtime/ASLParserListener.py @@ -296,6 +296,15 @@ def exitMax_concurrency_decl(self, ctx:ASLParser.Max_concurrency_declContext): pass + # Enter a parse tree produced by ASLParser#max_concurrency_path_decl. + def enterMax_concurrency_path_decl(self, ctx:ASLParser.Max_concurrency_path_declContext): + pass + + # Exit a parse tree produced by ASLParser#max_concurrency_path_decl. + def exitMax_concurrency_path_decl(self, ctx:ASLParser.Max_concurrency_path_declContext): + pass + + # Enter a parse tree produced by ASLParser#parameters_decl. def enterParameters_decl(self, ctx:ASLParser.Parameters_declContext): pass diff --git a/localstack/services/stepfunctions/asl/antlr/runtime/ASLParserVisitor.py b/localstack/services/stepfunctions/asl/antlr/runtime/ASLParserVisitor.py index 9e2dd9184ea23..fe1690393bdb4 100644 --- a/localstack/services/stepfunctions/asl/antlr/runtime/ASLParserVisitor.py +++ b/localstack/services/stepfunctions/asl/antlr/runtime/ASLParserVisitor.py @@ -169,6 +169,11 @@ def visitMax_concurrency_decl(self, ctx:ASLParser.Max_concurrency_declContext): return self.visitChildren(ctx) + # Visit a parse tree produced by ASLParser#max_concurrency_path_decl. + def visitMax_concurrency_path_decl(self, ctx:ASLParser.Max_concurrency_path_declContext): + return self.visitChildren(ctx) + + # Visit a parse tree produced by ASLParser#parameters_decl. def visitParameters_decl(self, ctx:ASLParser.Parameters_declContext): return self.visitChildren(ctx) diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/distributed_iteration_component.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/distributed_iteration_component.py index 109eb3ae27f30..c201b90c3b0bf 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/distributed_iteration_component.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/distributed_iteration_component.py @@ -42,7 +42,7 @@ JobPool, ) from localstack.services.stepfunctions.asl.component.state.state_execution.state_map.max_concurrency import ( - MaxConcurrency, + DEFAULT_MAX_CONCURRENCY_VALUE, ) from localstack.services.stepfunctions.asl.component.states import States from localstack.services.stepfunctions.asl.eval.environment import Environment @@ -123,7 +123,9 @@ def _map_run(self, env: Environment) -> None: # TODO: add watch on map_run_record update event and adjust the number of running workers accordingly. max_concurrency = self._map_run_record.max_concurrency workers_number = ( - len(input_items) if max_concurrency == MaxConcurrency.DEFAULT else max_concurrency + len(input_items) + if max_concurrency == DEFAULT_MAX_CONCURRENCY_VALUE + else max_concurrency ) self._set_active_workers(workers_number=workers_number, env=env) diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/inline_iteration_component.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/inline_iteration_component.py index 837387944ae96..664b87d033837 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/inline_iteration_component.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/inline_iteration_component.py @@ -26,7 +26,7 @@ JobPool, ) from localstack.services.stepfunctions.asl.component.state.state_execution.state_map.max_concurrency import ( - MaxConcurrency, + DEFAULT_MAX_CONCURRENCY_VALUE, ) from localstack.services.stepfunctions.asl.component.states import States from localstack.services.stepfunctions.asl.eval.environment import Environment @@ -99,7 +99,9 @@ def _eval_body(self, env: Environment) -> None: ) number_of_workers = ( - len(input_items) if max_concurrency == MaxConcurrency.DEFAULT else max_concurrency + len(input_items) + if max_concurrency == DEFAULT_MAX_CONCURRENCY_VALUE + else max_concurrency ) for _ in range(number_of_workers): self._launch_worker(env=env) diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/max_concurrency.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/max_concurrency.py index bdafe576882de..21f20eefc65df 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/max_concurrency.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/max_concurrency.py @@ -1,10 +1,80 @@ +import abc from typing import Final -from localstack.services.stepfunctions.asl.component.component import Component +from localstack.aws.api.stepfunctions import ExecutionFailedEventDetails, HistoryEventType +from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( + FailureEvent, + FailureEventException, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name import ( + StatesErrorName, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name_type import ( + StatesErrorNameType, +) +from localstack.services.stepfunctions.asl.component.eval_component import EvalComponent +from localstack.services.stepfunctions.asl.eval.environment import Environment +from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails +from localstack.services.stepfunctions.asl.utils.encoding import to_json_str +from localstack.services.stepfunctions.asl.utils.json_path import JSONPathUtils +DEFAULT_MAX_CONCURRENCY_VALUE: Final[int] = 0 # No limit. -class MaxConcurrency(Component): - DEFAULT: Final[int] = 0 # No limit. - def __init__(self, num: int = DEFAULT): - self.num: Final[int] = num +class MaxConcurrencyDecl(EvalComponent, abc.ABC): + @abc.abstractmethod + def _eval_max_concurrency(self, env: Environment) -> int: ... + + def _eval_body(self, env: Environment) -> None: + max_concurrency_value = self._eval_max_concurrency(env=env) + env.stack.append(max_concurrency_value) + + +class MaxConcurrency(MaxConcurrencyDecl): + max_concurrency_value: Final[int] + + def __init__(self, num: int = DEFAULT_MAX_CONCURRENCY_VALUE): + super().__init__() + self.max_concurrency_value = num + + def _eval_max_concurrency(self, env: Environment) -> int: + return self.max_concurrency_value + + +class MaxConcurrencyPath(MaxConcurrency): + max_concurrency_path: Final[str] + + def __init__(self, max_concurrency_path: str): + super().__init__() + self.max_concurrency_path = max_concurrency_path + + def _eval_max_concurrency(self, env: Environment) -> int: + inp = env.stack[-1] + max_concurrency_value = JSONPathUtils.extract_json(self.max_concurrency_path, inp) + + error_cause = None + if not isinstance(max_concurrency_value, int): + value_str = ( + to_json_str(max_concurrency_value) + if not isinstance(max_concurrency_value, str) + else max_concurrency_value + ) + error_cause = f'The MaxConcurrencyPath field refers to value "{value_str}" which is not a valid integer: {self.max_concurrency_path}' + elif max_concurrency_value < 0: + error_cause = f"Expected non-negative integer for MaxConcurrency, got '{max_concurrency_value}' instead." + + if error_cause is not None: + raise FailureEventException( + failure_event=FailureEvent( + env=env, + error_name=StatesErrorName(typ=StatesErrorNameType.StatesRuntime), + event_type=HistoryEventType.ExecutionFailed, + event_details=EventDetails( + executionFailedEventDetails=ExecutionFailedEventDetails( + error=StatesErrorNameType.StatesRuntime.to_name(), cause=error_cause + ) + ), + ) + ) + + return max_concurrency_value diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py index e06954e4256c6..a52b6a2a9656a 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py @@ -54,6 +54,7 @@ ) from localstack.services.stepfunctions.asl.component.state.state_execution.state_map.max_concurrency import ( MaxConcurrency, + MaxConcurrencyDecl, ) from localstack.services.stepfunctions.asl.component.state.state_props import StateProps from localstack.services.stepfunctions.asl.eval.environment import Environment @@ -66,7 +67,7 @@ class StateMap(ExecutionState): item_reader: Optional[ItemReader] item_selector: Optional[ItemSelector] parameters: Optional[Parameters] - max_concurrency: MaxConcurrency + max_concurrency_decl: MaxConcurrencyDecl result_path: Optional[ResultPath] result_selector: ResultSelector retry: Optional[RetryDecl] @@ -84,7 +85,7 @@ def from_state_props(self, state_props: StateProps) -> None: self.item_reader = state_props.get(ItemReader) self.item_selector = state_props.get(ItemSelector) self.parameters = state_props.get(Parameters) - self.max_concurrency = state_props.get(MaxConcurrency) or MaxConcurrency() + self.max_concurrency_decl = state_props.get(MaxConcurrencyDecl) or MaxConcurrency() self.result_path = state_props.get(ResultPath) or ResultPath( result_path_src=ResultPath.DEFAULT_PATH ) @@ -112,6 +113,8 @@ def from_state_props(self, state_props: StateProps) -> None: raise ValueError(f"Unknown value for IteratorDecl '{iteration_decl}'.") def _eval_execution(self, env: Environment) -> None: + max_concurrency_num = env.stack.pop() + self.items_path.eval(env) if self.item_reader: env.event_history.add_event( @@ -135,7 +138,7 @@ def _eval_execution(self, env: Environment) -> None: if isinstance(self.iteration_component, InlineIterator): eval_input = InlineIteratorEvalInput( state_name=self.name, - max_concurrency=self.max_concurrency.num, + max_concurrency=max_concurrency_num, input_items=input_items, parameters=self.parameters, item_selector=self.item_selector, @@ -143,7 +146,7 @@ def _eval_execution(self, env: Environment) -> None: elif isinstance(self.iteration_component, DistributedIterator): eval_input = DistributedIteratorEvalInput( state_name=self.name, - max_concurrency=self.max_concurrency.num, + max_concurrency=max_concurrency_num, input_items=input_items, parameters=self.parameters, item_selector=self.item_selector, @@ -152,7 +155,7 @@ def _eval_execution(self, env: Environment) -> None: elif isinstance(self.iteration_component, InlineItemProcessor): eval_input = InlineItemProcessorEvalInput( state_name=self.name, - max_concurrency=self.max_concurrency.num, + max_concurrency=max_concurrency_num, input_items=input_items, item_selector=self.item_selector, parameters=self.parameters, @@ -160,7 +163,7 @@ def _eval_execution(self, env: Environment) -> None: elif isinstance(self.iteration_component, DistributedItemProcessor): eval_input = DistributedItemProcessorEvalInput( state_name=self.name, - max_concurrency=self.max_concurrency.num, + max_concurrency=max_concurrency_num, input_items=input_items, item_reader=self.item_reader, item_selector=self.item_selector, @@ -184,6 +187,9 @@ def _eval_state(self, env: Environment) -> None: # Initialise the retry counter for execution states. env.context_object_manager.context_object["State"]["RetryCount"] = 0 + # Evaluate state level properties. + self.max_concurrency_decl.eval(env=env) + # Attempt to evaluate the state's logic through until it's successful, caught, or retries have run out. while True: try: diff --git a/localstack/services/stepfunctions/asl/component/state/state_props.py b/localstack/services/stepfunctions/asl/component/state/state_props.py index 6c3840cafa7b8..a2edb77cc6344 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_props.py +++ b/localstack/services/stepfunctions/asl/component/state/state_props.py @@ -7,6 +7,9 @@ from localstack.services.stepfunctions.asl.component.state.state_execution.state_map.item_reader.reader_config.max_items_decl import ( MaxItemsDecl, ) +from localstack.services.stepfunctions.asl.component.state.state_execution.state_map.max_concurrency import ( + MaxConcurrencyDecl, +) from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.service.resource import ( Resource, ) @@ -17,17 +20,19 @@ ) from localstack.services.stepfunctions.asl.parse.typed_props import TypedProps +UNIQUE_SUBINSTANCES: Final[set[type]] = { + Resource, + WaitFunction, + Timeout, + Heartbeat, + MaxItemsDecl, + MaxConcurrencyDecl, + ErrorDecl, + CauseDecl, +} + class StateProps(TypedProps): - _UNIQUE_SUBINSTANCES: Final[set[type]] = { - Resource, - WaitFunction, - Timeout, - Heartbeat, - MaxItemsDecl, - ErrorDecl, - CauseDecl, - } name: str def add(self, instance: Any) -> None: @@ -40,7 +45,7 @@ def add(self, instance: Any) -> None: raise ValueError(f"Next redefines End, from '{self.get(End)}' to '{instance}'.") # Subclasses - for typ in self._UNIQUE_SUBINSTANCES: + for typ in UNIQUE_SUBINSTANCES: if issubclass(inst_type, typ): super()._add(typ, instance) return diff --git a/localstack/services/stepfunctions/asl/parse/preprocessor.py b/localstack/services/stepfunctions/asl/parse/preprocessor.py index 8fcc9769da90c..346a76c547124 100644 --- a/localstack/services/stepfunctions/asl/parse/preprocessor.py +++ b/localstack/services/stepfunctions/asl/parse/preprocessor.py @@ -175,6 +175,7 @@ ) from localstack.services.stepfunctions.asl.component.state.state_execution.state_map.max_concurrency import ( MaxConcurrency, + MaxConcurrencyPath, ) from localstack.services.stepfunctions.asl.component.state.state_execution.state_map.mode import ( Mode, @@ -546,6 +547,10 @@ def visitMax_concurrency_decl( ) -> MaxConcurrency: return MaxConcurrency(num=int(ctx.INT().getText())) + def visitMax_concurrency_path_decl(self, ctx: ASLParser.Max_concurrency_path_declContext): + max_concurrency_path: str = self._inner_string_of(parse_tree=ctx.STRINGPATH()) + return MaxConcurrencyPath(max_concurrency_path=max_concurrency_path) + def visitMode_decl(self, ctx: ASLParser.Mode_declContext) -> Mode: mode_type: int = self.visit(ctx.mode_type()) return Mode(mode_type) diff --git a/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py b/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py index 0d32cde6afc05..e1084e2d4d4ad 100644 --- a/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py +++ b/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py @@ -12,6 +12,9 @@ class ScenariosTemplate(TemplateLoader): _THIS_FOLDER, "statemachines/catch_states_runtime.json5" ) PARALLEL_STATE: Final[str] = os.path.join(_THIS_FOLDER, "statemachines/parallel_state.json5") + MAX_CONCURRENCY: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/max_concurrency_path.json5" + ) PARALLEL_STATE_FAIL: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/parallel_state_fail.json5" ) diff --git a/tests/aws/services/stepfunctions/templates/scenarios/statemachines/max_concurrency_path.json5 b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/max_concurrency_path.json5 new file mode 100644 index 0000000000000..8c1b70c8b42bf --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/max_concurrency_path.json5 @@ -0,0 +1,28 @@ +{ + "Comment": "MAX_CONCURRENCY_PATH", + "StartAt": "MapState", + "States": { + "MapState": { + "Type": "Map", + "ItemsPath": "$.Values", + "MaxConcurrencyPath": "$.MaxConcurrencyValue", + "ItemProcessor": { + "ProcessorConfig": { + "Mode": "INLINE" + }, + "StartAt": "HandleItem", + "States": { + "HandleItem": { + "Type": "Pass", + "End": true + } + } + }, + "Next": "Final", + }, + "Final": { + "Type": "Pass", + "End": true + } + } +} diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py index 80abb3b50d080..15d25e4e6d765 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py @@ -114,6 +114,63 @@ def test_parallel_state( exec_input, ) + @markers.aws.validated + @pytest.mark.parametrize("max_concurrency_value", [dict(), "NoNumber", 0, 1]) + def test_max_concurrency_path( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + max_concurrency_value, + ): + # TODO: Investigate AWS's behaviour with stringified integer values such as "1", as when passed as + # execution inputs these are casted to integers. Future efforts should record more snapshot tests to assert + # the behaviour of such stringification on execution inputs + template = ST.load_sfn_template(ST.MAX_CONCURRENCY) + definition = json.dumps(template) + + exec_input = json.dumps( + {"MaxConcurrencyValue": max_concurrency_value, "Values": ["HelloWorld"]} + ) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + paths=[ + # TODO: AWS consistently appears to stall after startup when a negative MaxConcurrency value is given. + # Instead, the Provider V2 raises a State.Runtime exception and terminates. In the future we should + # reevaluate AWS's behaviour in these circumstances and choose whether too also 'hang'. + "$..events" + ] + ) + def test_max_concurrency_path_negative( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + ): + template = ST.load_sfn_template(ST.MAX_CONCURRENCY) + definition = json.dumps(template) + + exec_input = json.dumps({"MaxConcurrencyValue": -1, "Values": ["HelloWorld"]}) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + @markers.aws.validated def test_parallel_state_order( self, diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json index 4bfdf62592438..8da6dde63660f 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json @@ -15651,5 +15651,476 @@ } } } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_max_concurrency_path[max_concurrency_value0]": { + "recorded-date": "22-04-2024, 19:57:20", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "MaxConcurrencyValue": {}, + "Values": [ + "HelloWorld" + ] + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "MaxConcurrencyValue": {}, + "Values": [ + "HelloWorld" + ] + }, + "inputDetails": { + "truncated": false + }, + "name": "MapState" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "executionFailedEventDetails": { + "cause": "An error occurred while executing the state 'MapState' (entered at the event id #2). The MaxConcurrencyPath field refers to value \"{}\" which is not a valid integer: $.MaxConcurrencyValue", + "error": "States.Runtime" + }, + "id": 3, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_max_concurrency_path[0]": { + "recorded-date": "22-04-2024, 19:57:47", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "MaxConcurrencyValue": 0, + "Values": [ + "HelloWorld" + ] + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "MaxConcurrencyValue": 0, + "Values": [ + "HelloWorld" + ] + }, + "inputDetails": { + "truncated": false + }, + "name": "MapState" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "id": 3, + "mapStateStartedEventDetails": { + "length": 1 + }, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "MapStateStarted" + }, + { + "id": 4, + "mapIterationStartedEventDetails": { + "index": 0, + "name": "MapState" + }, + "previousEventId": 3, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 5, + "previousEventId": 4, + "stateEnteredEventDetails": { + "input": "\"HelloWorld\"", + "inputDetails": { + "truncated": false + }, + "name": "HandleItem" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 6, + "previousEventId": 5, + "stateExitedEventDetails": { + "name": "HandleItem", + "output": "\"HelloWorld\"", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 7, + "mapIterationSucceededEventDetails": { + "index": 0, + "name": "MapState" + }, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 8, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "MapStateSucceeded" + }, + { + "id": 9, + "previousEventId": 7, + "stateExitedEventDetails": { + "name": "MapState", + "output": "[\"HelloWorld\"]", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "MapStateExited" + }, + { + "id": 10, + "previousEventId": 9, + "stateEnteredEventDetails": { + "input": "[\"HelloWorld\"]", + "inputDetails": { + "truncated": false + }, + "name": "Final" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 11, + "previousEventId": 10, + "stateExitedEventDetails": { + "name": "Final", + "output": "[\"HelloWorld\"]", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": "[\"HelloWorld\"]", + "outputDetails": { + "truncated": false + } + }, + "id": 12, + "previousEventId": 11, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_max_concurrency_path[1]": { + "recorded-date": "22-04-2024, 19:58:01", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "MaxConcurrencyValue": 1, + "Values": [ + "HelloWorld" + ] + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "MaxConcurrencyValue": 1, + "Values": [ + "HelloWorld" + ] + }, + "inputDetails": { + "truncated": false + }, + "name": "MapState" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "id": 3, + "mapStateStartedEventDetails": { + "length": 1 + }, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "MapStateStarted" + }, + { + "id": 4, + "mapIterationStartedEventDetails": { + "index": 0, + "name": "MapState" + }, + "previousEventId": 3, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 5, + "previousEventId": 4, + "stateEnteredEventDetails": { + "input": "\"HelloWorld\"", + "inputDetails": { + "truncated": false + }, + "name": "HandleItem" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 6, + "previousEventId": 5, + "stateExitedEventDetails": { + "name": "HandleItem", + "output": "\"HelloWorld\"", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 7, + "mapIterationSucceededEventDetails": { + "index": 0, + "name": "MapState" + }, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 8, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "MapStateSucceeded" + }, + { + "id": 9, + "previousEventId": 7, + "stateExitedEventDetails": { + "name": "MapState", + "output": "[\"HelloWorld\"]", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "MapStateExited" + }, + { + "id": 10, + "previousEventId": 9, + "stateEnteredEventDetails": { + "input": "[\"HelloWorld\"]", + "inputDetails": { + "truncated": false + }, + "name": "Final" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 11, + "previousEventId": 10, + "stateExitedEventDetails": { + "name": "Final", + "output": "[\"HelloWorld\"]", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": "[\"HelloWorld\"]", + "outputDetails": { + "truncated": false + } + }, + "id": 12, + "previousEventId": 11, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_max_concurrency_path_negative": { + "recorded-date": "22-04-2024, 12:40:52", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "MaxConcurrencyValue": -1, + "Values": [ + "HelloWorld" + ] + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_max_concurrency_path[NoNumber]": { + "recorded-date": "22-04-2024, 19:57:33", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "MaxConcurrencyValue": "NoNumber", + "Values": [ + "HelloWorld" + ] + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "MaxConcurrencyValue": "NoNumber", + "Values": [ + "HelloWorld" + ] + }, + "inputDetails": { + "truncated": false + }, + "name": "MapState" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "executionFailedEventDetails": { + "cause": "An error occurred while executing the state 'MapState' (entered at the event id #2). The MaxConcurrencyPath field refers to value \"NoNumber\" which is not a valid integer: $.MaxConcurrencyValue", + "error": "States.Runtime" + }, + "id": 3, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json index f06853b390894..ca715e51082ed 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json @@ -173,6 +173,21 @@ "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_retry_multiple_retriers": { "last_validated_date": "2023-08-08T11:20:58+00:00" }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_max_concurrency_path[0]": { + "last_validated_date": "2024-04-22T19:57:47+00:00" + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_max_concurrency_path[1]": { + "last_validated_date": "2024-04-22T19:58:01+00:00" + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_max_concurrency_path[NoNumber]": { + "last_validated_date": "2024-04-22T19:57:33+00:00" + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_max_concurrency_path[max_concurrency_value0]": { + "last_validated_date": "2024-04-22T19:57:20+00:00" + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_max_concurrency_path_negative": { + "last_validated_date": "2024-04-22T12:40:52+00:00" + }, "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_parallel_state": { "last_validated_date": "2023-07-17T10:41:25+00:00" }, From ba5801432900153f4118a28142e346029cb6042a Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 23 Apr 2024 10:37:10 +0200 Subject: [PATCH 089/169] Feature: Eventbridge v2: CRUD (#10613) --- localstack/services/events/event_bus.py | 33 + localstack/services/events/models_v2.py | 96 ++ localstack/services/events/provider_v2.py | 563 +++++++++- localstack/services/events/rule.py | 186 ++++ localstack/services/events/target.py | 300 ++++++ localstack/testing/aws/asf_utils.py | 7 +- tests/aws/services/events/conftest.py | 75 ++ tests/aws/services/events/test_events.py | 445 +++++++- .../services/events/test_events.snapshot.json | 962 +++++++++++++++++- .../events/test_events.validation.json | 94 +- .../events/test_events_integrations.py | 1 - 11 files changed, 2701 insertions(+), 61 deletions(-) create mode 100644 localstack/services/events/event_bus.py create mode 100644 localstack/services/events/models_v2.py create mode 100644 localstack/services/events/rule.py create mode 100644 localstack/services/events/target.py diff --git a/localstack/services/events/event_bus.py b/localstack/services/events/event_bus.py new file mode 100644 index 0000000000000..c014461be20c5 --- /dev/null +++ b/localstack/services/events/event_bus.py @@ -0,0 +1,33 @@ +from typing import Optional + +from localstack.aws.api.events import Arn, EventBusName, TagList +from localstack.services.events.models_v2 import EventBus, RuleDict + + +class EventBusService: + def __init__( + self, + name: EventBusName, + region: str, + account_id: str, + event_source_name: Optional[str] = None, + tags: Optional[TagList] = None, + policy: Optional[str] = None, + rules: Optional[RuleDict] = None, + ): + self.event_bus = EventBus( + name, + region, + account_id, + event_source_name, + tags, + policy, + rules, + ) + + @property + def arn(self): + return self.event_bus.arn + + +EventBusServiceDict = dict[Arn, EventBusService] diff --git a/localstack/services/events/models_v2.py b/localstack/services/events/models_v2.py new file mode 100644 index 0000000000000..89b232fe30291 --- /dev/null +++ b/localstack/services/events/models_v2.py @@ -0,0 +1,96 @@ +from dataclasses import dataclass, field +from typing import Optional + +from localstack.aws.api.core import ServiceException +from localstack.aws.api.events import ( + Arn, + CreatedBy, + EventBusName, + EventPattern, + ManagedBy, + RoleArn, + RuleDescription, + RuleName, + RuleState, + ScheduleExpression, + TagList, + Target, + TargetId, +) +from localstack.services.stores import ( + AccountRegionBundle, + BaseStore, + LocalAttribute, +) + +TargetDict = dict[TargetId, Target] + + +@dataclass +class Rule: + name: RuleName + region: str + account_id: str + schedule_expression: Optional[ScheduleExpression] = None + event_pattern: Optional[EventPattern] = None + state: Optional[RuleState] = None + description: Optional[RuleDescription] = None + role_arn: Optional[RoleArn] = None + tags: TagList = field(default_factory=list) + event_bus_name: EventBusName = "default" + targets: TargetDict = field(default_factory=dict) + managed_by: Optional[ManagedBy] = None # can only be set by AWS services + created_by: CreatedBy = field(init=False) + arn: Arn = field(init=False) + + def __post_init__(self): + if self.event_bus_name == "default": + self.arn = f"arn:aws:events:{self.region}:{self.account_id}:rule/{self.name}" + else: + self.arn = f"arn:aws:events:{self.region}:{self.account_id}:rule/{self.event_bus_name}/{self.name}" + self.created_by = self.account_id + if self.tags is None: + self.tags = [] + if self.targets is None: + self.targets = {} + if self.state is None: + self.state = RuleState.ENABLED + + +RuleDict = dict[RuleName, Rule] + + +@dataclass +class EventBus: + name: EventBusName + region: str + account_id: str + event_source_name: Optional[str] = None + tags: TagList = field(default_factory=list) + policy: Optional[str] = None + rules: RuleDict = field(default_factory=dict) + arn: Arn = field(init=False) + + def __post_init__(self): + self.arn = f"arn:aws:events:{self.region}:{self.account_id}:event-bus/{self.name}" + if self.rules is None: + self.rules = {} + if self.tags is None: + self.tags = [] + + +EventBusDict = dict[EventBusName, EventBus] + + +class EventsStore(BaseStore): + # Map of eventbus names to eventbus objects. The name MUST be unique per account and region (works with AccountRegionBundle) + event_buses: EventBusDict = LocalAttribute(default=dict) + + +events_store = AccountRegionBundle("events", EventsStore) + + +class ValidationException(ServiceException): + code: str = "ValidationException" + sender_fault: bool = True + status_code: int = 400 diff --git a/localstack/services/events/provider_v2.py b/localstack/services/events/provider_v2.py index 278f612b593d2..c82497e2d5323 100644 --- a/localstack/services/events/provider_v2.py +++ b/localstack/services/events/provider_v2.py @@ -1,30 +1,95 @@ +import base64 import logging +from typing import Optional from localstack.aws.api import RequestContext, handler from localstack.aws.api.events import ( + Arn, + Boolean, CreateEventBusResponse, + DescribeEventBusResponse, + DescribeRuleResponse, + EndpointId, + EventBusList, EventBusName, EventBusNameOrArn, EventPattern, EventsApi, EventSourceName, + LimitMax100, + ListEventBusesResponse, + ListRuleNamesByTargetResponse, + ListRulesResponse, + ListTargetsByRuleResponse, + NextToken, + PutEventsRequestEntryList, + PutEventsResponse, + PutPartnerEventsRequestEntryList, + PutPartnerEventsResponse, PutRuleResponse, + PutTargetsResponse, + RemoveTargetsResponse, + ResourceAlreadyExistsException, + ResourceNotFoundException, RoleArn, RuleDescription, RuleName, + RuleResponseList, RuleState, ScheduleExpression, TagList, + Target, + TargetArn, + TargetId, + TargetIdList, + TargetList, ) +from localstack.aws.api.events import EventBus as ApiTypeEventBus +from localstack.aws.api.events import Rule as ApiTypeRule +from localstack.services.events.event_bus import EventBusService, EventBusServiceDict +from localstack.services.events.models_v2 import ( + EventBus, + EventBusDict, + EventsStore, + Rule, + RuleDict, + TargetDict, + ValidationException, + events_store, +) +from localstack.services.events.rule import RuleService, RuleServiceDict +from localstack.services.events.target import TargetSender, TargetSenderDict, TargetSenderFactory from localstack.services.plugins import ServiceLifecycleHook LOG = logging.getLogger(__name__) +def decode_next_token(token: NextToken) -> int: + """Decode a pagination token from base64 to integer.""" + return int.from_bytes(base64.b64decode(token), "big") + + +def encode_next_token(token: int) -> NextToken: + """Encode a pagination token to base64 from integer.""" + return base64.b64encode(token.to_bytes(128, "big")).decode("utf-8") + + +def get_filtered_dict(name_prefix: str, input_dict: dict) -> dict: + """Filter dictionary by prefix.""" + return {name: value for name, value in input_dict.items() if name.startswith(name_prefix)} + + class EventsProvider(EventsApi, ServiceLifecycleHook): + # api methods are grouped by resource type and sorted in hierarchical order + # each group is sorted alphabetically def __init__(self): - self._rules = {} - self._event_buses = {} + self._event_bus_services_store: EventBusServiceDict = {} + self._rule_services_store: RuleServiceDict = {} + self._target_sender_store: TargetSenderDict = {} + + ########## + # EventBus + ########## @handler("CreateEventBus") def create_event_bus( @@ -35,15 +100,171 @@ def create_event_bus( tags: TagList = None, **kwargs, ) -> CreateEventBusResponse: - event_bus_arn = f"arn:aws:events:{context.region}:{context.account_id}:event-bus/{name}" - event_bus = {"Name": name, "Arn": event_bus_arn} - self._event_buses[name] = event_bus + region = context.region + account_id = context.account_id + store = self.get_store(context) + if name in store.event_buses.keys(): + raise ResourceAlreadyExistsException(f"Event bus {name} already exists.") + event_bus_service = self.create_event_bus_service( + name, region, account_id, event_source_name, tags + ) + store.event_buses[event_bus_service.event_bus.name] = event_bus_service.event_bus response = CreateEventBusResponse( - EventBusArn=event_bus_arn, + EventBusArn=event_bus_service.arn, ) return response + @handler("DeleteEventBus") + def delete_event_bus(self, context: RequestContext, name: EventBusName, **kwargs) -> None: + if name == "default": + raise ValidationException("Cannot delete event bus default.") + store = self.get_store(context) + try: + if event_bus := self.get_event_bus(name, store): + del self._event_bus_services_store[event_bus.arn] + if rules := event_bus.rules: + self._delete_rule_services(rules) + del store.event_buses[name] + except ResourceNotFoundException as error: + return error + + @handler("DescribeEventBus") + def describe_event_bus( + self, context: RequestContext, name: EventBusNameOrArn = None, **kwargs + ) -> DescribeEventBusResponse: + name = self._extract_event_bus_name(name) + store = self.get_store(context) + event_bus = self.get_event_bus(name, store) + + response = self._event_bus_dict_to_api_type_event_bus(event_bus) + return response + + @handler("ListEventBuses") + def list_event_buses( + self, + context: RequestContext, + name_prefix: EventBusName = None, + next_token: NextToken = None, + limit: LimitMax100 = None, + **kwargs, + ) -> ListEventBusesResponse: + store = self.get_store(context) + event_buses = ( + get_filtered_dict(name_prefix, store.event_buses) if name_prefix else store.event_buses + ) + limited_event_buses, next_token = self._get_limited_dict_and_next_token( + event_buses, next_token, limit + ) + + response = ListEventBusesResponse( + EventBuses=self._event_bust_dict_to_api_type_list(limited_event_buses) + ) + if next_token is not None: + response["NextToken"] = next_token + return response + + ####### + # Rules + ####### + @handler("EnableRule") + def enable_rule( + self, + context: RequestContext, + name: RuleName, + event_bus_name: EventBusNameOrArn = None, + **kwargs, + ) -> None: + store = self.get_store(context) + event_bus_name = self._extract_event_bus_name(event_bus_name) + event_bus = self.get_event_bus(event_bus_name, store) + rule = self.get_rule(name, event_bus) + rule.state = RuleState.ENABLED + + @handler("DeleteRule") + def delete_rule( + self, + context: RequestContext, + name: RuleName, + event_bus_name: EventBusNameOrArn = None, + force: Boolean = None, + **kwargs, + ) -> None: + store = self.get_store(context) + event_bus_name = self._extract_event_bus_name(event_bus_name) + event_bus = self.get_event_bus(event_bus_name, store) + try: + rule = self.get_rule(name, event_bus) + if rule.targets and not force: + raise ValidationException("Rule can't be deleted since it has targets.") + self._delete_rule_services(rule) + del event_bus.rules[name] + except ResourceNotFoundException as error: + return error + + @handler("DescribeRule") + def describe_rule( + self, + context: RequestContext, + name: RuleName, + event_bus_name: EventBusNameOrArn = None, + **kwargs, + ) -> DescribeRuleResponse: + store = self.get_store(context) + event_bus_name = self._extract_event_bus_name(event_bus_name) + event_bus = self.get_event_bus(event_bus_name, store) + rule = self.get_rule(name, event_bus) + + response = self._rule_dict_to_api_type_rule(rule) + return response + + @handler("DisableRule") + def disable_rule( + self, + context: RequestContext, + name: RuleName, + event_bus_name: EventBusNameOrArn = None, + **kwargs, + ) -> None: + store = self.get_store(context) + event_bus_name = self._extract_event_bus_name(event_bus_name) + event_bus = self.get_event_bus(event_bus_name, store) + rule = self.get_rule(name, event_bus) + rule.state = RuleState.DISABLED + + @handler("ListRules") + def list_rules( + self, + context: RequestContext, + name_prefix: RuleName = None, + event_bus_name: EventBusNameOrArn = None, + next_token: NextToken = None, + limit: LimitMax100 = None, + **kwargs, + ) -> ListRulesResponse: + store = self.get_store(context) + event_bus_name = self._extract_event_bus_name(event_bus_name) + event_bus = self.get_event_bus(event_bus_name, store) + rules = get_filtered_dict(name_prefix, event_bus.rules) if name_prefix else event_bus.rules + limited_rules, next_token = self._get_limited_dict_and_next_token(rules, next_token, limit) + + response = ListRulesResponse(Rules=list(self._rule_dict_to_api_type_list(limited_rules))) + if next_token is not None: + response["NextToken"] = next_token + return response + + @handler("ListRuleNamesByTarget") + def list_rule_names_by_target( + self, + context: RequestContext, + target_arn: TargetArn, + event_bus_name: EventBusNameOrArn = None, + next_token: NextToken = None, + limit: LimitMax100 = None, + **kwargs, + ) -> ListRuleNamesByTargetResponse: + raise NotImplementedError + @handler("PutRule") def put_rule( self, @@ -58,18 +279,324 @@ def put_rule( event_bus_name: EventBusNameOrArn = None, **kwargs, ) -> PutRuleResponse: - rule = { - "Name": name, - "ScheduleExpression": schedule_expression, - "EventPattern": event_pattern, - "State": state, - "Description": description, - "RoleArn": role_arn, - "EventBusName": event_bus_name, - } - self._rules[name] = rule + region = context.region + account_id = context.account_id + event_bus_name = self._extract_event_bus_name(event_bus_name) + store = self.get_store(context) + event_bus = self.get_event_bus(event_bus_name, store) + existing_rule = event_bus.rules.get(name) + targets = existing_rule.targets if existing_rule else None + rule_service = self.create_rule_service( + name, + region, + account_id, + schedule_expression, + event_pattern, + state, + description, + role_arn, + tags, + event_bus_name, + targets, + ) + event_bus.rules[name] = rule_service.rule + response = PutRuleResponse(RuleArn=rule_service.arn) + return response + + ######### + # Targets + ######### + + @handler("ListTargetsByRule") + def list_targets_by_rule( + self, + context: RequestContext, + rule: RuleName, + event_bus_name: EventBusNameOrArn = None, + next_token: NextToken = None, + limit: LimitMax100 = None, + **kwargs, + ) -> ListTargetsByRuleResponse: + store = self.get_store(context) + event_bus_name = self._extract_event_bus_name(event_bus_name) + event_bus = self.get_event_bus(event_bus_name, store) + rule = self.get_rule(rule, event_bus) + targets = rule.targets + limited_targets, next_token = self._get_limited_dict_and_next_token( + targets, next_token, limit + ) + + response = ListTargetsByRuleResponse(Targets=list(limited_targets.values())) + if next_token is not None: + response["NextToken"] = next_token + return response + + @handler("PutTargets") + def put_targets( + self, + context: RequestContext, + rule: RuleName, + targets: TargetList, + event_bus_name: EventBusNameOrArn = None, + **kwargs, + ) -> PutTargetsResponse: + region = context.region + account_id = context.account_id + rule_service = self.get_rule_service(context, rule, event_bus_name) + failed_entries = rule_service.add_targets(targets) + rule_arn = rule_service.arn + for target in targets: + self.create_target_sender(target, region, account_id, rule_arn) - response = PutRuleResponse( - RuleArn=f"arn:aws:events:{context.region}:{context.account_id}:rule/{name}", + response = PutTargetsResponse( + FailedEntryCount=len(failed_entries), FailedEntries=failed_entries ) return response + + @handler("RemoveTargets") + def remove_targets( + self, + context: RequestContext, + rule: RuleName, + ids: TargetIdList, + event_bus_name: EventBusNameOrArn = None, + force: Boolean = None, + **kwargs, + ) -> RemoveTargetsResponse: + rule_service = self.get_rule_service(context, rule, event_bus_name) + failed_entries = rule_service.remove_targets(ids) + self._delete_target_sender(ids, rule_service.rule) + + response = RemoveTargetsResponse( + FailedEntryCount=len(failed_entries), FailedEntries=failed_entries + ) + return response + + ######## + # Events + ######## + + @handler("PutEvents") + def put_events( + self, + context: RequestContext, + entries: PutEventsRequestEntryList, + endpoint_id: EndpointId = None, + **kwargs, + ) -> PutEventsResponse: + failed_entries = self._put_entries(context, entries) + + response = PutEventsResponse(FailedEntryCount=len(failed_entries), Entries=failed_entries) + return response + + @handler("PutPartnerEvents") + def put_partner_events( + self, context: RequestContext, entries: PutPartnerEventsRequestEntryList, **kwargs + ) -> PutPartnerEventsResponse: + raise NotImplementedError + + ######### + # Methods + ######### + + def get_store(self, context: RequestContext) -> EventsStore: + """Returns the events store for the account and region. + On first call, creates the default event bus for the account region.""" + region = context.region + account_id = context.account_id + store = events_store[account_id][region] + # create default event bus for account region on first call + default_event_bus_name = "default" + if default_event_bus_name not in store.event_buses.keys(): + event_bus_service = self.create_event_bus_service( + default_event_bus_name, region, account_id, None, None + ) + store.event_buses[event_bus_service.event_bus.name] = event_bus_service.event_bus + return store + + def get_event_bus(self, name: EventBusName, store: EventsStore) -> EventBus: + if event_bus := store.event_buses.get(name): + return event_bus + raise ResourceNotFoundException(f"Event bus {name} does not exist.") + + def get_rule(self, name: RuleName, event_bus: EventBus) -> Rule: + if rule := event_bus.rules.get(name): + return rule + raise ResourceNotFoundException(f"Rule {name} does not exist on EventBus {event_bus.name}.") + + def get_target(self, target_id: TargetId, rule: Rule) -> Target: + if target := rule.targets.get(target_id): + return target + raise ResourceNotFoundException(f"Target {target_id} does not exist on Rule {rule.name}.") + + def get_rule_service( + self, context: RequestContext, rule_name: RuleName, event_bus_name: EventBusName + ) -> RuleService: + store = self.get_store(context) + event_bus_name = self._extract_event_bus_name(event_bus_name) + event_bus = self.get_event_bus(event_bus_name, store) + rule = self.get_rule(rule_name, event_bus) + return self._rule_services_store[rule.arn] + + def create_event_bus_service( + self, + name: EventBusName, + region: str, + account_id: str, + event_source_name: Optional[EventSourceName], + tags: Optional[TagList], + ) -> EventBusService: + event_bus_service = EventBusService( + name, + region, + account_id, + event_source_name, + tags, + ) + self._event_bus_services_store[event_bus_service.arn] = event_bus_service + return event_bus_service + + def create_rule_service( + self, + name: RuleName, + region: str, + account_id: str, + schedule_expression: Optional[ScheduleExpression], + event_pattern: Optional[EventPattern], + state: Optional[RuleState], + description: Optional[RuleDescription], + role_arn: Optional[RoleArn], + tags: Optional[TagList], + event_bus_name: Optional[EventBusName], + targets: Optional[TargetDict], + ) -> RuleService: + rule_service = RuleService( + name, + region, + account_id, + schedule_expression, + event_pattern, + state, + description, + role_arn, + tags, + event_bus_name, + targets, + ) + self._rule_services_store[rule_service.arn] = rule_service + return rule_service + + def create_target_sender( + self, target: Target, region: str, account_id: str, rule_arn: Arn + ) -> TargetSender: + target_sender = TargetSenderFactory( + target, region, account_id, rule_arn + ).get_target_sender() + self._target_sender_store[target_sender.arn] = target_sender + return target_sender + + def _get_limited_dict_and_next_token( + self, input_dict: dict, next_token: NextToken | None, limit: LimitMax100 | None + ) -> tuple[dict, NextToken]: + """Return a slice of the given dictionary starting from next_token with length of limit + and new last index encoded as a next_token for pagination.""" + input_dict_len = len(input_dict) + start_index = decode_next_token(next_token) if next_token is not None else 0 + end_index = start_index + limit if limit is not None else input_dict_len + limited_dict = dict(list(input_dict.items())[start_index:end_index]) + + next_token = ( + encode_next_token(end_index) + # return a next_token (encoded integer of next starting index) if not all items are returned + if end_index < input_dict_len + else None + ) + return limited_dict, next_token + + def _extract_event_bus_name( + self, event_bus_name_or_arn: EventBusNameOrArn | None + ) -> EventBusName: + """Return the event bus name. Input can be either an event bus name or ARN.""" + if not event_bus_name_or_arn: + return "default" + return event_bus_name_or_arn.split("/")[-1] + + def _event_bust_dict_to_api_type_list(self, event_buses: EventBusDict) -> EventBusList: + """Return a converted dict of EventBus model objects as a list of event buses in API type EventBus format.""" + event_bus_list = [ + self._event_bus_dict_to_api_type_event_bus(event_bus) + for event_bus in event_buses.values() + ] + return event_bus_list + + def _event_bus_dict_to_api_type_event_bus(self, event_bus: EventBus) -> ApiTypeEventBus: + event_bus_api_type = { + "Name": event_bus.name, + "Arn": event_bus.arn, + } + if event_bus.policy: + event_bus_api_type["Policy"] = event_bus.policy + + return event_bus_api_type + + def _delete_rule_services(self, rules: RuleDict | Rule) -> None: + """ + Delete all rule services associated to the input from the store. + Accepts a single Rule object or a dict of Rule objects as input. + """ + if isinstance(rules, Rule): + rules = {rules.name: rules} + for rule in rules.values(): + del self._rule_services_store[rule.arn] + + def _rule_dict_to_api_type_list(self, rules: RuleDict) -> RuleResponseList: + """Return a converted dict of Rule model objects as a list of rules in API type Rule format.""" + rule_list = [self._rule_dict_to_api_type_rule(rule) for rule in rules.values()] + return rule_list + + def _rule_dict_to_api_type_rule(self, rule: Rule) -> ApiTypeRule: + rule = { + "Name": rule.name, + "Arn": rule.arn, + "EventPattern": rule.event_pattern, + "State": rule.state, + "Description": rule.description, + "ScheduleExpression": rule.schedule_expression, + "RoleArn": rule.role_arn, + "ManagedBy": rule.managed_by, + "EventBusName": rule.event_bus_name, + "CreatedBy": rule.created_by, + } + return {key: value for key, value in rule.items() if value is not None} + + def _delete_target_sender(self, ids: TargetIdList, rule) -> None: + for target_id in ids: + if target := rule.targets.get(target_id): + target_arn = target["Arn"] + try: + del self._target_sender_store[target_arn] + except KeyError: + LOG.error(f"Error deleting target service {target_arn}.") + + def _put_entries(self, context: RequestContext, entries: PutEventsRequestEntryList) -> list: + failed_entries = [] + for event in entries: + event_bus_name = event.get("EventBusName", "default") + store = self.get_store(context) + event_bus = self.get_event_bus(event_bus_name, store) + # TODO add pattern matching + matching_rules = [rule for rule in event_bus.rules.values()] + for rule in matching_rules: + for target in rule.targets.values(): + target_sender = self._target_sender_store[target["Arn"]] + try: + target_sender.send_event(event) + except Exception as error: + failed_entries.append( + { + "Entry": event, + "ErrorCode": "InternalException", + "ErrorMessage": str(error), + } + ) + return failed_entries diff --git a/localstack/services/events/rule.py b/localstack/services/events/rule.py new file mode 100644 index 0000000000000..aad9d985b09a5 --- /dev/null +++ b/localstack/services/events/rule.py @@ -0,0 +1,186 @@ +import re +from typing import Optional + +from localstack.aws.api.events import ( + Arn, + EventBusName, + EventPattern, + LimitExceededException, + ManagedBy, + PutTargetsResultEntryList, + RemoveTargetsResultEntryList, + RoleArn, + RuleDescription, + RuleName, + RuleState, + ScheduleExpression, + TagList, + Target, + TargetIdList, + TargetList, +) +from localstack.services.events.models_v2 import Rule, TargetDict, ValidationException + +TARGET_ID_REGEX = re.compile(r"^[\.\-_A-Za-z0-9]+$") +TARGET_ARN_REGEX = re.compile(r"arn:[\d\w:\-/]*") +RULE_SCHEDULE_CRON_REGEX = re.compile(r"^cron\(.*\)") +RULE_SCHEDULE_RATE_REGEX = re.compile(r"^rate\(\d*\s(minute|minutes|hour|hours|day|days)\)") + + +class RuleService: + def __init__( + self, + name: RuleName, + region: Optional[str] = None, + account_id: Optional[str] = None, + schedule_expression: Optional[ScheduleExpression] = None, + event_pattern: Optional[EventPattern] = None, + state: Optional[RuleState] = None, + description: Optional[RuleDescription] = None, + role_arn: Optional[RoleArn] = None, + tags: Optional[TagList] = None, + event_bus_name: Optional[EventBusName] = None, + targets: Optional[TargetDict] = None, + managed_by: Optional[ManagedBy] = None, + ): + self._validate_input(event_pattern, schedule_expression, event_bus_name) + # required to keep data and functionality separate for persistence + self.rule = Rule( + name, + region, + account_id, + schedule_expression, + event_pattern, + state, + description, + role_arn, + tags, + event_bus_name, + targets, + managed_by, + ) + + @property + def arn(self) -> Arn: + return self.rule.arn + + @property + def state(self) -> RuleState: + return self.rule.state + + def enable(self) -> None: + self.rule.state = RuleState.ENABLED + + def disable(self) -> None: + self.rule.state = RuleState.DISABLED + + def add_targets(self, targets: TargetList) -> PutTargetsResultEntryList: + failed_entries = self.validate_targets_input(targets) + for target in targets: + target_id = target["Id"] + if target_id not in self.rule.targets and self._check_target_limit_reached(): + raise LimitExceededException( + "The requested resource exceeds the maximum number allowed." + ) + target = Target(**target) + self.rule.targets[target_id] = target + return failed_entries + + def remove_targets( + self, target_ids: TargetIdList, force: bool = False + ) -> RemoveTargetsResultEntryList: + delete_errors = [] + for target_id in target_ids: + if target_id in self.rule.targets: + if self.rule.managed_by and not force: + delete_errors.append( + { + "TargetId": target_id, + "ErrorCode": "ManagedRuleException", + "ErrorMessage": f"Rule '{self.rule.name}' is managed by an AWS service can only be modified if force is True.", + } + ) + else: + del self.rule.targets[target_id] + else: + delete_errors.append( + { + "TargetId": target_id, + "ErrorCode": "ResourceNotFoundException", + "ErrorMessage": f"Rule '{self.rule.name}' does not have a target with the Id '{target_id}'.", + } + ) + return delete_errors + + def validate_targets_input(self, targets: TargetList) -> PutTargetsResultEntryList: + validation_errors = [] + for index, target in enumerate(targets): + id = target.get("Id") + arn = target.get("Arn", "") + if not TARGET_ID_REGEX.match(id): + validation_errors.append( + { + "TargetId": id, + "ErrorCode": "ValidationException", + "ErrorMessage": f"Value '{id}' at 'targets.{index + 1}.member.id' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\.\\-_A-Za-z0-9]+", + } + ) + + if len(id) > 64: + validation_errors.append( + { + "TargetId": id, + "ErrorCode": "ValidationException", + "ErrorMessage": f"Value '{id}' at 'targets.{index + 1}.member.id' failed to satisfy constraint: Member must have length less than or equal to 64", + } + ) + + if not TARGET_ARN_REGEX.match(arn): + validation_errors.append( + { + "TargetId": id, + "ErrorCode": "ValidationException", + "ErrorMessage": f"Parameter {arn} is not valid. Reason: Provided Arn is not in correct format.", + } + ) + + if ":sqs:" in arn and arn.endswith(".fifo") and not target.get("SqsParameters"): + validation_errors.append( + { + "TargetId": id, + "ErrorCode": "ValidationException", + "ErrorMessage": f"Parameter(s) SqsParameters must be specified for target: {id}.", + } + ) + + return validation_errors + + def _validate_input( + self, + event_pattern: Optional[EventPattern], + schedule_expression: Optional[ScheduleExpression], + event_bus_name: Optional[EventBusName] = "default", + ) -> None: + if not event_pattern and not schedule_expression: + raise ValidationException( + "Parameter(s) EventPattern or ScheduleExpression must be specified." + ) + + if schedule_expression: + if event_bus_name != "default": + raise ValidationException( + "ScheduleExpression is supported only on the default event bus." + ) + if not ( + RULE_SCHEDULE_CRON_REGEX.match(schedule_expression) + or RULE_SCHEDULE_RATE_REGEX.match(schedule_expression) + ): + raise ValidationException("Parameter ScheduleExpression is not valid.") + + def _check_target_limit_reached(self) -> bool: + if len(self.rule.targets) >= 5: + return True + return False + + +RuleServiceDict = dict[Arn, RuleService] diff --git a/localstack/services/events/target.py b/localstack/services/events/target.py new file mode 100644 index 0000000000000..bb2d423bacaf8 --- /dev/null +++ b/localstack/services/events/target.py @@ -0,0 +1,300 @@ +import json +import logging +import uuid +from abc import ABC, abstractmethod + +from botocore.client import BaseClient + +from localstack.aws.api.events import ( + Arn, + Target, +) +from localstack.aws.connect import connect_to +from localstack.utils import collections +from localstack.utils.aws.arns import ( + extract_service_from_arn, + firehose_name, + sqs_queue_url_for_arn, +) +from localstack.utils.aws.client_types import ServicePrincipal +from localstack.utils.strings import to_bytes +from localstack.utils.time import now_utc + +LOG = logging.getLogger(__name__) + + +class TargetSender(ABC): + def __init__( + self, + target: Target, + region: str, + account_id: str, + rule_arn: Arn, + service: str, + ): + self.target = target + self.region = region + self.account_id = account_id + self.rule_arn = rule_arn + self.service = service + + self._validate_input(target) + self._client: BaseClient | None = None + + @property + def arn(self): + return self.target["Arn"] + + @property + def client(self): + """Lazy initialization of internal botoclient factory.""" + if self._client is None: + self._client = self._initialize_client() + return self._client + + @abstractmethod + def send_event(self): + pass + + def _validate_input(self, target: Target): + """Provide a default implementation that does nothing if no specific validation is needed.""" + # TODO add For Lambda and Amazon SNS resources, EventBridge relies on resource-based policies. + pass + + def _initialize_client(self) -> BaseClient: + """Initializes internal botocore client. + If a role from a target is provided, the client will be initialized with the assumed role. + If no role is provided the client will be initialized with the account ID and region. + In both cases event bridge is requested as service principal""" + service_principal = ServicePrincipal.events + if role_arn := self.target.get("role_arn"): + # assumed role sessions expires after 6 hours in AWS, currently no expiration in LocalStack + client_factory = connect_to.with_assumed_role( + role_arn=role_arn, service_principal=service_principal, region_name=self.region + ) + else: + client_factory = connect_to(aws_access_key_id=self.account_id, region_name=self.region) + client = client_factory.get_client(self.service) + client = client.request_metadata( + service_principal=service_principal, source_arn=self.rule_arn + ) + return client + + +TargetSenderDict = dict[Arn, TargetSender] + + +class ApiGatewayTargetSender(TargetSender): + def send_event(self, event): + raise NotImplementedError("ApiGateway target is not yet implemented") + + def _validate_input(self, target: Target): + super()._validate_input(target) + if not collections.get_safe(target, "$.RoleArn"): + raise ValueError("RoleArn is required for ApiGateway target") + + +class AppSyncTargetSender(TargetSender): + def send_event(self, event): + raise NotImplementedError("AppSync target is not yet implemented") + + +class BatchTargetSender(TargetSender): + def send_event(self, event): + raise NotImplementedError("Batch target is not yet implemented") + + def _validate_input(self, target: Target): + if not collections.get_safe(target, "$.BatchParameters.JobDefinition"): + raise ValueError("BatchParameters.JobDefinition is required for Batch target") + if not collections.get_safe(target, "$.BatchParameters.JobName"): + raise ValueError("BatchParameters.JobName is required for Batch target") + + +class ContainerTargetSender(TargetSender): + def send_event(self, event): + raise NotImplementedError("ECS target is not yet implemented") + + def _validate_input(self, target: Target): + super()._validate_input(target) + if not collections.get_safe(target, "$.EcsParameters.TaskDefinitionArn"): + raise ValueError("EcsParameters.TaskDefinitionArn is required for ECS target") + + +class EventsTargetSender(TargetSender): + def send_event(self, event): + eventbus_name = self.target["Arn"].split(":")[-1].split("/")[-1] + source = ( + event.get("source") + if event.get("source") is not None + else self.service + if self.service + else "" + ) + detail_type = event.get("detail-type") if event.get("detail-type") is not None else "" + # TODO add validation and tests for eventbridge to eventbridge requires Detail, DetailType, and Source + # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/events/client/put_events.html + detail = event.get("detail", event) + resources = ( + event.get("resources") + if event.get("resources") is not None + else ([self.rule_arn] if self.rule_arn else []) + ) + + self.client.put_events( + Entries=[ + { + "EventBusName": eventbus_name, + "Source": source, + "DetailType": detail_type, + "Detail": json.dumps(detail), + "Resources": resources, + } + ] + ) + + +class FirehoseTargetSender(TargetSender): + def send_event(self, event): + delivery_stream_name = firehose_name(self.target["Arn"]) + self.client.put_record( + DeliveryStreamName=delivery_stream_name, Record={"Data": to_bytes(json.dumps(event))} + ) + + +class KinesisTargetSender(TargetSender): + def send_event(self, event): + partition_key_path = self.target["KinesisParameters"]["PartitionKeyPath"] + stream_name = self.target["Arn"].split("/")[-1] + partition_key = event.get(partition_key_path, event["id"]) + self.client.put_record( + StreamName=stream_name, + Data=to_bytes(json.dumps(event)), + PartitionKey=partition_key, + ) + + def _validate_input(self, target: Target): + super()._validate_input(target) + if not collections.get_safe(target, "$.RoleArn"): + raise ValueError("RoleArn is required for Kinesis target") + if not collections.get_safe(target, "$.KinesisParameters.PartitionKeyPath"): + raise ValueError("KinesisParameters.PartitionKeyPath is required for Kinesis target") + + +class LambdaTargetSender(TargetSender): + def send_event(self, event): + asynchronous = True # TODO clarify default behavior of AWS + self.client.invoke( + FunctionName=self.target["Arn"], + Payload=to_bytes(json.dumps(event)), + InvocationType="Event" if asynchronous else "RequestResponse", + ) + + +class LogsTargetSender(TargetSender): + def send_event(self, event): + log_group_name = self.target["Arn"].split(":")[6] + log_stream_name = str(uuid.uuid4()) # Unique log stream name + self.client.create_log_stream(logGroupName=log_group_name, logStreamName=log_stream_name) + self.client.put_log_events( + logGroupName=log_group_name, + logStreamName=log_stream_name, + logEvents=[{"timestamp": now_utc(millis=True), "message": json.dumps(event)}], + ) + + +class RedshiftTargetSender(TargetSender): + def send_event(self, event): + raise NotImplementedError("Redshift target is not yet implemented") + + def _validate_input(self, target: Target): + super()._validate_input(target) + if not collections.get_safe(target, "$.RedshiftDataParameters.Database"): + raise ValueError("RedshiftDataParameters.Database is required for Redshift target") + + +class SagemakerTargetSender(TargetSender): + def send_event(self, event): + raise NotImplementedError("Sagemaker target is not yet implemented") + + +class SnsTargetSender(TargetSender): + def send_event(self, event): + self.client.publish(TopicArn=self.target["Arn"], Message=json.dumps(event)) + + +class SqsTargetSender(TargetSender): + def send_event(self, event): + queue_url = sqs_queue_url_for_arn(self.target["Arn"]) + msg_group_id = self.target.get("SqsParameters", {}).get("MessageGroupId", None) + kwargs = {"MessageGroupId": msg_group_id} if msg_group_id else {} + self.client.send_message( + QueueUrl=queue_url, MessageBody=json.dumps(event, separators=(",", ":")), **kwargs + ) + + +class StatesTargetSender(TargetSender): + """Step Functions Target Sender""" + + def send_event(self, event): + self.client.start_execution(stateMachineArn=self.target["Arn"], input=json.dumps(event)) + + def _validate_input(self, target: Target): + super()._validate_input(target) + if not collections.get_safe(target, "$.RoleArn"): + raise ValueError("RoleArn is required for StepFunctions target") + + +class SystemsManagerSender(TargetSender): + """EC2 Run Command Target Sender""" + + def send_event(self, event): + raise NotImplementedError("Systems Manager target is not yet implemented") + + def _validate_input(self, target: Target): + super()._validate_input(target) + if not collections.get_safe(target, "$.RoleArn"): + raise ValueError( + "RoleArn is required for SystemManager target to invoke a EC2 run command" + ) + if not collections.get_safe(target, "$.RunCommandParameters.RunCommandTargets"): + raise ValueError( + "RunCommandParameters.RunCommandTargets is required for Systems Manager target" + ) + + +class TargetSenderFactory: + # supported targets: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-targets.html + target_map = { + "apigateway": ApiGatewayTargetSender, + "appsync": AppSyncTargetSender, + "batch": BatchTargetSender, + "ecs": ContainerTargetSender, + "events": EventsTargetSender, + "firehose": FirehoseTargetSender, + "kinesis": KinesisTargetSender, + "lambda": LambdaTargetSender, + "logs": LogsTargetSender, + "redshift": RedshiftTargetSender, + "sns": SnsTargetSender, + "sqs": SqsTargetSender, + "sagemaker": SagemakerTargetSender, + "ssm": SystemsManagerSender, + # TODO custom endpoints via http target + } + + def __init__(self, target: Target, region: str, account_id: str, rule_arn: Arn): + self.target = target + self.region = region + self.account_id = account_id + self.rule_arn = rule_arn + + def get_target_sender(self) -> TargetSender: + service = extract_service_from_arn(self.target["Arn"]) + if service in self.target_map: + target_sender_class = self.target_map[service] + else: + raise Exception(f"Unsupported target for Service: {service}") + target_sender = target_sender_class( + self.target, self.region, self.account_id, self.rule_arn, service + ) + return target_sender diff --git a/localstack/testing/aws/asf_utils.py b/localstack/testing/aws/asf_utils.py index c1d8c2aad1533..233bc78fdfe28 100644 --- a/localstack/testing/aws/asf_utils.py +++ b/localstack/testing/aws/asf_utils.py @@ -123,9 +123,10 @@ def check_provider_signature(sub_class: type, base_class: type, method_name: str sub_spec = inspect.getfullargspec(sub_function) base_spec = inspect.getfullargspec(base_function) - assert ( - sub_spec == base_spec - ), f"{sub_class.__name__}#{method_name} breaks with {base_class.__name__}#{method_name}" + assert sub_spec == base_spec, ( + f"{sub_class.__name__}#{method_name} breaks with {base_class.__name__}#{method_name}. " + f"This can also be caused by 'from __future__ import annotations' in a provider file!" + ) except AttributeError: # the function is not defined in the superclass pass diff --git a/tests/aws/services/events/conftest.py b/tests/aws/services/events/conftest.py index a2ac7e91327ee..fbb6df5269434 100644 --- a/tests/aws/services/events/conftest.py +++ b/tests/aws/services/events/conftest.py @@ -1,4 +1,5 @@ import json +import logging from typing import Tuple import pytest @@ -7,6 +8,80 @@ from localstack.utils.strings import short_uid from localstack.utils.sync import retry +LOG = logging.getLogger(__name__) + + +@pytest.fixture +def create_event_bus(aws_client): + event_bus_names = [] + + def _create_event_bus(**kwargs): + response = aws_client.events.create_event_bus(**kwargs) + event_bus_names.append(kwargs["Name"]) + return response + + yield _create_event_bus + + for event_bus_name in event_bus_names: + try: + response = aws_client.events.list_rules(EventBusName=event_bus_name) + rules = [rule["Name"] for rule in response["Rules"]] + + # Delete all rules for the current event bus + for rule in rules: + try: + response = aws_client.events.list_targets_by_rule( + Rule=rule, EventBusName=event_bus_name + ) + targets = [target["Id"] for target in response["Targets"]] + + # Remove all targets for the current rule + if targets: + for target in targets: + aws_client.events.remove_targets( + Rule=rule, EventBusName=event_bus_name, Ids=[target] + ) + + aws_client.events.delete_rule(Name=rule, EventBusName=event_bus_name) + except Exception as e: + LOG.warning(f"Failed to delete rule {rule}: {e}") + + aws_client.events.delete_event_bus(Name=event_bus_name) + except Exception as e: + LOG.warning(f"Failed to delete event bus {event_bus_name}: {e}") + + +@pytest.fixture +def put_rule(aws_client): + rules = [] + + def _put_rule(**kwargs): + if "EventBusName" not in kwargs: + kwargs["EventBusName"] = "default" + response = aws_client.events.put_rule(**kwargs) + rules.append((kwargs["Name"], kwargs["EventBusName"])) + return response + + yield _put_rule + + for rule, event_bus_name in rules: + try: + response = aws_client.events.list_targets_by_rule( + Rule=rule, EventBusName=event_bus_name + ) + targets = [target["Id"] for target in response["Targets"]] + + # Remove all targets for the current rule + if targets: + for target in targets: + aws_client.events.remove_targets( + Rule=rule, EventBusName=event_bus_name, Ids=[target] + ) + + aws_client.events.delete_rule(Name=rule, EventBusName=event_bus_name) + except Exception as e: + LOG.warning(f"Failed to delete rule {rule}: {e}") + @pytest.fixture def events_allow_event_rule_to_sqs_queue(aws_client): diff --git a/tests/aws/services/events/test_events.py b/tests/aws/services/events/test_events.py index 6861de3c81e65..0dc10211621a9 100644 --- a/tests/aws/services/events/test_events.py +++ b/tests/aws/services/events/test_events.py @@ -570,7 +570,7 @@ def test_put_target_id_validation( ], ) snapshot.add_transformer(snapshot.transform.regex(target_id, "invalid-target-id")) - snapshot.match("error", e.value.response) + snapshot.match("put-targets-invalid-id-error", e.value.response) target_id = f"{long_uid()}-{long_uid()}-extra" with pytest.raises(ClientError) as e: @@ -581,7 +581,7 @@ def test_put_target_id_validation( ], ) snapshot.add_transformer(snapshot.transform.regex(target_id, "second-invalid-target-id")) - snapshot.match("length_error", e.value.response) + snapshot.match("put-targets-length-error", e.value.response) target_id = f"test-With_valid.Characters-{short_uid()}" aws_client.events.put_targets( @@ -706,18 +706,116 @@ def _get_sqs_messages(): assert message_body["time"] == "2022-01-01T00:00:00Z" -class TestEventsEventBus: +class TestEventBus: @markers.aws.validated - def test_create_custom_event_bus(self, aws_client, cleanups, snapshot): + @pytest.mark.skipif( + not is_v2_provider() and not is_aws_cloud(), + reason="V1 provider does not support this feature", + ) + @pytest.mark.parametrize("regions", [["us-east-1"], ["us-east-1", "us-west-1", "eu-central-1"]]) + def test_create_list_describe_delete_custom_event_buses( + self, aws_client_factory, regions, snapshot + ): + bus_name = f"test-bus-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(bus_name, "")) + + for region in regions: + # overwriting randomized region https://docs.localstack.cloud/contributing/multi-account-region-testing/ + # requires manually adding region replacement for snapshot + snapshot.add_transformer(snapshot.transform.regex(region, "")) + events = aws_client_factory(region_name=region).events + + response = events.create_event_bus(Name=bus_name) + snapshot.match(f"create-custom-event-bus-{region}", response) + + response = events.list_event_buses(NamePrefix=bus_name) + snapshot.match(f"list-event-buses-after-create-{region}", response) + + response = events.describe_event_bus(Name=bus_name) + snapshot.match(f"describe-custom-event-bus-{region}", response) + + # multiple event buses with same name in multiple regions before deleting them + for region in regions: + events = aws_client_factory(region_name=region).events + + response = events.delete_event_bus(Name=bus_name) + snapshot.match(f"delete-custom-event-bus-{region}", response) + + response = events.list_event_buses(NamePrefix=bus_name) + snapshot.match(f"list-event-buses-after-delete-{region}", response) + + @markers.aws.validated + def test_create_multiple_event_buses_same_name(self, create_event_bus, aws_client, snapshot): + bus_name = f"test-bus-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(bus_name, "")) + create_event_bus(Name=bus_name) + + with pytest.raises(aws_client.events.exceptions.ResourceAlreadyExistsException) as e: + create_event_bus(Name=bus_name) + snapshot.match("create-multiple-event-buses-same-name", e) + + @markers.aws.validated + def test_describe_delete_not_existing_event_bus(self, aws_client, snapshot): + bus_name = f"this-bus-does-not-exist-1234567890-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(bus_name, "")) + + with pytest.raises(aws_client.events.exceptions.ResourceNotFoundException) as e: + aws_client.events.describe_event_bus(Name=bus_name) + snapshot.match("describe-not-existing-event-bus-error", e) + + aws_client.events.delete_event_bus(Name=bus_name) + snapshot.match("delete-not-existing-event-bus", e) + + @markers.aws.validated + def test_delete_default_event_bus(self, aws_client, snapshot): + with pytest.raises(aws_client.events.exceptions.ClientError) as e: + aws_client.events.delete_event_bus(Name="default") + snapshot.match("delete-default-event-bus-error", e) + + @markers.aws.validated + def test_list_event_buses_with_prefix(self, create_event_bus, aws_client, snapshot): events = aws_client.events - bus_name = "test-bus" + bus_name = f"unique-prefix-1234567890-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(bus_name, "")) - response = events.create_event_bus(Name=bus_name) - cleanups.append(lambda: events.delete_event_bus(Name=bus_name)) + bus_name_not_match = "no-prefix-match" + snapshot.add_transformer(snapshot.transform.regex(bus_name_not_match, "")) - snapshot.match("create-custom-event-bus", response) + create_event_bus(Name=bus_name) + create_event_bus(Name=bus_name_not_match) + + response = events.list_event_buses(NamePrefix=bus_name) + snapshot.match("list-event-buses-prefix-complete-name", response) + + response = events.list_event_buses(NamePrefix=bus_name.split("-")[0]) + snapshot.match("list-event-buses-prefix", response) + + @markers.aws.validated + @pytest.mark.skipif( + not is_v2_provider() and not is_aws_cloud(), + reason="V1 provider does not support this feature", + ) + def test_list_event_buses_with_limit(self, create_event_bus, aws_client, snapshot): + snapshot.add_transformer(snapshot.transform.jsonpath("$..NextToken", "next_token")) + events = aws_client.events + bus_name_prefix = f"test-bus-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(bus_name_prefix, "")) + count = 6 + + for i in range(count): + bus_name = f"{bus_name_prefix}-{i}" + create_event_bus(Name=bus_name) + + response = events.list_event_buses(Limit=int(count / 2), NamePrefix=bus_name_prefix) + snapshot.match("list-event-buses-limit", response) + + response = events.list_event_buses( + Limit=int(count / 2) + 2, NextToken=response["NextToken"], NamePrefix=bus_name_prefix + ) + snapshot.match("list-event-buses-limit-next-token", response) @markers.aws.unknown + @pytest.mark.skipif(is_aws_cloud(), reason="not validated") @pytest.mark.parametrize("strategy", ["standard", "domain", "path"]) @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_into_event_bus( @@ -788,6 +886,7 @@ def test_put_events_into_event_bus( @markers.aws.validated @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") + # TODO simplify and use sqs as target def test_put_events_to_default_eventbus_for_custom_eventbus( self, events_create_event_bus, @@ -919,7 +1018,7 @@ def test_put_events_to_default_eventbus_for_custom_eventbus( assert_valid_event(received_event) - @markers.aws.validated + @markers.aws.validated # TODO fix condition for this test, only succeeds if run on its own @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_nonexistent_event_bus( self, @@ -940,8 +1039,6 @@ def test_put_events_nonexistent_event_bus( snapshot.transform.regex(nonexistent_event_bus, ""), ] ) - # create SQS queue + add rules & targets so that we can check the default event bus received the message - # even if one entry was wrong queue_url = sqs_create_queue() queue_arn = sqs_get_queue_arn(queue_url) @@ -964,8 +1061,6 @@ def test_put_events_nonexistent_event_bus( Targets=[{"Id": default_bus_target_id, "Arn": queue_arn}], ) - # create two entries, one with no EventBus specified (so it will target the default one) - # and one with a nonexistent EventBusName, which should be ignored entries = [ { "Source": "MySource", @@ -973,7 +1068,7 @@ def test_put_events_nonexistent_event_bus( "Detail": json.dumps({"message": "for the default event bus"}), }, { - "EventBusName": nonexistent_event_bus, + "EventBusName": nonexistent_event_bus, # nonexistent EventBusName, message should be ignored "Source": "MySource", "DetailType": "CustomType", "Detail": json.dumps({"message": "for the custom event bus"}), @@ -982,7 +1077,7 @@ def test_put_events_nonexistent_event_bus( response = aws_client.events.put_events(Entries=entries) snapshot.match("put-events", response) - def _get_sqs_messages(): + def _get_sqs_messages(): # TODO cleanup use exiting fixture resp = aws_client.sqs.receive_message( QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=1 ) @@ -996,7 +1091,323 @@ def _get_sqs_messages(): messages = retry(_get_sqs_messages, retries=10, sleep=0.1) snapshot.match("get-events", messages) - # try to get the custom EventBus we passed the Event to with pytest.raises(ClientError) as e: aws_client.events.describe_event_bus(Name=nonexistent_event_bus) - snapshot.match("non-existent-bus", e.value.response) + snapshot.match("non-existent-bus-error", e.value.response) + + +class TestEventRule: + @markers.aws.validated + @pytest.mark.parametrize("bus_name", ["custom", "default"]) + def test_put_list_with_prefix_describe_delete_rule( + self, bus_name, create_event_bus, put_rule, aws_client, snapshot + ): + if bus_name == "custom": + bus_name = f"bus-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(bus_name, "")) + create_event_bus(Name=bus_name) + + rule_name = f"test-rule-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(rule_name, "")) + response = put_rule( + Name=rule_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + EventBusName=bus_name, + ) + snapshot.match("put-rule", response) + + # NamePrefix required for default bus against AWS + response = aws_client.events.list_rules(NamePrefix=rule_name, EventBusName=bus_name) + snapshot.match("list-rules", response) + + response = aws_client.events.describe_rule(Name=rule_name, EventBusName=bus_name) + snapshot.match("describe-rule", response) + + response = aws_client.events.delete_rule(Name=rule_name, EventBusName=bus_name) + snapshot.match("delete-rule", response) + + response = aws_client.events.list_rules(NamePrefix=rule_name, EventBusName=bus_name) + snapshot.match("list-rules-after-delete", response) + + @markers.aws.validated + def test_put_multiple_rules_with_same_name( + self, create_event_bus, put_rule, aws_client, snapshot + ): + event_bus_name = f"bus-{short_uid()}" + create_event_bus(Name=event_bus_name) + snapshot.add_transformer(snapshot.transform.regex(event_bus_name, "")) + + rule_name = f"test-rule-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(rule_name, "")) + + response = put_rule( + Name=rule_name, + EventBusName=event_bus_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + ) + snapshot.match("put-rule", response) + + # put_rule updates the rule if it already exists + response = put_rule( + Name=rule_name, + EventBusName=event_bus_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + ) + snapshot.match("re-put-rule", response) + + response = aws_client.events.list_rules(EventBusName=event_bus_name) + snapshot.match("list-rules", response) + + @markers.aws.validated + def test_list_rule_with_limit(self, create_event_bus, put_rule, aws_client, snapshot): + snapshot.add_transformer(snapshot.transform.jsonpath("$..NextToken", "next_token")) + + event_bus_name = f"bus-{short_uid()}" + create_event_bus(Name=event_bus_name) + snapshot.add_transformer(snapshot.transform.regex(event_bus_name, "")) + + rule_name_prefix = f"test-rule-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(rule_name_prefix, "")) + count = 6 + + for i in range(count): + rule_name = f"{rule_name_prefix}-{i}" + put_rule( + Name=rule_name, + EventBusName=event_bus_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + ) + + response = aws_client.events.list_rules(Limit=int(count / 2), EventBusName=event_bus_name) + snapshot.match("list-rules-limit", response) + + response = aws_client.events.list_rules( + Limit=int(count / 2) + 2, NextToken=response["NextToken"], EventBusName=event_bus_name + ) + snapshot.match("list-rules-limit-next-token", response) + + @markers.aws.validated + @pytest.mark.skipif( + not is_v2_provider() and not is_aws_cloud(), + reason="V1 provider does not support this feature", + ) + def test_describe_nonexistent_rule(self, aws_client, snapshot): + rule_name = f"this-rule-does-not-exist-1234567890-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(rule_name, "")) + + with pytest.raises(aws_client.events.exceptions.ResourceNotFoundException) as e: + aws_client.events.describe_rule(Name=rule_name) + snapshot.match("describe-not-existing-rule-error", e) + + @markers.aws.validated + @pytest.mark.parametrize("bus_name", ["custom", "default"]) + def test_disable_re_enable_rule( + self, create_event_bus, put_rule, aws_client, snapshot, bus_name + ): + if bus_name == "custom": + bus_name = f"bus-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(bus_name, "")) + create_event_bus(Name=bus_name) + + rule_name = f"test-rule-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(rule_name, "")) + put_rule( + Name=rule_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + EventBusName=bus_name, + ) + + response = aws_client.events.disable_rule(Name=rule_name, EventBusName=bus_name) + snapshot.match("disable-rule", response) + + response = aws_client.events.describe_rule(Name=rule_name, EventBusName=bus_name) + snapshot.match("describe-rule-disabled", response) + + response = aws_client.events.enable_rule(Name=rule_name, EventBusName=bus_name) + snapshot.match("enable-rule", response) + + response = aws_client.events.describe_rule(Name=rule_name, EventBusName=bus_name) + snapshot.match("describe-rule-enabled", response) + + @markers.aws.validated + def test_delete_rule_with_targets( + self, put_rule, sqs_create_queue, sqs_get_queue_arn, aws_client, snapshot + ): + rule_name = f"test-rule-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(rule_name, "")) + put_rule( + Name=rule_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + ) + + target_id = f"test-target-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(target_id, "")) + + queue_url = sqs_create_queue() + queue_arn = sqs_get_queue_arn(queue_url) + snapshot.add_transformer(snapshot.transform.regex(queue_arn, "")) + + aws_client.events.put_targets( + Rule=rule_name, + Targets=[ + { + "Id": target_id, + "Arn": queue_arn, + } + ], + ) + + with pytest.raises(aws_client.events.exceptions.ClientError) as e: + aws_client.events.delete_rule(Name=rule_name) + snapshot.match("delete-rule-with-targets-error", e) + + @markers.aws.validated + def test_update_rule_with_targets( + self, put_rule, sqs_create_queue, sqs_get_queue_arn, aws_client, snapshot + ): + rule_name = f"test-rule-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(rule_name, "")) + put_rule( + Name=rule_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + ) + + target_id = f"test-target-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(target_id, "")) + + queue_url = sqs_create_queue() + queue_arn = sqs_get_queue_arn(queue_url) + snapshot.add_transformer(snapshot.transform.regex(queue_arn, "")) + + aws_client.events.put_targets( + Rule=rule_name, + Targets=[ + { + "Id": target_id, + "Arn": queue_arn, + } + ], + ) + + response = aws_client.events.list_targets_by_rule(Rule=rule_name) + snapshot.match("list-targets", response) + + response = put_rule( + Name=rule_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + ) + snapshot.match("update-rule", response) + + response = aws_client.events.list_targets_by_rule(Rule=rule_name) + snapshot.match("list-targets-after-update", response) + + +class TestEventTarget: + @markers.aws.validated + @pytest.mark.parametrize("bus_name", ["custom", "default"]) + def test_put_list_remove_target( + self, + bus_name, + create_event_bus, + put_rule, + sqs_create_queue, + sqs_get_queue_arn, + aws_client, + snapshot, + ): + kwargs = {} + if bus_name == "custom": + bus_name = f"bus-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(bus_name, "")) + create_event_bus(Name=bus_name) + kwargs["EventBusName"] = bus_name # required for custom event bus, optional for default + + rule_name = f"test-rule-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(rule_name, "")) + put_rule( + Name=rule_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + EventBusName=bus_name, + ) + + queue_url = sqs_create_queue() + queue_arn = sqs_get_queue_arn(queue_url) + snapshot.add_transformer(snapshot.transform.regex(queue_arn, "")) + target_id = f"test-target-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(target_id, "")) + response = aws_client.events.put_targets( + Rule=rule_name, + Targets=[ + { + "Id": target_id, + "Arn": queue_arn, + } + ], + **kwargs, + ) + snapshot.match("put-target", response) + + response = aws_client.events.list_targets_by_rule(Rule=rule_name, **kwargs) + snapshot.match("list-targets", response) + + response = aws_client.events.remove_targets(Rule=rule_name, Ids=[target_id], **kwargs) + snapshot.match("remove-target", response) + + response = aws_client.events.list_targets_by_rule(Rule=rule_name, **kwargs) + snapshot.match("list-targets-after-delete", response) + + @markers.aws.validated + @pytest.mark.skipif( + not is_v2_provider() and not is_aws_cloud(), + reason="V1 provider does not support this feature", + ) + def test_add_exceed_fife_targets_per_rule( + self, put_rule, sqs_create_queue, sqs_get_queue_arn, aws_client, snapshot + ): + rule_name = f"test-rule-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(rule_name, "")) + put_rule( + Name=rule_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + ) + queue_url = sqs_create_queue() + queue_arn = sqs_get_queue_arn(queue_url) + snapshot.add_transformer(snapshot.transform.regex(queue_arn, "")) + + targets = [{"Id": f"test-target-{i}", "Arn": queue_arn} for i in range(7)] + snapshot.add_transformer(snapshot.transform.regex("test-target-", "")) + + with pytest.raises(aws_client.events.exceptions.LimitExceededException) as error: + aws_client.events.put_targets(Rule=rule_name, Targets=targets) + snapshot.match("put-targets-client-error", error) + + @markers.aws.validated + @pytest.mark.skipif( + not is_v2_provider() and not is_aws_cloud(), + reason="V1 provider does not support this feature", + ) + def test_list_target_by_rule_limit( + self, put_rule, sqs_create_queue, sqs_get_queue_arn, aws_client, snapshot + ): + snapshot.add_transformer(snapshot.transform.jsonpath("$..NextToken", "next_token")) + rule_name = f"test-rule-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(rule_name, "")) + put_rule( + Name=rule_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + ) + queue_url = sqs_create_queue() + queue_arn = sqs_get_queue_arn(queue_url) + snapshot.add_transformer(snapshot.transform.regex(queue_arn, "")) + + targets = [{"Id": f"test-target-{i}", "Arn": queue_arn} for i in range(5)] + snapshot.add_transformer(snapshot.transform.regex("test-target-", "")) + aws_client.events.put_targets(Rule=rule_name, Targets=targets) + + response = aws_client.events.list_targets_by_rule(Rule=rule_name, Limit=3) + snapshot.match("list-targets-limit", response) + + response = aws_client.events.list_targets_by_rule( + Rule=rule_name, NextToken=response["NextToken"] + ) + snapshot.match("list-targets-limit-next-token", response) diff --git a/tests/aws/services/events/test_events.snapshot.json b/tests/aws/services/events/test_events.snapshot.json index 20a17f89a230a..8c113ce47c615 100644 --- a/tests/aws/services/events/test_events.snapshot.json +++ b/tests/aws/services/events/test_events.snapshot.json @@ -1,8 +1,8 @@ { "tests/aws/services/events/test_events.py::TestEvents::test_put_target_id_validation": { - "recorded-date": "26-03-2024, 14:07:18", + "recorded-date": "18-04-2024, 15:47:35", "recorded-content": { - "error": { + "put-targets-invalid-id-error": { "Error": { "Code": "ValidationException", "Message": "1 validation error detected: Value '!@#$@!#$' at 'targets.1.member.id' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\.\\-_A-Za-z0-9]+" @@ -12,7 +12,7 @@ "HTTPStatusCode": 400 } }, - "length_error": { + "put-targets-length-error": { "Error": { "Code": "ValidationException", "Message": "1 validation error detected: Value 'second-invalid-target-id' at 'targets.1.member.id' failed to satisfy constraint: Member must have length less than or equal to 64" @@ -80,8 +80,942 @@ ] } }, - "tests/aws/services/events/test_events.py::TestEventsEventBus::test_put_events_to_default_eventbus_for_custom_eventbus": { - "recorded-date": "26-03-2024, 14:07:59", + "tests/aws/services/events/test_events.py::TestEventRule::test_put_rule[custom]": { + "recorded-date": "04-04-2024, 10:47:20", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events::111111111111:rule//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_put_rule[default]": { + "recorded-date": "04-04-2024, 10:47:21", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events::111111111111:rule/", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_put_list_with_prefix_describe_delete_rule[custom]": { + "recorded-date": "22-04-2024, 13:07:35", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events::111111111111:rule//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events::111111111111:rule//", + "EventBusName": "", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "", + "State": "ENABLED" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-rule": { + "Arn": "arn:aws:events::111111111111:rule//", + "CreatedBy": "111111111111", + "EventBusName": "", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "", + "State": "ENABLED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "delete-rule": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules-after-delete": { + "Rules": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_put_list_with_prefix_describe_delete_rule[default]": { + "recorded-date": "22-04-2024, 13:07:36", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events::111111111111:rule/", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events::111111111111:rule/", + "EventBusName": "default", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "", + "State": "ENABLED" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-rule": { + "Arn": "arn:aws:events::111111111111:rule/", + "CreatedBy": "111111111111", + "EventBusName": "default", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "", + "State": "ENABLED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "delete-rule": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules-after-delete": { + "Rules": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_put_multiple_rules_with_same_name": { + "recorded-date": "22-04-2024, 13:07:38", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events::111111111111:rule//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "re-put-rule": { + "RuleArn": "arn:aws:events::111111111111:rule//", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events::111111111111:rule//", + "EventBusName": "", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "", + "State": "ENABLED" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_with_limit": { + "recorded-date": "22-04-2024, 13:07:40", + "recorded-content": { + "list-rules-limit": { + "NextToken": "", + "Rules": [ + { + "Arn": "arn:aws:events::111111111111:rule//-0", + "EventBusName": "", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "-0", + "State": "ENABLED" + }, + { + "Arn": "arn:aws:events::111111111111:rule//-1", + "EventBusName": "", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "-1", + "State": "ENABLED" + }, + { + "Arn": "arn:aws:events::111111111111:rule//-2", + "EventBusName": "", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "-2", + "State": "ENABLED" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules-limit-next-token": { + "Rules": [ + { + "Arn": "arn:aws:events::111111111111:rule//-3", + "EventBusName": "", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "-3", + "State": "ENABLED" + }, + { + "Arn": "arn:aws:events::111111111111:rule//-4", + "EventBusName": "", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "-4", + "State": "ENABLED" + }, + { + "Arn": "arn:aws:events::111111111111:rule//-5", + "EventBusName": "", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "-5", + "State": "ENABLED" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_describe_nonexistent_rule": { + "recorded-date": "22-04-2024, 13:07:42", + "recorded-content": { + "describe-not-existing-rule-error": " does not exist on EventBus default.') tblen=3>" + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_disable_re_enable_rule[custom]": { + "recorded-date": "22-04-2024, 13:07:43", + "recorded-content": { + "disable-rule": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-rule-disabled": { + "Arn": "arn:aws:events::111111111111:rule//", + "CreatedBy": "111111111111", + "EventBusName": "", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "", + "State": "DISABLED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "enable-rule": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-rule-enabled": { + "Arn": "arn:aws:events::111111111111:rule//", + "CreatedBy": "111111111111", + "EventBusName": "", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "", + "State": "ENABLED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_disable_re_enable_rule[default]": { + "recorded-date": "22-04-2024, 13:07:45", + "recorded-content": { + "disable-rule": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-rule-disabled": { + "Arn": "arn:aws:events::111111111111:rule/", + "CreatedBy": "111111111111", + "EventBusName": "default", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "", + "State": "DISABLED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "enable-rule": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-rule-enabled": { + "Arn": "arn:aws:events::111111111111:rule/", + "CreatedBy": "111111111111", + "EventBusName": "default", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "", + "State": "ENABLED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventTarget::test_put_list_remove_target[custom]": { + "recorded-date": "22-04-2024, 13:07:17", + "recorded-content": { + "put-target": { + "FailedEntries": [], + "FailedEntryCount": 0, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-targets": { + "Targets": [ + { + "Arn": "", + "Id": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "remove-target": { + "FailedEntries": [], + "FailedEntryCount": 0, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-targets-after-delete": { + "Targets": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventTarget::test_put_list_remove_target[default]": { + "recorded-date": "22-04-2024, 13:07:18", + "recorded-content": { + "put-target": { + "FailedEntries": [], + "FailedEntryCount": 0, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-targets": { + "Targets": [ + { + "Arn": "", + "Id": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "remove-target": { + "FailedEntries": [], + "FailedEntryCount": 0, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-targets-after-delete": { + "Targets": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventTarget::test_add_exceed_fife_targets_per_rule": { + "recorded-date": "22-04-2024, 13:07:20", + "recorded-content": { + "put-targets-client-error": "" + } + }, + "tests/aws/services/events/test_events.py::TestEventTarget::test_list_target_by_rule_limit": { + "recorded-date": "22-04-2024, 13:07:22", + "recorded-content": { + "list-targets-limit": { + "NextToken": "", + "Targets": [ + { + "Arn": "", + "Id": "0" + }, + { + "Arn": "", + "Id": "1" + }, + { + "Arn": "", + "Id": "2" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-targets-limit-next-token": { + "Targets": [ + { + "Arn": "", + "Id": "3" + }, + { + "Arn": "", + "Id": "4" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_delete_rule_with_targets": { + "recorded-date": "22-04-2024, 13:07:46", + "recorded-content": { + "delete-rule-with-targets-error": "" + } + }, + "tests/aws/services/events/test_events.py::TestEventsEventBus::test_delete_default_event_bus": { + "recorded-date": "16-04-2024, 15:10:07", + "recorded-content": { + "delete-default-event-bus-error": "" + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_update_rule_with_targets": { + "recorded-date": "22-04-2024, 13:07:48", + "recorded-content": { + "list-targets": { + "Targets": [ + { + "Arn": "", + "Id": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "update-rule": { + "RuleArn": "arn:aws:events::111111111111:rule/", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-targets-after-update": { + "Targets": [ + { + "Arn": "", + "Id": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_create_list_describe_delete_custom_event_buses[regions0]": { + "recorded-date": "23-04-2024, 06:11:32", + "recorded-content": { + "create-custom-event-bus-us-east-1": { + "EventBusArn": "arn:aws:events::111111111111:event-bus/", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-event-buses-after-create-us-east-1": { + "EventBuses": [ + { + "Arn": "arn:aws:events::111111111111:event-bus/", + "Name": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-custom-event-bus-us-east-1": { + "Arn": "arn:aws:events::111111111111:event-bus/", + "Name": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "delete-custom-event-bus-us-east-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-event-buses-after-delete-us-east-1": { + "EventBuses": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_create_list_describe_delete_custom_event_buses[regions1]": { + "recorded-date": "23-04-2024, 06:11:34", + "recorded-content": { + "create-custom-event-bus-us-east-1": { + "EventBusArn": "arn:aws:events::111111111111:event-bus/", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-event-buses-after-create-us-east-1": { + "EventBuses": [ + { + "Arn": "arn:aws:events::111111111111:event-bus/", + "Name": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-custom-event-bus-us-east-1": { + "Arn": "arn:aws:events::111111111111:event-bus/", + "Name": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "create-custom-event-bus-us-west-1": { + "EventBusArn": "arn:aws:events::111111111111:event-bus/", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-event-buses-after-create-us-west-1": { + "EventBuses": [ + { + "Arn": "arn:aws:events::111111111111:event-bus/", + "Name": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-custom-event-bus-us-west-1": { + "Arn": "arn:aws:events::111111111111:event-bus/", + "Name": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "create-custom-event-bus-eu-central-1": { + "EventBusArn": "arn:aws:events::111111111111:event-bus/", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-event-buses-after-create-eu-central-1": { + "EventBuses": [ + { + "Arn": "arn:aws:events::111111111111:event-bus/", + "Name": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-custom-event-bus-eu-central-1": { + "Arn": "arn:aws:events::111111111111:event-bus/", + "Name": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "delete-custom-event-bus-us-east-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-event-buses-after-delete-us-east-1": { + "EventBuses": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "delete-custom-event-bus-us-west-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-event-buses-after-delete-us-west-1": { + "EventBuses": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "delete-custom-event-bus-eu-central-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-event-buses-after-delete-eu-central-1": { + "EventBuses": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_create_multiple_event_buses_same_name": { + "recorded-date": "22-04-2024, 13:11:10", + "recorded-content": { + "create-multiple-event-buses-same-name": " already exists.') tblen=4>" + } + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_describe_delete_not_existing_event_bus": { + "recorded-date": "22-04-2024, 13:11:12", + "recorded-content": { + "describe-not-existing-event-bus-error": " does not exist.') tblen=3>", + "delete-not-existing-event-bus": " does not exist.') tblen=3>" + } + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_delete_default_event_bus": { + "recorded-date": "22-04-2024, 13:11:12", + "recorded-content": { + "delete-default-event-bus-error": "" + } + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_list_event_buses_with_prefix": { + "recorded-date": "22-04-2024, 13:19:19", + "recorded-content": { + "list-event-buses-prefix-complete-name": { + "EventBuses": [ + { + "Arn": "arn:aws:events::111111111111:event-bus/", + "Name": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-event-buses-prefix": { + "EventBuses": [ + { + "Arn": "arn:aws:events::111111111111:event-bus/", + "Name": "" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_list_event_buses_with_limit": { + "recorded-date": "22-04-2024, 13:11:15", + "recorded-content": { + "list-event-buses-limit": { + "EventBuses": [ + { + "Arn": "arn:aws:events::111111111111:event-bus/-0", + "Name": "-0" + }, + { + "Arn": "arn:aws:events::111111111111:event-bus/-1", + "Name": "-1" + }, + { + "Arn": "arn:aws:events::111111111111:event-bus/-2", + "Name": "-2" + } + ], + "NextToken": "", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-event-buses-limit-next-token": { + "EventBuses": [ + { + "Arn": "arn:aws:events::111111111111:event-bus/-3", + "Name": "-3" + }, + { + "Arn": "arn:aws:events::111111111111:event-bus/-4", + "Name": "-4" + }, + { + "Arn": "arn:aws:events::111111111111:event-bus/-5", + "Name": "-5" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_put_events_to_default_eventbus_for_custom_eventbus": { + "recorded-date": "22-04-2024, 13:11:43", "recorded-content": { "create-custom-event-bus": { "EventBusArn": "arn:aws:events::111111111111:event-bus/", @@ -159,8 +1093,8 @@ } } }, - "tests/aws/services/events/test_events.py::TestEventsEventBus::test_put_events_nonexistent_event_bus": { - "recorded-date": "26-03-2024, 14:11:46", + "tests/aws/services/events/test_events.py::TestEventBus::test_put_events_nonexistent_event_bus": { + "recorded-date": "22-04-2024, 13:12:23", "recorded-content": { "put-events": { "Entries": [ @@ -197,7 +1131,7 @@ } } ], - "non-existent-bus": { + "non-existent-bus-error": { "Error": { "Code": "ResourceNotFoundException", "Message": "Event bus does not exist." @@ -208,17 +1142,5 @@ } } } - }, - "tests/aws/services/events/test_events.py::TestEventsEventBus::test_create_custom_event_bus": { - "recorded-date": "27-03-2024, 09:15:34", - "recorded-content": { - "create-custom-event-bus": { - "EventBusArn": "arn:aws:events::111111111111:event-bus/test-bus", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } } } diff --git a/tests/aws/services/events/test_events.validation.json b/tests/aws/services/events/test_events.validation.json index 941fb027501ad..9c1bb67593892 100644 --- a/tests/aws/services/events/test_events.validation.json +++ b/tests/aws/services/events/test_events.validation.json @@ -1,4 +1,76 @@ { + "tests/aws/services/events/test_events.py::TestEventBus::test_create_list_describe_delete_custom_event_buses[regions0]": { + "last_validated_date": "2024-04-23T06:11:32+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_create_list_describe_delete_custom_event_buses[regions1]": { + "last_validated_date": "2024-04-23T06:11:34+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_create_multiple_event_buses_same_name": { + "last_validated_date": "2024-04-22T13:11:10+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_delete_default_event_bus": { + "last_validated_date": "2024-04-22T13:11:12+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_describe_delete_not_existing_event_bus": { + "last_validated_date": "2024-04-22T13:11:12+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_list_event_buses_with_limit": { + "last_validated_date": "2024-04-22T13:11:15+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_list_event_buses_with_prefix": { + "last_validated_date": "2024-04-22T13:19:19+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_put_events_nonexistent_event_bus": { + "last_validated_date": "2024-04-22T13:12:23+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_put_events_to_default_eventbus_for_custom_eventbus": { + "last_validated_date": "2024-04-22T13:11:43+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_delete_rule_with_targets": { + "last_validated_date": "2024-04-22T13:07:46+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_describe_nonexistent_rule": { + "last_validated_date": "2024-04-22T13:07:42+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_disable_re_enable_rule[custom]": { + "last_validated_date": "2024-04-22T13:07:43+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_disable_re_enable_rule[default]": { + "last_validated_date": "2024-04-22T13:07:45+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_with_limit": { + "last_validated_date": "2024-04-22T13:07:40+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_put_list_with_prefix_describe_delete_rule[custom]": { + "last_validated_date": "2024-04-22T13:07:35+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_put_list_with_prefix_describe_delete_rule[default]": { + "last_validated_date": "2024-04-22T13:07:36+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_put_multiple_rules_with_same_name": { + "last_validated_date": "2024-04-22T13:07:38+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_put_rule[custom]": { + "last_validated_date": "2024-04-04T10:47:20+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_put_rule[default]": { + "last_validated_date": "2024-04-04T10:47:21+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_update_rule_with_targets": { + "last_validated_date": "2024-04-22T13:07:48+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventTarget::test_add_exceed_fife_targets_per_rule": { + "last_validated_date": "2024-04-22T13:07:20+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventTarget::test_list_target_by_rule_limit": { + "last_validated_date": "2024-04-22T13:07:22+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventTarget::test_put_list_remove_target[custom]": { + "last_validated_date": "2024-04-22T13:07:17+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventTarget::test_put_list_remove_target[default]": { + "last_validated_date": "2024-04-22T13:07:18+00:00" + }, "tests/aws/services/events/test_events.py::TestEvents::test_create_connection_validations": { "last_validated_date": "2024-03-26T14:07:16+00:00" }, @@ -24,13 +96,31 @@ "last_validated_date": "2024-03-26T14:06:58+00:00" }, "tests/aws/services/events/test_events.py::TestEvents::test_put_target_id_validation": { - "last_validated_date": "2024-03-26T14:07:18+00:00" + "last_validated_date": "2024-04-18T15:47:35+00:00" }, "tests/aws/services/events/test_events.py::TestEventsEventBus::test_create_custom_event_bus": { "last_validated_date": "2024-03-27T09:15:34+00:00" }, + "tests/aws/services/events/test_events.py::TestEventsEventBus::test_create_list_describe_delete_custom_event_buses[regions0]": { + "last_validated_date": "2024-04-03T13:49:07+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventsEventBus::test_create_list_describe_delete_custom_event_buses[regions1]": { + "last_validated_date": "2024-04-03T13:49:10+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventsEventBus::test_create_multiple_event_buses_same_name": { + "last_validated_date": "2024-04-16T15:09:51+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventsEventBus::test_delete_default_event_bus": { + "last_validated_date": "2024-04-16T15:10:07+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventsEventBus::test_describe_delete_not_existing_event_bus": { + "last_validated_date": "2024-04-17T07:32:19+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventsEventBus::test_list_event_buses_with_limit": { + "last_validated_date": "2024-04-03T14:53:31+00:00" + }, "tests/aws/services/events/test_events.py::TestEventsEventBus::test_put_events_nonexistent_event_bus": { - "last_validated_date": "2024-03-26T14:11:46+00:00" + "last_validated_date": "2024-04-16T15:10:30+00:00" }, "tests/aws/services/events/test_events.py::TestEventsEventBus::test_put_events_to_default_eventbus_for_custom_eventbus": { "last_validated_date": "2024-03-26T14:07:59+00:00" diff --git a/tests/aws/services/events/test_events_integrations.py b/tests/aws/services/events/test_events_integrations.py index 9cdf39bd3bb86..2124fdca6204c 100644 --- a/tests/aws/services/events/test_events_integrations.py +++ b/tests/aws/services/events/test_events_integrations.py @@ -20,7 +20,6 @@ @markers.aws.validated -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_target_sqs(put_events_with_filter_to_sqs): entries = [ { From bcee501eef8a37231825a78f2e4861f1a9620817 Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:23:40 +0100 Subject: [PATCH 090/169] StepFunctions: Fix TaskToken Creation Logic (#10707) --- .../payload_binding_path_context_obj.py | 11 +- .../service/state_task_service_callback.py | 13 + .../state_execution/state_task/state_task.py | 4 - .../asl/eval/contextobject/contex_object.py | 5 +- .../stepfunctions/asl/eval/environment.py | 7 + .../query_context_object_values.json5 | 1 - .../templates/callbacks/callback_templates.py | 6 + .../sqs_wait_for_task_token_call_chain.json5 | 32 ++ ...it_for_task_token_no_token_parameter.json5 | 18 + .../v2/callback/test_callback.py | 76 ++++ .../v2/callback/test_callback.snapshot.json | 426 ++++++++++++++++++ .../v2/callback/test_callback.validation.json | 6 + 12 files changed, 590 insertions(+), 15 deletions(-) create mode 100644 tests/aws/services/stepfunctions/templates/callbacks/statemachines/sqs_wait_for_task_token_call_chain.json5 create mode 100644 tests/aws/services/stepfunctions/templates/callbacks/statemachines/sqs_wait_for_task_token_no_token_parameter.json5 diff --git a/localstack/services/stepfunctions/asl/component/common/payload/payloadvalue/payloadbinding/payload_binding_path_context_obj.py b/localstack/services/stepfunctions/asl/component/common/payload/payloadvalue/payloadbinding/payload_binding_path_context_obj.py index 6ebea026aeef8..8d1ce12ecdc22 100644 --- a/localstack/services/stepfunctions/asl/component/common/payload/payloadvalue/payloadbinding/payload_binding_path_context_obj.py +++ b/localstack/services/stepfunctions/asl/component/common/payload/payloadvalue/payloadbinding/payload_binding_path_context_obj.py @@ -19,12 +19,7 @@ def from_raw(cls, string_dollar: str, string_path_context_obj: str): return cls(field=field, path_context_obj=path_context_obj) def _eval_val(self, env: Environment) -> Any: - if self.path_context_obj.endswith("Task.Token"): - task_token = env.context_object_manager.update_task_token() - env.callback_pool_manager.add(task_token) - value = task_token - else: - value = JSONPathUtils.extract_json( - self.path_context_obj, env.context_object_manager.context_object - ) + value = JSONPathUtils.extract_json( + self.path_context_obj, env.context_object_manager.context_object + ) return value diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py index 9ffbad44184f4..98ca010f86880 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py @@ -140,6 +140,19 @@ def _from_error(self, env: Environment, ex: Exception) -> FailureEvent: return self._get_callback_outcome_failure_event(env=env, ex=ex) return super()._from_error(env=env, ex=ex) + def _eval_body(self, env: Environment) -> None: + # Generate a TaskToken uuid within the context object, if this task resources has a waitForTaskToken condition. + # This logic provisions a TaskToken callback uuid to support waitForTaskToken workflows as described in : + # https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token + if self._is_condition() and self.resource.condition == ResourceCondition.WaitForTaskToken: + task_token = env.context_object_manager.update_task_token() + env.callback_pool_manager.add(task_token) + + super()._eval_body(env=env) + + # Ensure the TaskToken field is reset, as this is only available during waitForTaskToken task evaluations. + env.context_object_manager.context_object.pop("Task", None) + def _after_eval_execution( self, env: Environment, diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py index f566144e06ded..97ad17eadf8d4 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py @@ -83,7 +83,3 @@ def _from_error(self, env: Environment, ex: Exception) -> FailureEvent: if isinstance(ex, TimeoutError): return self._get_timed_out_failure_event(env) return super()._from_error(env=env, ex=ex) - - def _eval_body(self, env: Environment) -> None: - super(StateTask, self)._eval_body(env=env) - env.context_object_manager.context_object["Task"] = None diff --git a/localstack/services/stepfunctions/asl/eval/contextobject/contex_object.py b/localstack/services/stepfunctions/asl/eval/contextobject/contex_object.py index 7e9e6029f2300..7dbb1acd99a09 100644 --- a/localstack/services/stepfunctions/asl/eval/contextobject/contex_object.py +++ b/localstack/services/stepfunctions/asl/eval/contextobject/contex_object.py @@ -1,4 +1,4 @@ -from typing import Any, Final, Optional, TypedDict +from typing import Any, Final, NotRequired, Optional, TypedDict from localstack.utils.strings import long_uid @@ -41,7 +41,7 @@ class ContextObject(TypedDict): Execution: Execution State: Optional[State] StateMachine: StateMachine - Task: Optional[Task] # Null if the Parameters field is outside a task state. + Task: NotRequired[Task] # Null if the Parameters field is outside a task state. Map: Optional[Map] # Only available when processing a Map state. @@ -60,3 +60,4 @@ def update_task_token(self) -> str: class ContextObjectInitData(TypedDict): Execution: Execution StateMachine: StateMachine + Task: Optional[Task] diff --git a/localstack/services/stepfunctions/asl/eval/environment.py b/localstack/services/stepfunctions/asl/eval/environment.py index afea27dc6c00b..8b205e53486db 100644 --- a/localstack/services/stepfunctions/asl/eval/environment.py +++ b/localstack/services/stepfunctions/asl/eval/environment.py @@ -15,6 +15,7 @@ ContextObject, ContextObjectInitData, ContextObjectManager, + Task, ) from localstack.services.stepfunctions.asl.eval.event.event_history import ( EventHistory, @@ -70,12 +71,17 @@ def __init__( self.aws_execution_details = aws_execution_details self.callback_pool_manager = CallbackPoolManager(activity_store=activity_store) self.map_run_record_pool_manager = MapRunRecordPoolManager() + self.context_object_manager = ContextObjectManager( context_object=ContextObject( Execution=context_object_init["Execution"], StateMachine=context_object_init["StateMachine"], ) ) + task: Optional[Task] = context_object_init.get("Task") + if task is not None: + self.context_object_manager.context_object["Task"] = task + self.activity_store = activity_store self._frames = list() @@ -90,6 +96,7 @@ def as_frame_of(cls, env: Environment, event_history_frame_cache: EventHistoryCo context_object_init = ContextObjectInitData( Execution=env.context_object_manager.context_object["Execution"], StateMachine=env.context_object_manager.context_object["StateMachine"], + Task=env.context_object_manager.context_object.get("Task"), ) frame = cls( aws_execution_details=env.aws_execution_details, diff --git a/tests/aws/services/stepfunctions/templates/base/statemachines/query_context_object_values.json5 b/tests/aws/services/stepfunctions/templates/base/statemachines/query_context_object_values.json5 index 67e45a4d6d2ef..fafc30bc4db7d 100644 --- a/tests/aws/services/stepfunctions/templates/base/statemachines/query_context_object_values.json5 +++ b/tests/aws/services/stepfunctions/templates/base/statemachines/query_context_object_values.json5 @@ -33,6 +33,5 @@ "context_object.$": "$$", } } - } } diff --git a/tests/aws/services/stepfunctions/templates/callbacks/callback_templates.py b/tests/aws/services/stepfunctions/templates/callbacks/callback_templates.py index 6729ee7ef87bc..7558c61be3393 100644 --- a/tests/aws/services/stepfunctions/templates/callbacks/callback_templates.py +++ b/tests/aws/services/stepfunctions/templates/callbacks/callback_templates.py @@ -25,9 +25,15 @@ class CallbackTemplates(TemplateLoader): SQS_WAIT_FOR_TASK_TOKEN: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/sqs_wait_for_task_token.json5" ) + SQS_WAIT_FOR_TASK_TOKEN_CALL_CHAIN: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/sqs_wait_for_task_token_call_chain.json5" + ) SQS_WAIT_FOR_TASK_TOKEN_WITH_TIMEOUT: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/sqs_wait_for_task_token_with_timeout.json5" ) + SQS_WAIT_FOR_TASK_TOKEN_NO_TOKEN_PARAMETER: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/sqs_wait_for_task_token_no_token_parameter.json5" + ) SQS_HEARTBEAT_SUCCESS_ON_TASK_TOKEN: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/sqs_hearbeat_success_on_task_token.json5" ) diff --git a/tests/aws/services/stepfunctions/templates/callbacks/statemachines/sqs_wait_for_task_token_call_chain.json5 b/tests/aws/services/stepfunctions/templates/callbacks/statemachines/sqs_wait_for_task_token_call_chain.json5 new file mode 100644 index 0000000000000..f77265a343514 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/callbacks/statemachines/sqs_wait_for_task_token_call_chain.json5 @@ -0,0 +1,32 @@ +{ + "Comment": "SQS_WAIT_FOR_TASK_TOKEN_CALL_CHAIN", + "StartAt": "State1", + "States": { + "State1": { + "Type": "Task", + "Resource": "arn:aws:states:::sqs:sendMessage.waitForTaskToken", + "Parameters": { + "QueueUrl.$": "$.QueueUrl", + "MessageBody": { + "Message.$": "$.Message", + "TaskToken.$": "$$.Task.Token" + } + }, + ResultPath: "$.State1Output", + "Next": "State2" + }, + "State2": { + "Type": "Task", + "Resource": "arn:aws:states:::sqs:sendMessage.waitForTaskToken", + "Parameters": { + "QueueUrl.$": "$.QueueUrl", + "MessageBody": { + "Message.$": "$.Message", + "TaskToken.$": "$$.Task.Token" + } + }, + ResultPath: "$.State2Output", + "End": true + } + } +} diff --git a/tests/aws/services/stepfunctions/templates/callbacks/statemachines/sqs_wait_for_task_token_no_token_parameter.json5 b/tests/aws/services/stepfunctions/templates/callbacks/statemachines/sqs_wait_for_task_token_no_token_parameter.json5 new file mode 100644 index 0000000000000..463ab9d6b6930 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/callbacks/statemachines/sqs_wait_for_task_token_no_token_parameter.json5 @@ -0,0 +1,18 @@ +{ + "Comment": "SQS_WAIT_FOR_TASK_TOKEN_NO_TOKEN_PARAMETER", + "StartAt": "State1", + "States": { + "State1": { + "Type": "Task", + "Resource": "arn:aws:states:::sqs:sendMessage.waitForTaskToken", + "TimeoutSeconds": 5, + "Parameters": { + "QueueUrl.$": "$.QueueUrl", + "MessageBody": { + "Context.$": "$", + } + }, + "End": true + }, + } +} diff --git a/tests/aws/services/stepfunctions/v2/callback/test_callback.py b/tests/aws/services/stepfunctions/v2/callback/test_callback.py index dd8807318df67..d1f57b8ce7b36 100644 --- a/tests/aws/services/stepfunctions/v2/callback/test_callback.py +++ b/tests/aws/services/stepfunctions/v2/callback/test_callback.py @@ -621,3 +621,79 @@ def _sqs_task_token_handler(): executionArn=execution_arn ) sfn_snapshot.match(f"execution_history_{i}", execution_history) + + @markers.aws.validated + def test_sqs_wait_for_task_token_call_chain( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sqs_create_queue, + sqs_send_task_success_state_machine, + sfn_snapshot, + ): + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..MessageId", + replacement="message_id", + replace_reference=True, + ) + ) + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..TaskToken", + replacement="task_token", + replace_reference=True, + ) + ) + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) + + queue_name = f"queue-{short_uid()}" + queue_url = sqs_create_queue(QueueName=queue_name) + sfn_snapshot.add_transformer(RegexTransformer(queue_url, "sqs_queue_url")) + sfn_snapshot.add_transformer(RegexTransformer(queue_name, "sqs_queue_name")) + + sqs_send_task_success_state_machine(queue_url) + + template = CT.load_sfn_template(CT.SQS_WAIT_FOR_TASK_TOKEN_CALL_CHAIN) + definition = json.dumps(template) + + exec_input = json.dumps({"QueueUrl": queue_url, "Message": "HelloWorld"}) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + + @markers.aws.validated + def test_sqs_wait_for_task_token_no_token_parameter( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sqs_create_queue, + sqs_send_task_success_state_machine, + sfn_snapshot, + ): + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) + + queue_name = f"queue-{short_uid()}" + queue_url = sqs_create_queue(QueueName=queue_name) + sfn_snapshot.add_transformer(RegexTransformer(queue_url, "sqs_queue_url")) + sfn_snapshot.add_transformer(RegexTransformer(queue_name, "sqs_queue_name")) + + template = CT.load_sfn_template(CT.SQS_WAIT_FOR_TASK_TOKEN_NO_TOKEN_PARAMETER) + definition = json.dumps(template) + + exec_input = json.dumps({"QueueUrl": queue_url}) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) diff --git a/tests/aws/services/stepfunctions/v2/callback/test_callback.snapshot.json b/tests/aws/services/stepfunctions/v2/callback/test_callback.snapshot.json index 8a8b4d85a24d6..ed76a744b6e04 100644 --- a/tests/aws/services/stepfunctions/v2/callback/test_callback.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/callback/test_callback.snapshot.json @@ -2684,5 +2684,431 @@ } } } + }, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_token_call_chain": { + "recorded-date": "22-04-2024, 14:04:58", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "HelloWorld" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "arn:aws:iam::111111111111:role/" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "HelloWorld" + }, + "inputDetails": { + "truncated": false + }, + "name": "State1" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "MessageBody": { + "Message": "HelloWorld", + "TaskToken": "" + }, + "QueueUrl": "sqs_queue_url" + }, + "region": "", + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "MD5OfMessageBody": "", + "MessageId": "", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskSucceededEventDetails": { + "output": "\"HelloWorld\"", + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 7, + "previousEventId": 6, + "stateExitedEventDetails": { + "name": "State1", + "output": { + "QueueUrl": "sqs_queue_url", + "Message": "HelloWorld", + "State1Output": "HelloWorld" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "id": 8, + "previousEventId": 7, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url", + "Message": "HelloWorld", + "State1Output": "HelloWorld" + }, + "inputDetails": { + "truncated": false + }, + "name": "State2" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 9, + "previousEventId": 8, + "taskScheduledEventDetails": { + "parameters": { + "MessageBody": { + "Message": "HelloWorld", + "TaskToken": "" + }, + "QueueUrl": "sqs_queue_url" + }, + "region": "", + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 10, + "previousEventId": 9, + "taskStartedEventDetails": { + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 11, + "previousEventId": 10, + "taskSubmittedEventDetails": { + "output": { + "MD5OfMessageBody": "", + "MessageId": "", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 12, + "previousEventId": 11, + "taskSucceededEventDetails": { + "output": "\"HelloWorld\"", + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSucceeded" + }, + { + "id": 13, + "previousEventId": 12, + "stateExitedEventDetails": { + "name": "State2", + "output": { + "QueueUrl": "sqs_queue_url", + "Message": "HelloWorld", + "State1Output": "HelloWorld", + "State2Output": "HelloWorld" + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "QueueUrl": "sqs_queue_url", + "Message": "HelloWorld", + "State1Output": "HelloWorld", + "State2Output": "HelloWorld" + }, + "outputDetails": { + "truncated": false + } + }, + "id": 14, + "previousEventId": 13, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_token_no_token_parameter": { + "recorded-date": "22-04-2024, 17:15:14", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "arn:aws:iam::111111111111:role/" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "sqs_queue_url" + }, + "inputDetails": { + "truncated": false + }, + "name": "State1" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "MessageBody": { + "Context": { + "QueueUrl": "sqs_queue_url" + } + }, + "QueueUrl": "sqs_queue_url" + }, + "region": "", + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs", + "timeoutInSeconds": 5 + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "MD5OfMessageBody": "", + "MessageId": "", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskTimedOutEventDetails": { + "error": "States.Timeout", + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskTimedOut" + }, + { + "executionFailedEventDetails": { + "error": "States.Timeout" + }, + "id": 7, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "ExecutionFailed" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/stepfunctions/v2/callback/test_callback.validation.json b/tests/aws/services/stepfunctions/v2/callback/test_callback.validation.json index 2aaa9c1e7e0b7..e14c5e7d21caa 100644 --- a/tests/aws/services/stepfunctions/v2/callback/test_callback.validation.json +++ b/tests/aws/services/stepfunctions/v2/callback/test_callback.validation.json @@ -17,6 +17,12 @@ "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_token": { "last_validated_date": "2024-04-18T06:19:31+00:00" }, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_token_call_chain": { + "last_validated_date": "2024-04-22T14:04:58+00:00" + }, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_token_no_token_parameter": { + "last_validated_date": "2024-04-22T17:15:14+00:00" + }, "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_token_timeout": { "last_validated_date": "2024-04-18T06:20:28+00:00" }, From cf5d6a4472603283e634be797a9abadedd225479 Mon Sep 17 00:00:00 2001 From: Giovanni Grano Date: Tue, 23 Apr 2024 09:56:14 -0400 Subject: [PATCH 091/169] Fix TTL for tables with RANGE keys (#10699) --- localstack/services/dynamodb/provider.py | 18 +++++-- tests/aws/services/dynamodb/test_dynamodb.py | 53 +++++++++++++++++++- 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/localstack/services/dynamodb/provider.py b/localstack/services/dynamodb/provider.py index 6e2ed2480a620..345d016c268ae 100644 --- a/localstack/services/dynamodb/provider.py +++ b/localstack/services/dynamodb/provider.py @@ -433,9 +433,15 @@ def delete_expired_items() -> int: items_to_delete = result.get("Items", []) no_expired_items += len(items_to_delete) table_description = client.describe_table(TableName=table_name) - partition_key = _get_partition_key(table_description) + partition_key, range_key = _get_hash_and_range_key(table_description) keys_to_delete = [ - {partition_key: item.get(partition_key)} for item in items_to_delete + {partition_key: item.get(partition_key)} + if range_key is None + else { + partition_key: item.get(partition_key), + range_key: item.get(range_key), + } + for item in items_to_delete ] delete_requests = [{"DeleteRequest": {"Key": key}} for key in keys_to_delete] for i in range(0, len(delete_requests), 25): @@ -450,11 +456,15 @@ def delete_expired_items() -> int: return no_expired_items -def _get_partition_key(table_description: DescribeTableOutput) -> str: +def _get_hash_and_range_key(table_description: DescribeTableOutput) -> [str, str | None]: key_schema = table_description.get("Table", {}).get("KeySchema", []) + hash_key, range_key = None, None for key in key_schema: if key["KeyType"] == "HASH": - return key["AttributeName"] + hash_key = key["AttributeName"] + if key["KeyType"] == "RANGE": + range_key = key["AttributeName"] + return hash_key, range_key class ExpiredItemsWorker: diff --git a/tests/aws/services/dynamodb/test_dynamodb.py b/tests/aws/services/dynamodb/test_dynamodb.py index 9293d84313477..4d748cb9d5abf 100644 --- a/tests/aws/services/dynamodb/test_dynamodb.py +++ b/tests/aws/services/dynamodb/test_dynamodb.py @@ -106,7 +106,7 @@ def test_large_data_download(self, aws_client, ddb_test_table): assert len(result["Items"]) == num_items @markers.aws.only_localstack - def test_time_to_live_deletion(self, aws_client, ddb_test_table): + def test_time_to_live_deletion(self, aws_client, ddb_test_table, cleanups): table_name = ddb_test_table aws_client.dynamodb.update_time_to_live( TableName=table_name, @@ -136,6 +136,57 @@ def test_time_to_live_deletion(self, aws_client, ddb_test_table): ) assert not result.get("Item") + # create a table with a range key + table_with_range_key = f"test-table-{short_uid()}" + aws_client.dynamodb.create_table( + TableName=table_with_range_key, + KeySchema=[ + {"AttributeName": "id", "KeyType": "HASH"}, + {"AttributeName": "range", "KeyType": "RANGE"}, + ], + AttributeDefinitions=[ + {"AttributeName": "id", "AttributeType": "S"}, + {"AttributeName": "range", "AttributeType": "S"}, + ], + ProvisionedThroughput={"ReadCapacityUnits": 5, "WriteCapacityUnits": 5}, + ) + cleanups.append(lambda: aws_client.dynamodb.delete_table(TableName=table_with_range_key)) + aws_client.dynamodb.update_time_to_live( + TableName=table_with_range_key, + TimeToLiveSpecification={"Enabled": True, "AttributeName": "expiringAt"}, + ) + exp = int(time.time()) - 10 # expired + items = [ + { + PARTITION_KEY: {"S": "expired"}, + "range": {"S": "range_one"}, + "expiringAt": {"N": str(exp)}, + }, + { + PARTITION_KEY: {"S": "not-expired"}, + "range": {"S": "range_two"}, + "expiringAt": {"N": str(exp + 120)}, + }, + ] + for item in items: + aws_client.dynamodb.put_item(TableName=table_with_range_key, Item=item) + + url = f"{config.internal_service_url()}/_aws/dynamodb/expired" + response = requests.delete(url) + assert response.status_code == 200 + assert response.json() == {"ExpiredItems": 1} + + result = aws_client.dynamodb.get_item( + TableName=table_with_range_key, + Key={PARTITION_KEY: {"S": "not-expired"}, "range": {"S": "range_two"}}, + ) + assert result.get("Item") + result = aws_client.dynamodb.get_item( + TableName=table_with_range_key, + Key={PARTITION_KEY: {"S": "expired"}, "range": {"S": "range_one"}}, + ) + assert not result.get("Item") + @markers.aws.only_localstack def test_time_to_live(self, aws_client, ddb_test_table): # check response for nonexistent table From 457f04adcc470a36917af6d1dd047572f9465ee0 Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Tue, 23 Apr 2024 15:55:02 +0100 Subject: [PATCH 092/169] Capture example lambda triggered by SQS event (#10700) --- .../cloudformation/resources/test_sam.py | 27 +++++++++ .../resources/test_sam.validation.json | 3 + tests/aws/templates/sam_sqs_template.yml | 60 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 tests/aws/templates/sam_sqs_template.yml diff --git a/tests/aws/services/cloudformation/resources/test_sam.py b/tests/aws/services/cloudformation/resources/test_sam.py index 36d6aef50db1d..7520d50203329 100644 --- a/tests/aws/services/cloudformation/resources/test_sam.py +++ b/tests/aws/services/cloudformation/resources/test_sam.py @@ -4,6 +4,7 @@ from localstack.testing.pytest import markers from localstack.utils.strings import short_uid +from localstack.utils.sync import retry @markers.aws.validated @@ -35,3 +36,29 @@ def test_sam_template(deploy_cfn_template, aws_client): result = aws_client.lambda_.invoke(FunctionName=func_name) result = json.load(result["Payload"]) assert result == {"hello": "world"} + + +@markers.aws.validated +def test_sam_sqs_event(deploy_cfn_template, aws_client): + result_key = f"event-{short_uid()}" + stack = deploy_cfn_template( + template_path=os.path.join( + os.path.dirname(__file__), "../../../templates/sam_sqs_template.yml" + ), + parameters={"ResultKey": result_key}, + ) + + queue_url = stack.outputs["QueueUrl"] + bucket_name = stack.outputs["BucketName"] + + message_body = "test" + aws_client.sqs.send_message(QueueUrl=queue_url, MessageBody=message_body) + + def get_object(): + return json.loads( + aws_client.s3.get_object(Bucket=bucket_name, Key=result_key)["Body"].read().decode() + )["Records"][0]["body"] + + body = retry(get_object, retries=10, sleep=5.0) + + assert body == message_body diff --git a/tests/aws/services/cloudformation/resources/test_sam.validation.json b/tests/aws/services/cloudformation/resources/test_sam.validation.json index 5b77534c1dcc3..a1d04e8974f94 100644 --- a/tests/aws/services/cloudformation/resources/test_sam.validation.json +++ b/tests/aws/services/cloudformation/resources/test_sam.validation.json @@ -1,5 +1,8 @@ { "tests/aws/services/cloudformation/resources/test_sam.py::test_sam_policies": { "last_validated_date": "2023-07-11T16:08:53+00:00" + }, + "tests/aws/services/cloudformation/resources/test_sam.py::test_sam_sqs_event": { + "last_validated_date": "2024-04-19T19:45:49+00:00" } } diff --git a/tests/aws/templates/sam_sqs_template.yml b/tests/aws/templates/sam_sqs_template.yml new file mode 100644 index 0000000000000..2c9985c9c5a03 --- /dev/null +++ b/tests/aws/templates/sam_sqs_template.yml @@ -0,0 +1,60 @@ +AWSTemplateFormatVersion: 2010-09-09 +Transform: AWS::Serverless-2016-10-31 + +Description: | + Example of SAM EventSourceMapping failure + +Globals: + Function: + Timeout: 10 + +Parameters: + ResultKey: + Type: String + +Resources: + Bucket: + Type: AWS::S3::Bucket + + Lambda: + Type: AWS::Serverless::Function + Properties: + AutoPublishAlias: live + InlineCode: | + import boto3 + import json + import os + def handler(event, context): + client = boto3.client("s3") + body = json.dumps(event) + client.put_object(Bucket=os.environ["BUCKET_NAME"], Key=os.environ["RESULT_KEY"], Body=body) + return {"event": event} + Handler: index.handler + Policies: + - S3CrudPolicy: + BucketName: !Ref Bucket + Environment: + Variables: + BUCKET_NAME: !Ref Bucket + RESULT_KEY: !Ref ResultKey + Runtime: python3.11 + Events: + SourceSQSEvent: + Type: SQS + Properties: + Queue: !GetAtt SQSQueue.Arn + BatchSize: 10 + MaximumBatchingWindowInSeconds: 10 + + SQSQueue: + Type: AWS::SQS::Queue + Properties: + MessageRetentionPeriod: 3600 + VisibilityTimeout: 60 + + +Outputs: + QueueUrl: + Value: !GetAtt SQSQueue.QueueUrl + BucketName: + Value: !Ref Bucket From c106af7447423384532741134669927daa92f049 Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Wed, 24 Apr 2024 15:14:45 +0200 Subject: [PATCH 093/169] avoid failing host_path_for_path_in_docker if no docker daemon is available (#10690) --- localstack/utils/container_utils/container_client.py | 6 ++++++ localstack/utils/container_utils/docker_sdk_client.py | 2 ++ localstack/utils/docker_utils.py | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/localstack/utils/container_utils/container_client.py b/localstack/utils/container_utils/container_client.py index d5dbbd20825fb..9f799f4a49cd5 100644 --- a/localstack/utils/container_utils/container_client.py +++ b/localstack/utils/container_utils/container_client.py @@ -464,6 +464,7 @@ class ContainerConfiguration: platform: Optional[str] = None ulimits: Optional[List[Ulimit]] = None labels: Optional[Dict[str, str]] = None + init: Optional[bool] = None class ContainerConfigurator(Protocol): @@ -840,6 +841,8 @@ def create_container_from_config(self, container_config: ContainerConfiguration) privileged=container_config.privileged, platform=container_config.platform, labels=container_config.labels, + ulimits=container_config.ulimits, + init=container_config.init, ) @abstractmethod @@ -869,6 +872,8 @@ def create_container( privileged: Optional[bool] = None, labels: Optional[Dict[str, str]] = None, platform: Optional[DockerPlatform] = None, + ulimits: Optional[List[Ulimit]] = None, + init: Optional[bool] = None, ) -> str: """Creates a container with the given image @@ -941,6 +946,7 @@ def run_container_from_config( platform=container_config.platform, privileged=container_config.privileged, ulimits=container_config.ulimits, + init=container_config.init, ) @abstractmethod diff --git a/localstack/utils/container_utils/docker_sdk_client.py b/localstack/utils/container_utils/docker_sdk_client.py index 572c0cc6dbca0..6b61a59c0f4f8 100644 --- a/localstack/utils/container_utils/docker_sdk_client.py +++ b/localstack/utils/container_utils/docker_sdk_client.py @@ -6,6 +6,7 @@ import re import socket import threading +from functools import lru_cache from time import sleep from typing import Dict, List, Optional, Tuple, Union, cast from urllib.parse import quote @@ -468,6 +469,7 @@ def get_container_ip(self, container_name_or_id: str) -> str: LOG.info("Container has more than one assigned network. Picking the first one...") return networks[network_names[0]]["IPAddress"] + @lru_cache(maxsize=None) def has_docker(self) -> bool: try: if not self.docker_client: diff --git a/localstack/utils/docker_utils.py b/localstack/utils/docker_utils.py index ad5548659c88c..bab738135f053 100644 --- a/localstack/utils/docker_utils.py +++ b/localstack/utils/docker_utils.py @@ -102,7 +102,7 @@ def get_host_path_for_path_in_docker(path): :param path: Path to be replaced (subpath of DEFAULT_VOLUME_DIR) :return: Path on the host """ - if config.is_in_docker: + if config.is_in_docker and DOCKER_CLIENT.has_docker(): volume = get_default_volume_dir_mount() if volume: From 3c2fd07d9610de001288cd3931ba4303c3f6607f Mon Sep 17 00:00:00 2001 From: Joel Scheuner Date: Wed, 24 Apr 2024 16:34:16 +0200 Subject: [PATCH 094/169] Add custom Lambda runtimes endpoint (#10710) --- .../services/lambda_/custom_endpoints.py | 35 +++++++++++++++++++ localstack/services/lambda_/plugins.py | 17 +++++++++ 2 files changed, 52 insertions(+) create mode 100644 localstack/services/lambda_/custom_endpoints.py diff --git a/localstack/services/lambda_/custom_endpoints.py b/localstack/services/lambda_/custom_endpoints.py new file mode 100644 index 0000000000000..c93542f5768f8 --- /dev/null +++ b/localstack/services/lambda_/custom_endpoints.py @@ -0,0 +1,35 @@ +import urllib.parse +from typing import List, TypedDict + +from rolo import Request, route + +from localstack.aws.api.lambda_ import Runtime +from localstack.services.lambda_.runtimes import ( + ALL_RUNTIMES, + DEPRECATED_RUNTIMES, + SUPPORTED_RUNTIMES, +) + + +class LambdaRuntimesResponse(TypedDict, total=False): + Runtimes: List[Runtime] + + +class LambdaCustomEndpoints: + @route("/_aws/lambda/runtimes") + def runtimes(self, request: Request) -> LambdaRuntimesResponse: + """This metadata endpoint needs to be loaded before the Lambda provider. + It can be used by the Webapp to query supported Lambda runtimes of an unknown LocalStack version.""" + query_params = urllib.parse.parse_qs(request.environ["QUERY_STRING"]) + # Query parameter values are all lists. Example: { "filter": ["all"] } + filter_params = query_params.get("filter", []) + runtimes = set() + if "all" in filter_params: + runtimes.update(ALL_RUNTIMES) + if "deprecated" in filter_params: + runtimes.update(DEPRECATED_RUNTIMES) + # By default (i.e., without any filter param), we return the supported runtimes because that is most useful. + if "supported" in filter_params or len(runtimes) == 0: + runtimes.update(SUPPORTED_RUNTIMES) + + return LambdaRuntimesResponse(Runtimes=list(runtimes)) diff --git a/localstack/services/lambda_/plugins.py b/localstack/services/lambda_/plugins.py index b415a559e0342..646dc170fb9b8 100644 --- a/localstack/services/lambda_/plugins.py +++ b/localstack/services/lambda_/plugins.py @@ -1,11 +1,17 @@ import logging +from werkzeug.routing import Rule + from localstack.config import LAMBDA_DOCKER_NETWORK from localstack.packages import Package, package from localstack.runtime import hooks +from localstack.services.edge import ROUTER +from localstack.services.lambda_.custom_endpoints import LambdaCustomEndpoints LOG = logging.getLogger(__name__) +CUSTOM_ROUTER_RULES: list[Rule] = [] + @package(name="lambda-runtime") def lambda_runtime_package() -> Package: @@ -27,3 +33,14 @@ def validate_configuration() -> None: LOG.warning( "The configuration LAMBDA_DOCKER_NETWORK=host is currently not supported with the new lambda provider." ) + + +@hooks.on_infra_start() +def register_custom_endpoints() -> None: + global CUSTOM_ROUTER_RULES + CUSTOM_ROUTER_RULES = ROUTER.add(LambdaCustomEndpoints()) + + +@hooks.on_infra_shutdown() +def remove_custom_endpoints() -> None: + ROUTER.remove(CUSTOM_ROUTER_RULES) From e8c8629e63a195ef3a36baf3c3f6245b3cb74408 Mon Sep 17 00:00:00 2001 From: Thomas Rausch Date: Wed, 24 Apr 2024 18:20:43 +0200 Subject: [PATCH 095/169] create pattern construct for a webapp extension (#10701) --- localstack/extensions/patterns/__init__.py | 0 localstack/extensions/patterns/webapp.py | 333 +++++++++++++++++++++ 2 files changed, 333 insertions(+) create mode 100644 localstack/extensions/patterns/__init__.py create mode 100644 localstack/extensions/patterns/webapp.py diff --git a/localstack/extensions/patterns/__init__.py b/localstack/extensions/patterns/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/localstack/extensions/patterns/webapp.py b/localstack/extensions/patterns/webapp.py new file mode 100644 index 0000000000000..ab69d935d729c --- /dev/null +++ b/localstack/extensions/patterns/webapp.py @@ -0,0 +1,333 @@ +import importlib +import logging +import mimetypes +import typing as t +from functools import cached_property + +from rolo.gateway import HandlerChain +from rolo.router import RuleAdapter, WithHost +from werkzeug.routing import Submount + +from localstack import config +from localstack.aws.api import RequestContext +from localstack.extensions.api import Extension, http + +if t.TYPE_CHECKING: + # although jinja2 is included transitively via moto, let's make sure jinja2 stays optional + import jinja2 + +LOG = logging.getLogger(__name__) + +_default = object() + + +class WebAppExtension(Extension): + """ + EXPERIMENTAL! This class is experimental and the API may change without notice. + + A webapp extension serves routes, templates, and static files via a submount and a subdomain through + localstack. + + It assumes you have the following directory layout:: + + my_extension + ├── extension.py + ├── __init__.py + ├── static <-- make sure static resources get packaged! + │ ├── __init__.py + │ ├── favicon.ico + │ └── style.css + └── templates <-- jinja2 templates + └── index.html + + Given this layout, you can define your extensions in ``my_extension.extension`` like this. Routes defined in the + extension itself are automatically registered:: + + class MyExtension(WebAppExtension): + name = "my-extension" + + @route("/") + def index(request: Request) -> Response: + # reference `static/style.css` to serve the static file from your package + return self.render_template_response("index.html") + + @route("/hello") + def hello(request: Request): + return {"message": "Hello World!"} + + This will create an extension that localstack serves via: + + * Submount: https://localhost.localstack.cloud:4566/_extension/my-extension + * Subdomain: https://my-extension.localhost.localstack.cloud:4566/ + + Both are created for full flexibility: + + * Subdomains: create a domain namespace that can be helpful for some extensions, especially when + running on the local machine + * Submounts: for some environments, like in ephemeral instances where subdomains are harder to control, + submounts are more convenient + + Any routes added by the extension will be served relative to these URLs. + """ + + def __init__( + self, + mount: str = None, + submount: str | None = _default, + subdomain: str | None = _default, + template_package_path: str | None = _default, + static_package_path: str | None = _default, + static_url_path: str = None, + ): + """ + Overwrite to customize your extension. For example, you can disable certain behavior by calling + ``super( ).__init__(subdomain=None, static_package_path=None)``, which will disable serving through + a subdomain, and disable static file serving. + + :param mount: the "mount point" which will be used as default value for the submount and + subdirectory, i.e., ``.localhost.localstack.cloud`` and + ``localhost.localstack.cloud/_extension/``. Defaults to the extension name. Note that, + in case the mount name clashes with another extension, extensions may overwrite each other's + routes. + :param submount: the submount path, needs to start with a trailing slash (default + ``/_extension/``) + :param subdomain: the subdomain (defaults to the value of ``mount``) + :param template_package_path: the path to the templates within the module. defaults to + ``templates`` which expands to ``.templates``) + :param static_package_path: the package serving static files. defaults to ``static``, which expands to + ``.static``. + :param static_url_path: the URL path to serve static files from (defaults to `/static`) + """ + mount = mount or self.name + + self.submount = f"/_extension/{mount}" if submount is _default else submount + self.subdomain = mount if subdomain is _default else subdomain + + self.template_package_path = ( + "templates" if template_package_path is _default else template_package_path + ) + self.static_package_path = ( + "static" if static_package_path is _default else static_package_path + ) + self.static_url_path = static_url_path or "/static" + + self.static_resource_module = None + + def collect_routes(self, routes: list[t.Any]): + """ + This method can be overwritten to add more routes to the controller. Everything in ``routes`` will + be added to a ``RuleAdapter`` and subsequently mounted into the gateway router. + + Here are some examples:: + + class MyRoutes: + @route("/hello") + def hello(request): + return "Hello World!" + + class MyExtension(WebAppExtension): + name = "my-extension" + + def collect_routes(self, routes: list[t.Any]): + + # scans all routes of MyRoutes + routes.append(MyRoutes()) + # use rule adapters to add routes without decorators + routes.append(RuleAdapter("/say-hello", self.say_hello)) + + # no idea why you would want to do this, but you can :-) + @route("/empty-dict") + def _inline_handler(request: Request) -> Response: + return Response.for_json({}) + routes.append(_inline_handler) + + def say_hello(request: Request): + return {"message": "Hello World!"} + + This creates the following routes available through both subdomain and submount. + + With subdomain: + + * ``my-extension.localhost.localstack.cloud:4566/hello`` + * ``my-extension.localhost.localstack.cloud:4566/say-hello`` + * ``my-extension.localhost.localstack.cloud:4566/empty-dict`` + * ``my-extension.localhost.localstack.cloud:4566/static`` <- automatically added static file endpoint + + With submount: + + * ``localhost.localstack.cloud:4566/_extension/my-extension/hello`` + * ``localhost.localstack.cloud:4566/_extension/my-extension/say-hello`` + * ``localhost.localstack.cloud:4566/_extension/my-extension/empty-dict`` + * ``localhost.localstack.cloud:4566/_extension/my-extension/static`` <- auto-added static file serving + + :param routes: the routes being collected + """ + pass + + @cached_property + def template_env(self) -> t.Optional["jinja2.Environment"]: + """ + Returns the singleton jinja2 template environment. By default, the environment uses a + ``PackageLoader`` that loads from ``my_extension.templates`` (where ``my_extension`` is the root + module of the extension, and ``templates`` refers to ``self.template_package_path``, + which is ``templates`` by default). + + :return: a template environment + """ + if self.template_package_path: + return self._create_template_env() + return None + + def _create_template_env(self) -> "jinja2.Environment": + """ + Factory method to create the jinja2 template environment. + :return: a new jinja2 environment + """ + import jinja2 + + return jinja2.Environment( + loader=jinja2.PackageLoader( + self.get_extension_module_root(), self.template_package_path + ), + autoescape=jinja2.select_autoescape(), + ) + + def render_template(self, template_name, **context) -> str: + """ + Uses the ``template_env`` to render a template and return the string value. + + :param template_name: the template name + :param context: template context + :return: the rendered result + """ + template = self.template_env.get_template(template_name) + return template.render(**context) + + def render_template_response(self, template_name, **context) -> http.Response: + """ + Uses the ``template_env`` to render a template into an HTTP response. It guesses the mimetype from the + template's file name. + + :param template_name: the template name + :param context: template context + :return: the rendered result as response + """ + template = self.template_env.get_template(template_name) + + mimetype = mimetypes.guess_type(template.filename) + mimetype = mimetype[0] if mimetype and mimetype[0] else "text/plain" + + return http.Response(response=template.render(**context), mimetype=mimetype) + + def on_extension_load(self): + logging.getLogger(self.get_extension_module_root()).setLevel( + logging.DEBUG if config.DEBUG else logging.INFO + ) + + if self.static_package_path and not self.static_resource_module: + try: + self.static_resource_module = importlib.import_module( + self.get_extension_module_root() + "." + self.static_package_path + ) + except ModuleNotFoundError: + LOG.warning("disabling static resources for extension %s", self.name) + + def _preprocess_request( + self, chain: HandlerChain, context: RequestContext, _response: http.Response + ): + """ + Default pre-processor, which implements a default behavior to add a trailing slash to the path if the + submount is used directly. For instance ``/_extension/my-extension``, then it forwards to + ``/_extension/my-extension/``. This is so you can reference relative paths like ```` in your HTML safely, and it will work with both subdomain and submount. + """ + path = context.request.path + + if path == self.submount.rstrip("/"): + chain.respond(301, headers={"Location": context.request.url + "/"}) + + def update_gateway_routes(self, router: http.Router[http.RouteHandler]): + from localstack.aws.handlers import preprocess_request + + if self.submount: + preprocess_request.append(self._preprocess_request) + + # adding self here makes sure that any ``@route`` decorators to the extension are mapped automatically + routes = [self] + + if self.static_resource_module: + routes.append( + RuleAdapter(f"{self.static_url_path}/", self._serve_static_file) + ) + + self.collect_routes(routes) + + app = RuleAdapter(routes) + + if self.submount: + router.add(Submount(self.submount, [app])) + LOG.info( + "%s extension available at %s%s", + self.name, + config.external_service_url(), + self.submount, + ) + + if self.subdomain: + router.add(WithHost(f"{self.subdomain}.<__host__>", [app])) + self._configure_cors_for_subdomain() + LOG.info( + "%s extension available at %s", + self.name, + config.external_service_url(subdomains=self.subdomain), + ) + + def _serve_static_file(self, _request: http.Request, path: str): + """Route for serving static files, for ``/_extension/my-extension/static/``.""" + return http.Response.for_resource(self.static_resource_module, path) + + def _configure_cors_for_subdomain(self): + """ + Automatically configures CORS for the subdomain, for both HTTP and HTTPS. + """ + from localstack.aws.handlers.cors import ALLOWED_CORS_ORIGINS + + for protocol in ("http", "https"): + url = self.get_subdomain_url(protocol) + LOG.debug("adding %s to ALLOWED_CORS_ORIGINS", url) + ALLOWED_CORS_ORIGINS.append(url) + + def get_subdomain_url(self, protocol: str = "https") -> str: + """ + Returns the URL that serves the extension under its subdomain + ``https://my-extension.localhost.localstack.cloud:4566/``. + + :return: a URL this extension is served at + """ + if not self.subdomain: + raise ValueError(f"Subdomain for extension {self.name} is not set") + return config.external_service_url(subdomains=self.subdomain, protocol=protocol) + + def get_submount_url(self, protocol: str = "https") -> str: + """ + Returns the URL that serves the extension under its submount + ``https://localhost.localstack.cloud:4566/_extension/my-extension``. + + :return: a URL this extension is served at + """ + + if not self.submount: + raise ValueError(f"Submount for extension {self.name} is not set") + + return f"{config.external_service_url(protocol=protocol)}{self.submount}" + + @classmethod + def get_extension_module_root(cls) -> str: + """ + Returns the root of the extension module. For instance, if the extension lives in + ``my_extension/plugins/extension.py``, then this will return ``my_extension``. Used to set up the + logger as well as the template environment and the static file module. + + :return: the root module the extension lives in + """ + return cls.__module__.split(".")[0] From 6f971ac81f8bc7d9a7f1d62dfc7c63cd71f714e2 Mon Sep 17 00:00:00 2001 From: "localstack[bot]" Date: Thu, 25 Apr 2024 06:36:31 +0000 Subject: [PATCH 096/169] release version 3.4.0 --- VERSION | 2 +- localstack/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index e010372c3c264..18091983f59dd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.3.1.dev +3.4.0 diff --git a/localstack/version.py b/localstack/version.py index a5294c45d8845..903a158adda39 100644 --- a/localstack/version.py +++ b/localstack/version.py @@ -1 +1 @@ -__version__ = "3.3.1.dev" +__version__ = "3.4.0" From da912857c96e00a72ce26e88c5c0cd6542caf615 Mon Sep 17 00:00:00 2001 From: "localstack[bot]" Date: Thu, 25 Apr 2024 06:36:44 +0000 Subject: [PATCH 097/169] prepare next development iteration --- VERSION | 2 +- localstack/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 18091983f59dd..e452fd36c227e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.4.0 +3.4.1.dev diff --git a/localstack/version.py b/localstack/version.py index 903a158adda39..580cf550e2c33 100644 --- a/localstack/version.py +++ b/localstack/version.py @@ -1 +1 @@ -__version__ = "3.4.0" +__version__ = "3.4.1.dev" From 9287822c5c1c2184c0b957d6228981765147cc0d Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Thu, 25 Apr 2024 16:34:17 +0530 Subject: [PATCH 098/169] update release banner in README (#10686) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b64ad949c831b..c85ad0e4628c3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

-:zap: We are thrilled to announce the release of LocalStack 3.3 :zap: +:zap: We are thrilled to announce the release of LocalStack 3.4 :zap:

@@ -93,7 +93,7 @@ Start LocalStack inside a Docker container by running: / /___/ /_/ / /__/ /_/ / /___/ / /_/ /_/ / /__/ ,< /_____/\____/\___/\__,_/_//____/\__/\__,_/\___/_/|_| - 💻 LocalStack CLI 3.3.0 + 💻 LocalStack CLI 3.4.0 👤 Profile: default [12:47:13] starting LocalStack in Docker mode 🐳 localstack.py:494 From aa713554f744a81c06d498ef5a44675d9b42de62 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Thu, 25 Apr 2024 17:15:12 +0200 Subject: [PATCH 099/169] fix headers for empty S3 responses (#10717) --- localstack/aws/protocol/serializer.py | 1 - localstack/services/s3/provider.py | 9 ++++-- localstack/services/s3/utils.py | 30 +++++++++++++++++++ localstack/services/s3/v3/provider.py | 8 ++++- tests/aws/services/s3/test_s3.py | 23 ++++++++++---- tests/aws/services/s3/test_s3.snapshot.json | 2 +- tests/aws/services/s3/test_s3.validation.json | 7 +++-- 7 files changed, 67 insertions(+), 13 deletions(-) diff --git a/localstack/aws/protocol/serializer.py b/localstack/aws/protocol/serializer.py index fb17bbc484454..5ac2319793a4e 100644 --- a/localstack/aws/protocol/serializer.py +++ b/localstack/aws/protocol/serializer.py @@ -1501,7 +1501,6 @@ def _serialize_response( request_id, ) self._serialize_content_type(response, shape, shape_members, mime_type) - self._prepare_additional_traits_in_response(response, operation_model, request_id) def _serialize_error( self, diff --git a/localstack/services/s3/provider.py b/localstack/services/s3/provider.py index 7d94a03afd981..245ecc3b69bb7 100644 --- a/localstack/services/s3/provider.py +++ b/localstack/services/s3/provider.py @@ -134,7 +134,11 @@ WebsiteConfiguration, ) from localstack.aws.forwarder import NotImplementedAvoidFallbackError -from localstack.aws.handlers import preprocess_request, serve_custom_service_request_handlers +from localstack.aws.handlers import ( + modify_service_response, + preprocess_request, + serve_custom_service_request_handlers, +) from localstack.constants import AWS_REGION_US_EAST_1, DEFAULT_AWS_ACCOUNT_ID from localstack.services.edge import ROUTER from localstack.services.moto import call_moto @@ -161,6 +165,7 @@ get_lifecycle_rule_from_object, get_object_checksum_for_algorithm, get_permission_from_header, + s3_response_handler, serialize_expiration_header, validate_kms_key_id, verify_checksum, @@ -230,8 +235,8 @@ def on_after_init(self): preprocess_request.append(self._cors_handler) register_website_hosting_routes(router=ROUTER) serve_custom_service_request_handlers.append(s3_cors_request_handler) + modify_service_response.append(self.service, s3_response_handler) # registering of virtual host routes happens with the hook on_infra_ready in virtual_host.py - # create a AWS managed KMS key at start and save it in the store for persistence? def __init__(self) -> None: super().__init__() diff --git a/localstack/services/s3/utils.py b/localstack/services/s3/utils.py index 325d65914b018..4441663de7794 100644 --- a/localstack/services/s3/utils.py +++ b/localstack/services/s3/utils.py @@ -46,7 +46,9 @@ TagSet, ) from localstack.aws.api.s3 import Type as GranteeType +from localstack.aws.chain import HandlerChain from localstack.aws.connect import connect_to +from localstack.http import Response from localstack.services.s3.constants import ( ALL_USERS_ACL_GRANTEE, AUTHENTICATED_USERS_ACL_GRANTEE, @@ -93,6 +95,34 @@ _gmt_zone_info = ZoneInfo("GMT") +def s3_response_handler(chain: HandlerChain, context: RequestContext, response: Response): + """ + This response handler is taking care of removing certain headers from S3 responses. + We cannot handle this in the serializer, because the serializer handler calls `Response.update_from`, which does + not allow you to remove headers, only add them. + This handler can delete headers from the response. + """ + # some requests, for example coming frome extensions, are flagged as S3 requests. This check confirms that it is + # indeed truly an S3 request by checking if it parsed properly as an S3 operation + if not context.service_operation: + return + + # if AWS returns 204, it will not return a body, Content-Length and Content-Type + # the web server is already taking care of deleting the body, but it's more explicit to remove it here + if response.status_code == 204: + response.data = b"" + response.headers.pop("Content-Type", None) + response.headers.pop("Content-Length", None) + + elif ( + response.status_code == 200 + and context.request.method == "PUT" + and response.headers.get("Content-Length") in (0, None) + ): + # AWS does not return a Content-Type if the Content-Length is 0 + response.headers.pop("Content-Type", None) + + def get_owner_for_account_id(account_id: str): """ This method returns the S3 Owner from the account id. for now, this is hardcoded as it was in moto, but we can then diff --git a/localstack/services/s3/v3/provider.py b/localstack/services/s3/v3/provider.py index 458da9ceceed3..330a5ffa24099 100644 --- a/localstack/services/s3/v3/provider.py +++ b/localstack/services/s3/v3/provider.py @@ -199,7 +199,11 @@ VersioningConfiguration, WebsiteConfiguration, ) -from localstack.aws.handlers import preprocess_request, serve_custom_service_request_handlers +from localstack.aws.handlers import ( + modify_service_response, + preprocess_request, + serve_custom_service_request_handlers, +) from localstack.constants import AWS_REGION_US_EAST_1 from localstack.services.edge import ROUTER from localstack.services.plugins import ServiceLifecycleHook @@ -244,6 +248,7 @@ parse_post_object_tagging_xml, parse_range_header, parse_tagging_header, + s3_response_handler, serialize_expiration_header, str_to_rfc_1123_datetime, validate_dict_fields, @@ -306,6 +311,7 @@ def __init__(self, storage_backend: S3ObjectStore = None) -> None: def on_after_init(self): preprocess_request.append(self._cors_handler) serve_custom_service_request_handlers.append(s3_cors_request_handler) + modify_service_response.append(self.service, s3_response_handler) register_website_hosting_routes(router=ROUTER) def accept_state_visitor(self, visitor: StateVisitor): diff --git a/tests/aws/services/s3/test_s3.py b/tests/aws/services/s3/test_s3.py index 812fc0656ae18..f9f506f0e5c29 100644 --- a/tests/aws/services/s3/test_s3.py +++ b/tests/aws/services/s3/test_s3.py @@ -971,7 +971,6 @@ def test_put_and_get_bucket_policy(self, s3_bucket, snapshot, aws_client, allow_ ], } response = aws_client.s3.put_bucket_policy(Bucket=s3_bucket, Policy=json.dumps(policy)) - # assert response["ResponseMetadata"]["HTTPStatusCode"] == 204 snapshot.match("put-bucket-policy", response) # retrieve and check policy config @@ -4856,6 +4855,7 @@ def get_xml_content(http_response_content: bytes) -> bytes: resp_dict = xmltodict.parse(resp.content) assert "CopyObjectResult" in resp_dict assert resp_dict["CopyObjectResult"]["@xmlns"] == "http://s3.amazonaws.com/doc/2006-03-01/" + assert resp.status_code == 200 multipart_key = "multipart-key" create_multipart = aws_client.s3.create_multipart_upload( @@ -4866,6 +4866,16 @@ def get_xml_content(http_response_content: bytes) -> bytes: upload_part_url = f"{bucket_url}/{multipart_key}?UploadId={upload_id}&PartNumber=1" resp = s3_http_client.put(upload_part_url, headers=headers) assert not resp.content, resp.content + assert resp.status_code == 200 + assert resp.headers.get("Content-Type") is None + assert resp.headers["Content-Length"] == "0" + + # DeleteObjectTagging + resp = s3_http_client.delete(get_object_tagging_url, headers=headers) + assert not resp.content, resp.content + assert resp.status_code == 204 + assert resp.headers.get("Content-Type") is None + assert resp.headers.get("Content-Length") is None @markers.aws.validated def test_s3_timestamp_precision(self, s3_bucket, aws_client, aws_http_client_factory): @@ -6053,11 +6063,11 @@ def test_delete_has_empty_content_length_header(self, s3_bucket, aws_client): response = requests.delete(url, headers=headers, verify=False) assert not response.content assert response.status_code == 204 - # AWS does not send a content-length header at all, legacy localstack sends a 0 length header - assert response.headers.get("content-length") in [ - "0", - None, - ], f"Unexpected content-length in headers {response.headers}" + assert response.headers.get("x-amz-id-2") is not None + # AWS does not return a Content-Type when the body is empty and it returns 204 + assert response.headers.get("content-type") is None + # AWS does not send a content-length header at all + assert response.headers.get("content-length") is None @markers.aws.validated def test_head_has_correct_content_length_header(self, s3_bucket, aws_client): @@ -11002,6 +11012,7 @@ def test_presigned_post_with_different_user_credentials( verify=False, ) assert response.status_code == 204 + assert response.headers.get("Content-Type") is None get_obj = aws_client.s3.get_object(Bucket=bucket_name, Key=object_key) snapshot.match("get-obj", get_obj) diff --git a/tests/aws/services/s3/test_s3.snapshot.json b/tests/aws/services/s3/test_s3.snapshot.json index 576e2951ec6f8..b34756dbe6990 100644 --- a/tests/aws/services/s3/test_s3.snapshot.json +++ b/tests/aws/services/s3/test_s3.snapshot.json @@ -11612,7 +11612,7 @@ } }, "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_presigned_post_with_different_user_credentials": { - "recorded-date": "11-04-2024, 20:50:35", + "recorded-date": "24-04-2024, 18:30:08", "recorded-content": { "get-obj": { "AcceptRanges": "bytes", diff --git a/tests/aws/services/s3/test_s3.validation.json b/tests/aws/services/s3/test_s3.validation.json index aba7e54031a6e..6caabeedee35e 100644 --- a/tests/aws/services/s3/test_s3.validation.json +++ b/tests/aws/services/s3/test_s3.validation.json @@ -237,7 +237,7 @@ "last_validated_date": "2024-03-21T08:05:13+00:00" }, "tests/aws/services/s3/test_s3.py::TestS3::test_response_structure": { - "last_validated_date": "2024-01-03T16:46:18+00:00" + "last_validated_date": "2024-04-24T18:48:32+00:00" }, "tests/aws/services/s3/test_s3.py::TestS3::test_s3_analytics_configurations": { "last_validated_date": "2023-08-03T02:25:40+00:00" @@ -573,7 +573,10 @@ "last_validated_date": "2023-08-04T21:58:54+00:00" }, "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_presigned_post_with_different_user_credentials": { - "last_validated_date": "2024-04-11T20:50:35+00:00" + "last_validated_date": "2024-04-24T18:30:08+00:00" + }, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_delete_has_empty_content_length_header": { + "last_validated_date": "2024-04-24T18:42:46+00:00" }, "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_signature_authentication[s3-False]": { "last_validated_date": "2023-08-04T22:00:25+00:00" From 4745c42da753086b3afd2042b8a04015d5de1483 Mon Sep 17 00:00:00 2001 From: Daniel Fangl Date: Thu, 25 Apr 2024 17:51:38 +0200 Subject: [PATCH 100/169] Avoid concurrent pulling of identical images in lambda (#10720) --- .../invocation/docker_runtime_executor.py | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/localstack/services/lambda_/invocation/docker_runtime_executor.py b/localstack/services/lambda_/invocation/docker_runtime_executor.py index 715d8975bb11f..1b9bd695f1be5 100644 --- a/localstack/services/lambda_/invocation/docker_runtime_executor.py +++ b/localstack/services/lambda_/invocation/docker_runtime_executor.py @@ -3,6 +3,8 @@ import logging import shutil import tempfile +import threading +from collections import defaultdict from functools import cache from pathlib import Path from typing import Callable, Dict, Literal, Optional @@ -60,6 +62,7 @@ """ PULLED_IMAGES: set[(str, DockerPlatform)] = set() +PULL_LOCKS: dict[(str, DockerPlatform), threading.RLock] = defaultdict(threading.RLock) HOT_RELOADING_ENV_VARIABLE = "LOCALSTACK_HOT_RELOADING_PATHS" @@ -89,13 +92,37 @@ def get_image_name_for_function(function_version: FunctionVersion) -> str: return f"localstack/prebuild-lambda-{function_version.id.qualified_arn().replace(':', '_').replace('$', '_').lower()}" -def get_default_image_for_runtime(runtime: str) -> str: +def get_default_image_for_runtime(runtime: Runtime) -> str: postfix = IMAGE_MAPPING.get(runtime) if not postfix: raise ValueError(f"Unsupported runtime {runtime}!") return f"{IMAGE_PREFIX}{postfix}" +def _ensure_runtime_image_present(image: str, platform: DockerPlatform) -> None: + # Pull image for a given platform upon function creation such that invocations do not time out. + if (image, platform) in PULLED_IMAGES: + return + # use a lock to avoid concurrent pulling of the same image + with PULL_LOCKS[(image, platform)]: + if (image, platform) in PULLED_IMAGES: + return + try: + CONTAINER_CLIENT.pull_image(image, platform) + PULLED_IMAGES.add((image, platform)) + except NoSuchImage as e: + LOG.debug("Unable to pull image %s for runtime executor preparation.", image) + raise e + except DockerNotAvailable as e: + HINT_LOG.error( + "Failed to pull Docker image because Docker is not available in the LocalStack container " + "but required to run Lambda functions. Please add the Docker volume mount " + '"/var/run/docker.sock:/var/run/docker.sock" to your LocalStack startup. ' + "https://docs.localstack.cloud/user-guide/aws/lambda/#docker-not-available" + ) + raise e + + class RuntimeImageResolver: """ Resolves Lambda runtimes to corresponding docker images @@ -459,25 +486,7 @@ def prepare_version(cls, function_version: FunctionVersion) -> None: function_version.config.code.prepare_for_execution() image_name = resolver.get_image_for_runtime(function_version.config.runtime) platform = docker_platform(function_version.config.architectures[0]) - # Pull image for a given platform upon function creation such that invocations do not time out. - if (image_name, platform) not in PULLED_IMAGES: - try: - # FIXME multiple concurrent pulls could take place, which will slow them all down - CONTAINER_CLIENT.pull_image(image_name, platform) - PULLED_IMAGES.add((image_name, platform)) - except NoSuchImage as e: - LOG.debug( - "Unable to pull image %s for runtime executor preparation.", image_name - ) - raise e - except DockerNotAvailable as e: - HINT_LOG.error( - "Failed to pull Docker image because Docker is not available in the LocalStack container " - "but required to run Lambda functions. Please add the Docker volume mount " - '"/var/run/docker.sock:/var/run/docker.sock" to your LocalStack startup. ' - "https://docs.localstack.cloud/user-guide/aws/lambda/#docker-not-available" - ) - raise e + _ensure_runtime_image_present(image_name, platform) if config.LAMBDA_PREBUILD_IMAGES: prepare_image(function_version, platform) From 61b1654a1c6adde4c52549aeee99bfde7833f8f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20N=C3=A9meth?= Date: Thu, 25 Apr 2024 19:36:02 +0200 Subject: [PATCH 101/169] CFN_PER_RESOURCE_TIMEOUT env variable (#10721) --- localstack/config.py | 3 +++ localstack/services/cloudformation/resource_provider.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/localstack/config.py b/localstack/config.py index 56d84930ab587..cf91238316afa 100644 --- a/localstack/config.py +++ b/localstack/config.py @@ -1041,6 +1041,9 @@ def populate_edge_configuration( # Show exceptions for CloudFormation deploy errors CFN_VERBOSE_ERRORS = is_env_true("CFN_VERBOSE_ERRORS") +# Set the timeout to deploy each individual CloudFormation resource +CFN_PER_RESOURCE_TIMEOUT = int(os.environ.get("CFN_PER_RESOURCE_TIMEOUT") or 300) + # How localstack will react to encountering unsupported resource types. # By default unsupported resource types will be ignored. # EXPERIMENTAL diff --git a/localstack/services/cloudformation/resource_provider.py b/localstack/services/cloudformation/resource_provider.py index 23c26e7f56663..2b9b607d3f406 100644 --- a/localstack/services/cloudformation/resource_provider.py +++ b/localstack/services/cloudformation/resource_provider.py @@ -8,6 +8,7 @@ from dataclasses import dataclass, field from enum import Enum, auto from logging import Logger +from math import ceil from typing import TYPE_CHECKING, Any, Callable, Generic, Optional, Type, TypedDict, TypeVar import botocore @@ -397,11 +398,13 @@ def deploy_loop( self, resource: dict, raw_payload: ResourceProviderPayload, - max_iterations: int = 30, + max_timeout: int = config.CFN_PER_RESOURCE_TIMEOUT, sleep_time: float = 5, ) -> ProgressEvent[Properties]: payload = copy.deepcopy(raw_payload) + max_iterations = max(ceil(max_timeout / sleep_time), 2) + for current_iteration in range(max_iterations): resource_type = get_resource_type( {"Type": raw_payload["resourceType"]} From 82c42f791bd43caefaf9e1a84981f7ab96f53941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristopher=20Pinz=C3=B3n?= Date: Thu, 25 Apr 2024 12:48:18 -0500 Subject: [PATCH 102/169] add utility for resource providers (#10718) --- .../services/cloudformation/provider_utils.py | 34 ++++++++++++++++++- .../cloudformation/test_provider_utils.py | 9 +++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 tests/unit/services/cloudformation/test_provider_utils.py diff --git a/localstack/services/cloudformation/provider_utils.py b/localstack/services/cloudformation/provider_utils.py index aea2a43886e04..2d475aa8bfa7d 100644 --- a/localstack/services/cloudformation/provider_utils.py +++ b/localstack/services/cloudformation/provider_utils.py @@ -11,7 +11,7 @@ import uuid from copy import deepcopy from pathlib import Path -from typing import Callable +from typing import Callable, List, Optional def generate_default_name(stack_name: str, logical_resource_id: str): @@ -139,6 +139,38 @@ def fix_boto_parameters_based_on_report(original_params: dict, report: str) -> d return params +def convert_values_to_numbers(input_dict: dict, keys_to_skip: Optional[List[str]] = None): + """ + Recursively converts all string values that represent valid integers + in a dictionary (including nested dictionaries and lists) to integers. + + Example: + original_dict = {'Gid': '1322', 'SecondaryGids': ['1344', '1452'], 'Uid': '13234'} + output_dict = {'Gid': 1322, 'SecondaryGids': [1344, 1452], 'Uid': 13234} + + :param input_dict input dict with values to convert + :param keys_to_skip keys to which values are not meant to be converted + :return output_dict + """ + + keys_to_skip = keys_to_skip or [] + + def recursive_convert(obj): + if isinstance(obj, dict): + return { + key: recursive_convert(value) if key not in keys_to_skip else value + for key, value in obj.items() + } + elif isinstance(obj, list): + return [recursive_convert(item) for item in obj] + elif isinstance(obj, str) and obj.isdigit(): + return int(obj) + else: + return obj + + return recursive_convert(input_dict) + + # LocalStack specific utilities def get_schema_path(file_path: Path) -> Path: file_name_base = file_path.name.removesuffix(".py").removesuffix(".py.enc") diff --git a/tests/unit/services/cloudformation/test_provider_utils.py b/tests/unit/services/cloudformation/test_provider_utils.py new file mode 100644 index 0000000000000..eff2b09fee118 --- /dev/null +++ b/tests/unit/services/cloudformation/test_provider_utils.py @@ -0,0 +1,9 @@ +import localstack.services.cloudformation.provider_utils as utils + + +class TestDictUtils: + def test_convert_values_to_numbers(self): + original = {"Parameter": "1", "SecondParameter": ["2", "2"], "ThirdParameter": "3"} + transformed = utils.convert_values_to_numbers(original, ["ThirdParameter"]) + + assert transformed == {"Parameter": 1, "SecondParameter": [2, 2], "ThirdParameter": "3"} From fcd5927e16f158ca0449c1194a151a12983f805a Mon Sep 17 00:00:00 2001 From: Simon Walker Date: Thu, 25 Apr 2024 19:07:29 +0100 Subject: [PATCH 103/169] Hide docker sdk error unless trace logging (#10711) --- localstack/utils/container_utils/docker_sdk_client.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/localstack/utils/container_utils/docker_sdk_client.py b/localstack/utils/container_utils/docker_sdk_client.py index 6b61a59c0f4f8..8c7839a58054e 100644 --- a/localstack/utils/container_utils/docker_sdk_client.py +++ b/localstack/utils/container_utils/docker_sdk_client.py @@ -17,6 +17,8 @@ from docker.models.containers import Container from docker.utils.socket import STDERR, STDOUT, frames_iter +from localstack.config import LS_LOG +from localstack.constants import TRACE_LOG_LEVELS from localstack.utils.collections import ensure_list from localstack.utils.container_utils.container_client import ( AccessDenied, @@ -75,7 +77,12 @@ def _create_client(): try: return docker.from_env(timeout=DOCKER_SDK_DEFAULT_TIMEOUT_SECONDS) except DockerException as e: - LOG.debug("Creating Docker SDK client failed: %s", e, exc_info=e) + LOG.debug( + "Creating Docker SDK client failed: %s. " + "If you want to use Docker as container runtime, make sure to mount the socket at /var/run/docker.sock", + e, + exc_info=LS_LOG in TRACE_LOG_LEVELS, + ) if attempt < DOCKER_SDK_DEFAULT_RETRIES: # wait for a second before retrying sleep(1) From 0aebc1ed232094dccfb83b38c77f6545fa8fb4dd Mon Sep 17 00:00:00 2001 From: Viren Nadkarni Date: Mon, 29 Apr 2024 12:21:06 +0530 Subject: [PATCH 104/169] Fix SubtypesInstanceManager not loading subtypes lazily (#10728) --- localstack/utils/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localstack/utils/objects.py b/localstack/utils/objects.py index 6fca96ffd437e..a30c0880e660b 100644 --- a/localstack/utils/objects.py +++ b/localstack/utils/objects.py @@ -94,7 +94,7 @@ def get(cls, subtype_name: str, raise_if_missing: bool = True): # lazily load subtype instance (required if new plugins are dynamically loaded at runtime) for clazz in get_all_subclasses(base_type): impl_name = clazz.impl_name() - if impl_name not in instances: + if impl_name not in instances and subtype_name == impl_name: instances[impl_name] = clazz() instance = instances.get(subtype_name) if not instance and raise_if_missing: From 976b0d00b44d225e5a63b0c4be832ee80a78a1ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 07:05:16 +0200 Subject: [PATCH 105/169] Bump the docker-base-images group with 2 updates (#10747) --- Dockerfile | 4 ++-- Dockerfile.s3 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5809b9a55f7c5..13b85840a7dda 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # java-builder: Stage to build a custom JRE (with jlink) -FROM eclipse-temurin:11@sha256:8cf7bf0f13cc47e56c7461420e1a80585612afcbb0073602f846748d31cb1aba as java-builder +FROM eclipse-temurin:11@sha256:abccfc31cefa4f3fad66630fceb51e59c7656a2ebfd1a831423dadaf684397fa as java-builder # create a custom, minimized JRE via jlink RUN jlink --add-modules \ @@ -29,7 +29,7 @@ jdk.localedata --include-locales en,th \ # base: Stage which installs necessary runtime dependencies (OS packages, java,...) -FROM python:3.11.9-slim-bookworm@sha256:dad770592ab3582ab2dabcf0e18a863df9d86bd9d23efcfa614110ce49ac20e4 as base +FROM python:3.11.9-slim-bookworm@sha256:6d2502238109c929569ae99355e28890c438cb11bc88ef02cd189c173b3db07c as base ARG TARGETARCH # Install runtime OS package dependencies diff --git a/Dockerfile.s3 b/Dockerfile.s3 index 7fbfc00fa3ee9..359796d93f38a 100644 --- a/Dockerfile.s3 +++ b/Dockerfile.s3 @@ -1,5 +1,5 @@ # base: Stage which installs necessary runtime dependencies (OS packages, filesystem...) -FROM python:3.11.9-slim-bookworm@sha256:dad770592ab3582ab2dabcf0e18a863df9d86bd9d23efcfa614110ce49ac20e4 as base +FROM python:3.11.9-slim-bookworm@sha256:6d2502238109c929569ae99355e28890c438cb11bc88ef02cd189c173b3db07c as base ARG TARGETARCH # set workdir From da5a2c0828caf390511993e60dc1c11a36a78562 Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Tue, 30 Apr 2024 09:11:48 +0200 Subject: [PATCH 106/169] migrate from enhancement request to feature request (#10743) --- ...hancement-request.yml => feature-request.yml} | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) rename .github/ISSUE_TEMPLATE/{enhancement-request.yml => feature-request.yml} (70%) diff --git a/.github/ISSUE_TEMPLATE/enhancement-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml similarity index 70% rename from .github/ISSUE_TEMPLATE/enhancement-request.yml rename to .github/ISSUE_TEMPLATE/feature-request.yml index 8ac3a73bdd70e..747700eb3bca9 100644 --- a/.github/ISSUE_TEMPLATE/enhancement-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -1,7 +1,7 @@ -name: ✨ Enhancement request -description: Request a new enhancement -title: "enhancement request: " -labels: ["type: enhancement", "status: triage needed"] +name: ✨ Feature request +description: Request a new feature +title: "feature request: <title>" +labels: ["type: feature", "status: triage needed"] body: - type: markdown attributes: @@ -10,20 +10,20 @@ body: - type: checkboxes attributes: label: Is there an existing issue for this? - description: Please search to see if an issue already exists for the enhancement you are requesting. + description: Please search to see if an issue already exists for the feature you are requesting. options: - label: I have searched the existing issues required: true - type: textarea attributes: - label: Enhancement description - description: Please describe the enhancement you would like LocalStack to have + label: Feature description + description: Please describe the feature you would like LocalStack to have validations: required: true - type: textarea attributes: label: 🧑‍💻 Implementation - description: If you are a developer and have an idea how to implement this enhancement, please sketch it out here. + description: If you are a developer and have an idea how to implement this feature, please sketch it out here. validations: required: false - type: textarea From a577f1e17da8e14a12962dd3db78246e8f2a5eeb Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Tue, 30 Apr 2024 10:35:51 +0200 Subject: [PATCH 107/169] Upgrade pinned Python dependencies (#10748) Co-authored-by: LocalStack Bot <localstack-bot@users.noreply.github.com> Co-authored-by: Alexander Rashed <alexander.rashed@localstack.cloud> --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- requirements-base-runtime.txt | 6 ++-- requirements-basic.txt | 2 +- requirements-dev.txt | 32 +++++++++---------- requirements-runtime.txt | 14 ++++---- requirements-test.txt | 20 ++++++------ requirements-typehint.txt | 60 +++++++++++++++++------------------ 8 files changed, 69 insertions(+), 69 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3e865ae550dd3..9db53d18ac46e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.4.1 + rev: v0.4.2 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/pyproject.toml b/pyproject.toml index cbe71963c4bf9..412f0bb141326 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,7 +78,7 @@ runtime = [ # pinned / updated by ASF update action "awscli==1.32.84", "airspeed-ext>=0.6.3", - "amazon_kclpy>=2.0.6,!=2.1.0", + "amazon_kclpy>=2.0.6,!=2.1.0,!=2.1.4", # antlr4-python3-runtime: exact pin because antlr4 runtime is tightly coupled to the generated parser code "antlr4-python3-runtime==4.13.1", "apispec>=5.1.1", diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index 5a0c1240b2e8c..88b590e2d8c10 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -10,7 +10,7 @@ attrs==23.2.0 # via localstack-twisted awscrt==0.20.9 # via localstack-core (pyproject.toml) -blinker==1.7.0 +blinker==1.8.1 # via # flask # quart @@ -130,7 +130,7 @@ pyopenssl==24.1.0 # via # localstack-core (pyproject.toml) # localstack-twisted -pyproject-hooks==1.0.0 +pyproject-hooks==1.1.0 # via build python-dateutil==2.9.0.post0 # via botocore @@ -178,7 +178,7 @@ urllib3==2.2.1 # docker # localstack-core (pyproject.toml) # requests -websocket-client==1.7.0 +websocket-client==1.8.0 # via docker werkzeug==3.0.2 # via diff --git a/requirements-basic.txt b/requirements-basic.txt index 151aae1bc6cac..05edf7f7d6c53 100644 --- a/requirements-basic.txt +++ b/requirements-basic.txt @@ -42,7 +42,7 @@ pycparser==2.22 # via cffi pygments==2.17.2 # via rich -pyproject-hooks==1.0.0 +pyproject-hooks==1.1.0 # via build python-dotenv==1.0.1 # via localstack-core (pyproject.toml) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6578f3bc041c5..a0bad023e92a9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -37,7 +37,7 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.138.0 +aws-cdk-lib==2.139.1 # via localstack-core aws-sam-translator==1.87.0 # via @@ -49,7 +49,7 @@ awscli==1.32.84 # via localstack-core awscrt==0.20.9 # via localstack-core -blinker==1.7.0 +blinker==1.8.1 # via # flask # quart @@ -108,11 +108,11 @@ constantly==23.10.4 # via localstack-twisted constructs==10.3.0 # via aws-cdk-lib -coverage==6.5.0 +coverage==7.4.4 # via # coveralls # localstack-core -coveralls==3.3.1 +coveralls==4.0.0 # via localstack-core (pyproject.toml) crontab==1.0.1 # via localstack-core @@ -154,7 +154,7 @@ docopt==0.6.2 # via coveralls docutils==0.16 # via awscli -filelock==3.13.4 +filelock==3.14.0 # via virtualenv flask==3.0.3 # via @@ -312,13 +312,13 @@ pbr==6.0.0 # jschema-to-python # sarif-om # stevedore -platformdirs==4.2.0 +platformdirs==4.2.1 # via virtualenv pluggy==1.5.0 # via # localstack-core # pytest -plumbum==1.8.2 +plumbum==1.8.3 # via pandoc plux==1.9.0 # via @@ -353,13 +353,13 @@ pyasn1==0.6.0 # via rsa pycparser==2.22 # via cffi -pydantic==2.7.0 +pydantic==2.7.1 # via aws-sam-translator -pydantic-core==2.18.1 +pydantic-core==2.18.2 # via pydantic pygments==2.17.2 # via rich -pymongo==4.6.3 +pymongo==4.7.0 # via localstack-core pyopenssl==24.1.0 # via @@ -369,9 +369,9 @@ pypandoc==1.13 # via localstack-core (pyproject.toml) pyparsing==3.1.2 # via moto-ext -pyproject-hooks==1.0.0 +pyproject-hooks==1.1.0 # via build -pytest==8.1.1 +pytest==8.2.0 # via # localstack-core # pytest-rerunfailures @@ -414,7 +414,7 @@ referencing==0.31.1 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2024.4.16 +regex==2024.4.28 # via cfn-lint requests==2.31.0 # via @@ -449,7 +449,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core (pyproject.toml) -ruff==0.4.1 +ruff==0.4.2 # via localstack-core (pyproject.toml) s3transfer==0.10.1 # via @@ -509,9 +509,9 @@ urllib3==2.2.1 # opensearch-py # requests # responses -virtualenv==20.25.3 +virtualenv==20.26.1 # via pre-commit -websocket-client==1.7.0 +websocket-client==1.8.0 # via # docker # localstack-core diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 5641155033bd8..c213975c6411f 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -37,7 +37,7 @@ awscli==1.32.84 # via localstack-core (pyproject.toml) awscrt==0.20.9 # via localstack-core -blinker==1.7.0 +blinker==1.8.1 # via # flask # quart @@ -261,13 +261,13 @@ pyasn1==0.6.0 # via rsa pycparser==2.22 # via cffi -pydantic==2.7.0 +pydantic==2.7.1 # via aws-sam-translator -pydantic-core==2.18.1 +pydantic-core==2.18.2 # via pydantic pygments==2.17.2 # via rich -pymongo==4.6.3 +pymongo==4.7.0 # via localstack-core (pyproject.toml) pyopenssl==24.1.0 # via @@ -276,7 +276,7 @@ pyopenssl==24.1.0 # localstack-twisted pyparsing==3.1.2 # via moto-ext -pyproject-hooks==1.0.0 +pyproject-hooks==1.1.0 # via build python-dateutil==2.9.0.post0 # via @@ -305,7 +305,7 @@ referencing==0.31.1 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2024.4.16 +regex==2024.4.28 # via cfn-lint requests==2.31.0 # via @@ -381,7 +381,7 @@ urllib3==2.2.1 # opensearch-py # requests # responses -websocket-client==1.7.0 +websocket-client==1.8.0 # via docker werkzeug==3.0.2 # via diff --git a/requirements-test.txt b/requirements-test.txt index 8ff9b61fc02ee..1ea868fdf0cb6 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -37,7 +37,7 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.138.0 +aws-cdk-lib==2.139.1 # via localstack-core (pyproject.toml) aws-sam-translator==1.87.0 # via @@ -49,7 +49,7 @@ awscli==1.32.84 # via localstack-core awscrt==0.20.9 # via localstack-core -blinker==1.7.0 +blinker==1.8.1 # via # flask # quart @@ -106,7 +106,7 @@ constantly==23.10.4 # via localstack-twisted constructs==10.3.0 # via aws-cdk-lib -coverage==7.4.4 +coverage==7.5.0 # via localstack-core (pyproject.toml) crontab==1.0.1 # via localstack-core @@ -324,13 +324,13 @@ pyasn1==0.6.0 # via rsa pycparser==2.22 # via cffi -pydantic==2.7.0 +pydantic==2.7.1 # via aws-sam-translator -pydantic-core==2.18.1 +pydantic-core==2.18.2 # via pydantic pygments==2.17.2 # via rich -pymongo==4.6.3 +pymongo==4.7.0 # via localstack-core pyopenssl==24.1.0 # via @@ -338,9 +338,9 @@ pyopenssl==24.1.0 # localstack-twisted pyparsing==3.1.2 # via moto-ext -pyproject-hooks==1.0.0 +pyproject-hooks==1.1.0 # via build -pytest==8.1.1 +pytest==8.2.0 # via # localstack-core (pyproject.toml) # pytest-rerunfailures @@ -382,7 +382,7 @@ referencing==0.31.1 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2024.4.16 +regex==2024.4.28 # via cfn-lint requests==2.31.0 # via @@ -472,7 +472,7 @@ urllib3==2.2.1 # opensearch-py # requests # responses -websocket-client==1.7.0 +websocket-client==1.8.0 # via # docker # localstack-core (pyproject.toml) diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 2b047a726a1cb..355ce06282fef 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -37,7 +37,7 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.138.0 +aws-cdk-lib==2.139.1 # via localstack-core aws-sam-translator==1.87.0 # via @@ -49,7 +49,7 @@ awscli==1.32.84 # via localstack-core awscrt==0.20.9 # via localstack-core -blinker==1.7.0 +blinker==1.8.1 # via # flask # quart @@ -71,7 +71,7 @@ botocore==1.34.84 # localstack-snapshot # moto-ext # s3transfer -botocore-stubs==1.34.89 +botocore-stubs==1.34.94 # via boto3-stubs build==1.2.1 # via @@ -112,11 +112,11 @@ constantly==23.10.4 # via localstack-twisted constructs==10.3.0 # via aws-cdk-lib -coverage==6.5.0 +coverage==7.4.4 # via # coveralls # localstack-core -coveralls==3.3.1 +coveralls==4.0.0 # via localstack-core crontab==1.0.1 # via localstack-core @@ -158,7 +158,7 @@ docopt==0.6.2 # via coveralls docutils==0.16 # via awscli -filelock==3.13.4 +filelock==3.14.0 # via virtualenv flask==3.0.3 # via @@ -289,7 +289,7 @@ mypy-boto3-acm==1.34.0 # via boto3-stubs mypy-boto3-acm-pca==1.34.28 # via boto3-stubs -mypy-boto3-amplify==1.34.63 +mypy-boto3-amplify==1.34.94 # via boto3-stubs mypy-boto3-apigateway==1.34.56 # via boto3-stubs @@ -301,7 +301,7 @@ mypy-boto3-appconfigdata==1.34.24 # via boto3-stubs mypy-boto3-application-autoscaling==1.34.0 # via boto3-stubs -mypy-boto3-appsync==1.34.49 +mypy-boto3-appsync==1.34.92 # via boto3-stubs mypy-boto3-athena==1.34.23 # via boto3-stubs @@ -311,7 +311,7 @@ mypy-boto3-backup==1.34.64 # via boto3-stubs mypy-boto3-batch==1.34.83 # via boto3-stubs -mypy-boto3-ce==1.34.71 +mypy-boto3-ce==1.34.90 # via boto3-stubs mypy-boto3-cloudcontrol==1.34.0 # via boto3-stubs @@ -327,17 +327,17 @@ mypy-boto3-codecommit==1.34.6 # via boto3-stubs mypy-boto3-cognito-identity==1.34.0 # via boto3-stubs -mypy-boto3-cognito-idp==1.34.59 +mypy-boto3-cognito-idp==1.34.93 # via boto3-stubs mypy-boto3-dms==1.34.0 # via boto3-stubs mypy-boto3-docdb==1.34.77 # via boto3-stubs -mypy-boto3-dynamodb==1.34.67 +mypy-boto3-dynamodb==1.34.91 # via boto3-stubs mypy-boto3-dynamodbstreams==1.34.0 # via boto3-stubs -mypy-boto3-ec2==1.34.86 +mypy-boto3-ec2==1.34.91 # via boto3-stubs mypy-boto3-ecr==1.34.0 # via boto3-stubs @@ -413,7 +413,7 @@ mypy-boto3-opensearch==1.34.43 # via boto3-stubs mypy-boto3-organizations==1.34.56 # via boto3-stubs -mypy-boto3-pi==1.34.0 +mypy-boto3-pi==1.34.90 # via boto3-stubs mypy-boto3-pipes==1.34.83 # via boto3-stubs @@ -421,7 +421,7 @@ mypy-boto3-qldb==1.34.49 # via boto3-stubs mypy-boto3-qldb-session==1.34.0 # via boto3-stubs -mypy-boto3-rds==1.34.83 +mypy-boto3-rds==1.34.93 # via boto3-stubs mypy-boto3-rds-data==1.34.6 # via boto3-stubs @@ -437,7 +437,7 @@ mypy-boto3-route53==1.34.31 # via boto3-stubs mypy-boto3-route53resolver==1.34.15 # via boto3-stubs -mypy-boto3-s3==1.34.65 +mypy-boto3-s3==1.34.91 # via boto3-stubs mypy-boto3-s3control==1.34.83 # via boto3-stubs @@ -459,19 +459,19 @@ mypy-boto3-sns==1.34.44 # via boto3-stubs mypy-boto3-sqs==1.34.0 # via boto3-stubs -mypy-boto3-ssm==1.34.61 +mypy-boto3-ssm==1.34.91 # via boto3-stubs mypy-boto3-sso-admin==1.34.0 # via boto3-stubs -mypy-boto3-stepfunctions==1.34.0 +mypy-boto3-stepfunctions==1.34.92 # via boto3-stubs mypy-boto3-sts==1.34.0 # via boto3-stubs -mypy-boto3-timestream-query==1.34.65 +mypy-boto3-timestream-query==1.34.94 # via boto3-stubs mypy-boto3-timestream-write==1.34.0 # via boto3-stubs -mypy-boto3-transcribe==1.34.0 +mypy-boto3-transcribe==1.34.94 # via boto3-stubs mypy-boto3-wafv2==1.34.58 # via boto3-stubs @@ -508,13 +508,13 @@ pbr==6.0.0 # jschema-to-python # sarif-om # stevedore -platformdirs==4.2.0 +platformdirs==4.2.1 # via virtualenv pluggy==1.5.0 # via # localstack-core # pytest -plumbum==1.8.2 +plumbum==1.8.3 # via pandoc plux==1.9.0 # via @@ -549,13 +549,13 @@ pyasn1==0.6.0 # via rsa pycparser==2.22 # via cffi -pydantic==2.7.0 +pydantic==2.7.1 # via aws-sam-translator -pydantic-core==2.18.1 +pydantic-core==2.18.2 # via pydantic pygments==2.17.2 # via rich -pymongo==4.6.3 +pymongo==4.7.0 # via localstack-core pyopenssl==24.1.0 # via @@ -565,9 +565,9 @@ pypandoc==1.13 # via localstack-core pyparsing==3.1.2 # via moto-ext -pyproject-hooks==1.0.0 +pyproject-hooks==1.1.0 # via build -pytest==8.1.1 +pytest==8.2.0 # via # localstack-core # pytest-rerunfailures @@ -610,7 +610,7 @@ referencing==0.31.1 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2024.4.16 +regex==2024.4.28 # via cfn-lint requests==2.31.0 # via @@ -645,7 +645,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core -ruff==0.4.1 +ruff==0.4.2 # via localstack-core s3transfer==0.10.1 # via @@ -806,9 +806,9 @@ urllib3==2.2.1 # opensearch-py # requests # responses -virtualenv==20.25.3 +virtualenv==20.26.1 # via pre-commit -websocket-client==1.7.0 +websocket-client==1.8.0 # via # docker # localstack-core From 5bfa1de3d22c43fd3b8879a7b012cc87851a54dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristopher=20Pinz=C3=B3n?= <cristopher.pinzon@gmail.com> Date: Tue, 30 Apr 2024 17:00:23 -0500 Subject: [PATCH 108/169] add resource providers for vpc endpoint and prefix list (#10735) --- .../resource_providers/aws_ec2_prefixlist.py | 167 +++++++++++++++ .../aws_ec2_prefixlist.schema.json | 152 ++++++++++++++ .../aws_ec2_prefixlist_plugin.py | 20 ++ .../resource_providers/aws_ec2_vpcendpoint.py | 180 ++++++++++++++++ .../aws_ec2_vpcendpoint.schema.json | 140 +++++++++++++ .../aws_ec2_vpcendpoint_plugin.py | 20 ++ .../resource_providers/ec2/test_ec2.py | 57 ++++++ .../ec2/test_ec2.snapshot.json | 193 ++++++++++++++++++ .../ec2/test_ec2.validation.json | 8 +- tests/aws/templates/ec2_prefixlist.yml | 23 +++ tests/aws/templates/ec2_vpc_endpoint.yml | 58 ++++++ 11 files changed, 1017 insertions(+), 1 deletion(-) create mode 100644 localstack/services/ec2/resource_providers/aws_ec2_prefixlist.py create mode 100644 localstack/services/ec2/resource_providers/aws_ec2_prefixlist.schema.json create mode 100644 localstack/services/ec2/resource_providers/aws_ec2_prefixlist_plugin.py create mode 100644 localstack/services/ec2/resource_providers/aws_ec2_vpcendpoint.py create mode 100644 localstack/services/ec2/resource_providers/aws_ec2_vpcendpoint.schema.json create mode 100644 localstack/services/ec2/resource_providers/aws_ec2_vpcendpoint_plugin.py create mode 100644 tests/aws/templates/ec2_prefixlist.yml create mode 100644 tests/aws/templates/ec2_vpc_endpoint.yml diff --git a/localstack/services/ec2/resource_providers/aws_ec2_prefixlist.py b/localstack/services/ec2/resource_providers/aws_ec2_prefixlist.py new file mode 100644 index 0000000000000..8308fb5bfa990 --- /dev/null +++ b/localstack/services/ec2/resource_providers/aws_ec2_prefixlist.py @@ -0,0 +1,167 @@ +# LocalStack Resource Provider Scaffolding v2 +from __future__ import annotations + +from pathlib import Path +from typing import Optional, TypedDict + +import localstack.services.cloudformation.provider_utils as util +from localstack.services.cloudformation.resource_provider import ( + OperationStatus, + ProgressEvent, + ResourceProvider, + ResourceRequest, +) + + +class EC2PrefixListProperties(TypedDict): + AddressFamily: Optional[str] + MaxEntries: Optional[int] + PrefixListName: Optional[str] + Arn: Optional[str] + Entries: Optional[list[Entry]] + OwnerId: Optional[str] + PrefixListId: Optional[str] + Tags: Optional[list[Tag]] + Version: Optional[int] + + +class Tag(TypedDict): + Key: Optional[str] + Value: Optional[str] + + +class Entry(TypedDict): + Cidr: Optional[str] + Description: Optional[str] + + +REPEATED_INVOCATION = "repeated_invocation" + + +class EC2PrefixListProvider(ResourceProvider[EC2PrefixListProperties]): + TYPE = "AWS::EC2::PrefixList" # Autogenerated. Don't change + SCHEMA = util.get_schema_path(Path(__file__)) # Autogenerated. Don't change + + def create( + self, + request: ResourceRequest[EC2PrefixListProperties], + ) -> ProgressEvent[EC2PrefixListProperties]: + """ + Create a new resource. + + Primary identifier fields: + - /properties/PrefixListId + + Required properties: + - PrefixListName + - MaxEntries + - AddressFamily + + + + Read-only properties: + - /properties/PrefixListId + - /properties/OwnerId + - /properties/Version + - /properties/Arn + + IAM permissions required: + - EC2:CreateManagedPrefixList + - EC2:DescribeManagedPrefixLists + - EC2:CreateTags + + """ + model = request.desired_state + + if not request.custom_context.get(REPEATED_INVOCATION): + create_params = util.select_attributes( + model, ["PrefixListName", "Entries", "MaxEntries", "AddressFamily", "Tags"] + ) + + if "Tags" in create_params: + create_params["TagSpecifications"] = [ + {"ResourceType": "prefix-list", "Tags": create_params.pop("Tags")} + ] + + response = request.aws_client_factory.ec2.create_managed_prefix_list(**create_params) + model["Arn"] = response["PrefixList"]["PrefixListId"] + model["OwnerId"] = response["PrefixList"]["OwnerId"] + model["PrefixListId"] = response["PrefixList"]["PrefixListId"] + model["Version"] = response["PrefixList"]["Version"] + request.custom_context[REPEATED_INVOCATION] = True + return ProgressEvent( + status=OperationStatus.IN_PROGRESS, + resource_model=model, + custom_context=request.custom_context, + ) + + response = request.aws_client_factory.ec2.describe_managed_prefix_lists( + PrefixListIds=[model["PrefixListId"]] + ) + if not response["PrefixLists"]: + return ProgressEvent( + status=OperationStatus.FAILED, + resource_model=model, + custom_context=request.custom_context, + message="Resource not found after creation", + ) + + return ProgressEvent( + status=OperationStatus.SUCCESS, + resource_model=model, + custom_context=request.custom_context, + ) + + def read( + self, + request: ResourceRequest[EC2PrefixListProperties], + ) -> ProgressEvent[EC2PrefixListProperties]: + """ + Fetch resource information + + IAM permissions required: + - EC2:GetManagedPrefixListEntries + - EC2:DescribeManagedPrefixLists + """ + raise NotImplementedError + + def delete( + self, + request: ResourceRequest[EC2PrefixListProperties], + ) -> ProgressEvent[EC2PrefixListProperties]: + """ + Delete a resource + + IAM permissions required: + - EC2:DeleteManagedPrefixList + - EC2:DescribeManagedPrefixLists + """ + + model = request.previous_state + response = request.aws_client_factory.ec2.describe_managed_prefix_lists( + PrefixListIds=[model["PrefixListId"]] + ) + + if not response["PrefixLists"]: + return ProgressEvent(status=OperationStatus.SUCCESS, resource_model=model) + + request.aws_client_factory.ec2.delete_managed_prefix_list( + PrefixListId=request.previous_state["PrefixListId"] + ) + return ProgressEvent(status=OperationStatus.IN_PROGRESS, resource_model=model) + + def update( + self, + request: ResourceRequest[EC2PrefixListProperties], + ) -> ProgressEvent[EC2PrefixListProperties]: + """ + Update a resource + + IAM permissions required: + - EC2:DescribeManagedPrefixLists + - EC2:GetManagedPrefixListEntries + - EC2:ModifyManagedPrefixList + - EC2:CreateTags + - EC2:DeleteTags + """ + raise NotImplementedError diff --git a/localstack/services/ec2/resource_providers/aws_ec2_prefixlist.schema.json b/localstack/services/ec2/resource_providers/aws_ec2_prefixlist.schema.json new file mode 100644 index 0000000000000..cb27aefee2bd3 --- /dev/null +++ b/localstack/services/ec2/resource_providers/aws_ec2_prefixlist.schema.json @@ -0,0 +1,152 @@ +{ + "typeName": "AWS::EC2::PrefixList", + "description": "Resource schema of AWS::EC2::PrefixList Type", + "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-rpdk.git", + "definitions": { + "Tag": { + "type": "object", + "properties": { + "Key": { + "type": "string", + "minLength": 1, + "maxLength": 128 + }, + "Value": { + "type": "string", + "maxLength": 256 + } + }, + "required": [ + "Key" + ], + "additionalProperties": false + }, + "Entry": { + "type": "object", + "properties": { + "Cidr": { + "type": "string", + "minLength": 1, + "maxLength": 46 + }, + "Description": { + "type": "string", + "minLength": 0, + "maxLength": 255 + } + }, + "required": [ + "Cidr" + ], + "additionalProperties": false + } + }, + "properties": { + "PrefixListName": { + "description": "Name of Prefix List.", + "type": "string", + "minLength": 1, + "maxLength": 255 + }, + "PrefixListId": { + "description": "Id of Prefix List.", + "type": "string" + }, + "OwnerId": { + "description": "Owner Id of Prefix List.", + "type": "string" + }, + "AddressFamily": { + "description": "Ip Version of Prefix List.", + "type": "string", + "enum": [ + "IPv4", + "IPv6" + ] + }, + "MaxEntries": { + "description": "Max Entries of Prefix List.", + "type": "integer", + "minimum": 1 + }, + "Version": { + "description": "Version of Prefix List.", + "type": "integer" + }, + "Tags": { + "description": "Tags for Prefix List", + "type": "array", + "items": { + "$ref": "#/definitions/Tag" + } + }, + "Entries": { + "description": "Entries of Prefix List.", + "type": "array", + "items": { + "$ref": "#/definitions/Entry" + } + }, + "Arn": { + "description": "The Amazon Resource Name (ARN) of the Prefix List.", + "type": "string" + } + }, + "required": [ + "PrefixListName", + "MaxEntries", + "AddressFamily" + ], + "readOnlyProperties": [ + "/properties/PrefixListId", + "/properties/OwnerId", + "/properties/Version", + "/properties/Arn" + ], + "primaryIdentifier": [ + "/properties/PrefixListId" + ], + "tagging": { + "taggable": true, + "tagOnCreate": true, + "tagUpdatable": true, + "cloudFormationSystemTags": true + }, + "handlers": { + "create": { + "permissions": [ + "EC2:CreateManagedPrefixList", + "EC2:DescribeManagedPrefixLists", + "EC2:CreateTags" + ] + }, + "read": { + "permissions": [ + "EC2:GetManagedPrefixListEntries", + "EC2:DescribeManagedPrefixLists" + ] + }, + "update": { + "permissions": [ + "EC2:DescribeManagedPrefixLists", + "EC2:GetManagedPrefixListEntries", + "EC2:ModifyManagedPrefixList", + "EC2:CreateTags", + "EC2:DeleteTags" + ] + }, + "delete": { + "permissions": [ + "EC2:DeleteManagedPrefixList", + "EC2:DescribeManagedPrefixLists" + ] + }, + "list": { + "permissions": [ + "EC2:DescribeManagedPrefixLists", + "EC2:GetManagedPrefixListEntries" + ] + } + }, + "additionalProperties": false +} diff --git a/localstack/services/ec2/resource_providers/aws_ec2_prefixlist_plugin.py b/localstack/services/ec2/resource_providers/aws_ec2_prefixlist_plugin.py new file mode 100644 index 0000000000000..5d8b993d28409 --- /dev/null +++ b/localstack/services/ec2/resource_providers/aws_ec2_prefixlist_plugin.py @@ -0,0 +1,20 @@ +from typing import Optional, Type + +from localstack.services.cloudformation.resource_provider import ( + CloudFormationResourceProviderPlugin, + ResourceProvider, +) + + +class EC2PrefixListProviderPlugin(CloudFormationResourceProviderPlugin): + name = "AWS::EC2::PrefixList" + + def __init__(self): + self.factory: Optional[Type[ResourceProvider]] = None + + def load(self): + from localstack.services.ec2.resource_providers.aws_ec2_prefixlist import ( + EC2PrefixListProvider, + ) + + self.factory = EC2PrefixListProvider diff --git a/localstack/services/ec2/resource_providers/aws_ec2_vpcendpoint.py b/localstack/services/ec2/resource_providers/aws_ec2_vpcendpoint.py new file mode 100644 index 0000000000000..420efcb8029ee --- /dev/null +++ b/localstack/services/ec2/resource_providers/aws_ec2_vpcendpoint.py @@ -0,0 +1,180 @@ +# LocalStack Resource Provider Scaffolding v2 +from __future__ import annotations + +from pathlib import Path +from typing import Optional, TypedDict + +import localstack.services.cloudformation.provider_utils as util +from localstack.services.cloudformation.resource_provider import ( + OperationStatus, + ProgressEvent, + ResourceProvider, + ResourceRequest, +) + + +class EC2VPCEndpointProperties(TypedDict): + ServiceName: Optional[str] + VpcId: Optional[str] + CreationTimestamp: Optional[str] + DnsEntries: Optional[list[str]] + Id: Optional[str] + NetworkInterfaceIds: Optional[list[str]] + PolicyDocument: Optional[str | dict] + PrivateDnsEnabled: Optional[bool] + RouteTableIds: Optional[list[str]] + SecurityGroupIds: Optional[list[str]] + SubnetIds: Optional[list[str]] + VpcEndpointType: Optional[str] + + +REPEATED_INVOCATION = "repeated_invocation" + + +class EC2VPCEndpointProvider(ResourceProvider[EC2VPCEndpointProperties]): + TYPE = "AWS::EC2::VPCEndpoint" # Autogenerated. Don't change + SCHEMA = util.get_schema_path(Path(__file__)) # Autogenerated. Don't change + + def create( + self, + request: ResourceRequest[EC2VPCEndpointProperties], + ) -> ProgressEvent[EC2VPCEndpointProperties]: + """ + Create a new resource. + + Primary identifier fields: + - /properties/Id + + Required properties: + - VpcId + - ServiceName + + Create-only properties: + - /properties/ServiceName + - /properties/VpcEndpointType + - /properties/VpcId + + Read-only properties: + - /properties/NetworkInterfaceIds + - /properties/CreationTimestamp + - /properties/DnsEntries + - /properties/Id + + IAM permissions required: + - ec2:CreateVpcEndpoint + - ec2:DescribeVpcEndpoints + + """ + model = request.desired_state + create_params = util.select_attributes( + model, + [ + "PolidyDocument", + "PrivateDnsEnabled", + "RouteTablesIds", + "SecurityGroupIds", + "ServiceName", + "SubnetIds", + "VpcEndpointType", + "VpcId", + ], + ) + + if not request.custom_context.get(REPEATED_INVOCATION): + response = request.aws_client_factory.ec2.create_vpc_endpoint(**create_params) + model["Id"] = response["VpcEndpoint"]["VpcEndpointId"] + model["DnsEntries"] = response["VpcEndpoint"]["DnsEntries"] + model["CreationTimestamp"] = response["VpcEndpoint"]["CreationTimestamp"] + model["NetworkInterfaceIds"] = response["VpcEndpoint"]["NetworkInterfaceIds"] + request.custom_context[REPEATED_INVOCATION] = True + return ProgressEvent( + status=OperationStatus.IN_PROGRESS, + resource_model=model, + custom_context=request.custom_context, + ) + + response = request.aws_client_factory.ec2.describe_vpc_endpoints( + VpcEndpointIds=[model["Id"]] + ) + if not response["VpcEndpoints"]: + return ProgressEvent( + status=OperationStatus.FAILED, + resource_model=model, + custom_context=request.custom_context, + message="Resource not found after creation", + ) + + state = response["VpcEndpoints"][0][ + "State" + ].lower() # API specifies capital but lowercase is returned + match state: + case "available": + return ProgressEvent(status=OperationStatus.SUCCESS, resource_model=model) + case "pending": + return ProgressEvent(status=OperationStatus.IN_PROGRESS, resource_model=model) + case "pendingacceptance": + return ProgressEvent(status=OperationStatus.IN_PROGRESS, resource_model=model) + case _: + return ProgressEvent( + status=OperationStatus.FAILED, + resource_model=model, + message=f"Invalid state '{state}' for resource", + ) + + def read( + self, + request: ResourceRequest[EC2VPCEndpointProperties], + ) -> ProgressEvent[EC2VPCEndpointProperties]: + """ + Fetch resource information + + IAM permissions required: + - ec2:DescribeVpcEndpoints + """ + raise NotImplementedError + + def delete( + self, + request: ResourceRequest[EC2VPCEndpointProperties], + ) -> ProgressEvent[EC2VPCEndpointProperties]: + """ + Delete a resource + + IAM permissions required: + - ec2:DeleteVpcEndpoints + - ec2:DescribeVpcEndpoints + """ + model = request.previous_state + response = request.aws_client_factory.ec2.describe_vpc_endpoints( + VpcEndpointIds=[model["Id"]] + ) + + if not response["VpcEndpoints"]: + return ProgressEvent( + status=OperationStatus.FAILED, + resource_model=model, + message="Resource not found for deletion", + ) + + state = response["VpcEndpoints"][0]["State"].lower() + match state: + case "deleted": + return ProgressEvent(status=OperationStatus.SUCCESS, resource_model=model) + case "deleting": + return ProgressEvent(status=OperationStatus.IN_PROGRESS, resource_model=model) + case _: + request.aws_client_factory.ec2.delete_vpc_endpoints(VpcEndpointIds=[model["Id"]]) + return ProgressEvent(status=OperationStatus.IN_PROGRESS, resource_model=model) + + def update( + self, + request: ResourceRequest[EC2VPCEndpointProperties], + ) -> ProgressEvent[EC2VPCEndpointProperties]: + """ + Update a resource + + IAM permissions required: + - ec2:ModifyVpcEndpoint + - ec2:DescribeVpcEndpoints + """ + raise NotImplementedError diff --git a/localstack/services/ec2/resource_providers/aws_ec2_vpcendpoint.schema.json b/localstack/services/ec2/resource_providers/aws_ec2_vpcendpoint.schema.json new file mode 100644 index 0000000000000..c8dcc84644d4c --- /dev/null +++ b/localstack/services/ec2/resource_providers/aws_ec2_vpcendpoint.schema.json @@ -0,0 +1,140 @@ +{ + "typeName": "AWS::EC2::VPCEndpoint", + "description": "Resource Type definition for AWS::EC2::VPCEndpoint", + "additionalProperties": false, + "properties": { + "Id": { + "type": "string" + }, + "CreationTimestamp": { + "type": "string" + }, + "DnsEntries": { + "type": "array", + "uniqueItems": false, + "insertionOrder": false, + "items": { + "type": "string" + } + }, + "NetworkInterfaceIds": { + "type": "array", + "uniqueItems": false, + "insertionOrder": false, + "items": { + "type": "string" + } + }, + "PolicyDocument": { + "type": [ + "string", + "object" + ], + "description": "A policy to attach to the endpoint that controls access to the service." + }, + "PrivateDnsEnabled": { + "type": "boolean", + "description": "Indicate whether to associate a private hosted zone with the specified VPC." + }, + "RouteTableIds": { + "type": "array", + "description": "One or more route table IDs.", + "uniqueItems": true, + "insertionOrder": false, + "items": { + "type": "string" + } + }, + "SecurityGroupIds": { + "type": "array", + "description": "The ID of one or more security groups to associate with the endpoint network interface.", + "uniqueItems": true, + "insertionOrder": false, + "items": { + "type": "string" + } + }, + "ServiceName": { + "type": "string", + "description": "The service name." + }, + "SubnetIds": { + "type": "array", + "description": "The ID of one or more subnets in which to create an endpoint network interface.", + "uniqueItems": true, + "insertionOrder": false, + "items": { + "type": "string" + } + }, + "VpcEndpointType": { + "type": "string", + "enum": [ + "Interface", + "Gateway", + "GatewayLoadBalancer" + ] + }, + "VpcId": { + "type": "string", + "description": "The ID of the VPC in which the endpoint will be used." + } + }, + "required": [ + "VpcId", + "ServiceName" + ], + "readOnlyProperties": [ + "/properties/NetworkInterfaceIds", + "/properties/CreationTimestamp", + "/properties/DnsEntries", + "/properties/Id" + ], + "createOnlyProperties": [ + "/properties/ServiceName", + "/properties/VpcEndpointType", + "/properties/VpcId" + ], + "primaryIdentifier": [ + "/properties/Id" + ], + "tagging": { + "taggable": false, + "tagOnCreate": false, + "tagUpdatable": false, + "cloudFormationSystemTags": false + }, + "handlers": { + "create": { + "permissions": [ + "ec2:CreateVpcEndpoint", + "ec2:DescribeVpcEndpoints" + ], + "timeoutInMinutes": 210 + }, + "read": { + "permissions": [ + "ec2:DescribeVpcEndpoints" + ] + }, + "update": { + "permissions": [ + "ec2:ModifyVpcEndpoint", + "ec2:DescribeVpcEndpoints" + ], + "timeoutInMinutes": 210 + }, + "delete": { + "permissions": [ + "ec2:DeleteVpcEndpoints", + "ec2:DescribeVpcEndpoints" + ], + "timeoutInMinutes": 210 + }, + "list": { + "permissions": [ + "ec2:DescribeVpcEndpoints" + ] + } + } +} diff --git a/localstack/services/ec2/resource_providers/aws_ec2_vpcendpoint_plugin.py b/localstack/services/ec2/resource_providers/aws_ec2_vpcendpoint_plugin.py new file mode 100644 index 0000000000000..e0e1d228a95de --- /dev/null +++ b/localstack/services/ec2/resource_providers/aws_ec2_vpcendpoint_plugin.py @@ -0,0 +1,20 @@ +from typing import Optional, Type + +from localstack.services.cloudformation.resource_provider import ( + CloudFormationResourceProviderPlugin, + ResourceProvider, +) + + +class EC2VPCEndpointProviderPlugin(CloudFormationResourceProviderPlugin): + name = "AWS::EC2::VPCEndpoint" + + def __init__(self): + self.factory: Optional[Type[ResourceProvider]] = None + + def load(self): + from localstack.services.ec2.resource_providers.aws_ec2_vpcendpoint import ( + EC2VPCEndpointProvider, + ) + + self.factory = EC2VPCEndpointProvider diff --git a/tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.py b/tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.py index 880e8f16fe4e6..0f9d251971a5d 100644 --- a/tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.py +++ b/tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.py @@ -2,6 +2,7 @@ import pytest from botocore.exceptions import ClientError +from localstack_snapshot.snapshots.transformer import SortingTransformer from localstack.testing.pytest import markers @@ -35,3 +36,59 @@ def test_deploy_instance_with_key_pair(deploy_cfn_template, aws_client, snapshot with pytest.raises(ClientError) as e: aws_client.ec2.describe_key_pairs(KeyNames=[key_name]) snapshot.match("key_pair_deleted", e.value.response) + + +@markers.aws.validated +def test_deploy_prefix_list(deploy_cfn_template, aws_client, snapshot): + stack = deploy_cfn_template( + template_path=os.path.join( + os.path.dirname(__file__), "../../../../templates/ec2_prefixlist.yml" + ) + ) + + snapshot.add_transformer(snapshot.transform.cloudformation_api()) + description = aws_client.cloudformation.describe_stack_resources(StackName=stack.stack_name) + snapshot.match("resource-description", description) + + prefix_id = stack.outputs["PrefixRef"] + prefix_list = aws_client.ec2.describe_managed_prefix_lists(PrefixListIds=[prefix_id]) + snapshot.match("prefix-list", prefix_list) + snapshot.add_transformer(snapshot.transform.key_value("PrefixListId")) + + +@markers.aws.validated +@markers.snapshot.skip_snapshot_verify( + paths=[ + "$..DnsEntries", + "$..Groups", + "$..NetworkInterfaceIds", + "$..SubnetIds", + ] +) +def test_deploy_vpc_endpoint(deploy_cfn_template, aws_client, snapshot): + stack = deploy_cfn_template( + template_path=os.path.join( + os.path.dirname(__file__), "../../../../templates/ec2_vpc_endpoint.yml" + ) + ) + + snapshot.add_transformer(snapshot.transform.cloudformation_api()) + + snapshot.add_transformer( + SortingTransformer("StackResources", lambda sr: sr["LogicalResourceId"]), priority=-1 + ) + description = aws_client.cloudformation.describe_stack_resources(StackName=stack.stack_name) + snapshot.match("resource-description", description) + + endpoint_id = stack.outputs["EndpointRef"] + endpoint = aws_client.ec2.describe_vpc_endpoints(VpcEndpointIds=[endpoint_id]) + snapshot.match("endpoint", endpoint) + + snapshot.add_transformer(snapshot.transform.key_value("VpcEndpointId")) + snapshot.add_transformer(snapshot.transform.key_value("DnsName")) + snapshot.add_transformer(snapshot.transform.key_value("HostedZoneId")) + snapshot.add_transformer(snapshot.transform.key_value("GroupId")) + snapshot.add_transformer(snapshot.transform.key_value("GroupName")) + snapshot.add_transformer(snapshot.transform.regex(stack.outputs["VpcId"], "vpc-id")) + snapshot.add_transformer(snapshot.transform.regex(stack.outputs["SubnetBId"], "subnet-b-id")) + snapshot.add_transformer(snapshot.transform.regex(stack.outputs["SubnetAId"], "subnet-a-id")) diff --git a/tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.snapshot.json b/tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.snapshot.json index 1132bff43ea34..54a9e40362151 100644 --- a/tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.snapshot.json +++ b/tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.snapshot.json @@ -29,5 +29,198 @@ } } } + }, + "tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.py::test_deploy_prefix_list": { + "recorded-date": "30-04-2024, 19:32:40", + "recorded-content": { + "resource-description": { + "StackResources": [ + { + "DriftInformation": { + "StackResourceDriftStatus": "NOT_CHECKED" + }, + "LogicalResourceId": "NewPrefixList", + "PhysicalResourceId": "<resource:2>", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::EC2::PrefixList", + "StackId": "arn:aws:cloudformation:<region>:111111111111:stack/<stack-name:1>/<resource:1>", + "StackName": "<stack-name:1>", + "Timestamp": "timestamp" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "prefix-list": { + "PrefixLists": [ + { + "AddressFamily": "IPv4", + "MaxEntries": 10, + "OwnerId": "111111111111", + "PrefixListArn": "arn:aws:ec2:<region>:111111111111:prefix-list/<resource:2>", + "PrefixListId": "<resource:2>", + "PrefixListName": "vpc-1-servers", + "State": "create-complete", + "Tags": [ + { + "Key": "Name", + "Value": "VPC-1-Servers" + } + ], + "Version": 1 + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.py::test_deploy_vpc_endpoint": { + "recorded-date": "30-04-2024, 20:01:19", + "recorded-content": { + "resource-description": { + "StackResources": [ + { + "DriftInformation": { + "StackResourceDriftStatus": "NOT_CHECKED" + }, + "LogicalResourceId": "CWLInterfaceEndpoint", + "PhysicalResourceId": "<vpc-endpoint-id:1>", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::EC2::VPCEndpoint", + "StackId": "arn:aws:cloudformation:<region>:111111111111:stack/<stack-name:1>/<resource:1>", + "StackName": "<stack-name:1>", + "Timestamp": "timestamp" + }, + { + "DriftInformation": { + "StackResourceDriftStatus": "NOT_CHECKED" + }, + "LogicalResourceId": "mySecurityGroup", + "PhysicalResourceId": "<group-id:1>", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::EC2::SecurityGroup", + "StackId": "arn:aws:cloudformation:<region>:111111111111:stack/<stack-name:1>/<resource:1>", + "StackName": "<stack-name:1>", + "Timestamp": "timestamp" + }, + { + "DriftInformation": { + "StackResourceDriftStatus": "NOT_CHECKED" + }, + "LogicalResourceId": "myVPC", + "PhysicalResourceId": "vpc-id", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::EC2::VPC", + "StackId": "arn:aws:cloudformation:<region>:111111111111:stack/<stack-name:1>/<resource:1>", + "StackName": "<stack-name:1>", + "Timestamp": "timestamp" + }, + { + "DriftInformation": { + "StackResourceDriftStatus": "NOT_CHECKED" + }, + "LogicalResourceId": "subnetA", + "PhysicalResourceId": "subnet-a-id", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::EC2::Subnet", + "StackId": "arn:aws:cloudformation:<region>:111111111111:stack/<stack-name:1>/<resource:1>", + "StackName": "<stack-name:1>", + "Timestamp": "timestamp" + }, + { + "DriftInformation": { + "StackResourceDriftStatus": "NOT_CHECKED" + }, + "LogicalResourceId": "subnetB", + "PhysicalResourceId": "subnet-b-id", + "ResourceStatus": "CREATE_COMPLETE", + "ResourceType": "AWS::EC2::Subnet", + "StackId": "arn:aws:cloudformation:<region>:111111111111:stack/<stack-name:1>/<resource:1>", + "StackName": "<stack-name:1>", + "Timestamp": "timestamp" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "endpoint": { + "VpcEndpoints": [ + { + "CreationTimestamp": "timestamp", + "DnsEntries": [ + { + "DnsName": "<vpc-endpoint-id:1>-g5hws96k.logs.<region>.vpce.amazonaws.com", + "HostedZoneId": "<hosted-zone-id:1>" + }, + { + "DnsName": "<vpc-endpoint-id:1>-g5hws96k-<region>b.logs.<region>.vpce.amazonaws.com", + "HostedZoneId": "<hosted-zone-id:1>" + }, + { + "DnsName": "<vpc-endpoint-id:1>-g5hws96k-<region>a.logs.<region>.vpce.amazonaws.com", + "HostedZoneId": "<hosted-zone-id:1>" + }, + { + "DnsName": "<dns-name:4>", + "HostedZoneId": "<hosted-zone-id:2>" + }, + { + "DnsName": "<dns-name:5>", + "HostedZoneId": "<hosted-zone-id:3>" + } + ], + "DnsOptions": { + "DnsRecordIpType": "ipv4" + }, + "Groups": [ + { + "GroupId": "<group-id:1>", + "GroupName": "<stack-name:1>-mySecurityGroup-RWU3KD7UZFAy" + } + ], + "IpAddressType": "ipv4", + "NetworkInterfaceIds": [ + "eni-0b89833f2bf9a89c0", + "eni-05151d42b885fbd35" + ], + "OwnerId": "111111111111", + "PolicyDocument": { + "Statement": [ + { + "Action": "*", + "Effect": "Allow", + "Principal": "*", + "Resource": "*" + } + ] + }, + "PrivateDnsEnabled": true, + "RequesterManaged": false, + "RouteTableIds": [], + "ServiceName": "com.amazonaws.<region>.logs", + "State": "available", + "SubnetIds": [ + "subnet-a-id", + "subnet-b-id" + ], + "Tags": [], + "VpcEndpointId": "<vpc-endpoint-id:1>", + "VpcEndpointType": "Interface", + "VpcId": "vpc-id" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.validation.json b/tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.validation.json index e0cdb63e68937..320d8da5177d9 100644 --- a/tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.validation.json +++ b/tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.validation.json @@ -1,5 +1,11 @@ { "tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.py::test_deploy_instance_with_key_pair": { "last_validated_date": "2024-01-30T21:09:52+00:00" + }, + "tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.py::test_deploy_prefix_list": { + "last_validated_date": "2024-04-26T16:18:18+00:00" + }, + "tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.py::test_deploy_vpc_endpoint": { + "last_validated_date": "2024-04-30T20:01:19+00:00" } -} \ No newline at end of file +} diff --git a/tests/aws/templates/ec2_prefixlist.yml b/tests/aws/templates/ec2_prefixlist.yml new file mode 100644 index 0000000000000..1cb2e7dac7ed8 --- /dev/null +++ b/tests/aws/templates/ec2_prefixlist.yml @@ -0,0 +1,23 @@ +Resources: + NewPrefixList: + Type: AWS::EC2::PrefixList + Properties: + PrefixListName: "vpc-1-servers" + AddressFamily: "IPv4" + MaxEntries: 10 + Entries: + - Cidr: "10.0.0.5/32" + Description: "Server 1" + - Cidr: "10.0.0.10/32" + Description: "Server 2" + Tags: + - Key: "Name" + Value: "VPC-1-Servers" + +Outputs: + PrefixRef: + Value: !Ref NewPrefixList + PrefixArn: + Value: !GetAtt NewPrefixList.Arn + PrefixId: + Value: !GetAtt NewPrefixList.PrefixListId diff --git a/tests/aws/templates/ec2_vpc_endpoint.yml b/tests/aws/templates/ec2_vpc_endpoint.yml new file mode 100644 index 0000000000000..901eed3e229d7 --- /dev/null +++ b/tests/aws/templates/ec2_vpc_endpoint.yml @@ -0,0 +1,58 @@ +Resources: + CWLInterfaceEndpoint: + Type: 'AWS::EC2::VPCEndpoint' + Properties: + VpcEndpointType: 'Interface' + ServiceName: !Sub 'com.amazonaws.${AWS::Region}.logs' + VpcId: !Ref myVPC + PrivateDnsEnabled: true + SubnetIds: + - !Ref subnetA + - !Ref subnetB + SecurityGroupIds: + - !Ref mySecurityGroup + myVPC: + Type: 'AWS::EC2::VPC' + Properties: + CidrBlock: 10.0.0.0/16 + EnableDnsSupport: true + EnableDnsHostnames: true + Tags: + - Key: 'Name' + Value: 'myVPC' + subnetA: + Type: 'AWS::EC2::Subnet' + Properties: + VpcId: !Ref myVPC + CidrBlock: '10.0.1.0/24' + AvailabilityZone: !Select [ 0, !GetAZs ] + subnetB: + Type: 'AWS::EC2::Subnet' + Properties: + VpcId: !Ref myVPC + CidrBlock: '10.0.2.0/24' + AvailabilityZone: !Select [ 1, !GetAZs ] + mySecurityGroup: + Type: 'AWS::EC2::SecurityGroup' + Properties: + GroupDescription: 'Allow HTTPS traffic from the VPC' + VpcId: !Ref myVPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: !GetAtt myVPC.CidrBlock + +Outputs: + EndpointRef: + Value: !Ref CWLInterfaceEndpoint + EndpointCreationTimestamp: + Value: !GetAtt CWLInterfaceEndpoint.CreationTimestamp + Id: + Value: !GetAtt CWLInterfaceEndpoint.Id + VpcId: + Value: !Ref myVPC + SubnetAId: + Value: !Ref subnetA + SubnetBId: + Value: !Ref subnetB From 0da986511e54ebd2657ac8cc7ba701e2d250fe81 Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Wed, 1 May 2024 08:21:53 +0200 Subject: [PATCH 109/169] StepFunctions: Improve Handling of Empty SendTaskFailure Calls (#10750) --- .../component/common/catch/catcher_decl.py | 5 +- .../common/error_name/custom_error_name.py | 10 +- .../component/common/error_name/error_name.py | 10 +- .../service/state_task_service_callback.py | 5 +- .../asl/eval/callback/callback.py | 6 +- .../templates/callbacks/callback_templates.py | 6 + .../sqs_parallel_wait_for_task_token.json5 | 53 ++ .../sqs_wait_for_task_token_catch.json5 | 42 ++ .../v2/callback/test_callback.py | 75 +++ .../v2/callback/test_callback.snapshot.json | 454 ++++++++++++++++++ .../v2/callback/test_callback.validation.json | 6 + 11 files changed, 656 insertions(+), 16 deletions(-) create mode 100644 tests/aws/services/stepfunctions/templates/callbacks/statemachines/sqs_parallel_wait_for_task_token.json5 create mode 100644 tests/aws/services/stepfunctions/templates/callbacks/statemachines/sqs_wait_for_task_token_catch.json5 diff --git a/localstack/services/stepfunctions/asl/component/common/catch/catcher_decl.py b/localstack/services/stepfunctions/asl/component/common/catch/catcher_decl.py index 1b4e0dd51614e..f306b49845e92 100644 --- a/localstack/services/stepfunctions/asl/component/common/catch/catcher_decl.py +++ b/localstack/services/stepfunctions/asl/component/common/catch/catcher_decl.py @@ -74,8 +74,9 @@ def _extract_error_cause(failure_event: FailureEvent) -> dict: f"Internal Error: invalid event details declaration in FailureEvent: '{failure_event}'." ) spec_event_details: dict = list(failure_event.event_details.values())[0] - error = spec_event_details["error"] - cause = spec_event_details.get("cause") or "" + # If no cause or error fields are given, AWS binds an empty string; otherwise it attaches the value. + error = spec_event_details.get("error", "") + cause = spec_event_details.get("cause", "") # Stepfunctions renames these fields to capital in this scenario. return { "Error": error, diff --git a/localstack/services/stepfunctions/asl/component/common/error_name/custom_error_name.py b/localstack/services/stepfunctions/asl/component/common/error_name/custom_error_name.py index 089ce1a918eef..6d4ed3954ad1f 100644 --- a/localstack/services/stepfunctions/asl/component/common/error_name/custom_error_name.py +++ b/localstack/services/stepfunctions/asl/component/common/error_name/custom_error_name.py @@ -1,17 +1,17 @@ -from typing import Final +from typing import Final, Optional from localstack.services.stepfunctions.asl.component.common.error_name.error_name import ErrorName +ILLEGAL_CUSTOM_ERROR_PREFIX: Final[str] = "States." + class CustomErrorName(ErrorName): """ States MAY report errors with other names, which MUST NOT begin with the prefix "States.". """ - _ILLEGAL_PREFIX: Final[str] = "States." - - def __init__(self, error_name: str): - if error_name.startswith(CustomErrorName._ILLEGAL_PREFIX): + def __init__(self, error_name: Optional[str]): + if error_name is not None and error_name.startswith(ILLEGAL_CUSTOM_ERROR_PREFIX): raise ValueError( f"Custom Error Names MUST NOT begin with the prefix 'States.', got '{error_name}'." ) diff --git a/localstack/services/stepfunctions/asl/component/common/error_name/error_name.py b/localstack/services/stepfunctions/asl/component/common/error_name/error_name.py index 5b3413bb43640..50e09e290aa4f 100644 --- a/localstack/services/stepfunctions/asl/component/common/error_name/error_name.py +++ b/localstack/services/stepfunctions/asl/component/common/error_name/error_name.py @@ -1,16 +1,18 @@ from __future__ import annotations import abc -from typing import Final +from typing import Final, Optional from localstack.services.stepfunctions.asl.component.component import Component class ErrorName(Component, abc.ABC): - def __init__(self, error_name: str): - self.error_name: Final[str] = error_name + error_name: Final[Optional[str]] - def matches(self, error_name: str) -> bool: + def __init__(self, error_name: Optional[str]): + self.error_name = error_name + + def matches(self, error_name: Optional[str]) -> bool: return self.error_name == error_name def __eq__(self, other): diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py index 98ca010f86880..ba061d0299458 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py @@ -1,6 +1,7 @@ import abc import json import time +from typing import Optional from localstack.aws.api.stepfunctions import ( HistoryEventExecutionDataDetails, @@ -120,10 +121,10 @@ def _get_callback_outcome_failure_event( self, env: Environment, ex: CallbackOutcomeFailureError ) -> FailureEvent: callback_outcome_failure: CallbackOutcomeFailure = ex.callback_outcome_failure - error: str = callback_outcome_failure.error + error: Optional[str] = callback_outcome_failure.error return FailureEvent( env=env, - error_name=CustomErrorName(error_name=callback_outcome_failure.error), + error_name=CustomErrorName(error_name=error), event_type=HistoryEventType.TaskFailed, event_details=EventDetails( taskFailedEventDetails=TaskFailedEventDetails( diff --git a/localstack/services/stepfunctions/asl/eval/callback/callback.py b/localstack/services/stepfunctions/asl/eval/callback/callback.py index 4cb3b77895594..d9811f2b9ecd2 100644 --- a/localstack/services/stepfunctions/asl/eval/callback/callback.py +++ b/localstack/services/stepfunctions/asl/eval/callback/callback.py @@ -26,10 +26,10 @@ def __init__(self, callback_id: CallbackId, output: str): class CallbackOutcomeFailure(CallbackOutcome): - error: Final[str] - cause: Final[str] + error: Final[Optional[str]] + cause: Final[Optional[str]] - def __init__(self, callback_id: CallbackId, error: str, cause: str): + def __init__(self, callback_id: CallbackId, error: Optional[str], cause: Optional[str]): super().__init__(callback_id=callback_id) self.error = error self.cause = cause diff --git a/tests/aws/services/stepfunctions/templates/callbacks/callback_templates.py b/tests/aws/services/stepfunctions/templates/callbacks/callback_templates.py index 7558c61be3393..7ba3bb912902e 100644 --- a/tests/aws/services/stepfunctions/templates/callbacks/callback_templates.py +++ b/tests/aws/services/stepfunctions/templates/callbacks/callback_templates.py @@ -37,3 +37,9 @@ class CallbackTemplates(TemplateLoader): SQS_HEARTBEAT_SUCCESS_ON_TASK_TOKEN: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/sqs_hearbeat_success_on_task_token.json5" ) + SQS_PARALLEL_WAIT_FOR_TASK_TOKEN: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/sqs_parallel_wait_for_task_token.json5" + ) + SQS_WAIT_FOR_TASK_TOKEN_CATCH: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/sqs_wait_for_task_token_catch.json5" + ) diff --git a/tests/aws/services/stepfunctions/templates/callbacks/statemachines/sqs_parallel_wait_for_task_token.json5 b/tests/aws/services/stepfunctions/templates/callbacks/statemachines/sqs_parallel_wait_for_task_token.json5 new file mode 100644 index 0000000000000..23815fa7a7f42 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/callbacks/statemachines/sqs_parallel_wait_for_task_token.json5 @@ -0,0 +1,53 @@ +{ + "Comment": "SQS_PARALLEL_WAIT_FOR_TASK_TOKEN", + "StartAt": "ParallelJob", + "States": { + "ParallelJob": { + "Type": "Parallel", + "Branches": [ + { + "StartAt": "SendMessageWithWait", + "States": { + "SendMessageWithWait": { + "Type": "Task", + "Resource": "arn:aws:states:::sqs:sendMessage.waitForTaskToken", + "Parameters": { + "QueueUrl.$": "$.QueueUrl", + "MessageBody": { + "Context.$": "$", + "TaskToken.$": "$$.Task.Token" + } + }, + "End": true + }, + } + } + ], + "Catch": [ + { + "ErrorEquals": [ + "States.Runtime" + ], + "ResultPath": "$.states_runtime_error", + "Next": "CaughtRuntimeError" + }, + { + "ErrorEquals": [ + "States.ALL" + ], + "ResultPath": "$.states_all_error", + "Next": "CaughtStatesALL" + } + ], + "End": true + }, + "CaughtRuntimeError": { + "Type": "Pass", + "End": true + }, + "CaughtStatesALL": { + "Type": "Pass", + "End": true + }, + } +} diff --git a/tests/aws/services/stepfunctions/templates/callbacks/statemachines/sqs_wait_for_task_token_catch.json5 b/tests/aws/services/stepfunctions/templates/callbacks/statemachines/sqs_wait_for_task_token_catch.json5 new file mode 100644 index 0000000000000..f38a5033cf57d --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/callbacks/statemachines/sqs_wait_for_task_token_catch.json5 @@ -0,0 +1,42 @@ +{ + "Comment": "SQS_WAIT_FOR_TASK_TOKEN_CATCH", + "StartAt": "SendMessageWithWait", + "States": { + "SendMessageWithWait": { + "Type": "Task", + "Resource": "arn:aws:states:::sqs:sendMessage.waitForTaskToken", + "Parameters": { + "QueueUrl.$": "$.QueueUrl", + "MessageBody": { + "Context.$": "$", + "TaskToken.$": "$$.Task.Token" + } + }, + "Catch": [ + { + "ErrorEquals": [ + "States.Runtime" + ], + "ResultPath": "$.states_runtime_error", + "Next": "CaughtRuntimeError" + }, + { + "ErrorEquals": [ + "States.ALL" + ], + "ResultPath": "$.states_all_error", + "Next": "CaughtStatesALL" + } + ], + "End": true + }, + "CaughtRuntimeError": { + "Type": "Pass", + "End": true + }, + "CaughtStatesALL": { + "Type": "Pass", + "End": true + } + } +} diff --git a/tests/aws/services/stepfunctions/v2/callback/test_callback.py b/tests/aws/services/stepfunctions/v2/callback/test_callback.py index d1f57b8ce7b36..b071a78b7b074 100644 --- a/tests/aws/services/stepfunctions/v2/callback/test_callback.py +++ b/tests/aws/services/stepfunctions/v2/callback/test_callback.py @@ -1,9 +1,11 @@ import json import threading +import pytest from localstack_snapshot.snapshots.transformer import JsonpathTransformer, RegexTransformer from localstack.services.stepfunctions.asl.eval.count_down_latch import CountDownLatch +from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers from localstack.utils.strings import short_uid from localstack.utils.sync import retry @@ -697,3 +699,76 @@ def test_sqs_wait_for_task_token_no_token_parameter( definition, exec_input, ) + + @markers.aws.validated + @pytest.mark.parametrize( + "template", + [CT.SQS_PARALLEL_WAIT_FOR_TASK_TOKEN, CT.SQS_WAIT_FOR_TASK_TOKEN_CATCH], + ids=["SQS_PARALLEL_WAIT_FOR_TASK_TOKEN", "SQS_WAIT_FOR_TASK_TOKEN_CATCH"], + ) + def test_sqs_failure_in_wait_for_task_tok_no_error_field( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sqs_create_queue, + sfn_snapshot, + template, + request, + ): + if ( + not is_aws_cloud() + and request.node.name + == "test_sqs_failure_in_wait_for_task_tok_no_error_field[SQS_PARALLEL_WAIT_FOR_TASK_TOKEN]" + ): + # TODO: The conditions in which TaskStateAborted error events are logged requires further investigations. + # These appear to be logged for Task state workers but only within Parallel states. The behaviour with + # other 'Abort' errors should also be investigated. + pytest.skip("Investigate occurrence logic of 'TaskStateAborted' errors") + + sfn_snapshot.add_transformer(sfn_snapshot.transform.sfn_sqs_integration()) + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..TaskToken", + replacement="task_token", + replace_reference=True, + ) + ) + + queue_name = f"queue-{short_uid()}" + queue_url = sqs_create_queue(QueueName=queue_name) + sfn_snapshot.add_transformer(RegexTransformer(queue_url, "<sqs_queue_url>")) + sfn_snapshot.add_transformer(RegexTransformer(queue_name, "<sqs_queue_name>")) + + def _empty_send_task_failure_on_sqs_message(): + def _get_message_body(): + receive_message_response = aws_client.sqs.receive_message( + QueueUrl=queue_url, MaxNumberOfMessages=1 + ) + return receive_message_response["Messages"][0]["Body"] + + message_body_str = retry(_get_message_body, retries=60, sleep=1) + message_body = json.loads(message_body_str) + task_token = message_body["TaskToken"] + aws_client.stepfunctions.send_task_failure(taskToken=task_token) + + thread_send_task_failure = threading.Thread( + target=_empty_send_task_failure_on_sqs_message, + args=(), + name="Thread_empty_send_task_failure_on_sqs_message", + ) + thread_send_task_failure.daemon = True + thread_send_task_failure.start() + + template = CT.load_sfn_template(template) + definition = json.dumps(template) + + exec_input = json.dumps({"QueueUrl": queue_url, "Message": "test_message_txt"}) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) diff --git a/tests/aws/services/stepfunctions/v2/callback/test_callback.snapshot.json b/tests/aws/services/stepfunctions/v2/callback/test_callback.snapshot.json index ed76a744b6e04..28b9eb464eea1 100644 --- a/tests/aws/services/stepfunctions/v2/callback/test_callback.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/callback/test_callback.snapshot.json @@ -3110,5 +3110,459 @@ } } } + }, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_failure_in_wait_for_task_tok_no_error_field[SQS_PARALLEL_WAIT_FOR_TASK_TOKEN]": { + "recorded-date": "29-04-2024, 10:07:12", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "QueueUrl": "<sqs_queue_url>", + "Message": "test_message_txt" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "arn:aws:iam::111111111111:role/<resource:1>" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "<sqs_queue_url>", + "Message": "test_message_txt" + }, + "inputDetails": { + "truncated": false + }, + "name": "ParallelJob" + }, + "timestamp": "timestamp", + "type": "ParallelStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "timestamp": "timestamp", + "type": "ParallelStateStarted" + }, + { + "id": 4, + "previousEventId": 3, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "<sqs_queue_url>", + "Message": "test_message_txt" + }, + "inputDetails": { + "truncated": false + }, + "name": "SendMessageWithWait" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 5, + "previousEventId": 4, + "taskScheduledEventDetails": { + "parameters": { + "MessageBody": { + "Context": { + "QueueUrl": "<sqs_queue_url>", + "Message": "test_message_txt" + }, + "TaskToken": "<task_token:1>" + }, + "QueueUrl": "<sqs_queue_url>" + }, + "region": "<region>", + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 6, + "previousEventId": 5, + "taskStartedEventDetails": { + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 7, + "previousEventId": 6, + "taskSubmittedEventDetails": { + "output": { + "MD5OfMessageBody": "<m-d5-of-message-body:1>", + "MessageId": "<uuid:1>", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "<uuid:2>" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "<uuid:2>" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "<uuid:2>" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 8, + "previousEventId": 7, + "taskFailedEventDetails": { + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskFailed" + }, + { + "id": 9, + "previousEventId": 8, + "timestamp": "timestamp", + "type": "TaskStateAborted" + }, + { + "id": 10, + "previousEventId": 8, + "timestamp": "timestamp", + "type": "ParallelStateFailed" + }, + { + "id": 11, + "previousEventId": 10, + "stateExitedEventDetails": { + "name": "ParallelJob", + "output": { + "QueueUrl": "<sqs_queue_url>", + "Message": "test_message_txt", + "states_all_error": { + "Error": null, + "Cause": null + } + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "ParallelStateExited" + }, + { + "id": 12, + "previousEventId": 11, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "<sqs_queue_url>", + "Message": "test_message_txt", + "states_all_error": { + "Error": null, + "Cause": null + } + }, + "inputDetails": { + "truncated": false + }, + "name": "CaughtStatesALL" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 13, + "previousEventId": 12, + "stateExitedEventDetails": { + "name": "CaughtStatesALL", + "output": { + "QueueUrl": "<sqs_queue_url>", + "Message": "test_message_txt", + "states_all_error": { + "Error": null, + "Cause": null + } + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "QueueUrl": "<sqs_queue_url>", + "Message": "test_message_txt", + "states_all_error": { + "Error": null, + "Cause": null + } + }, + "outputDetails": { + "truncated": false + } + }, + "id": 14, + "previousEventId": 13, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_failure_in_wait_for_task_tok_no_error_field[SQS_WAIT_FOR_TASK_TOKEN_CATCH]": { + "recorded-date": "29-04-2024, 10:07:27", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": { + "QueueUrl": "<sqs_queue_url>", + "Message": "test_message_txt" + }, + "inputDetails": { + "truncated": false + }, + "roleArn": "arn:aws:iam::111111111111:role/<resource:1>" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "<sqs_queue_url>", + "Message": "test_message_txt" + }, + "inputDetails": { + "truncated": false + }, + "name": "SendMessageWithWait" + }, + "timestamp": "timestamp", + "type": "TaskStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "taskScheduledEventDetails": { + "parameters": { + "MessageBody": { + "Context": { + "QueueUrl": "<sqs_queue_url>", + "Message": "test_message_txt" + }, + "TaskToken": "<task_token:1>" + }, + "QueueUrl": "<sqs_queue_url>" + }, + "region": "<region>", + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskScheduled" + }, + { + "id": 4, + "previousEventId": 3, + "taskStartedEventDetails": { + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskStarted" + }, + { + "id": 5, + "previousEventId": 4, + "taskSubmittedEventDetails": { + "output": { + "MD5OfMessageBody": "<m-d5-of-message-body:1>", + "MessageId": "<uuid:1>", + "SdkHttpMetadata": { + "AllHttpHeaders": { + "x-amzn-RequestId": [ + "<uuid:2>" + ], + "connection": [ + "keep-alive" + ], + "Content-Length": [ + "106" + ], + "Date": "date", + "Content-Type": [ + "application/x-amz-json-1.0" + ] + }, + "HttpHeaders": { + "connection": "keep-alive", + "Content-Length": "106", + "Content-Type": "application/x-amz-json-1.0", + "Date": "date", + "x-amzn-RequestId": "<uuid:2>" + }, + "HttpStatusCode": 200 + }, + "SdkResponseMetadata": { + "RequestId": "<uuid:2>" + } + }, + "outputDetails": { + "truncated": false + }, + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskSubmitted" + }, + { + "id": 6, + "previousEventId": 5, + "taskFailedEventDetails": { + "resource": "sendMessage.waitForTaskToken", + "resourceType": "sqs" + }, + "timestamp": "timestamp", + "type": "TaskFailed" + }, + { + "id": 7, + "previousEventId": 6, + "stateExitedEventDetails": { + "name": "SendMessageWithWait", + "output": { + "QueueUrl": "<sqs_queue_url>", + "Message": "test_message_txt", + "states_all_error": { + "Error": null, + "Cause": null + } + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "TaskStateExited" + }, + { + "id": 8, + "previousEventId": 7, + "stateEnteredEventDetails": { + "input": { + "QueueUrl": "<sqs_queue_url>", + "Message": "test_message_txt", + "states_all_error": { + "Error": null, + "Cause": null + } + }, + "inputDetails": { + "truncated": false + }, + "name": "CaughtStatesALL" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 9, + "previousEventId": 8, + "stateExitedEventDetails": { + "name": "CaughtStatesALL", + "output": { + "QueueUrl": "<sqs_queue_url>", + "Message": "test_message_txt", + "states_all_error": { + "Error": null, + "Cause": null + } + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "QueueUrl": "<sqs_queue_url>", + "Message": "test_message_txt", + "states_all_error": { + "Error": null, + "Cause": null + } + }, + "outputDetails": { + "truncated": false + } + }, + "id": 10, + "previousEventId": 9, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/stepfunctions/v2/callback/test_callback.validation.json b/tests/aws/services/stepfunctions/v2/callback/test_callback.validation.json index e14c5e7d21caa..f74e35a734cc7 100644 --- a/tests/aws/services/stepfunctions/v2/callback/test_callback.validation.json +++ b/tests/aws/services/stepfunctions/v2/callback/test_callback.validation.json @@ -8,6 +8,12 @@ "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sns_publish_wait_for_task_token": { "last_validated_date": "2024-02-01T20:51:35+00:00" }, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_failure_in_wait_for_task_tok_no_error_field[SQS_PARALLEL_WAIT_FOR_TASK_TOKEN]": { + "last_validated_date": "2024-04-29T10:07:12+00:00" + }, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_failure_in_wait_for_task_tok_no_error_field[SQS_WAIT_FOR_TASK_TOKEN_CATCH]": { + "last_validated_date": "2024-04-29T10:07:27+00:00" + }, "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_failure_in_wait_for_task_token": { "last_validated_date": "2024-04-18T06:24:24+00:00" }, From 10cab48bec978d1fe065b7f1610dae3f291f011d Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Thu, 2 May 2024 10:03:52 +0200 Subject: [PATCH 110/169] switch default gateway server from hypercorn to twisted (#10703) --- localstack/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localstack/config.py b/localstack/config.py index cf91238316afa..c98a9f6f8d264 100644 --- a/localstack/config.py +++ b/localstack/config.py @@ -658,7 +658,7 @@ def populate_edge_configuration( GATEWAY_WORKER_COUNT = int(os.environ.get("GATEWAY_WORKER_COUNT") or 1000) # the gateway server that should be used (supported: hypercorn, twisted dev: werkzeug) -GATEWAY_SERVER = os.environ.get("GATEWAY_SERVER", "").strip() or "hypercorn" +GATEWAY_SERVER = os.environ.get("GATEWAY_SERVER", "").strip() or "twisted" # IP of the docker bridge used to enable access between containers DOCKER_BRIDGE_IP = os.environ.get("DOCKER_BRIDGE_IP", "").strip() From 2f911c331d1828376af4cfeaf98601c98d72095e Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Thu, 2 May 2024 13:51:58 +0200 Subject: [PATCH 111/169] update ASF APIs, switch SQS to JSON again (#10741) Co-authored-by: LocalStack Bot <localstack-bot@users.noreply.github.com> Co-authored-by: Alexander Rashed <alexander.rashed@localstack.cloud> --- localstack/aws/api/ec2/__init__.py | 44 + localstack/aws/api/sqs/__init__.py | 166 +- localstack/aws/api/ssm/__init__.py | 113 + localstack/aws/api/stepfunctions/__init__.py | 41 + .../2012-11-05/README.md | 4 +- .../2012-11-05/service-2.json | 631 +++-- localstack/aws/protocol/serializer.py | 55 +- localstack/aws/protocol/service_router.py | 12 +- localstack/aws/spec.py | 11 +- .../lambda_/invocation/internal_sqs_queue.py | 10 +- localstack/services/sqs/provider.py | 34 +- localstack/services/sqs/query_api.py | 4 +- localstack/testing/pytest/fixtures.py | 6 + localstack/utils/aws/client_types.py | 2 +- pyproject.toml | 8 +- requirements-base-runtime.txt | 4 +- requirements-dev.txt | 6 +- requirements-runtime.txt | 6 +- requirements-test.txt | 6 +- requirements-typehint.txt | 8 +- tests/aws/services/sns/test_sns.py | 4 +- tests/aws/services/sqs/test_sqs.py | 25 +- tests/aws/services/sqs/test_sqs.snapshot.json | 2122 ++++++++--------- .../aws/services/sqs/test_sqs.validation.json | 348 ++- tests/aws/services/sqs/test_sqs_backdoor.py | 18 +- tests/aws/services/sqs/test_sqs_move_task.py | 6 - .../sqs/test_sqs_move_task.snapshot.json | 45 +- .../sqs/test_sqs_move_task.validation.json | 24 +- tests/unit/aws/protocol/test_parser.py | 8 +- .../unit/aws/protocol/test_parser_validate.py | 2 +- tests/unit/aws/protocol/test_serializer.py | 22 +- tests/unit/aws/test_mocking.py | 2 +- tests/unit/aws/test_service_router.py | 8 +- tests/unit/aws/test_skeleton.py | 8 +- tests/unit/aws/test_spec.py | 16 +- 35 files changed, 2053 insertions(+), 1776 deletions(-) rename localstack/aws/data/{sqs-json => sqs-query}/2012-11-05/README.md (78%) rename localstack/aws/data/{sqs-json => sqs-query}/2012-11-05/service-2.json (69%) diff --git a/localstack/aws/api/ec2/__init__.py b/localstack/aws/api/ec2/__init__.py index 5a913b3036d63..6c2610c89022b 100644 --- a/localstack/aws/api/ec2/__init__.py +++ b/localstack/aws/api/ec2/__init__.py @@ -1102,6 +1102,7 @@ class ImageAttributeName(str): uefiData = "uefiData" lastLaunchedTime = "lastLaunchedTime" imdsSupport = "imdsSupport" + deregistrationProtection = "deregistrationProtection" class ImageBlockPublicAccessDisabledState(str): @@ -2432,6 +2433,7 @@ class NetworkInterfaceAttribute(str): groupSet = "groupSet" sourceDestCheck = "sourceDestCheck" attachment = "attachment" + associatePublicIpAddress = "associatePublicIpAddress" class NetworkInterfaceCreationType(str): @@ -10965,6 +10967,8 @@ class Image(TypedDict, total=False): DeprecationTime: Optional[String] ImdsSupport: Optional[ImdsSupportValues] SourceInstanceId: Optional[String] + DeregistrationProtection: Optional[String] + LastLaunchedTime: Optional[String] ImageList = List[Image] @@ -12387,6 +12391,7 @@ class DescribeNetworkInterfaceAttributeResult(TypedDict, total=False): Groups: Optional[GroupIdentifierList] NetworkInterfaceId: Optional[String] SourceDestCheck: Optional[AttributeBooleanValue] + AssociatePublicIpAddress: Optional[Boolean] NetworkInterfacePermissionIdList = List[NetworkInterfacePermissionId] @@ -14421,6 +14426,15 @@ class DisableImageDeprecationResult(TypedDict, total=False): Return: Optional[Boolean] +class DisableImageDeregistrationProtectionRequest(ServiceRequest): + ImageId: ImageId + DryRun: Optional[Boolean] + + +class DisableImageDeregistrationProtectionResult(TypedDict, total=False): + Return: Optional[String] + + class DisableImageRequest(ServiceRequest): ImageId: ImageId DryRun: Optional[Boolean] @@ -14818,6 +14832,16 @@ class EnableImageDeprecationResult(TypedDict, total=False): Return: Optional[Boolean] +class EnableImageDeregistrationProtectionRequest(ServiceRequest): + ImageId: ImageId + WithCooldown: Optional[Boolean] + DryRun: Optional[Boolean] + + +class EnableImageDeregistrationProtectionResult(TypedDict, total=False): + Return: Optional[String] + + class EnableImageRequest(ServiceRequest): ImageId: ImageId DryRun: Optional[Boolean] @@ -15921,6 +15945,7 @@ class ImageAttribute(TypedDict, total=False): UefiData: Optional[AttributeValue] LastLaunchedTime: Optional[AttributeValue] ImdsSupport: Optional[AttributeValue] + DeregistrationProtection: Optional[AttributeValue] class UserBucket(TypedDict, total=False): @@ -16711,6 +16736,7 @@ class ModifyNetworkInterfaceAttributeRequest(ServiceRequest): EnaSrdSpecification: Optional[EnaSrdSpecification] EnablePrimaryIpv6: Optional[Boolean] ConnectionTrackingSpecification: Optional[ConnectionTrackingSpecificationRequest] + AssociatePublicIpAddress: Optional[Boolean] class ModifyPrivateDnsNameOptionsRequest(ServiceRequest): @@ -22964,6 +22990,12 @@ def disable_image_deprecation( ) -> DisableImageDeprecationResult: raise NotImplementedError + @handler("DisableImageDeregistrationProtection") + def disable_image_deregistration_protection( + self, context: RequestContext, image_id: ImageId, dry_run: Boolean = None, **kwargs + ) -> DisableImageDeregistrationProtectionResult: + raise NotImplementedError + @handler("DisableIpamOrganizationAdminAccount") def disable_ipam_organization_admin_account( self, @@ -23248,6 +23280,17 @@ def enable_image_deprecation( ) -> EnableImageDeprecationResult: raise NotImplementedError + @handler("EnableImageDeregistrationProtection") + def enable_image_deregistration_protection( + self, + context: RequestContext, + image_id: ImageId, + with_cooldown: Boolean = None, + dry_run: Boolean = None, + **kwargs, + ) -> EnableImageDeregistrationProtectionResult: + raise NotImplementedError + @handler("EnableIpamOrganizationAdminAccount") def enable_ipam_organization_admin_account( self, @@ -24459,6 +24502,7 @@ def modify_network_interface_attribute( ena_srd_specification: EnaSrdSpecification = None, enable_primary_ipv6: Boolean = None, connection_tracking_specification: ConnectionTrackingSpecificationRequest = None, + associate_public_ip_address: Boolean = None, **kwargs, ) -> None: raise NotImplementedError diff --git a/localstack/aws/api/sqs/__init__.py b/localstack/aws/api/sqs/__init__.py index 4a3fbf4453500..fb1ad5e814d22 100644 --- a/localstack/aws/api/sqs/__init__.py +++ b/localstack/aws/api/sqs/__init__.py @@ -4,8 +4,9 @@ Boolean = bool BoxedInteger = int -Integer = int +ExceptionMessage = str MessageAttributeName = str +NullableInteger = int String = str TagKey = str TagValue = str @@ -54,20 +55,26 @@ class QueueAttributeName(str): class BatchEntryIdsNotDistinct(ServiceException): - code: str = "AWS.SimpleQueueService.BatchEntryIdsNotDistinct" - sender_fault: bool = True + code: str = "BatchEntryIdsNotDistinct" + sender_fault: bool = False status_code: int = 400 class BatchRequestTooLong(ServiceException): - code: str = "AWS.SimpleQueueService.BatchRequestTooLong" - sender_fault: bool = True + code: str = "BatchRequestTooLong" + sender_fault: bool = False status_code: int = 400 class EmptyBatchRequest(ServiceException): - code: str = "AWS.SimpleQueueService.EmptyBatchRequest" - sender_fault: bool = True + code: str = "EmptyBatchRequest" + sender_fault: bool = False + status_code: int = 400 + + +class InvalidAddress(ServiceException): + code: str = "InvalidAddress" + sender_fault: bool = False status_code: int = 400 @@ -77,9 +84,15 @@ class InvalidAttributeName(ServiceException): status_code: int = 400 +class InvalidAttributeValue(ServiceException): + code: str = "InvalidAttributeValue" + sender_fault: bool = False + status_code: int = 400 + + class InvalidBatchEntryId(ServiceException): - code: str = "AWS.SimpleQueueService.InvalidBatchEntryId" - sender_fault: bool = True + code: str = "InvalidBatchEntryId" + sender_fault: bool = False status_code: int = 400 @@ -95,39 +108,87 @@ class InvalidMessageContents(ServiceException): status_code: int = 400 +class InvalidSecurity(ServiceException): + code: str = "InvalidSecurity" + sender_fault: bool = False + status_code: int = 400 + + +class KmsAccessDenied(ServiceException): + code: str = "KmsAccessDenied" + sender_fault: bool = False + status_code: int = 400 + + +class KmsDisabled(ServiceException): + code: str = "KmsDisabled" + sender_fault: bool = False + status_code: int = 400 + + +class KmsInvalidKeyUsage(ServiceException): + code: str = "KmsInvalidKeyUsage" + sender_fault: bool = False + status_code: int = 400 + + +class KmsInvalidState(ServiceException): + code: str = "KmsInvalidState" + sender_fault: bool = False + status_code: int = 400 + + +class KmsNotFound(ServiceException): + code: str = "KmsNotFound" + sender_fault: bool = False + status_code: int = 400 + + +class KmsOptInRequired(ServiceException): + code: str = "KmsOptInRequired" + sender_fault: bool = False + status_code: int = 400 + + +class KmsThrottled(ServiceException): + code: str = "KmsThrottled" + sender_fault: bool = False + status_code: int = 400 + + class MessageNotInflight(ServiceException): - code: str = "AWS.SimpleQueueService.MessageNotInflight" - sender_fault: bool = True + code: str = "MessageNotInflight" + sender_fault: bool = False status_code: int = 400 class OverLimit(ServiceException): code: str = "OverLimit" - sender_fault: bool = True - status_code: int = 403 + sender_fault: bool = False + status_code: int = 400 class PurgeQueueInProgress(ServiceException): - code: str = "AWS.SimpleQueueService.PurgeQueueInProgress" - sender_fault: bool = True - status_code: int = 403 + code: str = "PurgeQueueInProgress" + sender_fault: bool = False + status_code: int = 400 class QueueDeletedRecently(ServiceException): - code: str = "AWS.SimpleQueueService.QueueDeletedRecently" - sender_fault: bool = True + code: str = "QueueDeletedRecently" + sender_fault: bool = False status_code: int = 400 class QueueDoesNotExist(ServiceException): - code: str = "AWS.SimpleQueueService.NonExistentQueue" - sender_fault: bool = True + code: str = "QueueDoesNotExist" + sender_fault: bool = False status_code: int = 400 class QueueNameExists(ServiceException): - code: str = "QueueAlreadyExists" - sender_fault: bool = True + code: str = "QueueNameExists" + sender_fault: bool = False status_code: int = 400 @@ -137,21 +198,27 @@ class ReceiptHandleIsInvalid(ServiceException): status_code: int = 400 +class RequestThrottled(ServiceException): + code: str = "RequestThrottled" + sender_fault: bool = False + status_code: int = 400 + + class ResourceNotFoundException(ServiceException): code: str = "ResourceNotFoundException" - sender_fault: bool = True - status_code: int = 404 + sender_fault: bool = False + status_code: int = 400 class TooManyEntriesInBatchRequest(ServiceException): - code: str = "AWS.SimpleQueueService.TooManyEntriesInBatchRequest" - sender_fault: bool = True + code: str = "TooManyEntriesInBatchRequest" + sender_fault: bool = False status_code: int = 400 class UnsupportedOperation(ServiceException): - code: str = "AWS.SimpleQueueService.UnsupportedOperation" - sender_fault: bool = True + code: str = "UnsupportedOperation" + sender_fault: bool = False status_code: int = 400 @@ -195,7 +262,7 @@ class CancelMessageMoveTaskResult(TypedDict, total=False): class ChangeMessageVisibilityBatchRequestEntry(TypedDict, total=False): Id: String ReceiptHandle: String - VisibilityTimeout: Optional[Integer] + VisibilityTimeout: Optional[NullableInteger] ChangeMessageVisibilityBatchRequestEntryList = List[ChangeMessageVisibilityBatchRequestEntry] @@ -221,7 +288,7 @@ class ChangeMessageVisibilityBatchResult(TypedDict, total=False): class ChangeMessageVisibilityRequest(ServiceRequest): QueueUrl: String ReceiptHandle: String - VisibilityTimeout: Integer + VisibilityTimeout: NullableInteger TagMap = Dict[TagKey, TagValue] @@ -306,7 +373,10 @@ class ListDeadLetterSourceQueuesResult(TypedDict, total=False): class ListMessageMoveTasksRequest(ServiceRequest): SourceArn: String - MaxResults: Optional[Integer] + MaxResults: Optional[NullableInteger] + + +NullableLong = int class ListMessageMoveTasksResultEntry(TypedDict, total=False): @@ -314,9 +384,9 @@ class ListMessageMoveTasksResultEntry(TypedDict, total=False): Status: Optional[String] SourceArn: Optional[String] DestinationArn: Optional[String] - MaxNumberOfMessagesPerSecond: Optional[Integer] + MaxNumberOfMessagesPerSecond: Optional[NullableInteger] ApproximateNumberOfMessagesMoved: Optional[Long] - ApproximateNumberOfMessagesToMove: Optional[Long] + ApproximateNumberOfMessagesToMove: Optional[NullableLong] FailureReason: Optional[String] StartedTimestamp: Optional[Long] @@ -397,9 +467,9 @@ class ReceiveMessageRequest(ServiceRequest): QueueUrl: String AttributeNames: Optional[AttributeNameList] MessageAttributeNames: Optional[MessageAttributeNameList] - MaxNumberOfMessages: Optional[Integer] - VisibilityTimeout: Optional[Integer] - WaitTimeSeconds: Optional[Integer] + MaxNumberOfMessages: Optional[NullableInteger] + VisibilityTimeout: Optional[NullableInteger] + WaitTimeSeconds: Optional[NullableInteger] ReceiveRequestAttemptId: Optional[String] @@ -415,7 +485,7 @@ class RemovePermissionRequest(ServiceRequest): class SendMessageBatchRequestEntry(TypedDict, total=False): Id: String MessageBody: String - DelaySeconds: Optional[Integer] + DelaySeconds: Optional[NullableInteger] MessageAttributes: Optional[MessageBodyAttributeMap] MessageSystemAttributes: Optional[MessageBodySystemAttributeMap] MessageDeduplicationId: Optional[String] @@ -450,7 +520,7 @@ class SendMessageBatchResult(TypedDict, total=False): class SendMessageRequest(ServiceRequest): QueueUrl: String MessageBody: String - DelaySeconds: Optional[Integer] + DelaySeconds: Optional[NullableInteger] MessageAttributes: Optional[MessageBodyAttributeMap] MessageSystemAttributes: Optional[MessageBodySystemAttributeMap] MessageDeduplicationId: Optional[String] @@ -473,7 +543,7 @@ class SetQueueAttributesRequest(ServiceRequest): class StartMessageMoveTaskRequest(ServiceRequest): SourceArn: String DestinationArn: Optional[String] - MaxNumberOfMessagesPerSecond: Optional[Integer] + MaxNumberOfMessagesPerSecond: Optional[NullableInteger] class StartMessageMoveTaskResult(TypedDict, total=False): @@ -521,7 +591,7 @@ def change_message_visibility( context: RequestContext, queue_url: String, receipt_handle: String, - visibility_timeout: Integer, + visibility_timeout: NullableInteger, **kwargs, ) -> None: raise NotImplementedError @@ -600,7 +670,11 @@ def list_dead_letter_source_queues( @handler("ListMessageMoveTasks") def list_message_move_tasks( - self, context: RequestContext, source_arn: String, max_results: Integer = None, **kwargs + self, + context: RequestContext, + source_arn: String, + max_results: NullableInteger = None, + **kwargs, ) -> ListMessageMoveTasksResult: raise NotImplementedError @@ -632,9 +706,9 @@ def receive_message( queue_url: String, attribute_names: AttributeNameList = None, message_attribute_names: MessageAttributeNameList = None, - max_number_of_messages: Integer = None, - visibility_timeout: Integer = None, - wait_time_seconds: Integer = None, + max_number_of_messages: NullableInteger = None, + visibility_timeout: NullableInteger = None, + wait_time_seconds: NullableInteger = None, receive_request_attempt_id: String = None, **kwargs, ) -> ReceiveMessageResult: @@ -652,7 +726,7 @@ def send_message( context: RequestContext, queue_url: String, message_body: String, - delay_seconds: Integer = None, + delay_seconds: NullableInteger = None, message_attributes: MessageBodyAttributeMap = None, message_system_attributes: MessageBodySystemAttributeMap = None, message_deduplication_id: String = None, @@ -683,7 +757,7 @@ def start_message_move_task( context: RequestContext, source_arn: String, destination_arn: String = None, - max_number_of_messages_per_second: Integer = None, + max_number_of_messages_per_second: NullableInteger = None, **kwargs, ) -> StartMessageMoveTaskResult: raise NotImplementedError diff --git a/localstack/aws/api/ssm/__init__.py b/localstack/aws/api/ssm/__init__.py index 16cc7e3e58644..f7b4d0f9f5a95 100644 --- a/localstack/aws/api/ssm/__init__.py +++ b/localstack/aws/api/ssm/__init__.py @@ -14,6 +14,7 @@ AllowedPattern = str ApplyOnlyAtCronInterval = bool ApproveAfterDays = int +Architecture = str AssociationExecutionFilterValue = str AssociationExecutionId = str AssociationExecutionTargetsFilterValue = str @@ -70,6 +71,7 @@ DefaultBaseline = bool DefaultInstanceName = str DeliveryTimedOutCount = int +DescribeInstancePropertiesMaxResults = int DescriptionInDocument = str DocumentARN = str DocumentAuthor = str @@ -110,9 +112,15 @@ InstanceId = str InstanceInformationFilterValue = str InstanceInformationStringFilterKey = str +InstanceName = str InstancePatchStateFilterKey = str InstancePatchStateFilterValue = str +InstancePropertyFilterValue = str +InstancePropertyStringFilterKey = str +InstanceRole = str +InstanceState = str InstanceTagName = str +InstanceType = str InstancesCount = int Integer = int InventoryAggregatorExpression = str @@ -131,6 +139,7 @@ InventoryTypeDisplayName = str InvocationTraceOutput = str IsSubTypeSchema = bool +KeyName = str LastResourceDataSyncMessage = str ListOpsMetadataMaxResults = int MaintenanceWindowAllowUnassociatedTargets = bool @@ -272,6 +281,8 @@ PatchUnreportedNotApplicableCount = int PatchVendor = str PatchVersion = str +PlatformName = str +PlatformVersion = str Policy = str PolicyHash = str PolicyId = str @@ -642,6 +653,26 @@ class InstancePatchStateOperatorType(str): GreaterThan = "GreaterThan" +class InstancePropertyFilterKey(str): + InstanceIds = "InstanceIds" + AgentVersion = "AgentVersion" + PingStatus = "PingStatus" + PlatformTypes = "PlatformTypes" + DocumentName = "DocumentName" + ActivationIds = "ActivationIds" + IamRole = "IamRole" + ResourceType = "ResourceType" + AssociationStatus = "AssociationStatus" + + +class InstancePropertyFilterOperator(str): + Equal = "Equal" + NotEqual = "NotEqual" + BeginWith = "BeginWith" + LessThan = "LessThan" + GreaterThan = "GreaterThan" + + class InventoryAttributeDataType(str): string = "string" number = "number" @@ -1331,6 +1362,12 @@ class InvalidInstanceInformationFilterValue(ServiceException): status_code: int = 400 +class InvalidInstancePropertyFilterValue(ServiceException): + code: str = "InvalidInstancePropertyFilterValue" + sender_fault: bool = False + status_code: int = 400 + + class InvalidInventoryGroupException(ServiceException): code: str = "InvalidInventoryGroupException" sender_fault: bool = False @@ -3444,6 +3481,70 @@ class DescribeInstancePatchesResult(TypedDict, total=False): NextToken: Optional[NextToken] +InstancePropertyFilterValueSet = List[InstancePropertyFilterValue] + + +class InstancePropertyStringFilter(TypedDict, total=False): + Key: InstancePropertyStringFilterKey + Values: InstancePropertyFilterValueSet + Operator: Optional[InstancePropertyFilterOperator] + + +InstancePropertyStringFilterList = List[InstancePropertyStringFilter] + + +class InstancePropertyFilter(TypedDict, total=False): + key: InstancePropertyFilterKey + valueSet: InstancePropertyFilterValueSet + + +InstancePropertyFilterList = List[InstancePropertyFilter] + + +class DescribeInstancePropertiesRequest(ServiceRequest): + InstancePropertyFilterList: Optional[InstancePropertyFilterList] + FiltersWithOperator: Optional[InstancePropertyStringFilterList] + MaxResults: Optional[DescribeInstancePropertiesMaxResults] + NextToken: Optional[NextToken] + + +class InstanceProperty(TypedDict, total=False): + Name: Optional[InstanceName] + InstanceId: Optional[InstanceId] + InstanceType: Optional[InstanceType] + InstanceRole: Optional[InstanceRole] + KeyName: Optional[KeyName] + InstanceState: Optional[InstanceState] + Architecture: Optional[Architecture] + IPAddress: Optional[IPAddress] + LaunchTime: Optional[DateTime] + PingStatus: Optional[PingStatus] + LastPingDateTime: Optional[DateTime] + AgentVersion: Optional[Version] + PlatformType: Optional[PlatformType] + PlatformName: Optional[PlatformName] + PlatformVersion: Optional[PlatformVersion] + ActivationId: Optional[ActivationId] + IamRole: Optional[IamRole] + RegistrationDate: Optional[DateTime] + ResourceType: Optional[String] + ComputerName: Optional[ComputerName] + AssociationStatus: Optional[StatusName] + LastAssociationExecutionDate: Optional[DateTime] + LastSuccessfulAssociationExecutionDate: Optional[DateTime] + AssociationOverview: Optional[InstanceAggregatedAssociationOverview] + SourceId: Optional[SourceId] + SourceType: Optional[SourceType] + + +InstanceProperties = List[InstanceProperty] + + +class DescribeInstancePropertiesResult(TypedDict, total=False): + InstanceProperties: Optional[InstanceProperties] + NextToken: Optional[NextToken] + + class DescribeInventoryDeletionsRequest(ServiceRequest): DeletionId: Optional[UUID] NextToken: Optional[NextToken] @@ -6047,6 +6148,18 @@ def describe_instance_patches( ) -> DescribeInstancePatchesResult: raise NotImplementedError + @handler("DescribeInstanceProperties") + def describe_instance_properties( + self, + context: RequestContext, + instance_property_filter_list: InstancePropertyFilterList = None, + filters_with_operator: InstancePropertyStringFilterList = None, + max_results: DescribeInstancePropertiesMaxResults = None, + next_token: NextToken = None, + **kwargs, + ) -> DescribeInstancePropertiesResult: + raise NotImplementedError + @handler("DescribeInventoryDeletions") def describe_inventory_deletions( self, diff --git a/localstack/aws/api/stepfunctions/__init__.py b/localstack/aws/api/stepfunctions/__init__.py index 6024efc1da416..675147bac44e2 100644 --- a/localstack/aws/api/stepfunctions/__init__.py +++ b/localstack/aws/api/stepfunctions/__init__.py @@ -44,6 +44,9 @@ TraceHeader = str URL = str UnsignedInteger = int +ValidateStateMachineDefinitionCode = str +ValidateStateMachineDefinitionLocation = str +ValidateStateMachineDefinitionMessage = str VersionDescription = str VersionWeight = int includedDetails = bool @@ -177,6 +180,15 @@ class TestExecutionStatus(str): CAUGHT_ERROR = "CAUGHT_ERROR" +class ValidateStateMachineDefinitionResultCode(str): + OK = "OK" + FAIL = "FAIL" + + +class ValidateStateMachineDefinitionSeverity(str): + ERROR = "ERROR" + + class ValidationExceptionReason(str): API_DOES_NOT_SUPPORT_LABELED_ARNS = "API_DOES_NOT_SUPPORT_LABELED_ARNS" MISSING_REQUIRED_PARAMETER = "MISSING_REQUIRED_PARAMETER" @@ -1239,6 +1251,29 @@ class UpdateStateMachineOutput(TypedDict, total=False): stateMachineVersionArn: Optional[Arn] +class ValidateStateMachineDefinitionDiagnostic(TypedDict, total=False): + severity: ValidateStateMachineDefinitionSeverity + code: ValidateStateMachineDefinitionCode + message: ValidateStateMachineDefinitionMessage + location: Optional[ValidateStateMachineDefinitionLocation] + + +ValidateStateMachineDefinitionDiagnosticList = List[ValidateStateMachineDefinitionDiagnostic] +ValidateStateMachineDefinitionInput = TypedDict( + "ValidateStateMachineDefinitionInput", + { + "definition": Definition, + "type": Optional[StateMachineType], + }, + total=False, +) + + +class ValidateStateMachineDefinitionOutput(TypedDict, total=False): + result: ValidateStateMachineDefinitionResultCode + diagnostics: ValidateStateMachineDefinitionDiagnosticList + + class StepfunctionsApi: service = "stepfunctions" version = "2016-11-23" @@ -1559,3 +1594,9 @@ def update_state_machine_alias( **kwargs, ) -> UpdateStateMachineAliasOutput: raise NotImplementedError + + @handler("ValidateStateMachineDefinition", expand=False) + def validate_state_machine_definition( + self, context: RequestContext, request: ValidateStateMachineDefinitionInput, **kwargs + ) -> ValidateStateMachineDefinitionOutput: + raise NotImplementedError diff --git a/localstack/aws/data/sqs-json/2012-11-05/README.md b/localstack/aws/data/sqs-query/2012-11-05/README.md similarity index 78% rename from localstack/aws/data/sqs-json/2012-11-05/README.md rename to localstack/aws/data/sqs-query/2012-11-05/README.md index 5f8c79887e4dc..5935af1b1c4ac 100644 --- a/localstack/aws/data/sqs-json/2012-11-05/README.md +++ b/localstack/aws/data/sqs-query/2012-11-05/README.md @@ -1,8 +1,8 @@ This spec preserves the SQS query protocol spec, which was part of botocore until the protocol was switched to json with `botocore==1.31.81`. This switch removed a lot of spec data which is necessary for the proper parsing and serialization, which is why we have to preserve them on our own. -- The spec content was preserved from this state: https://github.com/boto/botocore/blob/143e3925dac58976b5e83864a3ed9a2dea1db91b/botocore/data/sqs/2012-11-05/service-2.json -- This was the last commit before the protocol switched back (again) to query (with https://github.com/boto/botocore/commit/143e3925dac58976b5e83864a3ed9a2dea1db91b). +- The spec content was preserved from this state: https://github.com/boto/botocore/blob/79c92132e266b15f62bc743ae0816c27d598c36e/botocore/data/sqs/2012-11-05/service-2.json +- This was the last commit before the protocol switched back (again) to json (with https://github.com/boto/botocore/commit/47a515f6727a7585487d58c069c7c0063c28899e). - The file is licensed with Apache License 2.0. - Modifications: - Removal of documentation strings with the following regex: `(,)?\n\s+"documentation":".*"` diff --git a/localstack/aws/data/sqs-json/2012-11-05/service-2.json b/localstack/aws/data/sqs-query/2012-11-05/service-2.json similarity index 69% rename from localstack/aws/data/sqs-json/2012-11-05/service-2.json rename to localstack/aws/data/sqs-query/2012-11-05/service-2.json index 12e34f8acf263..cc6988fd2acbd 100644 --- a/localstack/aws/data/sqs-json/2012-11-05/service-2.json +++ b/localstack/aws/data/sqs-query/2012-11-05/service-2.json @@ -2,17 +2,14 @@ "version":"2.0", "metadata":{ "apiVersion":"2012-11-05", - "awsQueryCompatible":{ - }, "endpointPrefix":"sqs", - "jsonVersion":"1.0", - "protocol":"json", + "protocol":"query", "serviceAbbreviation":"Amazon SQS", "serviceFullName":"Amazon Simple Queue Service", "serviceId":"SQS", "signatureVersion":"v4", - "targetPrefix":"AmazonSQS", - "uid":"sqs-2012-11-05" + "uid":"sqs-2012-11-05", + "xmlNamespace":"http://queue.amazonaws.com/doc/2012-11-05/" }, "operations":{ "AddPermission":{ @@ -23,12 +20,7 @@ }, "input":{"shape":"AddPermissionRequest"}, "errors":[ - {"shape":"OverLimit"}, - {"shape":"RequestThrottled"}, - {"shape":"QueueDoesNotExist"}, - {"shape":"InvalidAddress"}, - {"shape":"InvalidSecurity"}, - {"shape":"UnsupportedOperation"} + {"shape":"OverLimit"} ] }, "CancelMessageMoveTask":{ @@ -38,12 +30,12 @@ "requestUri":"/" }, "input":{"shape":"CancelMessageMoveTaskRequest"}, - "output":{"shape":"CancelMessageMoveTaskResult"}, + "output":{ + "shape":"CancelMessageMoveTaskResult", + "resultWrapper":"CancelMessageMoveTaskResult" + }, "errors":[ {"shape":"ResourceNotFoundException"}, - {"shape":"RequestThrottled"}, - {"shape":"InvalidAddress"}, - {"shape":"InvalidSecurity"}, {"shape":"UnsupportedOperation"} ] }, @@ -56,12 +48,7 @@ "input":{"shape":"ChangeMessageVisibilityRequest"}, "errors":[ {"shape":"MessageNotInflight"}, - {"shape":"ReceiptHandleIsInvalid"}, - {"shape":"RequestThrottled"}, - {"shape":"QueueDoesNotExist"}, - {"shape":"UnsupportedOperation"}, - {"shape":"InvalidAddress"}, - {"shape":"InvalidSecurity"} + {"shape":"ReceiptHandleIsInvalid"} ] }, "ChangeMessageVisibilityBatch":{ @@ -71,17 +58,15 @@ "requestUri":"/" }, "input":{"shape":"ChangeMessageVisibilityBatchRequest"}, - "output":{"shape":"ChangeMessageVisibilityBatchResult"}, + "output":{ + "shape":"ChangeMessageVisibilityBatchResult", + "resultWrapper":"ChangeMessageVisibilityBatchResult" + }, "errors":[ {"shape":"TooManyEntriesInBatchRequest"}, {"shape":"EmptyBatchRequest"}, {"shape":"BatchEntryIdsNotDistinct"}, - {"shape":"InvalidBatchEntryId"}, - {"shape":"RequestThrottled"}, - {"shape":"QueueDoesNotExist"}, - {"shape":"UnsupportedOperation"}, - {"shape":"InvalidAddress"}, - {"shape":"InvalidSecurity"} + {"shape":"InvalidBatchEntryId"} ] }, "CreateQueue":{ @@ -91,16 +76,13 @@ "requestUri":"/" }, "input":{"shape":"CreateQueueRequest"}, - "output":{"shape":"CreateQueueResult"}, + "output":{ + "shape":"CreateQueueResult", + "resultWrapper":"CreateQueueResult" + }, "errors":[ {"shape":"QueueDeletedRecently"}, - {"shape":"QueueNameExists"}, - {"shape":"RequestThrottled"}, - {"shape":"InvalidAddress"}, - {"shape":"InvalidAttributeName"}, - {"shape":"InvalidAttributeValue"}, - {"shape":"UnsupportedOperation"}, - {"shape":"InvalidSecurity"} + {"shape":"QueueNameExists"} ] }, "DeleteMessage":{ @@ -112,12 +94,7 @@ "input":{"shape":"DeleteMessageRequest"}, "errors":[ {"shape":"InvalidIdFormat"}, - {"shape":"ReceiptHandleIsInvalid"}, - {"shape":"RequestThrottled"}, - {"shape":"QueueDoesNotExist"}, - {"shape":"UnsupportedOperation"}, - {"shape":"InvalidSecurity"}, - {"shape":"InvalidAddress"} + {"shape":"ReceiptHandleIsInvalid"} ] }, "DeleteMessageBatch":{ @@ -127,17 +104,15 @@ "requestUri":"/" }, "input":{"shape":"DeleteMessageBatchRequest"}, - "output":{"shape":"DeleteMessageBatchResult"}, + "output":{ + "shape":"DeleteMessageBatchResult", + "resultWrapper":"DeleteMessageBatchResult" + }, "errors":[ {"shape":"TooManyEntriesInBatchRequest"}, {"shape":"EmptyBatchRequest"}, {"shape":"BatchEntryIdsNotDistinct"}, - {"shape":"InvalidBatchEntryId"}, - {"shape":"RequestThrottled"}, - {"shape":"QueueDoesNotExist"}, - {"shape":"UnsupportedOperation"}, - {"shape":"InvalidAddress"}, - {"shape":"InvalidSecurity"} + {"shape":"InvalidBatchEntryId"} ] }, "DeleteQueue":{ @@ -146,14 +121,7 @@ "method":"POST", "requestUri":"/" }, - "input":{"shape":"DeleteQueueRequest"}, - "errors":[ - {"shape":"RequestThrottled"}, - {"shape":"QueueDoesNotExist"}, - {"shape":"InvalidAddress"}, - {"shape":"UnsupportedOperation"}, - {"shape":"InvalidSecurity"} - ] + "input":{"shape":"DeleteQueueRequest"} }, "GetQueueAttributes":{ "name":"GetQueueAttributes", @@ -162,14 +130,12 @@ "requestUri":"/" }, "input":{"shape":"GetQueueAttributesRequest"}, - "output":{"shape":"GetQueueAttributesResult"}, + "output":{ + "shape":"GetQueueAttributesResult", + "resultWrapper":"GetQueueAttributesResult" + }, "errors":[ - {"shape":"InvalidAttributeName"}, - {"shape":"RequestThrottled"}, - {"shape":"QueueDoesNotExist"}, - {"shape":"UnsupportedOperation"}, - {"shape":"InvalidSecurity"}, - {"shape":"InvalidAddress"} + {"shape":"InvalidAttributeName"} ] }, "GetQueueUrl":{ @@ -179,13 +145,12 @@ "requestUri":"/" }, "input":{"shape":"GetQueueUrlRequest"}, - "output":{"shape":"GetQueueUrlResult"}, + "output":{ + "shape":"GetQueueUrlResult", + "resultWrapper":"GetQueueUrlResult" + }, "errors":[ - {"shape":"RequestThrottled"}, - {"shape":"QueueDoesNotExist"}, - {"shape":"InvalidAddress"}, - {"shape":"InvalidSecurity"}, - {"shape":"UnsupportedOperation"} + {"shape":"QueueDoesNotExist"} ] }, "ListDeadLetterSourceQueues":{ @@ -195,13 +160,12 @@ "requestUri":"/" }, "input":{"shape":"ListDeadLetterSourceQueuesRequest"}, - "output":{"shape":"ListDeadLetterSourceQueuesResult"}, + "output":{ + "shape":"ListDeadLetterSourceQueuesResult", + "resultWrapper":"ListDeadLetterSourceQueuesResult" + }, "errors":[ - {"shape":"QueueDoesNotExist"}, - {"shape":"RequestThrottled"}, - {"shape":"InvalidSecurity"}, - {"shape":"InvalidAddress"}, - {"shape":"UnsupportedOperation"} + {"shape":"QueueDoesNotExist"} ] }, "ListMessageMoveTasks":{ @@ -211,12 +175,12 @@ "requestUri":"/" }, "input":{"shape":"ListMessageMoveTasksRequest"}, - "output":{"shape":"ListMessageMoveTasksResult"}, + "output":{ + "shape":"ListMessageMoveTasksResult", + "resultWrapper":"ListMessageMoveTasksResult" + }, "errors":[ {"shape":"ResourceNotFoundException"}, - {"shape":"RequestThrottled"}, - {"shape":"InvalidAddress"}, - {"shape":"InvalidSecurity"}, {"shape":"UnsupportedOperation"} ] }, @@ -227,14 +191,10 @@ "requestUri":"/" }, "input":{"shape":"ListQueueTagsRequest"}, - "output":{"shape":"ListQueueTagsResult"}, - "errors":[ - {"shape":"RequestThrottled"}, - {"shape":"QueueDoesNotExist"}, - {"shape":"UnsupportedOperation"}, - {"shape":"InvalidAddress"}, - {"shape":"InvalidSecurity"} - ] + "output":{ + "shape":"ListQueueTagsResult", + "resultWrapper":"ListQueueTagsResult" + } }, "ListQueues":{ "name":"ListQueues", @@ -243,13 +203,10 @@ "requestUri":"/" }, "input":{"shape":"ListQueuesRequest"}, - "output":{"shape":"ListQueuesResult"}, - "errors":[ - {"shape":"RequestThrottled"}, - {"shape":"InvalidSecurity"}, - {"shape":"InvalidAddress"}, - {"shape":"UnsupportedOperation"} - ] + "output":{ + "shape":"ListQueuesResult", + "resultWrapper":"ListQueuesResult" + } }, "PurgeQueue":{ "name":"PurgeQueue", @@ -260,11 +217,7 @@ "input":{"shape":"PurgeQueueRequest"}, "errors":[ {"shape":"QueueDoesNotExist"}, - {"shape":"PurgeQueueInProgress"}, - {"shape":"RequestThrottled"}, - {"shape":"InvalidAddress"}, - {"shape":"InvalidSecurity"}, - {"shape":"UnsupportedOperation"} + {"shape":"PurgeQueueInProgress"} ] }, "ReceiveMessage":{ @@ -274,21 +227,12 @@ "requestUri":"/" }, "input":{"shape":"ReceiveMessageRequest"}, - "output":{"shape":"ReceiveMessageResult"}, + "output":{ + "shape":"ReceiveMessageResult", + "resultWrapper":"ReceiveMessageResult" + }, "errors":[ - {"shape":"UnsupportedOperation"}, - {"shape":"OverLimit"}, - {"shape":"RequestThrottled"}, - {"shape":"QueueDoesNotExist"}, - {"shape":"InvalidSecurity"}, - {"shape":"KmsDisabled"}, - {"shape":"KmsInvalidState"}, - {"shape":"KmsNotFound"}, - {"shape":"KmsOptInRequired"}, - {"shape":"KmsThrottled"}, - {"shape":"KmsAccessDenied"}, - {"shape":"KmsInvalidKeyUsage"}, - {"shape":"InvalidAddress"} + {"shape":"OverLimit"} ] }, "RemovePermission":{ @@ -297,14 +241,7 @@ "method":"POST", "requestUri":"/" }, - "input":{"shape":"RemovePermissionRequest"}, - "errors":[ - {"shape":"InvalidAddress"}, - {"shape":"RequestThrottled"}, - {"shape":"QueueDoesNotExist"}, - {"shape":"InvalidSecurity"}, - {"shape":"UnsupportedOperation"} - ] + "input":{"shape":"RemovePermissionRequest"} }, "SendMessage":{ "name":"SendMessage", @@ -313,21 +250,13 @@ "requestUri":"/" }, "input":{"shape":"SendMessageRequest"}, - "output":{"shape":"SendMessageResult"}, + "output":{ + "shape":"SendMessageResult", + "resultWrapper":"SendMessageResult" + }, "errors":[ {"shape":"InvalidMessageContents"}, - {"shape":"UnsupportedOperation"}, - {"shape":"RequestThrottled"}, - {"shape":"QueueDoesNotExist"}, - {"shape":"InvalidSecurity"}, - {"shape":"KmsDisabled"}, - {"shape":"KmsInvalidState"}, - {"shape":"KmsNotFound"}, - {"shape":"KmsOptInRequired"}, - {"shape":"KmsThrottled"}, - {"shape":"KmsAccessDenied"}, - {"shape":"KmsInvalidKeyUsage"}, - {"shape":"InvalidAddress"} + {"shape":"UnsupportedOperation"} ] }, "SendMessageBatch":{ @@ -337,25 +266,17 @@ "requestUri":"/" }, "input":{"shape":"SendMessageBatchRequest"}, - "output":{"shape":"SendMessageBatchResult"}, + "output":{ + "shape":"SendMessageBatchResult", + "resultWrapper":"SendMessageBatchResult" + }, "errors":[ {"shape":"TooManyEntriesInBatchRequest"}, {"shape":"EmptyBatchRequest"}, {"shape":"BatchEntryIdsNotDistinct"}, {"shape":"BatchRequestTooLong"}, {"shape":"InvalidBatchEntryId"}, - {"shape":"UnsupportedOperation"}, - {"shape":"RequestThrottled"}, - {"shape":"QueueDoesNotExist"}, - {"shape":"InvalidSecurity"}, - {"shape":"KmsDisabled"}, - {"shape":"KmsInvalidState"}, - {"shape":"KmsNotFound"}, - {"shape":"KmsOptInRequired"}, - {"shape":"KmsThrottled"}, - {"shape":"KmsAccessDenied"}, - {"shape":"KmsInvalidKeyUsage"}, - {"shape":"InvalidAddress"} + {"shape":"UnsupportedOperation"} ] }, "SetQueueAttributes":{ @@ -366,14 +287,7 @@ }, "input":{"shape":"SetQueueAttributesRequest"}, "errors":[ - {"shape":"InvalidAttributeName"}, - {"shape":"InvalidAttributeValue"}, - {"shape":"RequestThrottled"}, - {"shape":"QueueDoesNotExist"}, - {"shape":"UnsupportedOperation"}, - {"shape":"OverLimit"}, - {"shape":"InvalidAddress"}, - {"shape":"InvalidSecurity"} + {"shape":"InvalidAttributeName"} ] }, "StartMessageMoveTask":{ @@ -383,12 +297,12 @@ "requestUri":"/" }, "input":{"shape":"StartMessageMoveTaskRequest"}, - "output":{"shape":"StartMessageMoveTaskResult"}, + "output":{ + "shape":"StartMessageMoveTaskResult", + "resultWrapper":"StartMessageMoveTaskResult" + }, "errors":[ {"shape":"ResourceNotFoundException"}, - {"shape":"RequestThrottled"}, - {"shape":"InvalidAddress"}, - {"shape":"InvalidSecurity"}, {"shape":"UnsupportedOperation"} ] }, @@ -398,14 +312,7 @@ "method":"POST", "requestUri":"/" }, - "input":{"shape":"TagQueueRequest"}, - "errors":[ - {"shape":"InvalidAddress"}, - {"shape":"RequestThrottled"}, - {"shape":"QueueDoesNotExist"}, - {"shape":"InvalidSecurity"}, - {"shape":"UnsupportedOperation"} - ] + "input":{"shape":"TagQueueRequest"} }, "UntagQueue":{ "name":"UntagQueue", @@ -413,25 +320,24 @@ "method":"POST", "requestUri":"/" }, - "input":{"shape":"UntagQueueRequest"}, - "errors":[ - {"shape":"InvalidAddress"}, - {"shape":"RequestThrottled"}, - {"shape":"QueueDoesNotExist"}, - {"shape":"InvalidSecurity"}, - {"shape":"UnsupportedOperation"} - ] + "input":{"shape":"UntagQueueRequest"} } }, "shapes":{ "AWSAccountIdList":{ "type":"list", - "member":{"shape":"String"}, + "member":{ + "shape":"String", + "locationName":"AWSAccountId" + }, "flattened":true }, "ActionNameList":{ "type":"list", - "member":{"shape":"String"}, + "member":{ + "shape":"String", + "locationName":"ActionName" + }, "flattened":true }, "AddPermissionRequest":{ @@ -459,20 +365,31 @@ }, "AttributeNameList":{ "type":"list", - "member":{"shape":"QueueAttributeName"}, + "member":{ + "shape":"QueueAttributeName", + "locationName":"AttributeName" + }, "flattened":true }, "BatchEntryIdsNotDistinct":{ "type":"structure", "members":{ - "message":{"shape":"ExceptionMessage"} + }, + "error":{ + "code":"AWS.SimpleQueueService.BatchEntryIdsNotDistinct", + "httpStatusCode":400, + "senderFault":true }, "exception":true }, "BatchRequestTooLong":{ "type":"structure", "members":{ - "message":{"shape":"ExceptionMessage"} + }, + "error":{ + "code":"AWS.SimpleQueueService.BatchRequestTooLong", + "httpStatusCode":400, + "senderFault":true }, "exception":true }, @@ -500,13 +417,19 @@ }, "BatchResultErrorEntryList":{ "type":"list", - "member":{"shape":"BatchResultErrorEntry"}, + "member":{ + "shape":"BatchResultErrorEntry", + "locationName":"BatchResultErrorEntry" + }, "flattened":true }, "Binary":{"type":"blob"}, "BinaryList":{ "type":"list", - "member":{"shape":"Binary"} + "member":{ + "shape":"Binary", + "locationName":"BinaryListValue" + } }, "Boolean":{"type":"boolean"}, "BoxedInteger":{ @@ -559,13 +482,16 @@ "shape":"String" }, "VisibilityTimeout":{ - "shape":"NullableInteger" + "shape":"Integer" } } }, "ChangeMessageVisibilityBatchRequestEntryList":{ "type":"list", - "member":{"shape":"ChangeMessageVisibilityBatchRequestEntry"}, + "member":{ + "shape":"ChangeMessageVisibilityBatchRequestEntry", + "locationName":"ChangeMessageVisibilityBatchRequestEntry" + }, "flattened":true }, "ChangeMessageVisibilityBatchResult":{ @@ -594,7 +520,10 @@ }, "ChangeMessageVisibilityBatchResultEntryList":{ "type":"list", - "member":{"shape":"ChangeMessageVisibilityBatchResultEntry"}, + "member":{ + "shape":"ChangeMessageVisibilityBatchResultEntry", + "locationName":"ChangeMessageVisibilityBatchResultEntry" + }, "flattened":true }, "ChangeMessageVisibilityRequest":{ @@ -612,7 +541,7 @@ "shape":"String" }, "VisibilityTimeout":{ - "shape":"NullableInteger" + "shape":"Integer" } } }, @@ -624,10 +553,12 @@ "shape":"String" }, "Attributes":{ - "shape":"QueueAttributeMap" + "shape":"QueueAttributeMap", + "locationName":"Attribute" }, "tags":{ - "shape":"TagMap" + "shape":"TagMap", + "locationName":"Tag" } } }, @@ -671,7 +602,10 @@ }, "DeleteMessageBatchRequestEntryList":{ "type":"list", - "member":{"shape":"DeleteMessageBatchRequestEntry"}, + "member":{ + "shape":"DeleteMessageBatchRequestEntry", + "locationName":"DeleteMessageBatchRequestEntry" + }, "flattened":true }, "DeleteMessageBatchResult":{ @@ -700,7 +634,10 @@ }, "DeleteMessageBatchResultEntryList":{ "type":"list", - "member":{"shape":"DeleteMessageBatchResultEntry"}, + "member":{ + "shape":"DeleteMessageBatchResultEntry", + "locationName":"DeleteMessageBatchResultEntry" + }, "flattened":true }, "DeleteMessageRequest":{ @@ -730,11 +667,14 @@ "EmptyBatchRequest":{ "type":"structure", "members":{ - "message":{"shape":"ExceptionMessage"} + }, + "error":{ + "code":"AWS.SimpleQueueService.EmptyBatchRequest", + "httpStatusCode":400, + "senderFault":true }, "exception":true }, - "ExceptionMessage":{"type":"string"}, "GetQueueAttributesRequest":{ "type":"structure", "required":["QueueUrl"], @@ -751,7 +691,8 @@ "type":"structure", "members":{ "Attributes":{ - "shape":"QueueAttributeMap" + "shape":"QueueAttributeMap", + "locationName":"Attribute" } } }, @@ -775,31 +716,21 @@ } } }, - "InvalidAddress":{ - "type":"structure", - "members":{ - "message":{"shape":"ExceptionMessage"} - }, - "exception":true - }, + "Integer":{"type":"integer"}, "InvalidAttributeName":{ "type":"structure", "members":{ - "message":{"shape":"ExceptionMessage"} - }, - "exception":true - }, - "InvalidAttributeValue":{ - "type":"structure", - "members":{ - "message":{"shape":"ExceptionMessage"} }, "exception":true }, "InvalidBatchEntryId":{ "type":"structure", "members":{ - "message":{"shape":"ExceptionMessage"} + }, + "error":{ + "code":"AWS.SimpleQueueService.InvalidBatchEntryId", + "httpStatusCode":400, + "senderFault":true }, "exception":true }, @@ -807,70 +738,11 @@ "type":"structure", "members":{ }, - "deprecated":true, - "deprecatedMessage":"exception has been included in ReceiptHandleIsInvalid", "exception":true }, "InvalidMessageContents":{ "type":"structure", "members":{ - "message":{"shape":"ExceptionMessage"} - }, - "exception":true - }, - "InvalidSecurity":{ - "type":"structure", - "members":{ - "message":{"shape":"ExceptionMessage"} - }, - "exception":true - }, - "KmsAccessDenied":{ - "type":"structure", - "members":{ - "message":{"shape":"ExceptionMessage"} - }, - "exception":true - }, - "KmsDisabled":{ - "type":"structure", - "members":{ - "message":{"shape":"ExceptionMessage"} - }, - "exception":true - }, - "KmsInvalidKeyUsage":{ - "type":"structure", - "members":{ - "message":{"shape":"ExceptionMessage"} - }, - "exception":true - }, - "KmsInvalidState":{ - "type":"structure", - "members":{ - "message":{"shape":"ExceptionMessage"} - }, - "exception":true - }, - "KmsNotFound":{ - "type":"structure", - "members":{ - "message":{"shape":"ExceptionMessage"} - }, - "exception":true - }, - "KmsOptInRequired":{ - "type":"structure", - "members":{ - "message":{"shape":"ExceptionMessage"} - }, - "exception":true - }, - "KmsThrottled":{ - "type":"structure", - "members":{ - "message":{"shape":"ExceptionMessage"} }, "exception":true }, @@ -909,7 +781,7 @@ "shape":"String" }, "MaxResults":{ - "shape":"NullableInteger" + "shape":"Integer" } } }, @@ -917,8 +789,7 @@ "type":"structure", "members":{ "Results":{ - "shape":"ListMessageMoveTasksResultEntryList", - "flattened":true + "shape":"ListMessageMoveTasksResultEntryList" } } }, @@ -938,13 +809,13 @@ "shape":"String" }, "MaxNumberOfMessagesPerSecond":{ - "shape":"NullableInteger" + "shape":"Integer" }, "ApproximateNumberOfMessagesMoved":{ "shape":"Long" }, "ApproximateNumberOfMessagesToMove":{ - "shape":"NullableLong" + "shape":"Long" }, "FailureReason":{ "shape":"String" @@ -956,7 +827,10 @@ }, "ListMessageMoveTasksResultEntryList":{ "type":"list", - "member":{"shape":"ListMessageMoveTasksResultEntry"}, + "member":{ + "shape":"ListMessageMoveTasksResultEntry", + "locationName":"ListMessageMoveTasksResultEntry" + }, "flattened":true }, "ListQueueTagsRequest":{ @@ -972,7 +846,8 @@ "type":"structure", "members":{ "Tags":{ - "shape":"TagMap" + "shape":"TagMap", + "locationName":"Tag" } } }, @@ -1018,20 +893,25 @@ "shape":"String" }, "Attributes":{ - "shape":"MessageSystemAttributeMap" + "shape":"MessageSystemAttributeMap", + "locationName":"Attribute" }, "MD5OfMessageAttributes":{ "shape":"String" }, "MessageAttributes":{ - "shape":"MessageBodyAttributeMap" + "shape":"MessageBodyAttributeMap", + "locationName":"MessageAttribute" } } }, "MessageAttributeName":{"type":"string"}, "MessageAttributeNameList":{ "type":"list", - "member":{"shape":"MessageAttributeName"}, + "member":{ + "shape":"MessageAttributeName", + "locationName":"MessageAttributeName" + }, "flattened":true }, "MessageAttributeValue":{ @@ -1046,11 +926,13 @@ }, "StringListValues":{ "shape":"StringList", - "flattened":true + "flattened":true, + "locationName":"StringListValue" }, "BinaryListValues":{ "shape":"BinaryList", - "flattened":true + "flattened":true, + "locationName":"BinaryListValue" }, "DataType":{ "shape":"String" @@ -1059,32 +941,59 @@ }, "MessageBodyAttributeMap":{ "type":"map", - "key":{"shape":"String"}, - "value":{"shape":"MessageAttributeValue"}, + "key":{ + "shape":"String", + "locationName":"Name" + }, + "value":{ + "shape":"MessageAttributeValue", + "locationName":"Value" + }, "flattened":true }, "MessageBodySystemAttributeMap":{ "type":"map", - "key":{"shape":"MessageSystemAttributeNameForSends"}, - "value":{"shape":"MessageSystemAttributeValue"}, + "key":{ + "shape":"MessageSystemAttributeNameForSends", + "locationName":"Name" + }, + "value":{ + "shape":"MessageSystemAttributeValue", + "locationName":"Value" + }, "flattened":true }, "MessageList":{ "type":"list", - "member":{"shape":"Message"}, + "member":{ + "shape":"Message", + "locationName":"Message" + }, "flattened":true }, "MessageNotInflight":{ "type":"structure", "members":{ }, + "error":{ + "code":"AWS.SimpleQueueService.MessageNotInflight", + "httpStatusCode":400, + "senderFault":true + }, "exception":true }, "MessageSystemAttributeMap":{ "type":"map", - "key":{"shape":"MessageSystemAttributeName"}, - "value":{"shape":"String"}, - "flattened":true + "key":{ + "shape":"MessageSystemAttributeName", + "locationName":"Name" + }, + "value":{ + "shape":"String", + "locationName":"Value" + }, + "flattened":true, + "locationName":"Attribute" }, "MessageSystemAttributeName":{ "type":"string", @@ -1116,30 +1025,38 @@ }, "StringListValues":{ "shape":"StringList", - "flattened":true + "flattened":true, + "locationName":"StringListValue" }, "BinaryListValues":{ "shape":"BinaryList", - "flattened":true + "flattened":true, + "locationName":"BinaryListValue" }, "DataType":{ "shape":"String" } } }, - "NullableInteger":{"type":"integer"}, - "NullableLong":{"type":"long"}, "OverLimit":{ "type":"structure", "members":{ - "message":{"shape":"ExceptionMessage"} + }, + "error":{ + "code":"OverLimit", + "httpStatusCode":403, + "senderFault":true }, "exception":true }, "PurgeQueueInProgress":{ "type":"structure", "members":{ - "message":{"shape":"ExceptionMessage"} + }, + "error":{ + "code":"AWS.SimpleQueueService.PurgeQueueInProgress", + "httpStatusCode":403, + "senderFault":true }, "exception":true }, @@ -1154,9 +1071,16 @@ }, "QueueAttributeMap":{ "type":"map", - "key":{"shape":"QueueAttributeName"}, - "value":{"shape":"String"}, - "flattened":true + "key":{ + "shape":"QueueAttributeName", + "locationName":"Name" + }, + "value":{ + "shape":"String", + "locationName":"Value" + }, + "flattened":true, + "locationName":"Attribute" }, "QueueAttributeName":{ "type":"string", @@ -1188,33 +1112,47 @@ "QueueDeletedRecently":{ "type":"structure", "members":{ - "message":{"shape":"ExceptionMessage"} + }, + "error":{ + "code":"AWS.SimpleQueueService.QueueDeletedRecently", + "httpStatusCode":400, + "senderFault":true }, "exception":true }, "QueueDoesNotExist":{ "type":"structure", "members":{ - "message":{"shape":"ExceptionMessage"} + }, + "error":{ + "code":"AWS.SimpleQueueService.NonExistentQueue", + "httpStatusCode":400, + "senderFault":true }, "exception":true }, "QueueNameExists":{ "type":"structure", "members":{ - "message":{"shape":"ExceptionMessage"} + }, + "error":{ + "code":"QueueAlreadyExists", + "httpStatusCode":400, + "senderFault":true }, "exception":true }, "QueueUrlList":{ "type":"list", - "member":{"shape":"String"}, + "member":{ + "shape":"String", + "locationName":"QueueUrl" + }, "flattened":true }, "ReceiptHandleIsInvalid":{ "type":"structure", "members":{ - "message":{"shape":"ExceptionMessage"} }, "exception":true }, @@ -1232,13 +1170,13 @@ "shape":"MessageAttributeNameList" }, "MaxNumberOfMessages":{ - "shape":"NullableInteger" + "shape":"Integer" }, "VisibilityTimeout":{ - "shape":"NullableInteger" + "shape":"Integer" }, "WaitTimeSeconds":{ - "shape":"NullableInteger" + "shape":"Integer" }, "ReceiveRequestAttemptId":{ "shape":"String" @@ -1268,17 +1206,14 @@ } } }, - "RequestThrottled":{ - "type":"structure", - "members":{ - "message":{"shape":"ExceptionMessage"} - }, - "exception":true - }, "ResourceNotFoundException":{ "type":"structure", "members":{ - "message":{"shape":"ExceptionMessage"} + }, + "error":{ + "code":"ResourceNotFoundException", + "httpStatusCode":404, + "senderFault":true }, "exception":true }, @@ -1311,13 +1246,15 @@ "shape":"String" }, "DelaySeconds":{ - "shape":"NullableInteger" + "shape":"Integer" }, "MessageAttributes":{ - "shape":"MessageBodyAttributeMap" + "shape":"MessageBodyAttributeMap", + "locationName":"MessageAttribute" }, "MessageSystemAttributes":{ - "shape":"MessageBodySystemAttributeMap" + "shape":"MessageBodySystemAttributeMap", + "locationName":"MessageSystemAttribute" }, "MessageDeduplicationId":{ "shape":"String" @@ -1329,7 +1266,10 @@ }, "SendMessageBatchRequestEntryList":{ "type":"list", - "member":{"shape":"SendMessageBatchRequestEntry"}, + "member":{ + "shape":"SendMessageBatchRequestEntry", + "locationName":"SendMessageBatchRequestEntry" + }, "flattened":true }, "SendMessageBatchResult":{ @@ -1377,7 +1317,10 @@ }, "SendMessageBatchResultEntryList":{ "type":"list", - "member":{"shape":"SendMessageBatchResultEntry"}, + "member":{ + "shape":"SendMessageBatchResultEntry", + "locationName":"SendMessageBatchResultEntry" + }, "flattened":true }, "SendMessageRequest":{ @@ -1394,13 +1337,15 @@ "shape":"String" }, "DelaySeconds":{ - "shape":"NullableInteger" + "shape":"Integer" }, "MessageAttributes":{ - "shape":"MessageBodyAttributeMap" + "shape":"MessageBodyAttributeMap", + "locationName":"MessageAttribute" }, "MessageSystemAttributes":{ - "shape":"MessageBodySystemAttributeMap" + "shape":"MessageBodySystemAttributeMap", + "locationName":"MessageSystemAttribute" }, "MessageDeduplicationId":{ "shape":"String" @@ -1441,7 +1386,8 @@ "shape":"String" }, "Attributes":{ - "shape":"QueueAttributeMap" + "shape":"QueueAttributeMap", + "locationName":"Attribute" } } }, @@ -1456,7 +1402,7 @@ "shape":"String" }, "MaxNumberOfMessagesPerSecond":{ - "shape":"NullableInteger" + "shape":"Integer" } } }, @@ -1471,19 +1417,32 @@ "String":{"type":"string"}, "StringList":{ "type":"list", - "member":{"shape":"String"} + "member":{ + "shape":"String", + "locationName":"StringListValue" + } }, "TagKey":{"type":"string"}, "TagKeyList":{ "type":"list", - "member":{"shape":"TagKey"}, + "member":{ + "shape":"TagKey", + "locationName":"TagKey" + }, "flattened":true }, "TagMap":{ "type":"map", - "key":{"shape":"TagKey"}, - "value":{"shape":"TagValue"}, - "flattened":true + "key":{ + "shape":"TagKey", + "locationName":"Key" + }, + "value":{ + "shape":"TagValue", + "locationName":"Value" + }, + "flattened":true, + "locationName":"Tag" }, "TagQueueRequest":{ "type":"structure", @@ -1505,14 +1464,22 @@ "TooManyEntriesInBatchRequest":{ "type":"structure", "members":{ - "message":{"shape":"ExceptionMessage"} + }, + "error":{ + "code":"AWS.SimpleQueueService.TooManyEntriesInBatchRequest", + "httpStatusCode":400, + "senderFault":true }, "exception":true }, "UnsupportedOperation":{ "type":"structure", "members":{ - "message":{"shape":"ExceptionMessage"} + }, + "error":{ + "code":"AWS.SimpleQueueService.UnsupportedOperation", + "httpStatusCode":400, + "senderFault":true }, "exception":true }, diff --git a/localstack/aws/protocol/serializer.py b/localstack/aws/protocol/serializer.py index 5ac2319793a4e..f7631360175ef 100644 --- a/localstack/aws/protocol/serializer.py +++ b/localstack/aws/protocol/serializer.py @@ -1604,13 +1604,34 @@ class SqsQueryResponseSerializer(QueryResponseSerializer): - These double-escapes are corrected by replacing such strings with their original. """ + # those are deleted from the JSON specs, but need to be kept for legacy reason (sent in 'x-amzn-query-error') + QUERY_PREFIXED_ERRORS = { + "BatchEntryIdsNotDistinct", + "BatchRequestTooLong", + "EmptyBatchRequest", + "InvalidBatchEntryId", + "MessageNotInflight", + "PurgeQueueInProgress", + "QueueDeletedRecently", + "TooManyEntriesInBatchRequest", + "UnsupportedOperation", + } + # Some error code changed between JSON and query, and we need to have a way to map it for legacy reason JSON_TO_QUERY_ERROR_CODES = { "InvalidParameterValueException": "InvalidParameterValue", "MissingRequiredParameterException": "MissingParameter", "AccessDeniedException": "AccessDenied", + "QueueDoesNotExist": "AWS.SimpleQueueService.NonExistentQueue", + "QueueNameExists": "QueueAlreadyExists", } + SENDER_FAULT_ERRORS = ( + QUERY_PREFIXED_ERRORS + | JSON_TO_QUERY_ERROR_CODES.keys() + | {"OverLimit", "ResourceNotFoundException"} + ) + def _default_serialize(self, xmlnode: ETree.Element, params: str, _, name: str, __) -> None: """ Ensures that we "mark" characters in the node's text which need to be specifically encoded. @@ -1647,28 +1668,40 @@ def _add_error_tags( if error.code in self.JSON_TO_QUERY_ERROR_CODES: error_code = self.JSON_TO_QUERY_ERROR_CODES[error.code] + elif error.code in self.QUERY_PREFIXED_ERRORS: + error_code = f"AWS.SimpleQueueService.{error.code}" else: error_code = error.code code_tag.text = error_code message = self._get_error_message(error) if message: self._default_serialize(error_tag, message, None, "Message", mime_type) - if error.sender_fault: + if error.code in self.SENDER_FAULT_ERRORS or error.sender_fault: # The sender fault is either not set or "Sender" self._default_serialize(error_tag, "Sender", None, "Type", mime_type) class SqsJsonResponseSerializer(JSONResponseSerializer): + # those are deleted from the JSON specs, but need to be kept for legacy reason (sent in 'x-amzn-query-error') + QUERY_PREFIXED_ERRORS = { + "BatchEntryIdsNotDistinct", + "BatchRequestTooLong", + "EmptyBatchRequest", + "InvalidBatchEntryId", + "MessageNotInflight", + "PurgeQueueInProgress", + "QueueDeletedRecently", + "TooManyEntriesInBatchRequest", + "UnsupportedOperation", + } + # Some error code changed between JSON and query, and we need to have a way to map it for legacy reason JSON_TO_QUERY_ERROR_CODES = { "InvalidParameterValueException": "InvalidParameterValue", "MissingRequiredParameterException": "MissingParameter", "AccessDeniedException": "AccessDenied", - } - - QUERY_TO_JSON_ERROR_CODES = { - "NonExistentQueue": "QueueDoesNotExist", - "QueueAlreadyExists": "QueueNameExists", + "QueueDoesNotExist": "AWS.SimpleQueueService.NonExistentQueue", + "QueueNameExists": "QueueAlreadyExists", } def _serialize_error( @@ -1689,18 +1722,16 @@ def _serialize_error( # AWS: "com.amazon.coral.service#InvalidParameterValueException" # or AWS: "com.amazonaws.sqs#BatchRequestTooLong" # LocalStack: "InvalidParameterValue" - + super()._serialize_error(error, response, shape, operation_model, mime_type, request_id) # We need to add a prefix to certain errors, as they have been deleted in the specs. These will not change if error.code in self.JSON_TO_QUERY_ERROR_CODES: code = self.JSON_TO_QUERY_ERROR_CODES[error.code] + elif error.code in self.QUERY_PREFIXED_ERRORS: + code = f"AWS.SimpleQueueService.{error.code}" else: code = error.code - response.headers["x-amzn-query-error"] = f"{code};Sender" - error.code = error.code.removeprefix("AWS.SimpleQueueService.") - if error.code in self.QUERY_TO_JSON_ERROR_CODES: - error.code = self.QUERY_TO_JSON_ERROR_CODES[error.code] - super()._serialize_error(error, response, shape, operation_model, mime_type, request_id) + response.headers["x-amzn-query-error"] = f"{code};Sender" def gen_amzn_requestid(): diff --git a/localstack/aws/protocol/service_router.py b/localstack/aws/protocol/service_router.py index 2640253ee3747..58ca2be8c9536 100644 --- a/localstack/aws/protocol/service_router.py +++ b/localstack/aws/protocol/service_router.py @@ -161,7 +161,7 @@ def custom_path_addressing_rules(path: str) -> Optional[ServiceModelIdentifier]: """ if is_sqs_queue_url(path): - return ServiceModelIdentifier("sqs") + return ServiceModelIdentifier("sqs", protocol="query") if path.startswith("/2015-03-31/functions/"): return ServiceModelIdentifier("lambda") @@ -300,16 +300,16 @@ def resolve_conflicts( if service_name_candidates == {"sqs"}: # SQS now have 2 different specs for `query` and `json` protocol. From our current implementation with the # parser and serializer, we need to have 2 different service names for them, but they share one provider - # implementation. `sqs-json` represents the `json` protocol spec, and `sqs` the `query` protocol + # implementation. `sqs` represents the `json` protocol spec, and `sqs-query` the `query` protocol # (default again in botocore starting with 1.32.6). # The `application/x-amz-json-1.0` header is mandatory for requests targeting SQS with the `json` protocol. We - # can safely route them to the `sqs-json` JSON parser/serializer. If not present, route the request to the - # default sqs protocol (`query`). + # can safely route them to the `sqs` JSON parser/serializer. If not present, route the request to the + # sqs-query protocol. content_type = request.headers.get("Content-Type") return ( - ServiceModelIdentifier("sqs", "json") + ServiceModelIdentifier("sqs") if content_type == "application/x-amz-json-1.0" - else ServiceModelIdentifier("sqs") + else ServiceModelIdentifier("sqs", "query") ) diff --git a/localstack/aws/spec.py b/localstack/aws/spec.py index e80f1e13be6c9..3c769f8d7f555 100644 --- a/localstack/aws/spec.py +++ b/localstack/aws/spec.py @@ -97,8 +97,8 @@ def load_service( ) -> ServiceModel: """ Loads a service - :param service: to load, f.e. "sqs". For custom, internalized, service protocol specs (f.e. sqs-json) it's also - possible to directly define the protocol in the service name (f.e. use sqs-json) + :param service: to load, f.e. "sqs". For custom, internalized, service protocol specs (f.e. sqs-query) it's also + possible to directly define the protocol in the service name (f.e. use sqs-query) :param version: of the service to load, f.e. "2012-11-05", by default the latest version will be used :param protocol: specific protocol to load for the specific service, f.e. "json" for the "sqs" service if the service cannot be found @@ -112,7 +112,7 @@ def load_service( if protocol is not None and protocol != service_description.get("metadata", {}).get("protocol"): # if the protocol is defined, but not the one of the currently loaded service, # check if we already loaded the custom spec based on the naming convention (<service>-<protocol>), - # f.e. "sqs-json" + # f.e. "sqs-query" if service.endswith(f"-{protocol}"): # if so, we raise an exception raise UnknownServiceProtocolError(service_name=service, protocol=protocol) @@ -124,8 +124,9 @@ def load_service( raise UnknownServiceProtocolError(service_name=service, protocol=protocol) # remove potential protocol names from the service name - # FIXME add more protocols here if we have to internalize more than just sqs-json - service = service.removesuffix("-json") + # FIXME add more protocols here if we have to internalize more than just sqs-query + # TODO this should not contain specific internalized serivce names + service = {"sqs-query": "sqs"}.get(service, service) return ServiceModel(service_description, service) diff --git a/localstack/services/lambda_/invocation/internal_sqs_queue.py b/localstack/services/lambda_/invocation/internal_sqs_queue.py index 984cdb7fc5c48..41da58b681701 100644 --- a/localstack/services/lambda_/invocation/internal_sqs_queue.py +++ b/localstack/services/lambda_/invocation/internal_sqs_queue.py @@ -7,12 +7,12 @@ AttributeNameList, CreateQueueResult, GetQueueAttributesResult, - Integer, Message, MessageAttributeNameList, MessageBodyAttributeMap, MessageBodySystemAttributeMap, MessageSystemAttributeName, + NullableInteger, QueueAttributeMap, ReceiveMessageResult, SendMessageResult, @@ -132,9 +132,9 @@ def receive_message( QueueUrl: String, AttributeNames: AttributeNameList = None, MessageAttributeNames: MessageAttributeNameList = None, - MaxNumberOfMessages: Integer = None, - VisibilityTimeout: Integer = None, - WaitTimeSeconds: Integer = None, + MaxNumberOfMessages: NullableInteger = None, + VisibilityTimeout: NullableInteger = None, + WaitTimeSeconds: NullableInteger = None, ReceiveRequestAttemptId: String = None, ) -> ReceiveMessageResult: queue = self.queue_manager.get_queue(queue_name=QueueUrl) @@ -176,7 +176,7 @@ def send_message( self, QueueUrl: String, MessageBody: String, - DelaySeconds: Integer = None, + DelaySeconds: NullableInteger = None, MessageAttributes: MessageBodyAttributeMap = None, MessageSystemAttributes: MessageBodySystemAttributeMap = None, MessageDeduplicationId: String = None, diff --git a/localstack/services/sqs/provider.py b/localstack/services/sqs/provider.py index afedabc150994..c9521c9ee1541 100644 --- a/localstack/services/sqs/provider.py +++ b/localstack/services/sqs/provider.py @@ -33,7 +33,6 @@ EmptyBatchRequest, GetQueueAttributesResult, GetQueueUrlResult, - Integer, InvalidAttributeName, InvalidBatchEntryId, InvalidMessageContents, @@ -47,6 +46,7 @@ MessageBodyAttributeMap, MessageBodySystemAttributeMap, MessageSystemAttributeName, + NullableInteger, PurgeQueueInProgress, QueueAttributeMap, QueueAttributeName, @@ -620,11 +620,11 @@ class SqsDeveloperEndpoints: def __init__(self, stores=None): self.stores = stores or sqs_stores - self.service = load_service("sqs") + self.service = load_service("sqs-query") self.serializer = create_serializer(self.service) @route("/_aws/sqs/messages") - @aws_response_serializer("sqs", "ReceiveMessage") + @aws_response_serializer("sqs-query", "ReceiveMessage") def list_messages(self, request: Request) -> ReceiveMessageResult: """ This endpoint expects a ``QueueUrl`` request parameter (either as query arg or form parameter), similar to @@ -651,7 +651,7 @@ def list_messages(self, request: Request) -> ReceiveMessageResult: return self._get_and_serialize_messages(request, region, account_id, queue_name) @route("/_aws/sqs/messages/<region>/<account_id>/<queue_name>") - @aws_response_serializer("sqs", "ReceiveMessage") + @aws_response_serializer("sqs-query", "ReceiveMessage") def list_messages_for_queue_url( self, request: Request, region: str, account_id: str, queue_name: str ) -> ReceiveMessageResult: @@ -956,7 +956,7 @@ def change_message_visibility( context: RequestContext, queue_url: String, receipt_handle: String, - visibility_timeout: Integer, + visibility_timeout: NullableInteger, **kwargs, ) -> None: queue = self._resolve_queue(context, queue_url=queue_url) @@ -1039,7 +1039,7 @@ def send_message( context: RequestContext, queue_url: String, message_body: String, - delay_seconds: Integer = None, + delay_seconds: NullableInteger = None, message_attributes: MessageBodyAttributeMap = None, message_system_attributes: MessageBodySystemAttributeMap = None, message_deduplication_id: String = None, @@ -1145,7 +1145,7 @@ def _put_message( queue: SqsQueue, context: RequestContext, message_body: String, - delay_seconds: Integer = None, + delay_seconds: NullableInteger = None, message_attributes: MessageBodyAttributeMap = None, message_system_attributes: MessageBodySystemAttributeMap = None, message_deduplication_id: String = None, @@ -1184,9 +1184,9 @@ def receive_message( queue_url: String, attribute_names: AttributeNameList = None, message_attribute_names: MessageAttributeNameList = None, - max_number_of_messages: Integer = None, - visibility_timeout: Integer = None, - wait_time_seconds: Integer = None, + max_number_of_messages: NullableInteger = None, + visibility_timeout: NullableInteger = None, + wait_time_seconds: NullableInteger = None, receive_request_attempt_id: String = None, **kwargs, ) -> ReceiveMessageResult: @@ -1364,7 +1364,11 @@ def set_queue_attributes( ) def list_message_move_tasks( - self, context: RequestContext, source_arn: String, max_results: Integer = None, **kwargs + self, + context: RequestContext, + source_arn: String, + max_results: NullableInteger = None, + **kwargs, ) -> ListMessageMoveTasksResult: try: self._require_queue_by_arn(context, source_arn) @@ -1430,7 +1434,7 @@ def start_message_move_task( context: RequestContext, source_arn: String, destination_arn: String = None, - max_number_of_messages_per_second: Integer = None, + max_number_of_messages_per_second: NullableInteger = None, **kwargs, ) -> StartMessageMoveTaskResult: try: @@ -1828,5 +1832,7 @@ def message_filter_message_attributes(message: Message, names: Optional[MessageA if k.startswith(prefix): matched.append(k) break - - message["MessageAttributes"] = {k: attributes[k] for k in matched} + if matched: + message["MessageAttributes"] = {k: attributes[k] for k in matched} + else: + message.pop("MessageAttributes") diff --git a/localstack/services/sqs/query_api.py b/localstack/services/sqs/query_api.py index 3545c00fc96de..6d5a33ee4bd5d 100644 --- a/localstack/services/sqs/query_api.py +++ b/localstack/services/sqs/query_api.py @@ -34,7 +34,7 @@ LOG = logging.getLogger(__name__) -service = load_service("sqs") +service = load_service("sqs-query") parser = create_parser(service) serializer = create_serializer(service) @@ -215,7 +215,7 @@ def try_call_sqs(request: Request, region: str) -> Tuple[Dict, OperationModel]: region_name=region, aws_access_key_id=account_id or INTERNAL_AWS_ACCESS_KEY_ID, aws_secret_access_key=INTERNAL_AWS_SECRET_ACCESS_KEY, - ).sqs + ).sqs_query try: # using the layer below boto3.client("sqs").<operation>(...) to make the call diff --git a/localstack/testing/pytest/fixtures.py b/localstack/testing/pytest/fixtures.py index 749624bc946b9..25c89a7601961 100644 --- a/localstack/testing/pytest/fixtures.py +++ b/localstack/testing/pytest/fixtures.py @@ -1922,6 +1922,12 @@ def pytest_collection_modifyitems(config: Config, items: list[Item]): for mark in item.iter_markers(): if mark.name.endswith("only_localstack"): item.add_marker(only_localstack) + if hasattr(item, "fixturenames") and "snapshot" in item.fixturenames: + # add a marker that indicates that this test is snapshot validated + # if it uses the snapshot fixture -> allows selecting only snapshot + # validated tests in order to capture new snapshots for a whole + # test file with "-m snapshot_validated" + item.add_marker("snapshot_validated") @pytest.fixture diff --git a/localstack/utils/aws/client_types.py b/localstack/utils/aws/client_types.py index 374909aa7a1f5..7919ab88e2c14 100644 --- a/localstack/utils/aws/client_types.py +++ b/localstack/utils/aws/client_types.py @@ -231,7 +231,7 @@ class TypedServiceClientFactory(abc.ABC): sesv2: Union["SESV2Client", "MetadataRequestInjector[SESV2Client]"] sns: Union["SNSClient", "MetadataRequestInjector[SNSClient]"] sqs: Union["SQSClient", "MetadataRequestInjector[SQSClient]"] - sqs_json: Union["SQSClient", "MetadataRequestInjector[SQSClient]"] + sqs_query: Union["SQSClient", "MetadataRequestInjector[SQSClient]"] ssm: Union["SSMClient", "MetadataRequestInjector[SSMClient]"] sso_admin: Union["SSOAdminClient", "MetadataRequestInjector[SSOAdminClient]"] stepfunctions: Union["SFNClient", "MetadataRequestInjector[SFNClient]"] diff --git a/pyproject.toml b/pyproject.toml index 412f0bb141326..0a057be469f45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,9 +48,9 @@ Issues = "https://github.com/localstack/localstack/issues" # minimal required to actually run localstack on the host for services natively implemented in python base-runtime = [ # pinned / updated by ASF update action - "boto3==1.34.84", + "boto3==1.34.93", # pinned / updated by ASF update action - "botocore==1.34.84", + "botocore==1.34.93", "awscrt>=0.13.14", "cbor2>=5.2.0", "dnspython>=1.16.0", @@ -76,7 +76,7 @@ base-runtime = [ runtime = [ "localstack-core[base-runtime]", # pinned / updated by ASF update action - "awscli==1.32.84", + "awscli==1.32.93", "airspeed-ext>=0.6.3", "amazon_kclpy>=2.0.6,!=2.1.0,!=2.1.4", # antlr4-python3-runtime: exact pin because antlr4 runtime is tightly coupled to the generated parser code @@ -133,7 +133,7 @@ typehint = [ # typehint is an optional extension of the dev dependencies "localstack-core[dev]", # pinned / updated by ASF update action - "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.84", + "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.93", ] [tool.setuptools] diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index 88b590e2d8c10..162819220a94f 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -14,9 +14,9 @@ blinker==1.8.1 # via # flask # quart -boto3==1.34.84 +boto3==1.34.93 # via localstack-core (pyproject.toml) -botocore==1.34.84 +botocore==1.34.93 # via # boto3 # localstack-core (pyproject.toml) diff --git a/requirements-dev.txt b/requirements-dev.txt index a0bad023e92a9..dd200dfffb765 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.87.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.84 +awscli==1.32.93 # via localstack-core awscrt==0.20.9 # via localstack-core @@ -55,12 +55,12 @@ blinker==1.8.1 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.84 +boto3==1.34.93 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.84 +botocore==1.34.93 # via # aws-xray-sdk # awscli diff --git a/requirements-runtime.txt b/requirements-runtime.txt index c213975c6411f..84d07813733b6 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -33,7 +33,7 @@ aws-sam-translator==1.87.0 # localstack-core (pyproject.toml) aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.84 +awscli==1.32.93 # via localstack-core (pyproject.toml) awscrt==0.20.9 # via localstack-core @@ -43,12 +43,12 @@ blinker==1.8.1 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.84 +boto3==1.34.93 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.84 +botocore==1.34.93 # via # aws-xray-sdk # awscli diff --git a/requirements-test.txt b/requirements-test.txt index 1ea868fdf0cb6..c3d3bd4946a54 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.87.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.84 +awscli==1.32.93 # via localstack-core awscrt==0.20.9 # via localstack-core @@ -55,12 +55,12 @@ blinker==1.8.1 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.84 +boto3==1.34.93 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.84 +botocore==1.34.93 # via # aws-xray-sdk # awscli diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 355ce06282fef..f42f7d48feaf7 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.87.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.84 +awscli==1.32.93 # via localstack-core awscrt==0.20.9 # via localstack-core @@ -55,14 +55,14 @@ blinker==1.8.1 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.84 +boto3==1.34.93 # via # aws-sam-translator # localstack-core # moto-ext -boto3-stubs==1.34.84 +boto3-stubs==1.34.93 # via localstack-core (pyproject.toml) -botocore==1.34.84 +botocore==1.34.93 # via # aws-xray-sdk # awscli diff --git a/tests/aws/services/sns/test_sns.py b/tests/aws/services/sns/test_sns.py index de9b0e52e6356..59a217a0bd9bd 100644 --- a/tests/aws/services/sns/test_sns.py +++ b/tests/aws/services/sns/test_sns.py @@ -2124,7 +2124,7 @@ def test_message_to_fifo_sqs( aws_client.sns.publish(TopicArn=topic_arn, Message=message, **kwargs) - response = aws_client.sqs_json.receive_message( + response = aws_client.sqs.receive_message( QueueUrl=queue_url, WaitTimeSeconds=10, AttributeNames=["All"], @@ -2136,7 +2136,7 @@ def test_message_to_fifo_sqs( ) # republish the message, to check deduplication aws_client.sns.publish(TopicArn=topic_arn, Message=message, **kwargs) - response = aws_client.sqs_json.receive_message( + response = aws_client.sqs.receive_message( QueueUrl=queue_url, WaitTimeSeconds=1, AttributeNames=["All"], diff --git a/tests/aws/services/sqs/test_sqs.py b/tests/aws/services/sqs/test_sqs.py index fce7b961ad5f0..0f14951160182 100644 --- a/tests/aws/services/sqs/test_sqs.py +++ b/tests/aws/services/sqs/test_sqs.py @@ -76,7 +76,7 @@ def sqs_snapshot_transformer(snapshot): snapshot.add_transformer(snapshot.transform.sqs_api()) -@pytest.fixture(params=["sqs", "sqs_json"]) +@pytest.fixture(params=["sqs", "sqs_query"]) def aws_sqs_client(aws_client, request: str) -> "SQSClient": yield getattr(aws_client, request.param) @@ -3999,13 +3999,6 @@ def test_sse_kms_and_sqs_are_mutually_exclusive( snapshot.match("error", e.value) @markers.aws.validated - @markers.snapshot.skip_snapshot_verify( - paths=[ - "$.illegal_name_1.Messages[0].MessageAttributes", - "$.illegal_name_2.Messages[0].MessageAttributes", - # AWS does not return the field at all if there's an illegal name, we return empty dict - ] - ) def test_receive_message_message_attribute_names_filters( self, sqs_create_queue, snapshot, aws_sqs_client ): @@ -4266,20 +4259,20 @@ def test_sqs_permission_lifecycle(self, sqs_queue, aws_sqs_client, snapshot, acc def test_non_existent_queue(self, aws_client, sqs_create_queue, sqs_queue_exists, snapshot): queue_name = f"test-queue-{short_uid()}" queue_url = sqs_create_queue(QueueName=queue_name) - aws_client.sqs_json.delete_queue(QueueUrl=queue_url) + aws_client.sqs.delete_queue(QueueUrl=queue_url) assert poll_condition(lambda: not sqs_queue_exists(queue_url), timeout=5) with pytest.raises(ClientError) as e: - aws_client.sqs_json.get_queue_attributes(QueueUrl=queue_url) + aws_client.sqs.get_queue_attributes(QueueUrl=queue_url) snapshot.match("queue-does-not-exist", e.value.response) # validate both the client exception handling in boto and GetQueueUrl - with pytest.raises(aws_client.sqs_json.exceptions.QueueDoesNotExist) as e: - aws_client.sqs_json.get_queue_url(QueueName=queue_name) + with pytest.raises(aws_client.sqs.exceptions.QueueDoesNotExist) as e: + aws_client.sqs.get_queue_url(QueueName=queue_name) snapshot.match("queue-does-not-exist-url", e.value.response) with pytest.raises(ClientError) as e: - aws_client.sqs.get_queue_attributes(QueueUrl=queue_url) + aws_client.sqs_query.get_queue_attributes(QueueUrl=queue_url) snapshot.match("queue-does-not-exist-query", e.value.response) @@ -4624,8 +4617,8 @@ def test_endpoint_strategy_with_multi_region( assert region2 in queue_region2 # us-east-1 is the default region, so it's not necessarily part of the queue URL - client_region1 = aws_http_client_factory("sqs", region1) - client_region2 = aws_http_client_factory("sqs", region2) + client_region1 = aws_http_client_factory("sqs_query", region1) + client_region2 = aws_http_client_factory("sqs_query", region2) response = client_region1.get( queue_region1, params={"Action": "SendMessage", "MessageBody": "foobar"} @@ -4746,7 +4739,7 @@ def test_send_message_via_queue_url_with_json_protocol( # protocol should target the root path sqs_client = aws_client_factory( endpoint_url=queue_url, - ).sqs_json + ).sqs response = sqs_client.receive_message(QueueUrl=queue_url, WaitTimeSeconds=1) assert ( diff --git a/tests/aws/services/sqs/test_sqs.snapshot.json b/tests/aws/services/sqs/test_sqs.snapshot.json index aa8bc81e45243..3f357082ec4fe 100644 --- a/tests/aws/services/sqs/test_sqs.snapshot.json +++ b/tests/aws/services/sqs/test_sqs.snapshot.json @@ -1,12 +1,12 @@ { "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_max_number_of_messages[sqs]": { - "recorded-date": "06-12-2023, 13:51:19", + "recorded-date": "30-04-2024, 13:32:56", "recorded-content": { "send_max_number_of_messages": { "Error": { "Code": "InvalidParameterValue", - "Detail": null, "Message": "Value 11 for parameter MaxNumberOfMessages is invalid. Reason: Must be between 1 and 10, if provided.", + "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -16,14 +16,14 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_max_number_of_messages[sqs_json]": { - "recorded-date": "06-12-2023, 13:51:20", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_max_number_of_messages[sqs_query]": { + "recorded-date": "30-04-2024, 13:32:57", "recorded-content": { "send_max_number_of_messages": { "Error": { "Code": "InvalidParameterValue", + "Detail": null, "Message": "Value 11 for parameter MaxNumberOfMessages is invalid. Reason: Must be between 1 and 10, if provided.", - "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -34,13 +34,13 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_oversized_message[sqs]": { - "recorded-date": "06-12-2023, 13:51:31", + "recorded-date": "30-04-2024, 13:32:59", "recorded-content": { "send_oversized_message": { "Error": { "Code": "InvalidParameterValue", - "Detail": null, "Message": "One or more parameters are invalid. Reason: Message must be shorter than 262144 bytes.", + "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -50,14 +50,14 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_oversized_message[sqs_json]": { - "recorded-date": "06-12-2023, 13:51:33", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_oversized_message[sqs_query]": { + "recorded-date": "30-04-2024, 13:33:01", "recorded-content": { "send_oversized_message": { "Error": { "Code": "InvalidParameterValue", + "Detail": null, "Message": "One or more parameters are invalid. Reason: Message must be shorter than 262144 bytes.", - "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -68,13 +68,13 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_updated_maximum_message_size[sqs]": { - "recorded-date": "06-12-2023, 13:51:34", + "recorded-date": "30-04-2024, 13:33:02", "recorded-content": { "send_oversized_message": { "Error": { "Code": "InvalidParameterValue", - "Detail": null, "Message": "One or more parameters are invalid. Reason: Message must be shorter than 1024 bytes.", + "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -84,14 +84,14 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_updated_maximum_message_size[sqs_json]": { - "recorded-date": "06-12-2023, 13:51:35", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_updated_maximum_message_size[sqs_query]": { + "recorded-date": "30-04-2024, 13:33:04", "recorded-content": { "send_oversized_message": { "Error": { "Code": "InvalidParameterValue", + "Detail": null, "Message": "One or more parameters are invalid. Reason: Message must be shorter than 1024 bytes.", - "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -102,13 +102,13 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents[sqs]": { - "recorded-date": "06-12-2023, 13:51:37", + "recorded-date": "30-04-2024, 13:33:06", "recorded-content": { "send_oversized_message_batch": { "Error": { "Code": "AWS.SimpleQueueService.BatchRequestTooLong", - "Detail": null, "Message": "Batch requests cannot be longer than 262144 bytes. You have sent 262145 bytes.", + "QueryErrorCode": "BatchRequestTooLong", "Type": "Sender" }, "ResponseMetadata": { @@ -118,14 +118,14 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents[sqs_json]": { - "recorded-date": "06-12-2023, 13:51:38", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents[sqs_query]": { + "recorded-date": "30-04-2024, 13:33:08", "recorded-content": { "send_oversized_message_batch": { "Error": { "Code": "AWS.SimpleQueueService.BatchRequestTooLong", + "Detail": null, "Message": "Batch requests cannot be longer than 262144 bytes. You have sent 262145 bytes.", - "QueryErrorCode": "BatchRequestTooLong", "Type": "Sender" }, "ResponseMetadata": { @@ -136,7 +136,7 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents_with_updated_maximum_message_size[sqs]": { - "recorded-date": "06-12-2023, 13:51:39", + "recorded-date": "30-04-2024, 13:33:09", "recorded-content": { "send_oversized_message_batch": { "Successful": [ @@ -159,8 +159,8 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents_with_updated_maximum_message_size[sqs_json]": { - "recorded-date": "06-12-2023, 13:51:39", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents_with_updated_maximum_message_size[sqs_query]": { + "recorded-date": "30-04-2024, 13:33:10", "recorded-content": { "send_oversized_message_batch": { "Successful": [ @@ -184,7 +184,7 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_tag_untag_queue[sqs]": { - "recorded-date": "06-12-2023, 13:51:41", + "recorded-date": "30-04-2024, 13:35:30", "recorded-content": { "get-tag-1": { "Tags": { @@ -214,8 +214,8 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_tag_untag_queue[sqs_json]": { - "recorded-date": "06-12-2023, 13:51:43", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_tag_untag_queue[sqs_query]": { + "recorded-date": "30-04-2024, 13:35:33", "recorded-content": { "get-tag-1": { "Tags": { @@ -246,13 +246,13 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_fifo_queue_with_different_attributes_raises_error[sqs]": { - "recorded-date": "06-12-2023, 13:52:04", + "recorded-date": "30-04-2024, 13:33:13", "recorded-content": { "queue-already-exists": { "Error": { "Code": "QueueAlreadyExists", - "Detail": null, "Message": "A queue already exists with the same name and a different value for attribute ContentBasedDeduplication", + "QueryErrorCode": "QueueNameExists", "Type": "Sender" }, "ResponseMetadata": { @@ -262,14 +262,48 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_fifo_queue_with_different_attributes_raises_error[sqs_json]": { - "recorded-date": "06-12-2023, 13:52:04", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_fifo_queue_with_different_attributes_raises_error[sqs_query]": { + "recorded-date": "30-04-2024, 13:33:14", "recorded-content": { "queue-already-exists": { "Error": { "Code": "QueueAlreadyExists", - "Detail": null, "Message": "A queue already exists with the same name and a different value for attribute ContentBasedDeduplication", + "QueryErrorCode": "QueueNameExists", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_deduplication_id_too_long": { + "recorded-date": "30-04-2024, 13:35:34", + "recorded-content": { + "error-response": { + "Error": { + "Code": "InvalidParameterValue", + "Message": "Value aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa for parameter MessageDeduplicationId is invalid. Reason: MessageDeduplicationId can only include alphanumeric and punctuation characters. 1 to 128 in length.", + "QueryErrorCode": "InvalidParameterValueException", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_group_id_too_long": { + "recorded-date": "30-04-2024, 13:35:35", + "recorded-content": { + "error-response": { + "Error": { + "Code": "InvalidParameterValue", + "Message": "Value aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa for parameter MessageGroupId is invalid. Reason: MessageGroupId can only include alphanumeric and punctuation characters. 1 to 128 in length.", + "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -280,13 +314,13 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_different_attributes_raises_exception[sqs]": { - "recorded-date": "06-12-2023, 13:52:09", + "recorded-date": "30-04-2024, 13:33:18", "recorded-content": { "create_queue_01": { "Error": { "Code": "QueueAlreadyExists", - "Detail": null, "Message": "A queue already exists with the same name and a different value for attribute DelaySeconds", + "QueryErrorCode": "QueueNameExists", "Type": "Sender" }, "ResponseMetadata": { @@ -297,8 +331,8 @@ "create_queue_02": { "Error": { "Code": "QueueAlreadyExists", - "Detail": null, "Message": "A queue already exists with the same name and a different value for attribute DelaySeconds", + "QueryErrorCode": "QueueNameExists", "Type": "Sender" }, "ResponseMetadata": { @@ -308,14 +342,14 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_different_attributes_raises_exception[sqs_json]": { - "recorded-date": "06-12-2023, 13:52:11", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_different_attributes_raises_exception[sqs_query]": { + "recorded-date": "30-04-2024, 13:33:20", "recorded-content": { "create_queue_01": { "Error": { "Code": "QueueAlreadyExists", - "Detail": null, "Message": "A queue already exists with the same name and a different value for attribute DelaySeconds", + "QueryErrorCode": "QueueNameExists", "Type": "Sender" }, "ResponseMetadata": { @@ -326,8 +360,8 @@ "create_queue_02": { "Error": { "Code": "QueueAlreadyExists", - "Detail": null, "Message": "A queue already exists with the same name and a different value for attribute DelaySeconds", + "QueryErrorCode": "QueueNameExists", "Type": "Sender" }, "ResponseMetadata": { @@ -338,7 +372,7 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_and_update_queue_attributes[sqs]": { - "recorded-date": "06-12-2023, 13:52:14", + "recorded-date": "30-04-2024, 13:33:21", "recorded-content": { "get_queue_attributes": { "Attributes": { @@ -382,8 +416,8 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_and_update_queue_attributes[sqs_json]": { - "recorded-date": "06-12-2023, 13:52:15", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_and_update_queue_attributes[sqs_query]": { + "recorded-date": "30-04-2024, 13:33:22", "recorded-content": { "get_queue_attributes": { "Attributes": { @@ -428,7 +462,7 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_after_visibility_timeout_expiration[sqs]": { - "recorded-date": "06-12-2023, 13:52:53", + "recorded-date": "30-04-2024, 13:33:27", "recorded-content": { "visibility_timeout_expired": { "ResponseMetadata": { @@ -438,8 +472,8 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_after_visibility_timeout_expiration[sqs_json]": { - "recorded-date": "06-12-2023, 13:52:56", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_after_visibility_timeout_expiration[sqs_query]": { + "recorded-date": "30-04-2024, 13:33:30", "recorded-content": { "visibility_timeout_expired": { "ResponseMetadata": { @@ -449,14 +483,92 @@ } } }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_empty_message_groups_added_back_to_queue[sqs]": { + "recorded-date": "30-04-2024, 13:46:32", + "recorded-content": { + "inital-fifo-receive": { + "Messages": [ + { + "Body": "Message 1", + "MD5OfBody": "68390233272823b7adf13a1db79b2cd7", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "empty-fifo-receive": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "final-fifo-receive": { + "Messages": [ + { + "Body": "Message 2", + "MD5OfBody": "88ef8f31ed540f1c4c03d5fdb06a7935", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_empty_message_groups_added_back_to_queue[sqs_query]": { + "recorded-date": "30-04-2024, 13:46:34", + "recorded-content": { + "inital-fifo-receive": { + "Messages": [ + { + "Body": "Message 1", + "MD5OfBody": "68390233272823b7adf13a1db79b2cd7", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "empty-fifo-receive": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "final-fifo-receive": { + "Messages": [ + { + "Body": "Message 2", + "MD5OfBody": "88ef8f31ed540f1c4c03d5fdb06a7935", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_queue_send_message_with_delay_seconds_fails[sqs]": { - "recorded-date": "06-12-2023, 13:53:52", + "recorded-date": "30-04-2024, 13:33:33", "recorded-content": { "send_message": { "Error": { "Code": "InvalidParameterValue", - "Detail": null, "Message": "Value 2 for parameter DelaySeconds is invalid. Reason: The request include parameter that is not valid for this queue type.", + "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -466,14 +578,14 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_queue_send_message_with_delay_seconds_fails[sqs_json]": { - "recorded-date": "06-12-2023, 13:53:52", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_queue_send_message_with_delay_seconds_fails[sqs_query]": { + "recorded-date": "30-04-2024, 13:33:34", "recorded-content": { "send_message": { "Error": { "Code": "InvalidParameterValue", + "Detail": null, "Message": "Value 2 for parameter DelaySeconds is invalid. Reason: The request include parameter that is not valid for this queue type.", - "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -484,7 +596,7 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_attributes[sqs]": { - "recorded-date": "06-12-2023, 13:54:04", + "recorded-date": "30-04-2024, 13:36:56", "recorded-content": { "send_message": { "MD5OfMessageBody": "19c9e282d65f9733bc6b35d50062c7ee", @@ -543,8 +655,8 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_attributes[sqs_json]": { - "recorded-date": "06-12-2023, 13:54:05", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_attributes[sqs_query]": { + "recorded-date": "30-04-2024, 13:36:57", "recorded-content": { "send_message": { "MD5OfMessageBody": "19c9e282d65f9733bc6b35d50062c7ee", @@ -604,69 +716,15 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_delete_message_with_expired_receipt_handle[sqs]": { - "recorded-date": "06-12-2023, 13:54:34", - "recorded-content": { - "response": { - "Messages": [ - { - "Body": "g1-m1", - "MD5OfBody": "fc4286b824b39ddf3606c9f27ff664bd", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "error": { - "Error": { - "Code": "InvalidParameterValue", - "Detail": null, - "Message": "Value <receipt-handle:1> for parameter ReceiptHandle is invalid. Reason: The receipt handle has expired.", - "Type": "Sender" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } + "recorded-date": "30-04-2024, 13:39:23", + "recorded-content": {} }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_delete_message_with_expired_receipt_handle[sqs_json]": { - "recorded-date": "06-12-2023, 13:54:36", - "recorded-content": { - "response": { - "Messages": [ - { - "Body": "g1-m1", - "MD5OfBody": "fc4286b824b39ddf3606c9f27ff664bd", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "error": { - "Error": { - "Code": "InvalidParameterValue", - "Message": "Value <receipt-handle:1> for parameter ReceiptHandle is invalid. Reason: The receipt handle has expired.", - "QueryErrorCode": "InvalidParameterValueException", - "Type": "Sender" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_delete_message_with_expired_receipt_handle[sqs_query]": { + "recorded-date": "30-04-2024, 13:39:25", + "recorded-content": {} }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_set_content_based_deduplication_strategy[sqs]": { - "recorded-date": "06-12-2023, 13:54:37", + "recorded-date": "30-04-2024, 13:47:39", "recorded-content": { "before-update": { "Attributes": { @@ -718,8 +776,8 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_set_content_based_deduplication_strategy[sqs_json]": { - "recorded-date": "06-12-2023, 13:54:38", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_set_content_based_deduplication_strategy[sqs_query]": { + "recorded-date": "30-04-2024, 13:47:41", "recorded-content": { "before-update": { "Attributes": { @@ -772,13 +830,13 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_too_many_entries_in_batch_request[sqs]": { - "recorded-date": "06-12-2023, 13:54:48", + "recorded-date": "30-04-2024, 13:33:39", "recorded-content": { "test_too_many_entries_in_batch_request": { "Error": { "Code": "AWS.SimpleQueueService.TooManyEntriesInBatchRequest", - "Detail": null, "Message": "Maximum number of entries per request are 10. You have sent 20.", + "QueryErrorCode": "TooManyEntriesInBatchRequest", "Type": "Sender" }, "ResponseMetadata": { @@ -788,14 +846,14 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_too_many_entries_in_batch_request[sqs_json]": { - "recorded-date": "06-12-2023, 13:54:48", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_too_many_entries_in_batch_request[sqs_query]": { + "recorded-date": "30-04-2024, 13:33:40", "recorded-content": { "test_too_many_entries_in_batch_request": { "Error": { "Code": "AWS.SimpleQueueService.TooManyEntriesInBatchRequest", + "Detail": null, "Message": "Maximum number of entries per request are 10. You have sent 20.", - "QueryErrorCode": "TooManyEntriesInBatchRequest", "Type": "Sender" }, "ResponseMetadata": { @@ -806,13 +864,13 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_batch_id[sqs]": { - "recorded-date": "06-12-2023, 13:54:49", + "recorded-date": "30-04-2024, 13:33:40", "recorded-content": { "test_invalid_batch_id": { "Error": { "Code": "AWS.SimpleQueueService.InvalidBatchEntryId", - "Detail": null, "Message": "A batch entry id can only contain alphanumeric characters, hyphens and underscores. It can be at most 80 letters long.", + "QueryErrorCode": "InvalidBatchEntryId", "Type": "Sender" }, "ResponseMetadata": { @@ -822,14 +880,14 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_batch_id[sqs_json]": { - "recorded-date": "06-12-2023, 13:54:50", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_batch_id[sqs_query]": { + "recorded-date": "30-04-2024, 13:33:41", "recorded-content": { "test_invalid_batch_id": { "Error": { "Code": "AWS.SimpleQueueService.InvalidBatchEntryId", + "Detail": null, "Message": "A batch entry id can only contain alphanumeric characters, hyphens and underscores. It can be at most 80 letters long.", - "QueryErrorCode": "InvalidBatchEntryId", "Type": "Sender" }, "ResponseMetadata": { @@ -840,13 +898,13 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_deduplication_id_for_fifo_queue[sqs]": { - "recorded-date": "06-12-2023, 13:54:50", + "recorded-date": "30-04-2024, 13:33:42", "recorded-content": { "test_missing_deduplication_id_for_fifo_queue": { "Error": { "Code": "InvalidParameterValue", - "Detail": null, "Message": "The queue should either have ContentBasedDeduplication enabled or MessageDeduplicationId provided explicitly", + "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -856,14 +914,14 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_deduplication_id_for_fifo_queue[sqs_json]": { - "recorded-date": "06-12-2023, 13:54:51", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_deduplication_id_for_fifo_queue[sqs_query]": { + "recorded-date": "30-04-2024, 13:33:43", "recorded-content": { "test_missing_deduplication_id_for_fifo_queue": { "Error": { "Code": "InvalidParameterValue", + "Detail": null, "Message": "The queue should either have ContentBasedDeduplication enabled or MessageDeduplicationId provided explicitly", - "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -873,14 +931,40 @@ } } }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_message_size": { + "recorded-date": "30-04-2024, 13:33:44", + "recorded-content": { + "send-message-batch-result": { + "Failed": [ + { + "Code": "InvalidParameterValue", + "Id": "<uuid:1>", + "Message": "One or more parameters cannot be validated. Reason: Message must be shorter than 1024 bytes.", + "SenderFault": true + } + ], + "Successful": [ + { + "Id": "<uuid:2>", + "MD5OfMessageBody": "c9a34cfc85d982698c6ac89f76071abd", + "MessageId": "<uuid:3>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_message_group_id_for_fifo_queue[sqs]": { - "recorded-date": "06-12-2023, 13:54:51", + "recorded-date": "30-04-2024, 13:33:44", "recorded-content": { "test_missing_message_group_id_for_fifo_queue": { "Error": { "Code": "InvalidParameterValue", - "Detail": null, "Message": "The request must contain the parameter MessageGroupId.", + "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -890,14 +974,14 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_message_group_id_for_fifo_queue[sqs_json]": { - "recorded-date": "06-12-2023, 13:54:52", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_message_group_id_for_fifo_queue[sqs_query]": { + "recorded-date": "30-04-2024, 13:33:45", "recorded-content": { "test_missing_message_group_id_for_fifo_queue": { "Error": { "Code": "InvalidParameterValue", + "Detail": null, "Message": "The request must contain the parameter MessageGroupId.", - "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -908,12 +992,13 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs-]": { - "recorded-date": "06-12-2023, 13:54:56", + "recorded-date": "30-04-2024, 13:48:34", "recorded-content": { "error_response": { "Error": { "Code": "AWS.SimpleQueueService.InvalidBatchEntryId", "Message": "A batch entry id can only contain alphanumeric characters, hyphens and underscores. It can be at most 80 letters long.", + "QueryErrorCode": "InvalidBatchEntryId", "Type": "Sender" }, "ResponseMetadata": { @@ -924,12 +1009,13 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs-testLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongId]": { - "recorded-date": "06-12-2023, 13:54:57", + "recorded-date": "30-04-2024, 13:48:35", "recorded-content": { "error_response": { "Error": { "Code": "AWS.SimpleQueueService.InvalidBatchEntryId", "Message": "A batch entry id can only contain alphanumeric characters, hyphens and underscores. It can be at most 80 letters long.", + "QueryErrorCode": "InvalidBatchEntryId", "Type": "Sender" }, "ResponseMetadata": { @@ -940,12 +1026,13 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs-invalid:id]": { - "recorded-date": "06-12-2023, 13:54:58", + "recorded-date": "30-04-2024, 13:48:36", "recorded-content": { "error_response": { "Error": { "Code": "AWS.SimpleQueueService.InvalidBatchEntryId", "Message": "A batch entry id can only contain alphanumeric characters, hyphens and underscores. It can be at most 80 letters long.", + "QueryErrorCode": "InvalidBatchEntryId", "Type": "Sender" }, "ResponseMetadata": { @@ -955,14 +1042,13 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs_json-]": { - "recorded-date": "06-12-2023, 13:54:59", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs_query-]": { + "recorded-date": "30-04-2024, 13:48:37", "recorded-content": { "error_response": { "Error": { "Code": "AWS.SimpleQueueService.InvalidBatchEntryId", "Message": "A batch entry id can only contain alphanumeric characters, hyphens and underscores. It can be at most 80 letters long.", - "QueryErrorCode": "InvalidBatchEntryId", "Type": "Sender" }, "ResponseMetadata": { @@ -972,14 +1058,13 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs_json-testLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongId]": { - "recorded-date": "06-12-2023, 13:54:59", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs_query-testLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongId]": { + "recorded-date": "30-04-2024, 13:48:38", "recorded-content": { "error_response": { "Error": { "Code": "AWS.SimpleQueueService.InvalidBatchEntryId", "Message": "A batch entry id can only contain alphanumeric characters, hyphens and underscores. It can be at most 80 letters long.", - "QueryErrorCode": "InvalidBatchEntryId", "Type": "Sender" }, "ResponseMetadata": { @@ -989,14 +1074,13 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs_json-invalid:id]": { - "recorded-date": "06-12-2023, 13:55:00", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs_query-invalid:id]": { + "recorded-date": "30-04-2024, 13:48:38", "recorded-content": { "error_response": { "Error": { "Code": "AWS.SimpleQueueService.InvalidBatchEntryId", "Message": "A batch entry id can only contain alphanumeric characters, hyphens and underscores. It can be at most 80 letters long.", - "QueryErrorCode": "InvalidBatchEntryId", "Type": "Sender" }, "ResponseMetadata": { @@ -1007,12 +1091,13 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_with_too_large_batch[sqs]": { - "recorded-date": "06-12-2023, 13:55:04", + "recorded-date": "30-04-2024, 13:49:26", "recorded-content": { "error_response": { "Error": { "Code": "AWS.SimpleQueueService.TooManyEntriesInBatchRequest", "Message": "Maximum number of entries per request are 10. You have sent 20.", + "QueryErrorCode": "TooManyEntriesInBatchRequest", "Type": "Sender" }, "ResponseMetadata": { @@ -1022,14 +1107,13 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_with_too_large_batch[sqs_json]": { - "recorded-date": "06-12-2023, 13:55:09", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_with_too_large_batch[sqs_query]": { + "recorded-date": "30-04-2024, 13:49:32", "recorded-content": { "error_response": { "Error": { "Code": "AWS.SimpleQueueService.TooManyEntriesInBatchRequest", "Message": "Maximum number of entries per request are 10. You have sent 20.", - "QueryErrorCode": "TooManyEntriesInBatchRequest", "Type": "Sender" }, "ResponseMetadata": { @@ -1040,12 +1124,13 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_batch_with_too_large_batch[sqs]": { - "recorded-date": "06-12-2023, 13:55:14", + "recorded-date": "30-04-2024, 13:50:34", "recorded-content": { "error_response": { "Error": { "Code": "AWS.SimpleQueueService.TooManyEntriesInBatchRequest", "Message": "Maximum number of entries per request are 10. You have sent 20.", + "QueryErrorCode": "TooManyEntriesInBatchRequest", "Type": "Sender" }, "ResponseMetadata": { @@ -1055,14 +1140,13 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_batch_with_too_large_batch[sqs_json]": { - "recorded-date": "06-12-2023, 13:55:19", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_batch_with_too_large_batch[sqs_query]": { + "recorded-date": "30-04-2024, 13:50:41", "recorded-content": { "error_response": { "Error": { "Code": "AWS.SimpleQueueService.TooManyEntriesInBatchRequest", "Message": "Maximum number of entries per request are 10. You have sent 20.", - "QueryErrorCode": "TooManyEntriesInBatchRequest", "Type": "Sender" }, "ResponseMetadata": { @@ -1073,24 +1157,11 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_dead_letter_arn_rejected_before_lookup": { - "recorded-date": "06-12-2023, 13:55:25", - "recorded-content": { - "error_response": { - "Error": { - "Code": "InvalidParameterValue", - "Detail": null, - "Message": "Value {\"deadLetterTargetArn\": \"dummy\", \"maxReceiveCount\": 42} for parameter RedrivePolicy is invalid. Reason: Invalid value for deadLetterTargetArn.", - "Type": "Sender" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } + "recorded-date": "30-04-2024, 13:33:47", + "recorded-content": {} }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_binary_attributes[sqs]": { - "recorded-date": "06-12-2023, 13:55:32", + "recorded-date": "30-04-2024, 13:33:48", "recorded-content": { "binary-attrs-msg": { "Messages": [ @@ -1115,8 +1186,8 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_binary_attributes[sqs_json]": { - "recorded-date": "06-12-2023, 13:55:33", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_binary_attributes[sqs_query]": { + "recorded-date": "30-04-2024, 13:33:49", "recorded-content": { "binary-attrs-msg": { "Messages": [ @@ -1142,13 +1213,13 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_empty_string_attribute[sqs]": { - "recorded-date": "06-12-2023, 13:55:35", + "recorded-date": "30-04-2024, 13:35:41", "recorded-content": { "empty-string-attr": { "Error": { "Code": "InvalidParameterValue", - "Detail": null, "Message": "Message (user) attribute 'ErrorDetails' must contain a non-empty value of type 'String'.", + "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -1158,14 +1229,14 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_empty_string_attribute[sqs_json]": { - "recorded-date": "06-12-2023, 13:55:35", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_empty_string_attribute[sqs_query]": { + "recorded-date": "30-04-2024, 13:35:42", "recorded-content": { "empty-string-attr": { "Error": { "Code": "InvalidParameterValue", + "Detail": null, "Message": "Message (user) attribute 'ErrorDetails' must contain a non-empty value of type 'String'.", - "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -1175,68 +1246,89 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_fifo[sqs]": { - "recorded-date": "06-12-2023, 13:56:04", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_message_attributes": { + "recorded-date": "30-04-2024, 13:33:55", "recorded-content": { - "invalid-attr-name-1": { - "Error": { - "Code": "InvalidAttributeName", - "Detail": null, - "Message": "Unknown Attribute FifoQueue.", - "Type": "Sender" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - }, - "invalid-attr-name-2": { - "Error": { - "Code": "InvalidAttributeValue", - "Detail": null, - "Message": "Invalid value for the parameter FifoQueue. Reason: Modifying queue type is not supported.", - "Type": "Sender" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_fifo[sqs_json]": { - "recorded-date": "06-12-2023, 13:56:05", - "recorded-content": { - "invalid-attr-name-1": { - "Error": { - "Code": "InvalidAttributeName", - "Message": "Unknown Attribute FifoQueue.", - "QueryErrorCode": "InvalidAttributeName", - "Type": "Sender" + "dlq-arn": "arn:aws:sqs:<region>:111111111111:<resource:1>", + "sourcen-arn": "arn:aws:sqs:<region>:111111111111:<resource:2>", + "rec-pre-dlq": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "3d6b824fd8c1520e9a047d21fee6fb1f", + "Body": "message-1", + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "1", + "SentTimestamp": "timestamp", + "AWSTraceHeader": "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1" + }, + "MD5OfMessageAttributes": "adb59cd4678ea5a855436b949cd07ab6", + "MessageAttributes": { + "MyAttribute": { + "StringValue": "foobar", + "DataType": "String" + } + } }, - "message": "Unknown Attribute FifoQueue.", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 + { + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>", + "MD5OfBody": "95ef155b66299d14edf7ed57c468c13b", + "Body": "message-2", + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "1", + "SentTimestamp": "timestamp" + } } - }, - "invalid-attr-name-2": { - "Error": { - "Code": "InvalidAttributeValue", - "Message": "Invalid value for the parameter FifoQueue. Reason: Modifying queue type is not supported.", - "QueryErrorCode": "InvalidAttributeValue", - "Type": "Sender" + ], + "dlq-messages": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:3>", + "MD5OfBody": "3d6b824fd8c1520e9a047d21fee6fb1f", + "Body": "message-1", + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "2", + "SentTimestamp": "timestamp", + "AWSTraceHeader": "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1", + "DeadLetterQueueSourceArn": "arn:aws:sqs:<region>:111111111111:<resource:2>" + }, + "MD5OfMessageAttributes": "adb59cd4678ea5a855436b949cd07ab6", + "MessageAttributes": { + "MyAttribute": { + "StringValue": "foobar", + "DataType": "String" + } + } }, - "message": "Invalid value for the parameter FifoQueue. Reason: Modifying queue type is not supported.", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 + { + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:4>", + "MD5OfBody": "95ef155b66299d14edf7ed57c468c13b", + "Body": "message-2", + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "2", + "SentTimestamp": "timestamp", + "DeadLetterQueueSourceArn": "arn:aws:sqs:<region>:111111111111:<resource:2>" + } } - } + ] } }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_fifo[sqs]": { + "recorded-date": "30-04-2024, 13:33:56", + "recorded-content": {} + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_fifo[sqs_query]": { + "recorded-date": "30-04-2024, 13:33:57", + "recorded-content": {} + }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[sqs-True]": { - "recorded-date": "06-12-2023, 13:56:18", + "recorded-date": "30-04-2024, 13:34:01", "recorded-content": { "get-messages": { "Messages": [ @@ -1263,7 +1355,7 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[sqs-False]": { - "recorded-date": "06-12-2023, 13:56:21", + "recorded-date": "30-04-2024, 13:34:04", "recorded-content": { "get-messages": { "Messages": [ @@ -1289,8 +1381,8 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[sqs_json-True]": { - "recorded-date": "06-12-2023, 13:56:23", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[sqs_query-True]": { + "recorded-date": "30-04-2024, 13:34:06", "recorded-content": { "get-messages": { "Messages": [ @@ -1316,8 +1408,8 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[sqs_json-False]": { - "recorded-date": "06-12-2023, 13:56:26", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[sqs_query-False]": { + "recorded-date": "30-04-2024, 13:34:09", "recorded-content": { "get-messages": { "Messages": [ @@ -1344,7 +1436,7 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[sqs-True]": { - "recorded-date": "06-12-2023, 13:56:29", + "recorded-date": "30-04-2024, 13:34:11", "recorded-content": { "get-messages": { "Messages": [ @@ -1371,7 +1463,7 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[sqs-False]": { - "recorded-date": "06-12-2023, 13:56:31", + "recorded-date": "30-04-2024, 13:34:14", "recorded-content": { "get-messages": { "Messages": [ @@ -1397,8 +1489,8 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[sqs_json-True]": { - "recorded-date": "06-12-2023, 13:56:33", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[sqs_query-True]": { + "recorded-date": "30-04-2024, 13:34:17", "recorded-content": { "get-messages": { "Messages": [ @@ -1424,8 +1516,8 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[sqs_json-False]": { - "recorded-date": "06-12-2023, 13:56:35", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[sqs_query-False]": { + "recorded-date": "30-04-2024, 13:34:19", "recorded-content": { "get-messages": { "Messages": [ @@ -1452,13 +1544,13 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_posting_to_fifo_requires_deduplicationid_group_id[sqs]": { - "recorded-date": "06-12-2023, 13:56:49", + "recorded-date": "30-04-2024, 13:34:21", "recorded-content": { "invalid-parameter-value": { "Error": { "Code": "InvalidParameterValue", - "Detail": null, "Message": "The queue should either have ContentBasedDeduplication enabled or MessageDeduplicationId provided explicitly", + "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -1469,8 +1561,8 @@ "missing-parameter": { "Error": { "Code": "MissingParameter", - "Detail": null, "Message": "The request must contain the parameter MessageGroupId.", + "QueryErrorCode": "MissingRequiredParameterException", "Type": "Sender" }, "ResponseMetadata": { @@ -1481,8 +1573,8 @@ "invalid-parameter-value-query": { "Error": { "Code": "InvalidParameterValue", - "Detail": null, "Message": "The queue should either have ContentBasedDeduplication enabled or MessageDeduplicationId provided explicitly", + "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -1493,8 +1585,8 @@ "missing-parameter-query": { "Error": { "Code": "MissingParameter", - "Detail": null, "Message": "The request must contain the parameter MessageGroupId.", + "QueryErrorCode": "MissingRequiredParameterException", "Type": "Sender" }, "ResponseMetadata": { @@ -1504,14 +1596,14 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_posting_to_fifo_requires_deduplicationid_group_id[sqs_json]": { - "recorded-date": "06-12-2023, 13:56:50", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_posting_to_fifo_requires_deduplicationid_group_id[sqs_query]": { + "recorded-date": "30-04-2024, 13:34:22", "recorded-content": { "invalid-parameter-value": { "Error": { "Code": "InvalidParameterValue", + "Detail": null, "Message": "The queue should either have ContentBasedDeduplication enabled or MessageDeduplicationId provided explicitly", - "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -1522,8 +1614,8 @@ "missing-parameter": { "Error": { "Code": "MissingParameter", + "Detail": null, "Message": "The request must contain the parameter MessageGroupId.", - "QueryErrorCode": "MissingRequiredParameterException", "Type": "Sender" }, "ResponseMetadata": { @@ -1534,8 +1626,8 @@ "invalid-parameter-value-query": { "Error": { "Code": "InvalidParameterValue", - "Detail": null, "Message": "The queue should either have ContentBasedDeduplication enabled or MessageDeduplicationId provided explicitly", + "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -1546,8 +1638,8 @@ "missing-parameter-query": { "Error": { "Code": "MissingParameter", - "Detail": null, "Message": "The request must contain the parameter MessageGroupId.", + "QueryErrorCode": "MissingRequiredParameterException", "Type": "Sender" }, "ResponseMetadata": { @@ -1558,13 +1650,13 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_successive_purge_calls_fail[sqs]": { - "recorded-date": "04-01-2024, 10:07:25", + "recorded-date": "30-04-2024, 13:37:02", "recorded-content": { "purge_queue_error": { "Error": { "Code": "AWS.SimpleQueueService.PurgeQueueInProgress", - "Detail": null, "Message": "Only one PurgeQueue operation on <queue-name> is allowed every 60 seconds.", + "QueryErrorCode": "PurgeQueueInProgress", "Type": "Sender" }, "ResponseMetadata": { @@ -1574,14 +1666,14 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_successive_purge_calls_fail[sqs_json]": { - "recorded-date": "04-01-2024, 10:07:27", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_successive_purge_calls_fail[sqs_query]": { + "recorded-date": "30-04-2024, 13:37:04", "recorded-content": { "purge_queue_error": { "Error": { "Code": "AWS.SimpleQueueService.PurgeQueueInProgress", + "Detail": null, "Message": "Only one PurgeQueue operation on <queue-name> is allowed every 60 seconds.", - "QueryErrorCode": "PurgeQueueInProgress", "Type": "Sender" }, "ResponseMetadata": { @@ -1591,28 +1683,24 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_list_queues": { - "recorded-date": "14-11-2023, 11:57:28", - "recorded-content": {} - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_queue_attributes[sqs_json]": { - "recorded-date": "04-01-2024, 10:07:44", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_fifo_same_dedup_id_different_message_groups[sqs]": { + "recorded-date": "30-04-2024, 13:51:17", "recorded-content": { - "sse_kms_attributes": { - "Attributes": { - "KmsDataKeyReusePeriodSeconds": "6000", - "KmsMasterKeyId": "testKeyId", - "SqsManagedSseEnabled": "false" - }, + "same-dedup-different-grp-1": { + "Messages": [ + { + "Body": "Test", + "MD5OfBody": "0cbc6611f5540bd0809a388dc95a615b", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>" + } + ], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "sse_sqs_attributes": { - "Attributes": { - "SqsManagedSseEnabled": "true" - }, + "same-dedup-different-grp-2": { "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -1620,35 +1708,72 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_kms_and_sqs_are_mutually_exclusive[sqs]": { - "recorded-date": "06-12-2023, 13:57:48", - "recorded-content": { - "error": "An error occurred (InvalidAttributeName) when calling the SetQueueAttributes operation: You can use one type of server-side encryption (SSE) at one time. You can either enable KMS SSE or SQS SSE." - } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_kms_and_sqs_are_mutually_exclusive[sqs_json]": { - "recorded-date": "06-12-2023, 13:57:48", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_fifo_same_dedup_id_different_message_groups[sqs_query]": { + "recorded-date": "30-04-2024, 13:51:20", "recorded-content": { - "error": "An error occurred (InvalidAttributeName) when calling the SetQueueAttributes operation: You can use one type of server-side encryption (SSE) at one time. You can either enable KMS SSE or SQS SSE." + "same-dedup-different-grp-1": { + "Messages": [ + { + "Body": "Test", + "MD5OfBody": "0cbc6611f5540bd0809a388dc95a615b", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "same-dedup-different-grp-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_message_attribute_names_filters[sqs]": { - "recorded-date": "06-12-2023, 13:57:51", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_high_throughput_ordering[sqs]": { + "recorded-date": "30-04-2024, 13:34:27", "recorded-content": { - "send_message_response": { - "MD5OfMessageAttributes": "4c360f3fdafd970e05fae2f149d997f5", - "MD5OfMessageBody": "6e2baaf3b97dbeef01c0043275f9a0e7", - "MessageId": "<uuid:1>", + "same-dedup-different-grp-1": { + "Messages": [ + { + "Body": "Test1", + "MD5OfBody": "e1b849f9631ffc1829b2e31402373e3c", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>" + } + ], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "empty_filter": { + "same-dedup-different-grp-2": { "Messages": [ { - "Body": "msg", - "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "Body": "Test2", + "MD5OfBody": "c454552d52d55d3ef56408742887362b", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_high_throughput_ordering[sqs_query]": { + "recorded-date": "30-04-2024, 13:34:29", + "recorded-content": { + "same-dedup-different-grp-1": { + "Messages": [ + { + "Body": "Test1", + "MD5OfBody": "e1b849f9631ffc1829b2e31402373e3c", "MessageId": "<uuid:1>", "ReceiptHandle": "<receipt-handle:1>" } @@ -1658,27 +1783,12 @@ "HTTPStatusCode": 200 } }, - "all_name": { + "same-dedup-different-grp-2": { "Messages": [ { - "Body": "msg", - "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", - "MD5OfMessageAttributes": "4c360f3fdafd970e05fae2f149d997f5", - "MessageAttributes": { - "General": { - "DataType": "String", - "StringValue": "Kenobi" - }, - "Hello": { - "DataType": "String", - "StringValue": "There" - }, - "Help.Me": { - "DataType": "String", - "StringValue": "Me" - } - }, - "MessageId": "<uuid:1>", + "Body": "Test2", + "MD5OfBody": "c454552d52d55d3ef56408742887362b", + "MessageId": "<uuid:2>", "ReceiptHandle": "<receipt-handle:2>" } ], @@ -1686,109 +1796,141 @@ "HTTPHeaders": {}, "HTTPStatusCode": 200 } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_high_throughput_after_creation[sqs]": { + "recorded-date": "30-04-2024, 13:34:32", + "recorded-content": { + "same-dedup-different-grp-1": { + "Messages": [ + { + "Body": "Test1", + "MD5OfBody": "e1b849f9631ffc1829b2e31402373e3c", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } }, - "all_wildcard_asterisk": { - "Body": "msg", - "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", - "MD5OfMessageAttributes": "4c360f3fdafd970e05fae2f149d997f5", - "MessageAttributes": { - "General": { - "DataType": "String", - "StringValue": "Kenobi" - }, - "Hello": { - "DataType": "String", - "StringValue": "There" - }, - "Help.Me": { - "DataType": "String", - "StringValue": "Me" - } - }, - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:3>" + "same-dedup-different-grp-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } }, - "all_wildcard": { - "Body": "msg", - "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", - "MD5OfMessageAttributes": "4c360f3fdafd970e05fae2f149d997f5", - "MessageAttributes": { - "General": { - "DataType": "String", - "StringValue": "Kenobi" - }, - "Hello": { - "DataType": "String", - "StringValue": "There" - }, - "Help.Me": { - "DataType": "String", - "StringValue": "Me" - } + "set-to-high-throughput": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "ContentBasedDeduplication": "false", + "CreatedTimestamp": "timestamp", + "DeduplicationScope": "messageGroup", + "DelaySeconds": "0", + "FifoQueue": "true", + "FifoThroughputLimit": "perMessageGroupId", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "262144", + "MessageRetentionPeriod": "345600", + "QueueArn": "arn:aws:sqs:<region>:111111111111:<resource:1>", + "ReceiveMessageWaitTimeSeconds": "0", + "SqsManagedSseEnabled": "true", + "VisibilityTimeout": "30" }, - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:4>" + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } }, - "only_non_existing_names": { - "Body": "msg", - "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:5>" + "same-dedup-different-grp-high-throughput": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } }, - "only_existing": { - "Body": "msg", - "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", - "MD5OfMessageAttributes": "fca026605781cb4126a1e9044df24232", - "MessageAttributes": { - "General": { - "DataType": "String", - "StringValue": "Kenobi" - }, - "Hello": { - "DataType": "String", - "StringValue": "There" + "same-dedup-different-grp-high-throughput-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_high_throughput_after_creation[sqs_query]": { + "recorded-date": "30-04-2024, 13:34:35", + "recorded-content": { + "same-dedup-different-grp-1": { + "Messages": [ + { + "Body": "Test1", + "MD5OfBody": "e1b849f9631ffc1829b2e31402373e3c", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>" } - }, - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:6>" + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } }, - "existing_and_non_existing": { - "Body": "msg", - "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", - "MD5OfMessageAttributes": "a311262e065454b75da111d535b8dacd", - "MessageAttributes": { - "Hello": { - "DataType": "String", - "StringValue": "There" - } - }, - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:7>" + "same-dedup-different-grp-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } }, - "prefix_filter": { - "Body": "msg", - "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", - "MD5OfMessageAttributes": "83fee93c1bcd8b9a5a923ffacdc636c7", - "MessageAttributes": { - "Hello": { - "DataType": "String", - "StringValue": "There" - }, - "Help.Me": { - "DataType": "String", - "StringValue": "Me" - } + "set-to-high-throughput": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "ContentBasedDeduplication": "false", + "CreatedTimestamp": "timestamp", + "DeduplicationScope": "messageGroup", + "DelaySeconds": "0", + "FifoQueue": "true", + "FifoThroughputLimit": "perMessageGroupId", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "262144", + "MessageRetentionPeriod": "345600", + "QueueArn": "arn:aws:sqs:<region>:111111111111:<resource:1>", + "ReceiveMessageWaitTimeSeconds": "0", + "SqsManagedSseEnabled": "true", + "VisibilityTimeout": "30" }, - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:8>" + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } }, - "illegal_name_1": { + "same-dedup-different-grp-high-throughput": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "same-dedup-different-grp-high-throughput-2": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_regular_throughput_after_creation[sqs]": { + "recorded-date": "30-04-2024, 13:52:03", + "recorded-content": { + "same-dedup-different-grp-1": { "Messages": [ { - "Body": "msg", - "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "Body": "Test1", + "MD5OfBody": "e1b849f9631ffc1829b2e31402373e3c", "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:9>" + "ReceiptHandle": "<receipt-handle:1>" } ], "ResponseMetadata": { @@ -1796,70 +1938,241 @@ "HTTPStatusCode": 200 } }, - "illegal_name_2": { + "same-dedup-different-grp-2": { "Messages": [ { - "Body": "msg", - "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:10>" + "Body": "Test2", + "MD5OfBody": "c454552d52d55d3ef56408742887362b", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>" } ], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } - } - } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_message_attribute_names_filters[sqs_json]": { - "recorded-date": "06-12-2023, 13:57:53", - "recorded-content": { - "send_message_response": { - "MD5OfMessageAttributes": "4c360f3fdafd970e05fae2f149d997f5", - "MD5OfMessageBody": "6e2baaf3b97dbeef01c0043275f9a0e7", - "MessageId": "<uuid:1>", + }, + "set-to-regular-throughput": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "ContentBasedDeduplication": "false", + "CreatedTimestamp": "timestamp", + "DeduplicationScope": "queue", + "DelaySeconds": "0", + "FifoQueue": "true", + "FifoThroughputLimit": "perQueue", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "262144", + "MessageRetentionPeriod": "345600", + "QueueArn": "arn:aws:sqs:<region>:111111111111:<resource:1>", + "ReceiveMessageWaitTimeSeconds": "0", + "SqsManagedSseEnabled": "true", + "VisibilityTimeout": "30" + }, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "empty_filter": { + "same-dedup-different-grp-regular-throughput": { "Messages": [ { - "Body": "msg", - "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" + "Body": "Test3", + "MD5OfBody": "b3f66ec1535de7702c38e94408fa4a17", + "MessageId": "<uuid:3>", + "ReceiptHandle": "<receipt-handle:3>" } ], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } - }, - "all_name": { + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_regular_throughput_after_creation[sqs_query]": { + "recorded-date": "30-04-2024, 13:52:06", + "recorded-content": { + "same-dedup-different-grp-1": { "Messages": [ { - "Body": "msg", - "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", - "MD5OfMessageAttributes": "4c360f3fdafd970e05fae2f149d997f5", - "MessageAttributes": { - "General": { - "DataType": "String", - "StringValue": "Kenobi" - }, - "Hello": { - "DataType": "String", - "StringValue": "There" - }, - "Help.Me": { - "DataType": "String", - "StringValue": "Me" - } - }, + "Body": "Test1", + "MD5OfBody": "e1b849f9631ffc1829b2e31402373e3c", "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:2>" + "ReceiptHandle": "<receipt-handle:1>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "same-dedup-different-grp-2": { + "Messages": [ + { + "Body": "Test2", + "MD5OfBody": "c454552d52d55d3ef56408742887362b", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "set-to-regular-throughput": { + "Attributes": { + "ApproximateNumberOfMessages": "0", + "ApproximateNumberOfMessagesDelayed": "0", + "ApproximateNumberOfMessagesNotVisible": "0", + "ContentBasedDeduplication": "false", + "CreatedTimestamp": "timestamp", + "DeduplicationScope": "queue", + "DelaySeconds": "0", + "FifoQueue": "true", + "FifoThroughputLimit": "perQueue", + "LastModifiedTimestamp": "timestamp", + "MaximumMessageSize": "262144", + "MessageRetentionPeriod": "345600", + "QueueArn": "arn:aws:sqs:<region>:111111111111:<resource:1>", + "ReceiveMessageWaitTimeSeconds": "0", + "SqsManagedSseEnabled": "true", + "VisibilityTimeout": "30" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "same-dedup-different-grp-regular-throughput": { + "Messages": [ + { + "Body": "Test3", + "MD5OfBody": "b3f66ec1535de7702c38e94408fa4a17", + "MessageId": "<uuid:3>", + "ReceiptHandle": "<receipt-handle:3>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_queue_attributes[sqs]": { + "recorded-date": "30-04-2024, 13:34:43", + "recorded-content": { + "sse_kms_attributes": { + "Attributes": { + "KmsDataKeyReusePeriodSeconds": "6000", + "KmsMasterKeyId": "testKeyId", + "SqsManagedSseEnabled": "false" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "sse_sqs_attributes": { + "Attributes": { + "SqsManagedSseEnabled": "true" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_queue_attributes[sqs_query]": { + "recorded-date": "30-04-2024, 13:34:47", + "recorded-content": { + "sse_kms_attributes": { + "Attributes": { + "KmsDataKeyReusePeriodSeconds": "6000", + "KmsMasterKeyId": "testKeyId", + "SqsManagedSseEnabled": "false" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "sse_sqs_attributes": { + "Attributes": { + "SqsManagedSseEnabled": "true" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_kms_and_sqs_are_mutually_exclusive[sqs]": { + "recorded-date": "30-04-2024, 13:34:48", + "recorded-content": { + "error": "An error occurred (InvalidAttributeName) when calling the SetQueueAttributes operation: You can use one type of server-side encryption (SSE) at one time. You can either enable KMS SSE or SQS SSE." + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_kms_and_sqs_are_mutually_exclusive[sqs_query]": { + "recorded-date": "30-04-2024, 13:34:48", + "recorded-content": { + "error": "An error occurred (InvalidAttributeName) when calling the SetQueueAttributes operation: You can use one type of server-side encryption (SSE) at one time. You can either enable KMS SSE or SQS SSE." + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_message_attribute_names_filters[sqs]": { + "recorded-date": "30-04-2024, 13:34:51", + "recorded-content": { + "send_message_response": { + "MD5OfMessageAttributes": "4c360f3fdafd970e05fae2f149d997f5", + "MD5OfMessageBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "MessageId": "<uuid:1>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "empty_filter": { + "Messages": [ + { + "Body": "msg", + "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "all_name": { + "Messages": [ + { + "Body": "msg", + "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "MD5OfMessageAttributes": "4c360f3fdafd970e05fae2f149d997f5", + "MessageAttributes": { + "General": { + "DataType": "String", + "StringValue": "Kenobi" + }, + "Hello": { + "DataType": "String", + "StringValue": "There" + }, + "Help.Me": { + "DataType": "String", + "StringValue": "Me" + } + }, + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:2>" } ], "ResponseMetadata": { @@ -1912,7 +2225,6 @@ "only_non_existing_names": { "Body": "msg", "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", - "MessageAttributes": {}, "MessageId": "<uuid:1>", "ReceiptHandle": "<receipt-handle:5>" }, @@ -1993,18 +2305,21 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_attribute_names_filters[sqs]": { - "recorded-date": "06-12-2023, 13:57:54", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_message_attribute_names_filters[sqs_query]": { + "recorded-date": "30-04-2024, 13:34:55", "recorded-content": { - "all_attributes": { + "send_message_response": { + "MD5OfMessageAttributes": "4c360f3fdafd970e05fae2f149d997f5", + "MD5OfMessageBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "MessageId": "<uuid:1>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "empty_filter": { "Messages": [ { - "Attributes": { - "ApproximateFirstReceiveTimestamp": "timestamp", - "ApproximateReceiveCount": "1", - "SenderId": "<sender-id:1>", - "SentTimestamp": "timestamp" - }, "Body": "msg", "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", "MessageId": "<uuid:1>", @@ -2016,22 +2331,24 @@ "HTTPStatusCode": 200 } }, - "all_system_and_message_attributes": { + "all_name": { "Messages": [ { - "Attributes": { - "ApproximateFirstReceiveTimestamp": "timestamp", - "ApproximateReceiveCount": "2", - "SenderId": "<sender-id:1>", - "SentTimestamp": "timestamp" - }, "Body": "msg", "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", - "MD5OfMessageAttributes": "ae7155c6026991b6d54b11589678bf9c", + "MD5OfMessageAttributes": "4c360f3fdafd970e05fae2f149d997f5", "MessageAttributes": { - "Foo": { + "General": { "DataType": "String", - "StringValue": "Bar" + "StringValue": "Kenobi" + }, + "Hello": { + "DataType": "String", + "StringValue": "There" + }, + "Help.Me": { + "DataType": "String", + "StringValue": "Me" } }, "MessageId": "<uuid:1>", @@ -2043,44 +2360,219 @@ "HTTPStatusCode": 200 } }, - "single_attribute": { - "Messages": [ - { - "Attributes": { - "SenderId": "<sender-id:1>" - }, - "Body": "msg", - "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:3>" + "all_wildcard_asterisk": { + "Body": "msg", + "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "MD5OfMessageAttributes": "4c360f3fdafd970e05fae2f149d997f5", + "MessageAttributes": { + "General": { + "DataType": "String", + "StringValue": "Kenobi" + }, + "Hello": { + "DataType": "String", + "StringValue": "There" + }, + "Help.Me": { + "DataType": "String", + "StringValue": "Me" } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } + }, + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:3>" }, - "multiple_attributes": { - "Messages": [ - { - "Attributes": { - "SenderId": "<sender-id:1>" - }, - "Body": "msg", - "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:4>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } + "all_wildcard": { + "Body": "msg", + "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "MD5OfMessageAttributes": "4c360f3fdafd970e05fae2f149d997f5", + "MessageAttributes": { + "General": { + "DataType": "String", + "StringValue": "Kenobi" + }, + "Hello": { + "DataType": "String", + "StringValue": "There" + }, + "Help.Me": { + "DataType": "String", + "StringValue": "Me" + } + }, + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:4>" + }, + "only_non_existing_names": { + "Body": "msg", + "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:5>" + }, + "only_existing": { + "Body": "msg", + "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "MD5OfMessageAttributes": "fca026605781cb4126a1e9044df24232", + "MessageAttributes": { + "General": { + "DataType": "String", + "StringValue": "Kenobi" + }, + "Hello": { + "DataType": "String", + "StringValue": "There" + } + }, + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:6>" + }, + "existing_and_non_existing": { + "Body": "msg", + "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "MD5OfMessageAttributes": "a311262e065454b75da111d535b8dacd", + "MessageAttributes": { + "Hello": { + "DataType": "String", + "StringValue": "There" + } + }, + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:7>" + }, + "prefix_filter": { + "Body": "msg", + "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "MD5OfMessageAttributes": "83fee93c1bcd8b9a5a923ffacdc636c7", + "MessageAttributes": { + "Hello": { + "DataType": "String", + "StringValue": "There" + }, + "Help.Me": { + "DataType": "String", + "StringValue": "Me" + } + }, + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:8>" + }, + "illegal_name_1": { + "Messages": [ + { + "Body": "msg", + "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:9>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "illegal_name_2": { + "Messages": [ + { + "Body": "msg", + "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:10>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_attribute_names_filters[sqs]": { + "recorded-date": "30-04-2024, 13:34:57", + "recorded-content": { + "all_attributes": { + "Messages": [ + { + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "1", + "SenderId": "<sender-id:1>", + "SentTimestamp": "timestamp" + }, + "Body": "msg", + "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "all_system_and_message_attributes": { + "Messages": [ + { + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "2", + "SenderId": "<sender-id:1>", + "SentTimestamp": "timestamp" + }, + "Body": "msg", + "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "MD5OfMessageAttributes": "ae7155c6026991b6d54b11589678bf9c", + "MessageAttributes": { + "Foo": { + "DataType": "String", + "StringValue": "Bar" + } + }, + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:2>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "single_attribute": { + "Messages": [ + { + "Attributes": { + "SenderId": "<sender-id:1>" + }, + "Body": "msg", + "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:3>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "multiple_attributes": { + "Messages": [ + { + "Attributes": { + "SenderId": "<sender-id:1>" + }, + "Body": "msg", + "MD5OfBody": "6e2baaf3b97dbeef01c0043275f9a0e7", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:4>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_attribute_names_filters[sqs_json]": { - "recorded-date": "06-12-2023, 13:57:56", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_attribute_names_filters[sqs_query]": { + "recorded-date": "30-04-2024, 13:34:58", "recorded-content": { "all_attributes": { "Messages": [ @@ -2166,7 +2658,7 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_permission_lifecycle[sqs]": { - "recorded-date": "06-12-2023, 13:58:06", + "recorded-date": "30-04-2024, 13:35:03", "recorded-content": { "add-permission-response": { "ResponseMetadata": { @@ -2238,8 +2730,8 @@ "get-queue-policy-attribute-second-account-same-label": { "Error": { "Code": "InvalidParameterValue", - "Detail": null, "Message": "Value crossaccountpermission for parameter Label is invalid. Reason: Already exists.", + "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -2311,8 +2803,8 @@ "get-queue-policy-attribute-delete-non-existent-label": { "Error": { "Code": "InvalidParameterValue", - "Detail": null, "Message": "Value crossaccountpermission2 for parameter Label is invalid. Reason: can't find label.", + "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -2322,8 +2814,8 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_permission_lifecycle[sqs_json]": { - "recorded-date": "06-12-2023, 13:58:09", + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_permission_lifecycle[sqs_query]": { + "recorded-date": "30-04-2024, 13:35:07", "recorded-content": { "add-permission-response": { "ResponseMetadata": { @@ -2395,8 +2887,8 @@ "get-queue-policy-attribute-second-account-same-label": { "Error": { "Code": "InvalidParameterValue", + "Detail": null, "Message": "Value crossaccountpermission for parameter Label is invalid. Reason: Already exists.", - "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -2468,8 +2960,8 @@ "get-queue-policy-attribute-delete-non-existent-label": { "Error": { "Code": "InvalidParameterValue", + "Detail": null, "Message": "Value crossaccountpermission2 for parameter Label is invalid. Reason: can't find label.", - "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -2479,19 +2971,8 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_send_message_via_queue_url_with_json_protocol": { - "recorded-date": "06-12-2023, 13:59:07", - "recorded-content": { - "receive-json-on-queue-url": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_non_existent_queue": { - "recorded-date": "06-12-2023, 16:00:58", + "recorded-date": "30-04-2024, 13:35:47", "recorded-content": { "queue-does-not-exist": { "Error": { @@ -2531,620 +3012,15 @@ } } }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_deduplication_id_too_long": { - "recorded-date": "04-01-2024, 10:06:26", - "recorded-content": { - "error-response": { - "Error": { - "Code": "InvalidParameterValue", - "Detail": null, - "Message": "Value aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa for parameter MessageDeduplicationId is invalid. Reason: MessageDeduplicationId can only include alphanumeric and punctuation characters. 1 to 128 in length.", - "Type": "Sender" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_group_id_too_long": { - "recorded-date": "04-01-2024, 10:07:04", - "recorded-content": { - "error-response": { - "Error": { - "Code": "InvalidParameterValue", - "Detail": null, - "Message": "Value aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa for parameter MessageGroupId is invalid. Reason: MessageGroupId can only include alphanumeric and punctuation characters. 1 to 128 in length.", - "Type": "Sender" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_queue_attributes[sqs]": { - "recorded-date": "04-01-2024, 10:07:41", + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_send_message_via_queue_url_with_json_protocol": { + "recorded-date": "30-04-2024, 13:35:11", "recorded-content": { - "sse_kms_attributes": { - "Attributes": { - "KmsDataKeyReusePeriodSeconds": "6000", - "KmsMasterKeyId": "testKeyId", - "SqsManagedSseEnabled": "false" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "sse_sqs_attributes": { - "Attributes": { - "SqsManagedSseEnabled": "true" - }, + "receive-json-on-queue-url": { "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } } } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_message_size": { - "recorded-date": "03-01-2024, 01:58:35", - "recorded-content": { - "send-message-batch-result": { - "Failed": [ - { - "Code": "InvalidParameterValue", - "Id": "<uuid:1>", - "Message": "One or more parameters cannot be validated. Reason: Message must be shorter than 1024 bytes.", - "SenderFault": true - } - ], - "Successful": [ - { - "Id": "<uuid:2>", - "MD5OfMessageBody": "c9a34cfc85d982698c6ac89f76071abd", - "MessageId": "<uuid:3>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_empty_message_groups_added_back_to_queue[sqs]": { - "recorded-date": "12-02-2024, 16:08:11", - "recorded-content": { - "inital-fifo-receive": { - "Messages": [ - { - "Body": "Message 1", - "MD5OfBody": "68390233272823b7adf13a1db79b2cd7", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "empty-fifo-receive": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "final-fifo-receive": { - "Messages": [ - { - "Body": "Message 2", - "MD5OfBody": "88ef8f31ed540f1c4c03d5fdb06a7935", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:2>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_empty_message_groups_added_back_to_queue[sqs_json]": { - "recorded-date": "12-02-2024, 16:08:14", - "recorded-content": { - "inital-fifo-receive": { - "Messages": [ - { - "Body": "Message 1", - "MD5OfBody": "68390233272823b7adf13a1db79b2cd7", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "empty-fifo-receive": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "final-fifo-receive": { - "Messages": [ - { - "Body": "Message 2", - "MD5OfBody": "88ef8f31ed540f1c4c03d5fdb06a7935", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:2>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_fifo_same_dedup_id_different_message_groups[sqs_json]": { - "recorded-date": "15-02-2024, 13:18:31", - "recorded-content": { - "same-dedup-different-grp-1": { - "Messages": [ - { - "Body": "Test", - "MD5OfBody": "0cbc6611f5540bd0809a388dc95a615b", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "same-dedup-different-grp-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_fifo_same_dedup_id_different_message_groups[sqs]": { - "recorded-date": "15-02-2024, 13:18:33", - "recorded-content": { - "same-dedup-different-grp-1": { - "Messages": [ - { - "Body": "Test", - "MD5OfBody": "0cbc6611f5540bd0809a388dc95a615b", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "same-dedup-different-grp-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_high_throughput_ordering[sqs_json]": { - "recorded-date": "19-02-2024, 13:05:25", - "recorded-content": { - "same-dedup-different-grp-1": { - "Messages": [ - { - "Body": "Test1", - "MD5OfBody": "e1b849f9631ffc1829b2e31402373e3c", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "same-dedup-different-grp-2": { - "Messages": [ - { - "Body": "Test2", - "MD5OfBody": "c454552d52d55d3ef56408742887362b", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:2>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_high_throughput_ordering[sqs]": { - "recorded-date": "19-02-2024, 13:05:27", - "recorded-content": { - "same-dedup-different-grp-1": { - "Messages": [ - { - "Body": "Test1", - "MD5OfBody": "e1b849f9631ffc1829b2e31402373e3c", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "same-dedup-different-grp-2": { - "Messages": [ - { - "Body": "Test2", - "MD5OfBody": "c454552d52d55d3ef56408742887362b", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:2>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_high_throughput_after_creation[sqs_json]": { - "recorded-date": "26-02-2024, 18:54:41", - "recorded-content": { - "same-dedup-different-grp-1": { - "Messages": [ - { - "Body": "Test1", - "MD5OfBody": "e1b849f9631ffc1829b2e31402373e3c", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "same-dedup-different-grp-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "set-to-high-throughput": { - "Attributes": { - "ApproximateNumberOfMessages": "0", - "ApproximateNumberOfMessagesDelayed": "0", - "ApproximateNumberOfMessagesNotVisible": "0", - "ContentBasedDeduplication": "false", - "CreatedTimestamp": "timestamp", - "DeduplicationScope": "messageGroup", - "DelaySeconds": "0", - "FifoQueue": "true", - "FifoThroughputLimit": "perMessageGroupId", - "LastModifiedTimestamp": "timestamp", - "MaximumMessageSize": "262144", - "MessageRetentionPeriod": "345600", - "QueueArn": "arn:aws:sqs:<region>:111111111111:<resource:1>", - "ReceiveMessageWaitTimeSeconds": "0", - "SqsManagedSseEnabled": "true", - "VisibilityTimeout": "30" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "same-dedup-different-grp-high-throughput": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "same-dedup-different-grp-high-throughput-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_high_throughput_after_creation[sqs]": { - "recorded-date": "26-02-2024, 18:54:48", - "recorded-content": { - "same-dedup-different-grp-1": { - "Messages": [ - { - "Body": "Test1", - "MD5OfBody": "e1b849f9631ffc1829b2e31402373e3c", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "same-dedup-different-grp-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "set-to-high-throughput": { - "Attributes": { - "ApproximateNumberOfMessages": "0", - "ApproximateNumberOfMessagesDelayed": "0", - "ApproximateNumberOfMessagesNotVisible": "0", - "ContentBasedDeduplication": "false", - "CreatedTimestamp": "timestamp", - "DeduplicationScope": "messageGroup", - "DelaySeconds": "0", - "FifoQueue": "true", - "FifoThroughputLimit": "perMessageGroupId", - "LastModifiedTimestamp": "timestamp", - "MaximumMessageSize": "262144", - "MessageRetentionPeriod": "345600", - "QueueArn": "arn:aws:sqs:<region>:111111111111:<resource:1>", - "ReceiveMessageWaitTimeSeconds": "0", - "SqsManagedSseEnabled": "true", - "VisibilityTimeout": "30" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "same-dedup-different-grp-high-throughput": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "same-dedup-different-grp-high-throughput-2": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_regular_throughput_after_creation[sqs_json]": { - "recorded-date": "26-02-2024, 19:05:27", - "recorded-content": { - "same-dedup-different-grp-1": { - "Messages": [ - { - "Body": "Test1", - "MD5OfBody": "e1b849f9631ffc1829b2e31402373e3c", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "same-dedup-different-grp-2": { - "Messages": [ - { - "Body": "Test2", - "MD5OfBody": "c454552d52d55d3ef56408742887362b", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:2>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "set-to-regular-throughput": { - "Attributes": { - "ApproximateNumberOfMessages": "0", - "ApproximateNumberOfMessagesDelayed": "0", - "ApproximateNumberOfMessagesNotVisible": "0", - "ContentBasedDeduplication": "false", - "CreatedTimestamp": "timestamp", - "DeduplicationScope": "queue", - "DelaySeconds": "0", - "FifoQueue": "true", - "FifoThroughputLimit": "perQueue", - "LastModifiedTimestamp": "timestamp", - "MaximumMessageSize": "262144", - "MessageRetentionPeriod": "345600", - "QueueArn": "arn:aws:sqs:<region>:111111111111:<resource:1>", - "ReceiveMessageWaitTimeSeconds": "0", - "SqsManagedSseEnabled": "true", - "VisibilityTimeout": "30" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "same-dedup-different-grp-regular-throughput": { - "Messages": [ - { - "Body": "Test3", - "MD5OfBody": "b3f66ec1535de7702c38e94408fa4a17", - "MessageId": "<uuid:3>", - "ReceiptHandle": "<receipt-handle:3>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_regular_throughput_after_creation[sqs]": { - "recorded-date": "26-02-2024, 19:05:32", - "recorded-content": { - "same-dedup-different-grp-1": { - "Messages": [ - { - "Body": "Test1", - "MD5OfBody": "e1b849f9631ffc1829b2e31402373e3c", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "same-dedup-different-grp-2": { - "Messages": [ - { - "Body": "Test2", - "MD5OfBody": "c454552d52d55d3ef56408742887362b", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:2>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "set-to-regular-throughput": { - "Attributes": { - "ApproximateNumberOfMessages": "0", - "ApproximateNumberOfMessagesDelayed": "0", - "ApproximateNumberOfMessagesNotVisible": "0", - "ContentBasedDeduplication": "false", - "CreatedTimestamp": "timestamp", - "DeduplicationScope": "queue", - "DelaySeconds": "0", - "FifoQueue": "true", - "FifoThroughputLimit": "perQueue", - "LastModifiedTimestamp": "timestamp", - "MaximumMessageSize": "262144", - "MessageRetentionPeriod": "345600", - "QueueArn": "arn:aws:sqs:<region>:111111111111:<resource:1>", - "ReceiveMessageWaitTimeSeconds": "0", - "SqsManagedSseEnabled": "true", - "VisibilityTimeout": "30" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "same-dedup-different-grp-regular-throughput": { - "Messages": [ - { - "Body": "Test3", - "MD5OfBody": "b3f66ec1535de7702c38e94408fa4a17", - "MessageId": "<uuid:3>", - "ReceiptHandle": "<receipt-handle:3>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_message_attributes": { - "recorded-date": "08-03-2024, 23:14:22", - "recorded-content": { - "dlq-arn": "arn:aws:sqs:<region>:111111111111:<resource:1>", - "sourcen-arn": "arn:aws:sqs:<region>:111111111111:<resource:2>", - "rec-pre-dlq": [ - { - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>", - "MD5OfBody": "3d6b824fd8c1520e9a047d21fee6fb1f", - "Body": "message-1", - "Attributes": { - "ApproximateFirstReceiveTimestamp": "timestamp", - "ApproximateReceiveCount": "1", - "SentTimestamp": "timestamp", - "AWSTraceHeader": "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1" - }, - "MD5OfMessageAttributes": "adb59cd4678ea5a855436b949cd07ab6", - "MessageAttributes": { - "MyAttribute": { - "StringValue": "foobar", - "DataType": "String" - } - } - }, - { - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:2>", - "MD5OfBody": "95ef155b66299d14edf7ed57c468c13b", - "Body": "message-2", - "Attributes": { - "ApproximateFirstReceiveTimestamp": "timestamp", - "ApproximateReceiveCount": "1", - "SentTimestamp": "timestamp" - } - } - ], - "dlq-messages": [ - { - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:3>", - "MD5OfBody": "3d6b824fd8c1520e9a047d21fee6fb1f", - "Body": "message-1", - "Attributes": { - "ApproximateFirstReceiveTimestamp": "timestamp", - "ApproximateReceiveCount": "2", - "SentTimestamp": "timestamp", - "AWSTraceHeader": "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1", - "DeadLetterQueueSourceArn": "arn:aws:sqs:<region>:111111111111:<resource:2>" - }, - "MD5OfMessageAttributes": "adb59cd4678ea5a855436b949cd07ab6", - "MessageAttributes": { - "MyAttribute": { - "StringValue": "foobar", - "DataType": "String" - } - } - }, - { - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:4>", - "MD5OfBody": "95ef155b66299d14edf7ed57c468c13b", - "Body": "message-2", - "Attributes": { - "ApproximateFirstReceiveTimestamp": "timestamp", - "ApproximateReceiveCount": "2", - "SentTimestamp": "timestamp", - "DeadLetterQueueSourceArn": "arn:aws:sqs:<region>:111111111111:<resource:2>" - } - } - ] - } } } diff --git a/tests/aws/services/sqs/test_sqs.validation.json b/tests/aws/services/sqs/test_sqs.validation.json index 23f71f0a7443b..81fbf2255d88a 100644 --- a/tests/aws/services/sqs/test_sqs.validation.json +++ b/tests/aws/services/sqs/test_sqs.validation.json @@ -1,191 +1,311 @@ { - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_after_visibility_timeout_expiration": { - "last_validated_date": "2023-11-14T16:51:55+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_after_visibility_timeout_expiration[sqs]": { + "last_validated_date": "2024-04-30T13:33:26+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_batch_with_too_large_batch": { - "last_validated_date": "2023-11-14T10:59:06+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_after_visibility_timeout_expiration[sqs_query]": { + "last_validated_date": "2024-04-30T13:33:30+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_and_update_queue_attributes": { - "last_validated_date": "2023-11-14T10:57:52+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_batch_with_too_large_batch[sqs]": { + "last_validated_date": "2024-04-30T13:50:34+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_fifo_queue_with_different_attributes_raises_error": { - "last_validated_date": "2023-11-14T10:57:48+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_batch_with_too_large_batch[sqs_query]": { + "last_validated_date": "2024-04-30T13:50:41+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_different_attributes_raises_exception": { - "last_validated_date": "2023-11-14T10:57:50+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_and_update_queue_attributes[sqs]": { + "last_validated_date": "2024-04-30T13:33:21+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_and_update_queue_attributes[sqs_query]": { + "last_validated_date": "2024-04-30T13:33:22+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_fifo_queue_with_different_attributes_raises_error[sqs]": { + "last_validated_date": "2024-04-30T13:33:13+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_fifo_queue_with_different_attributes_raises_error[sqs_query]": { + "last_validated_date": "2024-04-30T13:33:14+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_and_get_attributes[sqs]": { + "last_validated_date": "2024-04-30T13:39:56+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_and_get_attributes[sqs_query]": { + "last_validated_date": "2024-04-30T13:39:58+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_recently_deleted[sqs]": { + "last_validated_date": "2024-04-30T13:39:58+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_recently_deleted[sqs_query]": { + "last_validated_date": "2024-04-30T13:39:59+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_different_attributes_raises_exception[sqs]": { + "last_validated_date": "2024-04-30T13:33:18+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_different_attributes_raises_exception[sqs_query]": { + "last_validated_date": "2024-04-30T13:33:20+00:00" }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_message_attributes": { - "last_validated_date": "2024-03-08T23:14:22+00:00" + "last_validated_date": "2024-04-30T13:33:55+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs-]": { + "last_validated_date": "2024-04-30T13:48:34+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs-invalid:id]": { + "last_validated_date": "2024-04-30T13:48:36+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[]": { - "last_validated_date": "2023-11-14T10:58:57+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs-testLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongId]": { + "last_validated_date": "2024-04-30T13:48:35+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[invalid:id]": { - "last_validated_date": "2023-11-14T10:58:59+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs_query-]": { + "last_validated_date": "2024-04-30T13:48:37+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[testLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongId]": { - "last_validated_date": "2023-11-14T10:58:58+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs_query-invalid:id]": { + "last_validated_date": "2024-04-30T13:48:38+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_with_too_large_batch": { - "last_validated_date": "2023-11-14T10:59:02+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs_query-testLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongId]": { + "last_validated_date": "2024-04-30T13:48:37+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_with_too_large_batch[sqs]": { + "last_validated_date": "2024-04-30T13:49:26+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_with_too_large_batch[sqs_query]": { + "last_validated_date": "2024-04-30T13:49:31+00:00" }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_high_throughput_after_creation[sqs]": { - "last_validated_date": "2024-02-26T18:54:48+00:00" + "last_validated_date": "2024-04-30T13:34:32+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_high_throughput_after_creation[sqs_json]": { - "last_validated_date": "2024-02-26T18:54:41+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_high_throughput_after_creation[sqs_query]": { + "last_validated_date": "2024-04-30T13:34:35+00:00" }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_regular_throughput_after_creation[sqs]": { - "last_validated_date": "2024-02-26T19:05:32+00:00" + "last_validated_date": "2024-04-30T13:52:02+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_regular_throughput_after_creation[sqs_query]": { + "last_validated_date": "2024-04-30T13:52:06+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_regular_throughput_after_creation[sqs_json]": { - "last_validated_date": "2024-02-26T19:05:27+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[sqs-False]": { + "last_validated_date": "2024-04-30T13:34:04+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[False]": { - "last_validated_date": "2023-11-14T10:59:33+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[sqs-True]": { + "last_validated_date": "2024-04-30T13:34:01+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[True]": { - "last_validated_date": "2023-11-14T10:59:31+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[sqs_query-False]": { + "last_validated_date": "2024-04-30T13:34:09+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[False]": { - "last_validated_date": "2023-11-14T10:59:37+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[sqs_query-True]": { + "last_validated_date": "2024-04-30T13:34:06+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[True]": { - "last_validated_date": "2023-11-14T10:59:35+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[sqs-False]": { + "last_validated_date": "2024-04-30T13:34:14+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_delete_message_with_expired_receipt_handle": { - "last_validated_date": "2023-11-14T10:58:50+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[sqs-True]": { + "last_validated_date": "2024-04-30T13:34:11+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[sqs_query-False]": { + "last_validated_date": "2024-04-30T13:34:19+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[sqs_query-True]": { + "last_validated_date": "2024-04-30T13:34:17+00:00" }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_empty_message_groups_added_back_to_queue[sqs]": { - "last_validated_date": "2024-02-12T16:08:11+00:00" + "last_validated_date": "2024-04-30T13:46:32+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_empty_message_groups_added_back_to_queue[sqs_json]": { - "last_validated_date": "2024-02-12T16:08:14+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_empty_message_groups_added_back_to_queue[sqs_query]": { + "last_validated_date": "2024-04-30T13:46:34+00:00" }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_high_throughput_ordering[sqs]": { - "last_validated_date": "2024-02-19T13:07:33+00:00" + "last_validated_date": "2024-04-30T13:34:27+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_high_throughput_ordering[sqs_query]": { + "last_validated_date": "2024-04-30T13:34:28+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_high_throughput_ordering[sqs_json]": { - "last_validated_date": "2024-02-19T13:07:31+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_attributes[sqs]": { + "last_validated_date": "2024-04-30T13:36:56+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_attributes": { - "last_validated_date": "2023-11-14T10:58:35+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_attributes[sqs_query]": { + "last_validated_date": "2024-04-30T13:36:57+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_group_visibility": { - "last_validated_date": "2024-01-03T00:55:48+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_queue_send_message_with_delay_seconds_fails[sqs]": { + "last_validated_date": "2024-04-30T13:33:33+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_queue_send_message_with_delay_seconds_fails": { - "last_validated_date": "2023-11-14T10:58:29+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_queue_send_message_with_delay_seconds_fails[sqs_query]": { + "last_validated_date": "2024-04-30T13:33:34+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_set_content_based_deduplication_strategy": { - "last_validated_date": "2023-11-14T10:58:51+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_set_content_based_deduplication_strategy[sqs]": { + "last_validated_date": "2024-04-30T13:47:39+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_batch_id": { - "last_validated_date": "2023-11-14T10:58:55+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_set_content_based_deduplication_strategy[sqs_query]": { + "last_validated_date": "2024-04-30T13:47:41+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_dead_letter_arn_rejected_before_lookup": { - "last_validated_date": "2023-11-14T10:59:08+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_batch_id[sqs]": { + "last_validated_date": "2024-04-30T13:33:40+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_batch_id[sqs_query]": { + "last_validated_date": "2024-04-30T13:33:41+00:00" }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_list_queues": { - "last_validated_date": "2023-12-08T16:13:26+00:00" + "last_validated_date": "2024-04-30T13:39:55+00:00" }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_deduplication_id_too_long": { - "last_validated_date": "2024-01-04T10:06:26+00:00" + "last_validated_date": "2024-04-30T13:35:34+00:00" }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_group_id_too_long": { - "last_validated_date": "2024-01-04T10:07:04+00:00" + "last_validated_date": "2024-04-30T13:35:35+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_retention": { - "last_validated_date": "2023-12-28T23:48:08+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_non_existent_queue": { + "last_validated_date": "2024-04-30T13:35:47+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_retention_fifo": { - "last_validated_date": "2023-12-29T10:21:20+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_posting_to_fifo_requires_deduplicationid_group_id[sqs]": { + "last_validated_date": "2024-04-30T13:34:21+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_retention_with_inflight": { - "last_validated_date": "2024-01-02T17:32:37+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_posting_to_fifo_requires_deduplicationid_group_id[sqs_query]": { + "last_validated_date": "2024-04-30T13:34:22+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_non_existent_queue": { - "last_validated_date": "2023-11-14T12:04:10+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_attribute_names_filters[sqs]": { + "last_validated_date": "2024-04-30T13:34:57+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_attribute_names_filters[sqs_query]": { + "last_validated_date": "2024-04-30T13:34:58+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_attributes_timestamp_types[sqs]": { + "last_validated_date": "2024-04-30T13:40:03+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_posting_to_fifo_requires_deduplicationid_group_id": { - "last_validated_date": "2023-11-14T11:18:18+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_attributes_timestamp_types[sqs_query]": { + "last_validated_date": "2024-04-30T13:40:03+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_attribute_names_filters": { - "last_validated_date": "2023-11-14T11:00:15+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_message_attribute_names_filters[sqs]": { + "last_validated_date": "2024-04-30T13:34:51+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_message_attribute_names_filters": { - "last_validated_date": "2023-11-14T11:00:14+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_message_attribute_names_filters[sqs_query]": { + "last_validated_date": "2024-04-30T13:34:55+00:00" }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_message_size": { - "last_validated_date": "2024-01-03T01:58:49+00:00" + "last_validated_date": "2024-04-30T13:33:44+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_deduplication_id_for_fifo_queue": { - "last_validated_date": "2023-11-14T10:58:55+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_deduplication_id_for_fifo_queue[sqs]": { + "last_validated_date": "2024-04-30T13:33:42+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_message_group_id_for_fifo_queue": { - "last_validated_date": "2023-11-14T10:58:56+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_deduplication_id_for_fifo_queue[sqs_query]": { + "last_validated_date": "2024-04-30T13:33:43+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents": { - "last_validated_date": "2023-11-14T10:57:37+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_message_group_id_for_fifo_queue[sqs]": { + "last_validated_date": "2024-04-30T13:33:44+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents_with_updated_maximum_message_size": { - "last_validated_date": "2023-11-14T10:57:37+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_message_group_id_for_fifo_queue[sqs_query]": { + "last_validated_date": "2024-04-30T13:33:45+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_binary_attributes": { - "last_validated_date": "2023-11-14T10:59:11+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_receive_multiple[sqs]": { + "last_validated_date": "2024-04-30T13:40:10+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_empty_string_attribute": { - "last_validated_date": "2023-11-14T11:04:45+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_receive_multiple[sqs_query]": { + "last_validated_date": "2024-04-30T13:40:12+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_updated_maximum_message_size": { - "last_validated_date": "2023-11-14T10:57:36+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch[sqs]": { + "last_validated_date": "2024-04-30T13:40:08+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_oversized_message": { - "last_validated_date": "2023-11-14T10:57:35+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch[sqs_query]": { + "last_validated_date": "2024-04-30T13:40:09+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_max_number_of_messages": { - "last_validated_date": "2023-11-14T10:57:30+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_empty_list[sqs]": { + "last_validated_date": "2024-04-30T13:40:12+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_fifo": { - "last_validated_date": "2023-11-14T11:15:03+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents[sqs]": { + "last_validated_date": "2024-04-30T13:33:06+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents[sqs_query]": { + "last_validated_date": "2024-04-30T13:33:08+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents_with_updated_maximum_message_size[sqs]": { + "last_validated_date": "2024-04-30T13:33:09+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents_with_updated_maximum_message_size[sqs_query]": { + "last_validated_date": "2024-04-30T13:33:10+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_binary_attributes[sqs]": { + "last_validated_date": "2024-04-30T13:33:48+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_binary_attributes[sqs_query]": { + "last_validated_date": "2024-04-30T13:33:49+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_empty_string_attribute[sqs]": { + "last_validated_date": "2024-04-30T13:35:41+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_empty_string_attribute[sqs_query]": { + "last_validated_date": "2024-04-30T13:35:42+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_updated_maximum_message_size[sqs]": { + "last_validated_date": "2024-04-30T13:33:02+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_updated_maximum_message_size[sqs_query]": { + "last_validated_date": "2024-04-30T13:33:04+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_oversized_message[sqs]": { + "last_validated_date": "2024-04-30T13:32:59+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_oversized_message[sqs_query]": { + "last_validated_date": "2024-04-30T13:33:01+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_max_number_of_messages[sqs]": { + "last_validated_date": "2024-04-30T13:32:56+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_max_number_of_messages[sqs_query]": { + "last_validated_date": "2024-04-30T13:32:57+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_message[sqs]": { + "last_validated_date": "2024-04-30T13:40:00+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_message[sqs_query]": { + "last_validated_date": "2024-04-30T13:40:01+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_message_encoded_content[sqs]": { + "last_validated_date": "2024-04-30T13:40:06+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_message_encoded_content[sqs_query]": { + "last_validated_date": "2024-04-30T13:40:07+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_message_multiple_queues": { + "last_validated_date": "2024-04-30T13:40:05+00:00" }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_fifo_same_dedup_id_different_message_groups[sqs]": { - "last_validated_date": "2024-02-19T12:34:04+00:00" + "last_validated_date": "2024-04-30T13:51:17+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_fifo_same_dedup_id_different_message_groups[sqs_json]": { - "last_validated_date": "2024-02-19T12:34:02+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_fifo_same_dedup_id_different_message_groups[sqs_query]": { + "last_validated_date": "2024-04-30T13:51:19+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_permission_lifecycle": { - "last_validated_date": "2023-11-14T11:00:19+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_permission_lifecycle[sqs]": { + "last_validated_date": "2024-04-30T13:35:03+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_kms_and_sqs_are_mutually_exclusive": { - "last_validated_date": "2023-11-14T11:00:12+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_permission_lifecycle[sqs_query]": { + "last_validated_date": "2024-04-30T13:35:07+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_queue_attributes": { - "last_validated_date": "2023-11-14T11:00:11+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_kms_and_sqs_are_mutually_exclusive[sqs]": { + "last_validated_date": "2024-04-30T13:34:48+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_queue_attributes[sqs]": { - "last_validated_date": "2024-01-04T10:07:41+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_kms_and_sqs_are_mutually_exclusive[sqs_query]": { + "last_validated_date": "2024-04-30T13:34:48+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_queue_attributes[sqs_json]": { - "last_validated_date": "2024-01-04T10:07:44+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_queue_attributes[sqs]": { + "last_validated_date": "2024-04-30T13:34:43+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_successive_purge_calls_fail": { - "last_validated_date": "2023-11-14T11:00:04+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_queue_attributes[sqs_query]": { + "last_validated_date": "2024-04-30T13:34:47+00:00" }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_successive_purge_calls_fail[sqs]": { - "last_validated_date": "2024-01-04T10:07:25+00:00" + "last_validated_date": "2024-04-30T13:37:02+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_successive_purge_calls_fail[sqs_query]": { + "last_validated_date": "2024-04-30T13:37:04+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_tag_untag_queue[sqs]": { + "last_validated_date": "2024-04-30T13:35:30+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_successive_purge_calls_fail[sqs_json]": { - "last_validated_date": "2024-01-04T10:07:26+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_tag_untag_queue[sqs_query]": { + "last_validated_date": "2024-04-30T13:35:33+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_tag_untag_queue": { - "last_validated_date": "2023-11-14T10:57:38+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_too_many_entries_in_batch_request[sqs]": { + "last_validated_date": "2024-04-30T13:33:39+00:00" }, - "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_too_many_entries_in_batch_request": { - "last_validated_date": "2023-11-14T10:58:54+00:00" + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_too_many_entries_in_batch_request[sqs_query]": { + "last_validated_date": "2024-04-30T13:33:40+00:00" }, "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_send_message_via_queue_url_with_json_protocol": { - "last_validated_date": "2023-11-14T20:27:28+00:00" + "last_validated_date": "2024-04-30T13:35:11+00:00" } } diff --git a/tests/aws/services/sqs/test_sqs_backdoor.py b/tests/aws/services/sqs/test_sqs_backdoor.py index 8a7f7fb97a4f5..e8580002ca575 100644 --- a/tests/aws/services/sqs/test_sqs_backdoor.py +++ b/tests/aws/services/sqs/test_sqs_backdoor.py @@ -48,7 +48,7 @@ def test_list_messages_has_no_side_effects( assert attributes[1]["ApproximateReceiveCount"] == "0" # do a real receive op that has a side effect - response = aws_client.sqs.receive_message( + response = aws_client.sqs_query.receive_message( QueueUrl=queue_url, VisibilityTimeout=0, MaxNumberOfMessages=1, AttributeNames=["All"] ) assert response["Messages"][0]["Body"] == "message-1" @@ -71,11 +71,13 @@ def test_list_messages_as_botocore_endpoint_url( queue_url = sqs_create_queue() - aws_client.sqs.send_message(QueueUrl=queue_url, MessageBody="message-1") - aws_client.sqs.send_message(QueueUrl=queue_url, MessageBody="message-2") + aws_client.sqs_query.send_message(QueueUrl=queue_url, MessageBody="message-1") + aws_client.sqs_query.send_message(QueueUrl=queue_url, MessageBody="message-2") # use the developer endpoint as boto client URL - client = aws_client_factory(endpoint_url="http://localhost:4566/_aws/sqs/messages").sqs + client = aws_client_factory( + endpoint_url="http://localhost:4566/_aws/sqs/messages" + ).sqs_query # max messages is ignored response = client.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=1) @@ -106,7 +108,9 @@ def test_fifo_list_messages_as_botocore_endpoint_url( aws_client.sqs.send_message(QueueUrl=queue_url, MessageBody="message-3", MessageGroupId="2") # use the developer endpoint as boto client URL - client = aws_client_factory(endpoint_url="http://localhost:4566/_aws/sqs/messages").sqs + client = aws_client_factory( + endpoint_url="http://localhost:4566/_aws/sqs/messages" + ).sqs_query # max messages is ignored response = client.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=1) @@ -131,7 +135,9 @@ def test_list_messages_with_invalid_action_raises_error( queue_url = sqs_create_queue() - client = aws_client_factory(endpoint_url="http://localhost:4566/_aws/sqs/messages").sqs + client = aws_client_factory( + endpoint_url="http://localhost:4566/_aws/sqs/messages" + ).sqs_query with pytest.raises(ClientError) as e: client.send_message(QueueUrl=queue_url, MessageBody="foobar") diff --git a/tests/aws/services/sqs/test_sqs_move_task.py b/tests/aws/services/sqs/test_sqs_move_task.py index e7b4b3ff9d7b8..f4cc2085a5ffe 100644 --- a/tests/aws/services/sqs/test_sqs_move_task.py +++ b/tests/aws/services/sqs/test_sqs_move_task.py @@ -48,7 +48,6 @@ def _factory(max_receive_count: int = 1) -> tuple[QueueUrl, QueueUrl]: @markers.aws.validated -@markers.snapshot.skip_snapshot_verify(paths=["$..Error.Detail"]) def test_cancel_with_invalid_task_handle(aws_client, snapshot): with pytest.raises(ClientError) as e: aws_client.sqs.cancel_message_move_task(TaskHandle="foobared") @@ -56,7 +55,6 @@ def test_cancel_with_invalid_task_handle(aws_client, snapshot): @markers.aws.validated -@markers.snapshot.skip_snapshot_verify(paths=["$..Error.Detail"]) def test_cancel_with_invalid_source_arn_in_task_handle(aws_client, snapshot): source_arn = "arn:aws:sqs:us-east-1:878966065785:test-queue-doesnt-exist-123456" task_handle = encode_move_task_handle("10f57157-fc38-4da9-a113-4de7e12d05dd", source_arn) @@ -67,7 +65,6 @@ def test_cancel_with_invalid_source_arn_in_task_handle(aws_client, snapshot): @markers.aws.validated -@markers.snapshot.skip_snapshot_verify(paths=["$..Error.Detail"]) def test_cancel_with_invalid_task_id_in_task_handle( sqs_create_queue, sqs_get_queue_arn, aws_client, snapshot ): @@ -82,7 +79,6 @@ def test_cancel_with_invalid_task_id_in_task_handle( @markers.aws.validated -@markers.snapshot.skip_snapshot_verify(paths=["$..Error.Detail"]) def test_source_needs_redrive_policy( sqs_create_queue, sqs_get_queue_arn, @@ -104,7 +100,6 @@ def test_source_needs_redrive_policy( @markers.aws.validated -@markers.snapshot.skip_snapshot_verify(paths=["$..Error.Detail"]) def test_destination_needs_to_exist( sqs_create_queue, sqs_create_dlq_pipe, @@ -501,7 +496,6 @@ def _wait_for_task_cancellation(): @markers.aws.validated -@markers.snapshot.skip_snapshot_verify(paths=["$..Error.Detail"]) def test_start_multiple_move_tasks( sqs_create_queue, sqs_create_dlq_pipe, diff --git a/tests/aws/services/sqs/test_sqs_move_task.snapshot.json b/tests/aws/services/sqs/test_sqs_move_task.snapshot.json index 8f37231835f9a..459ae88ce03d6 100644 --- a/tests/aws/services/sqs/test_sqs_move_task.snapshot.json +++ b/tests/aws/services/sqs/test_sqs_move_task.snapshot.json @@ -1,6 +1,6 @@ { "tests/aws/services/sqs/test_sqs_move_task.py::test_basic_move_task_workflow": { - "recorded-date": "03-01-2024, 20:25:54", + "recorded-date": "30-04-2024, 10:22:14", "recorded-content": { "start-message-move-task-response": { "TaskHandle": "<task-handle:1>", @@ -28,13 +28,13 @@ } }, "tests/aws/services/sqs/test_sqs_move_task.py::test_source_needs_redrive_policy": { - "recorded-date": "05-01-2024, 12:48:02", + "recorded-date": "30-04-2024, 10:22:04", "recorded-content": { "error": { "Error": { "Code": "InvalidParameterValue", - "Detail": null, "Message": "Source queue must be configured as a Dead Letter Queue.", + "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -45,13 +45,13 @@ } }, "tests/aws/services/sqs/test_sqs_move_task.py::test_cancel_with_invalid_task_handle": { - "recorded-date": "05-01-2024, 12:45:27", + "recorded-date": "30-04-2024, 10:22:02", "recorded-content": { "error": { "Error": { "Code": "InvalidParameterValue", - "Detail": null, "Message": "Value for parameter TaskHandle is invalid.", + "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -62,15 +62,16 @@ } }, "tests/aws/services/sqs/test_sqs_move_task.py::test_cancel_with_invalid_source_arn_in_task_handle": { - "recorded-date": "05-01-2024, 12:46:39", + "recorded-date": "30-04-2024, 10:22:02", "recorded-content": { "error": { "Error": { "Code": "ResourceNotFoundException", - "Detail": null, "Message": "The resource that you specified for the SourceArn parameter doesn't exist.", + "QueryErrorCode": "ResourceNotFoundException", "Type": "Sender" }, + "message": "The resource that you specified for the SourceArn parameter doesn't exist.", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 404 @@ -79,15 +80,16 @@ } }, "tests/aws/services/sqs/test_sqs_move_task.py::test_cancel_with_invalid_task_id_in_task_handle": { - "recorded-date": "05-01-2024, 12:47:48", + "recorded-date": "30-04-2024, 10:22:03", "recorded-content": { "error": { "Error": { "Code": "ResourceNotFoundException", - "Detail": null, "Message": "Task does not exist.", + "QueryErrorCode": "ResourceNotFoundException", "Type": "Sender" }, + "message": "Task does not exist.", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 404 @@ -96,7 +98,7 @@ } }, "tests/aws/services/sqs/test_sqs_move_task.py::test_move_task_with_throughput_limit": { - "recorded-date": "03-01-2024, 20:25:11", + "recorded-date": "30-04-2024, 10:22:54", "recorded-content": { "start-message-move-task-response": { "TaskHandle": "<task-handle:1>", @@ -108,15 +110,16 @@ } }, "tests/aws/services/sqs/test_sqs_move_task.py::test_destination_needs_to_exist": { - "recorded-date": "05-01-2024, 12:48:32", + "recorded-date": "30-04-2024, 10:22:06", "recorded-content": { "error": { "Error": { "Code": "ResourceNotFoundException", - "Detail": null, "Message": "The resource that you specified for the DestinationArn parameter doesn't exist.", + "QueryErrorCode": "ResourceNotFoundException", "Type": "Sender" }, + "message": "The resource that you specified for the DestinationArn parameter doesn't exist.", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 404 @@ -125,12 +128,12 @@ } }, "tests/aws/services/sqs/test_sqs_move_task.py::test_move_task_cancel": { - "recorded-date": "03-01-2024, 20:34:27", + "recorded-date": "30-04-2024, 10:23:25", "recorded-content": { "list-while": { "Results": [ { - "ApproximateNumberOfMessagesMoved": 0, + "ApproximateNumberOfMessagesMoved": 2, "ApproximateNumberOfMessagesToMove": 10, "DestinationArn": "arn:aws:sqs:<region>:111111111111:<resource:1>", "MaxNumberOfMessagesPerSecond": 1, @@ -146,7 +149,7 @@ } }, "cancel": { - "ApproximateNumberOfMessagesMoved": 0, + "ApproximateNumberOfMessagesMoved": 2, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -155,7 +158,7 @@ "list-after": { "Results": [ { - "ApproximateNumberOfMessagesMoved": 2, + "ApproximateNumberOfMessagesMoved": 8, "ApproximateNumberOfMessagesToMove": 10, "DestinationArn": "arn:aws:sqs:<region>:111111111111:<resource:1>", "MaxNumberOfMessagesPerSecond": 1, @@ -172,7 +175,7 @@ } }, "tests/aws/services/sqs/test_sqs_move_task.py::test_move_task_delete_destination_queue_while_running": { - "recorded-date": "03-01-2024, 20:47:31", + "recorded-date": "30-04-2024, 10:24:05", "recorded-content": { "list": { "Results": [ @@ -195,13 +198,13 @@ } }, "tests/aws/services/sqs/test_sqs_move_task.py::test_start_multiple_move_tasks": { - "recorded-date": "05-01-2024, 12:50:38", + "recorded-date": "30-04-2024, 10:24:13", "recorded-content": { "error": { "Error": { "Code": "InvalidParameterValue", - "Detail": null, "Message": "There is already a task running. Only one active task is allowed for a source queue arn at a given time.", + "QueryErrorCode": "InvalidParameterValueException", "Type": "Sender" }, "ResponseMetadata": { @@ -212,7 +215,7 @@ } }, "tests/aws/services/sqs/test_sqs_move_task.py::test_move_task_workflow_with_default_destination": { - "recorded-date": "07-03-2024, 19:07:58", + "recorded-date": "30-04-2024, 10:22:20", "recorded-content": { "source-arn": "arn:aws:sqs:<region>:111111111111:<resource:1>", "original-source": "arn:aws:sqs:<region>:111111111111:<resource:2>", @@ -241,7 +244,7 @@ } }, "tests/aws/services/sqs/test_sqs_move_task.py::test_move_task_workflow_with_multiple_sources_as_default_destination": { - "recorded-date": "07-03-2024, 18:42:16", + "recorded-date": "30-04-2024, 10:22:29", "recorded-content": { "source-arn": "arn:aws:sqs:<region>:111111111111:<resource:1>", "original-source-1": "arn:aws:sqs:<region>:111111111111:<resource:2>", diff --git a/tests/aws/services/sqs/test_sqs_move_task.validation.json b/tests/aws/services/sqs/test_sqs_move_task.validation.json index 74c9428ee7d64..b5bf2a05ed236 100644 --- a/tests/aws/services/sqs/test_sqs_move_task.validation.json +++ b/tests/aws/services/sqs/test_sqs_move_task.validation.json @@ -1,38 +1,38 @@ { "tests/aws/services/sqs/test_sqs_move_task.py::test_basic_move_task_workflow": { - "last_validated_date": "2024-01-03T20:25:53+00:00" + "last_validated_date": "2024-04-30T10:22:14+00:00" }, "tests/aws/services/sqs/test_sqs_move_task.py::test_cancel_with_invalid_source_arn_in_task_handle": { - "last_validated_date": "2024-01-05T12:46:39+00:00" + "last_validated_date": "2024-04-30T10:22:02+00:00" }, "tests/aws/services/sqs/test_sqs_move_task.py::test_cancel_with_invalid_task_handle": { - "last_validated_date": "2024-01-05T12:45:27+00:00" + "last_validated_date": "2024-04-30T10:22:02+00:00" }, "tests/aws/services/sqs/test_sqs_move_task.py::test_cancel_with_invalid_task_id_in_task_handle": { - "last_validated_date": "2024-01-05T12:47:48+00:00" + "last_validated_date": "2024-04-30T10:22:03+00:00" }, "tests/aws/services/sqs/test_sqs_move_task.py::test_destination_needs_to_exist": { - "last_validated_date": "2024-01-05T12:48:32+00:00" + "last_validated_date": "2024-04-30T10:22:06+00:00" }, "tests/aws/services/sqs/test_sqs_move_task.py::test_move_task_cancel": { - "last_validated_date": "2024-01-03T20:34:27+00:00" + "last_validated_date": "2024-04-30T10:23:24+00:00" }, "tests/aws/services/sqs/test_sqs_move_task.py::test_move_task_delete_destination_queue_while_running": { - "last_validated_date": "2024-01-03T20:47:31+00:00" + "last_validated_date": "2024-04-30T10:24:04+00:00" }, "tests/aws/services/sqs/test_sqs_move_task.py::test_move_task_with_throughput_limit": { - "last_validated_date": "2024-01-03T20:25:11+00:00" + "last_validated_date": "2024-04-30T10:22:53+00:00" }, "tests/aws/services/sqs/test_sqs_move_task.py::test_move_task_workflow_with_default_destination": { - "last_validated_date": "2024-03-07T19:07:57+00:00" + "last_validated_date": "2024-04-30T10:22:19+00:00" }, "tests/aws/services/sqs/test_sqs_move_task.py::test_move_task_workflow_with_multiple_sources_as_default_destination": { - "last_validated_date": "2024-03-07T18:42:16+00:00" + "last_validated_date": "2024-04-30T10:22:28+00:00" }, "tests/aws/services/sqs/test_sqs_move_task.py::test_source_needs_redrive_policy": { - "last_validated_date": "2024-01-05T12:48:02+00:00" + "last_validated_date": "2024-04-30T10:22:04+00:00" }, "tests/aws/services/sqs/test_sqs_move_task.py::test_start_multiple_move_tasks": { - "last_validated_date": "2024-01-05T12:50:37+00:00" + "last_validated_date": "2024-04-30T10:24:12+00:00" } } diff --git a/tests/unit/aws/protocol/test_parser.py b/tests/unit/aws/protocol/test_parser.py index c7cf4521ff1ca..3900748f03714 100644 --- a/tests/unit/aws/protocol/test_parser.py +++ b/tests/unit/aws/protocol/test_parser.py @@ -45,7 +45,7 @@ def test_query_parser(): def test_sqs_query_parse_tag_map_with_member_name_as_location(): # see https://github.com/localstack/localstack/issues/4391 - parser = create_parser(load_service("sqs")) + parser = create_parser(load_service("sqs-query")) # with "Tag." it works (this is the default request) request = HttpRequest( @@ -116,7 +116,7 @@ def test_query_parser_uri(): def test_query_parser_flattened_map(): """Simple test with a flattened map (SQS SetQueueAttributes request).""" - parser = QueryRequestParser(load_service("sqs")) + parser = QueryRequestParser(load_service("sqs-query")) request = HttpRequest( body=to_bytes( "Action=SetQueueAttributes&Version=2012-11-05&" @@ -250,7 +250,7 @@ def test_query_parser_non_flattened_list_structure_changed_name(): def test_query_parser_flattened_list_structure(): """Simple test with a flattened list of structures.""" - parser = QueryRequestParser(load_service("sqs")) + parser = QueryRequestParser(load_service("sqs-query")) request = HttpRequest( body=to_bytes( "Action=DeleteMessageBatch&" @@ -386,7 +386,7 @@ def test_query_parser_sqs_with_botocore(): def test_query_parser_empty_required_members_sqs_with_botocore(): _botocore_parser_integration_test( - service="sqs", + service="sqs-query", action="SendMessageBatch", QueueUrl="string", Entries=[], diff --git a/tests/unit/aws/protocol/test_parser_validate.py b/tests/unit/aws/protocol/test_parser_validate.py index d301b9f8b4af8..8bfe6d1ad615f 100644 --- a/tests/unit/aws/protocol/test_parser_validate.py +++ b/tests/unit/aws/protocol/test_parser_validate.py @@ -33,7 +33,7 @@ def test_missing_required_field_restjson(self): assert e.value.required_name == "TagList" def test_missing_required_field_query(self): - parser = create_parser(load_service("sqs")) + parser = create_parser(load_service("sqs-query")) op, params = parser.parse( HttpRequest( diff --git a/tests/unit/aws/protocol/test_serializer.py b/tests/unit/aws/protocol/test_serializer.py index 1fefee1f67780..84e49d40aedb6 100644 --- a/tests/unit/aws/protocol/test_serializer.py +++ b/tests/unit/aws/protocol/test_serializer.py @@ -476,7 +476,7 @@ def test_query_serializer_sqs_none_value_in_map(): def test_query_protocol_error_serialization(): exception = InvalidMessageContents("Exception message!") _botocore_error_serializer_integration_test( - "sqs", "SendMessage", exception, "InvalidMessageContents", 400, "Exception message!" + "sqs-query", "SendMessage", exception, "InvalidMessageContents", 400, "Exception message!" ) @@ -486,7 +486,7 @@ def test_query_protocol_error_serialization_plain(): ) # Load the SQS service - service = load_service("sqs") + service = load_service("sqs-query") # Use our serializer to serialize the response response_serializer = create_serializer(service) @@ -528,7 +528,7 @@ def test_query_protocol_error_serialization_plain(): def test_query_protocol_custom_error_serialization(): exception = CommonServiceException("InvalidParameterValue", "Parameter x was invalid!") _botocore_error_serializer_integration_test( - "sqs", + "sqs-query", "SendMessage", exception, "InvalidParameterValue", @@ -540,7 +540,7 @@ def test_query_protocol_custom_error_serialization(): def test_query_protocol_error_serialization_sender_fault(): exception = UnsupportedOperation("Operation not supported.") _botocore_error_serializer_integration_test( - "sqs", + "sqs-query", "SendMessage", exception, "AWS.SimpleQueueService.UnsupportedOperation", @@ -1866,7 +1866,7 @@ def test_json_protocol_cbor_serialization(headers_dict): class TestAwsResponseSerializerDecorator: def test_query_internal_error(self): - @aws_response_serializer("sqs", "ListQueues") + @aws_response_serializer("sqs-query", "ListQueues") def fn(request: Request): raise ValueError("oh noes!") @@ -1875,7 +1875,7 @@ def fn(request: Request): assert b"<Code>InternalError</Code>" in response.data def test_query_service_error(self): - @aws_response_serializer("sqs", "ListQueues") + @aws_response_serializer("sqs-query", "ListQueues") def fn(request: Request): raise UnsupportedOperation("Operation not supported.") @@ -1885,7 +1885,7 @@ def fn(request: Request): assert b"<Message>Operation not supported.</Message>" in response.data def test_query_valid_response(self): - @aws_response_serializer("sqs", "ListQueues") + @aws_response_serializer("sqs-query", "ListQueues") def fn(request: Request): from localstack.aws.api.sqs import ListQueuesResult @@ -1907,7 +1907,7 @@ def fn(request: Request): def test_query_valid_response_content_negotiation(self): # this test verifies that request header values are passed correctly to perform content negotation - @aws_response_serializer("sqs", "ListQueues") + @aws_response_serializer("sqs-query", "ListQueues") def fn(request: Request): from localstack.aws.api.sqs import ListQueuesResult @@ -1930,7 +1930,7 @@ def fn(request: Request): } def test_return_invalid_none_type_causes_internal_error(self): - @aws_response_serializer("sqs", "ListQueues") + @aws_response_serializer("sqs-query", "ListQueues") def fn(request: Request): return None @@ -1940,7 +1940,7 @@ def fn(request: Request): def test_response_pass_through(self): # returning a response directly will forego the serializer - @aws_response_serializer("sqs", "ListQueues") + @aws_response_serializer("sqs-query", "ListQueues") def fn(request: Request): return Response(b"ok", status=201) @@ -1959,7 +1959,7 @@ def fn(request: Request): def test_invoke_on_bound_method(self): class MyHandler: - @aws_response_serializer("sqs", "ListQueues") + @aws_response_serializer("sqs-query", "ListQueues") def handle(self, request: Request): from localstack.aws.api.sqs import ListQueuesResult diff --git a/tests/unit/aws/test_mocking.py b/tests/unit/aws/test_mocking.py index 2983d26dc2abf..87db62b2967c6 100644 --- a/tests/unit/aws/test_mocking.py +++ b/tests/unit/aws/test_mocking.py @@ -58,4 +58,4 @@ def test_get_mocking_skeleton(): context = create_aws_request_context("sqs", "CreateQueue", request) response = skeleton.invoke(context) # just a smoke test - assert b"<QueueUrl>" in response.data + assert b"QueueUrl" in response.data diff --git a/tests/unit/aws/test_service_router.py b/tests/unit/aws/test_service_router.py index 3aa077603f527..fc98996bc0a8c 100644 --- a/tests/unit/aws/test_service_router.py +++ b/tests/unit/aws/test_service_router.py @@ -158,10 +158,12 @@ def test_service_router_works_for_every_service( ): caplog.set_level("CRITICAL", "botocore") - # if we test the routing to the internalized sqs json, we want to use the service name "sqs-json" in order to - # instruct botocore to load the internalized spec instead of the default (query) + # if we test the routing to the internalized sqs query, we want to use the service name "sqs-query" in order to + # instruct botocore to load the internalized spec instead of the default (json) service_name = ( - "sqs-json" if service.service_name == "sqs" and protocol == "json" else service.service_name + "sqs-query" + if service.service_name == "sqs" and protocol == "query" + else service.service_name ) # Create a dummy request for the service router diff --git a/tests/unit/aws/test_skeleton.py b/tests/unit/aws/test_skeleton.py index 2816e873276a6..3f847d5e5cd94 100644 --- a/tests/unit/aws/test_skeleton.py +++ b/tests/unit/aws/test_skeleton.py @@ -155,7 +155,7 @@ def _get_sqs_request_headers(): def test_skeleton_e2e_sqs_send_message(): - sqs_service = load_service("sqs") + sqs_service = load_service("sqs-query") skeleton = Skeleton(sqs_service, TestSqsApi()) context = RequestContext() context.account = "test" @@ -210,7 +210,7 @@ def test_skeleton_e2e_sqs_send_message(): ], ) def test_skeleton_e2e_sqs_send_message_not_implemented(api_class, oracle_message): - sqs_service = load_service("sqs") + sqs_service = load_service("sqs-query") skeleton = Skeleton(sqs_service, api_class) context = RequestContext() context.account = "test" @@ -254,7 +254,7 @@ def delete_queue(_context: RequestContext, _request: ServiceRequest): table: DispatchTable = {} table["DeleteQueue"] = delete_queue - sqs_service = load_service("sqs") + sqs_service = load_service("sqs-query") skeleton = Skeleton(sqs_service, table) context = RequestContext() @@ -287,7 +287,7 @@ def delete_queue(_context: RequestContext, _request: ServiceRequest): def test_dispatch_missing_method_returns_internal_failure(): table: DispatchTable = {} - sqs_service = load_service("sqs") + sqs_service = load_service("sqs-query") skeleton = Skeleton(sqs_service, table) context = RequestContext() diff --git a/tests/unit/aws/test_spec.py b/tests/unit/aws/test_spec.py index 0f8774a7f464e..b20b57ad76087 100644 --- a/tests/unit/aws/test_spec.py +++ b/tests/unit/aws/test_spec.py @@ -71,13 +71,13 @@ def test_patching_loaders(): def test_loading_own_specs(): - """Ensure that the internalized specifications (f.e. the sqs-json spec) can be handled by the CustomLoader.""" + """Ensure that the internalized specifications (f.e. the sqs-query spec) can be handled by the CustomLoader.""" loader = CustomLoader({}) # first test that specs remain intact sqs_query_description = loader.load_service_model("sqs", "service-2") - assert sqs_query_description["metadata"]["protocol"] == "query" - sqs_json_description = loader.load_service_model("sqs-json", "service-2") - assert sqs_json_description["metadata"]["protocol"] == "json" + assert sqs_query_description["metadata"]["protocol"] == "json" + sqs_json_description = loader.load_service_model("sqs-query", "service-2") + assert sqs_json_description["metadata"]["protocol"] == "query" @pytest.mark.parametrize( @@ -90,9 +90,9 @@ def test_loading_own_specs(): # tests with a default and a specific protocol (SQS) ("sqs", "query", "sqs", "query"), ("sqs", "json", "sqs", "json"), - ("sqs", None, "sqs", "query"), - ("sqs-json", None, "sqs", "json"), - ("sqs-json", "json", "sqs", "json"), + ("sqs", None, "sqs", "json"), + ("sqs-query", None, "sqs", "query"), + ("sqs-query", "query", "sqs", "query"), ], ) def test_protocol_specific_loading( @@ -117,7 +117,7 @@ def test_protocol_specific_loading( # unknown protocol raises error ("sqs", "nonexistingprotocol", UnknownServiceProtocolError), # non-matching protocol in service naming convention and explicitly defined protocol - ("sqs-json", "query", UnknownServiceProtocolError), + ("sqs-query", "json", UnknownServiceProtocolError), ], ) def test_invalid_service_loading( From 3c06fd4b55c0b24c63e8c404a3dd13312a40f4b2 Mon Sep 17 00:00:00 2001 From: Giovanni Grano <me@giograno.com> Date: Thu, 2 May 2024 11:01:29 -0400 Subject: [PATCH 112/169] Improve DDB bytes encoding (#10740) --- localstack/utils/json.py | 5 +++-- .../lambda_/test_lambda_integration_dynamodbstreams.py | 1 - tests/unit/utils/test_json.py | 9 +++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 tests/unit/utils/test_json.py diff --git a/localstack/utils/json.py b/localstack/utils/json.py index 2f1d64303ec94..e2edcf3b4df35 100644 --- a/localstack/utils/json.py +++ b/localstack/utils/json.py @@ -1,3 +1,4 @@ +import base64 import decimal import json import logging @@ -47,11 +48,11 @@ def default(self, o): class BytesEncoder(json.JSONEncoder): - """Helper class that converts JSON documents with bytes""" + """Specialized JSON encoder that encode bytes into Base64 strings.""" def default(self, obj): if isinstance(obj, bytes): - return to_str(obj, errors="replace") + return to_str(base64.b64encode(obj)) return super().default(obj) diff --git a/tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py b/tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py index d7abab21d6e53..f3bf04e557472 100644 --- a/tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py +++ b/tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py @@ -63,7 +63,6 @@ def _get_lambda_logs_event(function_name, expected_num_events, retries=30): "$..TableDescription.ProvisionedThroughput.LastIncreaseDateTime", "$..TableDescription.StreamSpecification", "$..TableDescription.TableStatus", - "$..Records..dynamodb.NewImage.binary_key.B", "$..Records..dynamodb.SizeBytes", "$..Records..eventVersion", ], diff --git a/tests/unit/utils/test_json.py b/tests/unit/utils/test_json.py new file mode 100644 index 0000000000000..7680ab7793b61 --- /dev/null +++ b/tests/unit/utils/test_json.py @@ -0,0 +1,9 @@ +import json + +from localstack.utils.json import BytesEncoder + + +def test_json_encoder(): + payload = {"foo": b"foobar"} + result = json.dumps(payload, cls=BytesEncoder) + assert result == '{"foo": "Zm9vYmFy"}' From b823752302a96b730bc07ab078a40b675a9f0ba9 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Mon, 6 May 2024 09:28:17 +0200 Subject: [PATCH 113/169] Update ASF APIs, update route53resolver operations (#10769) Co-authored-by: LocalStack Bot <localstack-bot@users.noreply.github.com> Co-authored-by: Alexander Rashed <alexander.rashed@localstack.cloud> --- localstack/aws/api/dynamodb/__init__.py | 36 +++++++++++++++++- localstack/aws/api/ec2/__init__.py | 37 +++++++++++++++++++ localstack/aws/api/opensearch/__init__.py | 2 + .../aws/api/route53resolver/__init__.py | 10 +++++ localstack/aws/api/transcribe/__init__.py | 24 ++++++++++++ .../services/route53resolver/provider.py | 9 +++++ pyproject.toml | 8 ++-- requirements-base-runtime.txt | 4 +- requirements-dev.txt | 6 +-- requirements-runtime.txt | 6 +-- requirements-test.txt | 6 +-- requirements-typehint.txt | 8 ++-- 12 files changed, 135 insertions(+), 21 deletions(-) diff --git a/localstack/aws/api/dynamodb/__init__.py b/localstack/aws/api/dynamodb/__init__.py index feb967992af08..d817ea74d8201 100644 --- a/localstack/aws/api/dynamodb/__init__.py +++ b/localstack/aws/api/dynamodb/__init__.py @@ -671,6 +671,14 @@ class StreamSpecification(TypedDict, total=False): StreamViewType: Optional[StreamViewType] +LongObject = int + + +class OnDemandThroughput(TypedDict, total=False): + MaxReadRequestUnits: Optional[LongObject] + MaxWriteRequestUnits: Optional[LongObject] + + class ProvisionedThroughput(TypedDict, total=False): ReadCapacityUnits: PositiveLongObject WriteCapacityUnits: PositiveLongObject @@ -697,6 +705,7 @@ class GlobalSecondaryIndexInfo(TypedDict, total=False): KeySchema: Optional[KeySchema] Projection: Optional[Projection] ProvisionedThroughput: Optional[ProvisionedThroughput] + OnDemandThroughput: Optional[OnDemandThroughput] GlobalSecondaryIndexes = List[GlobalSecondaryIndexInfo] @@ -721,7 +730,6 @@ class SourceTableFeatureDetails(TypedDict, total=False): ItemCount = int TableCreationDateTime = datetime -LongObject = int class SourceTableDetails(TypedDict, total=False): @@ -732,6 +740,7 @@ class SourceTableDetails(TypedDict, total=False): KeySchema: KeySchema TableCreationDateTime: TableCreationDateTime ProvisionedThroughput: ProvisionedThroughput + OnDemandThroughput: Optional[OnDemandThroughput] ItemCount: Optional[ItemCount] BillingMode: Optional[BillingMode] @@ -967,6 +976,7 @@ class CreateGlobalSecondaryIndexAction(TypedDict, total=False): KeySchema: KeySchema Projection: Projection ProvisionedThroughput: Optional[ProvisionedThroughput] + OnDemandThroughput: Optional[OnDemandThroughput] class Replica(TypedDict, total=False): @@ -986,6 +996,10 @@ class TableClassSummary(TypedDict, total=False): LastUpdateDateTime: Optional[Date] +class OnDemandThroughputOverride(TypedDict, total=False): + MaxReadRequestUnits: Optional[LongObject] + + class ProvisionedThroughputOverride(TypedDict, total=False): ReadCapacityUnits: Optional[PositiveLongObject] @@ -993,6 +1007,7 @@ class ProvisionedThroughputOverride(TypedDict, total=False): class ReplicaGlobalSecondaryIndexDescription(TypedDict, total=False): IndexName: Optional[IndexName] ProvisionedThroughputOverride: Optional[ProvisionedThroughputOverride] + OnDemandThroughputOverride: Optional[OnDemandThroughputOverride] ReplicaGlobalSecondaryIndexDescriptionList = List[ReplicaGlobalSecondaryIndexDescription] @@ -1005,6 +1020,7 @@ class ReplicaDescription(TypedDict, total=False): ReplicaStatusPercentProgress: Optional[ReplicaStatusPercentProgress] KMSMasterKeyId: Optional[KMSMasterKeyId] ProvisionedThroughputOverride: Optional[ProvisionedThroughputOverride] + OnDemandThroughputOverride: Optional[OnDemandThroughputOverride] GlobalSecondaryIndexes: Optional[ReplicaGlobalSecondaryIndexDescriptionList] ReplicaInaccessibleDateTime: Optional[Date] ReplicaTableClassSummary: Optional[TableClassSummary] @@ -1032,6 +1048,7 @@ class CreateReplicaAction(TypedDict, total=False): class ReplicaGlobalSecondaryIndex(TypedDict, total=False): IndexName: IndexName ProvisionedThroughputOverride: Optional[ProvisionedThroughputOverride] + OnDemandThroughputOverride: Optional[OnDemandThroughputOverride] ReplicaGlobalSecondaryIndexList = List[ReplicaGlobalSecondaryIndex] @@ -1041,6 +1058,7 @@ class CreateReplicationGroupMemberAction(TypedDict, total=False): RegionName: RegionName KMSMasterKeyId: Optional[KMSMasterKeyId] ProvisionedThroughputOverride: Optional[ProvisionedThroughputOverride] + OnDemandThroughputOverride: Optional[OnDemandThroughputOverride] GlobalSecondaryIndexes: Optional[ReplicaGlobalSecondaryIndexList] TableClassOverride: Optional[TableClass] @@ -1064,6 +1082,7 @@ class GlobalSecondaryIndex(TypedDict, total=False): KeySchema: KeySchema Projection: Projection ProvisionedThroughput: Optional[ProvisionedThroughput] + OnDemandThroughput: Optional[OnDemandThroughput] GlobalSecondaryIndexList = List[GlobalSecondaryIndex] @@ -1092,6 +1111,7 @@ class CreateTableInput(ServiceRequest): TableClass: Optional[TableClass] DeletionProtectionEnabled: Optional[DeletionProtectionEnabled] ResourcePolicy: Optional[ResourcePolicy] + OnDemandThroughput: Optional[OnDemandThroughput] class RestoreSummary(TypedDict, total=False): @@ -1122,6 +1142,7 @@ class GlobalSecondaryIndexDescription(TypedDict, total=False): IndexSizeBytes: Optional[LongObject] ItemCount: Optional[LongObject] IndexArn: Optional[String] + OnDemandThroughput: Optional[OnDemandThroughput] GlobalSecondaryIndexDescriptionList = List[GlobalSecondaryIndexDescription] @@ -1163,6 +1184,7 @@ class TableDescription(TypedDict, total=False): ArchivalSummary: Optional[ArchivalSummary] TableClassSummary: Optional[TableClassSummary] DeletionProtectionEnabled: Optional[DeletionProtectionEnabled] + OnDemandThroughput: Optional[OnDemandThroughput] class CreateTableOutput(TypedDict, total=False): @@ -1421,6 +1443,7 @@ class TableCreationParameters(TypedDict, total=False): KeySchema: KeySchema BillingMode: Optional[BillingMode] ProvisionedThroughput: Optional[ProvisionedThroughput] + OnDemandThroughput: Optional[OnDemandThroughput] SSESpecification: Optional[SSESpecification] GlobalSecondaryIndexes: Optional[GlobalSecondaryIndexList] @@ -1666,7 +1689,8 @@ class GlobalSecondaryIndexAutoScalingUpdate(TypedDict, total=False): class UpdateGlobalSecondaryIndexAction(TypedDict, total=False): IndexName: IndexName - ProvisionedThroughput: ProvisionedThroughput + ProvisionedThroughput: Optional[ProvisionedThroughput] + OnDemandThroughput: Optional[OnDemandThroughput] class GlobalSecondaryIndexUpdate(TypedDict, total=False): @@ -1948,6 +1972,7 @@ class UpdateReplicationGroupMemberAction(TypedDict, total=False): RegionName: RegionName KMSMasterKeyId: Optional[KMSMasterKeyId] ProvisionedThroughputOverride: Optional[ProvisionedThroughputOverride] + OnDemandThroughputOverride: Optional[OnDemandThroughputOverride] GlobalSecondaryIndexes: Optional[ReplicaGlobalSecondaryIndexList] TableClassOverride: Optional[TableClass] @@ -1968,6 +1993,7 @@ class RestoreTableFromBackupInput(ServiceRequest): GlobalSecondaryIndexOverride: Optional[GlobalSecondaryIndexList] LocalSecondaryIndexOverride: Optional[LocalSecondaryIndexList] ProvisionedThroughputOverride: Optional[ProvisionedThroughput] + OnDemandThroughputOverride: Optional[OnDemandThroughput] SSESpecificationOverride: Optional[SSESpecification] @@ -1985,6 +2011,7 @@ class RestoreTableToPointInTimeInput(ServiceRequest): GlobalSecondaryIndexOverride: Optional[GlobalSecondaryIndexList] LocalSecondaryIndexOverride: Optional[LocalSecondaryIndexList] ProvisionedThroughputOverride: Optional[ProvisionedThroughput] + OnDemandThroughputOverride: Optional[OnDemandThroughput] SSESpecificationOverride: Optional[SSESpecification] @@ -2184,6 +2211,7 @@ class UpdateTableInput(ServiceRequest): ReplicaUpdates: Optional[ReplicationGroupUpdateList] TableClass: Optional[TableClass] DeletionProtectionEnabled: Optional[DeletionProtectionEnabled] + OnDemandThroughput: Optional[OnDemandThroughput] class UpdateTableOutput(TypedDict, total=False): @@ -2278,6 +2306,7 @@ def create_table( table_class: TableClass = None, deletion_protection_enabled: DeletionProtectionEnabled = None, resource_policy: ResourcePolicy = None, + on_demand_throughput: OnDemandThroughput = None, **kwargs, ) -> CreateTableOutput: raise NotImplementedError @@ -2643,6 +2672,7 @@ def restore_table_from_backup( global_secondary_index_override: GlobalSecondaryIndexList = None, local_secondary_index_override: LocalSecondaryIndexList = None, provisioned_throughput_override: ProvisionedThroughput = None, + on_demand_throughput_override: OnDemandThroughput = None, sse_specification_override: SSESpecification = None, **kwargs, ) -> RestoreTableFromBackupOutput: @@ -2661,6 +2691,7 @@ def restore_table_to_point_in_time( global_secondary_index_override: GlobalSecondaryIndexList = None, local_secondary_index_override: LocalSecondaryIndexList = None, provisioned_throughput_override: ProvisionedThroughput = None, + on_demand_throughput_override: OnDemandThroughput = None, sse_specification_override: SSESpecification = None, **kwargs, ) -> RestoreTableToPointInTimeOutput: @@ -2819,6 +2850,7 @@ def update_table( replica_updates: ReplicationGroupUpdateList = None, table_class: TableClass = None, deletion_protection_enabled: DeletionProtectionEnabled = None, + on_demand_throughput: OnDemandThroughput = None, **kwargs, ) -> UpdateTableOutput: raise NotImplementedError diff --git a/localstack/aws/api/ec2/__init__.py b/localstack/aws/api/ec2/__init__.py index 6c2610c89022b..d45500bd143ff 100644 --- a/localstack/aws/api/ec2/__init__.py +++ b/localstack/aws/api/ec2/__init__.py @@ -114,6 +114,7 @@ EfaSupportedFlag = bool EgressOnlyInternetGatewayId = str EipAllocationPublicIp = str +EkPubKeyValue = str ElasticGpuId = str ElasticInferenceAcceleratorCount = int ElasticIpAssociationId = str @@ -872,6 +873,16 @@ class Ec2InstanceConnectEndpointState(str): delete_failed = "delete-failed" +class EkPubKeyFormat(str): + der = "der" + tpmt = "tpmt" + + +class EkPubKeyType(str): + rsa_2048 = "rsa-2048" + ecc_sec_p384 = "ecc-sec-p384" + + class ElasticGpuState(str): ATTACHED = "ATTACHED" @@ -15203,6 +15214,20 @@ class GetInstanceMetadataDefaultsResult(TypedDict, total=False): AccountLevel: Optional[InstanceMetadataDefaultsResponse] +class GetInstanceTpmEkPubRequest(ServiceRequest): + InstanceId: InstanceId + KeyType: EkPubKeyType + KeyFormat: EkPubKeyFormat + DryRun: Optional[Boolean] + + +class GetInstanceTpmEkPubResult(TypedDict, total=False): + InstanceId: Optional[InstanceId] + KeyType: Optional[EkPubKeyType] + KeyFormat: Optional[EkPubKeyFormat] + KeyValue: Optional[EkPubKeyValue] + + VirtualizationTypeSet = List[VirtualizationType] @@ -23563,6 +23588,18 @@ def get_instance_metadata_defaults( ) -> GetInstanceMetadataDefaultsResult: raise NotImplementedError + @handler("GetInstanceTpmEkPub") + def get_instance_tpm_ek_pub( + self, + context: RequestContext, + instance_id: InstanceId, + key_type: EkPubKeyType, + key_format: EkPubKeyFormat, + dry_run: Boolean = None, + **kwargs, + ) -> GetInstanceTpmEkPubResult: + raise NotImplementedError + @handler("GetInstanceTypesFromInstanceRequirements") def get_instance_types_from_instance_requirements( self, diff --git a/localstack/aws/api/opensearch/__init__.py b/localstack/aws/api/opensearch/__init__.py index ad44925986e61..2808b5f646dc0 100644 --- a/localstack/aws/api/opensearch/__init__.py +++ b/localstack/aws/api/opensearch/__init__.py @@ -32,6 +32,7 @@ ErrorMessage = str ErrorType = str GUID = str +HostedZoneId = str IdentityPoolId = str InstanceCount = int InstanceRole = str @@ -1130,6 +1131,7 @@ class DomainStatus(TypedDict, total=False): Endpoint: Optional[ServiceUrl] EndpointV2: Optional[ServiceUrl] Endpoints: Optional[EndpointsMap] + DomainEndpointV2HostedZoneId: Optional[HostedZoneId] Processing: Optional[Boolean] UpgradeProcessing: Optional[Boolean] EngineVersion: Optional[VersionString] diff --git a/localstack/aws/api/route53resolver/__init__.py b/localstack/aws/api/route53resolver/__init__.py index 42b68425ab550..bacfb34cdf7ce 100644 --- a/localstack/aws/api/route53resolver/__init__.py +++ b/localstack/aws/api/route53resolver/__init__.py @@ -84,6 +84,11 @@ class FirewallDomainListStatus(str): UPDATING = "UPDATING" +class FirewallDomainRedirectionAction(str): + INSPECT_REDIRECTION_DOMAIN = "INSPECT_REDIRECTION_DOMAIN" + TRUST_REDIRECTION_DOMAIN = "TRUST_REDIRECTION_DOMAIN" + + class FirewallDomainUpdateOperation(str): ADD = "ADD" REMOVE = "REMOVE" @@ -523,6 +528,7 @@ class CreateFirewallRuleRequest(ServiceRequest): BlockOverrideDnsType: Optional[BlockOverrideDnsType] BlockOverrideTtl: Optional[BlockOverrideTtl] Name: Name + FirewallDomainRedirectionAction: Optional[FirewallDomainRedirectionAction] Qtype: Optional[Qtype] @@ -539,6 +545,7 @@ class FirewallRule(TypedDict, total=False): CreatorRequestId: Optional[CreatorRequestId] CreationTime: Optional[Rfc3339TimeString] ModificationTime: Optional[Rfc3339TimeString] + FirewallDomainRedirectionAction: Optional[FirewallDomainRedirectionAction] Qtype: Optional[Qtype] @@ -1275,6 +1282,7 @@ class UpdateFirewallRuleRequest(ServiceRequest): BlockOverrideDnsType: Optional[BlockOverrideDnsType] BlockOverrideTtl: Optional[BlockOverrideTtl] Name: Optional[Name] + FirewallDomainRedirectionAction: Optional[FirewallDomainRedirectionAction] Qtype: Optional[Qtype] @@ -1415,6 +1423,7 @@ def create_firewall_rule( block_override_domain: BlockOverrideDomain = None, block_override_dns_type: BlockOverrideDnsType = None, block_override_ttl: BlockOverrideTtl = None, + firewall_domain_redirection_action: FirewallDomainRedirectionAction = None, qtype: Qtype = None, **kwargs, ) -> CreateFirewallRuleResponse: @@ -1926,6 +1935,7 @@ def update_firewall_rule( block_override_dns_type: BlockOverrideDnsType = None, block_override_ttl: BlockOverrideTtl = None, name: Name = None, + firewall_domain_redirection_action: FirewallDomainRedirectionAction = None, qtype: Qtype = None, **kwargs, ) -> UpdateFirewallRuleResponse: diff --git a/localstack/aws/api/transcribe/__init__.py b/localstack/aws/api/transcribe/__init__.py index 1bdd14855a88f..c9ca944809aeb 100644 --- a/localstack/aws/api/transcribe/__init__.py +++ b/localstack/aws/api/transcribe/__init__.py @@ -52,6 +52,10 @@ class CLMLanguageCode(str): ja_JP = "ja-JP" +class CallAnalyticsFeature(str): + GENERATIVE_SUMMARIZATION = "GENERATIVE_SUMMARIZATION" + + class CallAnalyticsJobStatus(str): QUEUED = "QUEUED" IN_PROGRESS = "IN_PROGRESS" @@ -59,6 +63,11 @@ class CallAnalyticsJobStatus(str): COMPLETED = "COMPLETED" +class CallAnalyticsSkippedReasonCode(str): + INSUFFICIENT_CONVERSATION_CONTENT = "INSUFFICIENT_CONVERSATION_CONTENT" + FAILED_SAFETY_GUIDELINES = "FAILED_SAFETY_GUIDELINES" + + class InputType(str): REAL_TIME = "REAL_TIME" POST_CALL = "POST_CALL" @@ -382,9 +391,23 @@ class Media(TypedDict, total=False): RedactedMediaFileUri: Optional[Uri] +class CallAnalyticsSkippedFeature(TypedDict, total=False): + Feature: Optional[CallAnalyticsFeature] + ReasonCode: Optional[CallAnalyticsSkippedReasonCode] + Message: Optional[String] + + +CallAnalyticsSkippedFeatureList = List[CallAnalyticsSkippedFeature] + + +class CallAnalyticsJobDetails(TypedDict, total=False): + Skipped: Optional[CallAnalyticsSkippedFeatureList] + + class CallAnalyticsJob(TypedDict, total=False): CallAnalyticsJobName: Optional[CallAnalyticsJobName] CallAnalyticsJobStatus: Optional[CallAnalyticsJobStatus] + CallAnalyticsJobDetails: Optional[CallAnalyticsJobDetails] LanguageCode: Optional[LanguageCode] MediaSampleRateHertz: Optional[MediaSampleRateHertz] MediaFormat: Optional[MediaFormat] @@ -407,6 +430,7 @@ class CallAnalyticsJobSummary(TypedDict, total=False): CompletionTime: Optional[DateTime] LanguageCode: Optional[LanguageCode] CallAnalyticsJobStatus: Optional[CallAnalyticsJobStatus] + CallAnalyticsJobDetails: Optional[CallAnalyticsJobDetails] FailureReason: Optional[FailureReason] diff --git a/localstack/services/route53resolver/provider.py b/localstack/services/route53resolver/provider.py index f4f0926bda23e..79c090e78ffe5 100644 --- a/localstack/services/route53resolver/provider.py +++ b/localstack/services/route53resolver/provider.py @@ -31,6 +31,7 @@ FirewallDomainList, FirewallDomainListMetadata, FirewallDomainName, + FirewallDomainRedirectionAction, FirewallDomains, FirewallDomainUpdateOperation, FirewallFailOpenStatus, @@ -314,6 +315,7 @@ def create_firewall_rule( block_override_domain: BlockOverrideDomain = None, block_override_dns_type: BlockOverrideDnsType = None, block_override_ttl: BlockOverrideTtl = None, + firewall_domain_redirection_action: FirewallDomainRedirectionAction = None, qtype: Qtype = None, **kwargs, ) -> CreateFirewallRuleResponse: @@ -332,6 +334,8 @@ def create_firewall_rule( CreatorRequestId=creator_request_id, CreationTime=datetime.now(timezone.utc).isoformat(), ModificationTime=datetime.now(timezone.utc).isoformat(), + FirewallDomainRedirectionAction=firewall_domain_redirection_action, + Qtype=qtype, ) if store.firewall_rules.get(firewall_rule_group_id): store.firewall_rules[firewall_rule_group_id][firewall_domain_list_id] = firewall_rule @@ -393,6 +397,7 @@ def update_firewall_rule( block_override_dns_type: BlockOverrideDnsType = None, block_override_ttl: BlockOverrideTtl = None, name: Name = None, + firewall_domain_redirection_action: FirewallDomainRedirectionAction = None, qtype: Qtype = None, **kwargs, ) -> UpdateFirewallRuleResponse: @@ -416,6 +421,10 @@ def update_firewall_rule( firewall_rule["BlockOverrideTtl"] = block_override_ttl if name: firewall_rule["Name"] = name + if firewall_domain_redirection_action: + firewall_rule["FirewallDomainRedirectionAction"] = firewall_domain_redirection_action + if qtype: + firewall_rule["Qtype"] = qtype return UpdateFirewallRuleResponse( FirewallRule=firewall_rule, ) diff --git a/pyproject.toml b/pyproject.toml index 0a057be469f45..246449ec8cf67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,9 +48,9 @@ Issues = "https://github.com/localstack/localstack/issues" # minimal required to actually run localstack on the host for services natively implemented in python base-runtime = [ # pinned / updated by ASF update action - "boto3==1.34.93", + "boto3==1.34.98", # pinned / updated by ASF update action - "botocore==1.34.93", + "botocore==1.34.98", "awscrt>=0.13.14", "cbor2>=5.2.0", "dnspython>=1.16.0", @@ -76,7 +76,7 @@ base-runtime = [ runtime = [ "localstack-core[base-runtime]", # pinned / updated by ASF update action - "awscli==1.32.93", + "awscli==1.32.98", "airspeed-ext>=0.6.3", "amazon_kclpy>=2.0.6,!=2.1.0,!=2.1.4", # antlr4-python3-runtime: exact pin because antlr4 runtime is tightly coupled to the generated parser code @@ -133,7 +133,7 @@ typehint = [ # typehint is an optional extension of the dev dependencies "localstack-core[dev]", # pinned / updated by ASF update action - "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.93", + "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.98", ] [tool.setuptools] diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index 162819220a94f..7850a034b7b64 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -14,9 +14,9 @@ blinker==1.8.1 # via # flask # quart -boto3==1.34.93 +boto3==1.34.98 # via localstack-core (pyproject.toml) -botocore==1.34.93 +botocore==1.34.98 # via # boto3 # localstack-core (pyproject.toml) diff --git a/requirements-dev.txt b/requirements-dev.txt index dd200dfffb765..9cc0e643ba1ef 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.87.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.93 +awscli==1.32.98 # via localstack-core awscrt==0.20.9 # via localstack-core @@ -55,12 +55,12 @@ blinker==1.8.1 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.93 +boto3==1.34.98 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.93 +botocore==1.34.98 # via # aws-xray-sdk # awscli diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 84d07813733b6..6fdbffb48abc8 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -33,7 +33,7 @@ aws-sam-translator==1.87.0 # localstack-core (pyproject.toml) aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.93 +awscli==1.32.98 # via localstack-core (pyproject.toml) awscrt==0.20.9 # via localstack-core @@ -43,12 +43,12 @@ blinker==1.8.1 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.93 +boto3==1.34.98 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.93 +botocore==1.34.98 # via # aws-xray-sdk # awscli diff --git a/requirements-test.txt b/requirements-test.txt index c3d3bd4946a54..92a07069b2070 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.87.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.93 +awscli==1.32.98 # via localstack-core awscrt==0.20.9 # via localstack-core @@ -55,12 +55,12 @@ blinker==1.8.1 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.93 +boto3==1.34.98 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.93 +botocore==1.34.98 # via # aws-xray-sdk # awscli diff --git a/requirements-typehint.txt b/requirements-typehint.txt index f42f7d48feaf7..493acc613a122 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.87.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.93 +awscli==1.32.98 # via localstack-core awscrt==0.20.9 # via localstack-core @@ -55,14 +55,14 @@ blinker==1.8.1 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.93 +boto3==1.34.98 # via # aws-sam-translator # localstack-core # moto-ext -boto3-stubs==1.34.93 +boto3-stubs==1.34.98 # via localstack-core (pyproject.toml) -botocore==1.34.93 +botocore==1.34.98 # via # aws-xray-sdk # awscli From 532897c8c89d0472e69fbaaea3ff85c791b990c4 Mon Sep 17 00:00:00 2001 From: Max <max.hoheiser@gmail.com> Date: Mon, 6 May 2024 09:57:00 +0200 Subject: [PATCH 114/169] Fix: Firehose: Drop keys in destinations description not in respective return types (#10758) --- localstack/services/firehose/provider.py | 47 ++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/localstack/services/firehose/provider.py b/localstack/services/firehose/provider.py index 37308c6919f83..062b8e23c7ab0 100644 --- a/localstack/services/firehose/provider.py +++ b/localstack/services/firehose/provider.py @@ -18,6 +18,7 @@ AmazonOpenSearchServerlessDestinationConfiguration, AmazonOpenSearchServerlessDestinationUpdate, AmazonopensearchserviceDestinationConfiguration, + AmazonopensearchserviceDestinationDescription, AmazonopensearchserviceDestinationUpdate, BooleanObject, CreateDeliveryStreamOutput, @@ -34,6 +35,7 @@ DestinationDescriptionList, DestinationId, ElasticsearchDestinationConfiguration, + ElasticsearchDestinationDescription, ElasticsearchDestinationUpdate, ElasticsearchS3BackupMode, ExtendedS3DestinationConfiguration, @@ -96,6 +98,7 @@ s3_bucket_name, ) from localstack.utils.aws.client_types import ServicePrincipal +from localstack.utils.collections import select_from_typed_dict from localstack.utils.common import ( TIMESTAMP_FORMAT_MICROS, first_char_to_lower, @@ -195,6 +198,45 @@ def get_search_db_connection(endpoint: str, region_name: str): return OpenSearch(hosts=[endpoint], verify_certs=verify_certs, use_ssl=use_ssl) +def _drop_keys_in_destination_descriptions_not_in_output_types(destinations: list) -> list[dict]: + """For supported destinations, drops the keys in the description not defined in the respective destination description return type""" + for destination in destinations: + if amazon_open_search_service_destination_description := destination.get( + "AmazonopensearchserviceDestinationDescription" + ): + destination["AmazonopensearchserviceDestinationDescription"] = select_from_typed_dict( + AmazonopensearchserviceDestinationDescription, + amazon_open_search_service_destination_description, + filter=True, + ) + if elasticsearch_destination_description := destination.get( + "ElasticsearchDestinationDescription" + ): + destination["ElasticsearchDestinationDescription"] = select_from_typed_dict( + ElasticsearchDestinationDescription, + elasticsearch_destination_description, + filter=True, + ) + if http_endpoint_destination_description := destination.get( + "HttpEndpointDestinationDescription" + ): + destination["HttpEndpointDestinationDescription"] = select_from_typed_dict( + HttpEndpointDestinationConfiguration, + http_endpoint_destination_description, + filter=True, + ) + if redshift_destination_description := destination.get("RedshiftDestinationDescription"): + destination["RedshiftDestinationDescription"] = select_from_typed_dict( + RedshiftDestinationDescription, redshift_destination_description, filter=True + ) + if s3_destination_description := destination.get("S3DestinationDescription"): + destination["S3DestinationDescription"] = select_from_typed_dict( + S3DestinationDescription, s3_destination_description, filter=True + ) + + return destinations + + class FirehoseProvider(FirehoseApi): # maps a delivery_stream_arn to its kinesis thread; the arn encodes account id and region kinesis_listeners: dict[str, KinesisProcessorThread] @@ -378,6 +420,11 @@ def describe_delivery_stream( delivery_stream_description = _get_description_or_raise_not_found( context, delivery_stream_name ) + if destinations := delivery_stream_description.get("Destinations"): + delivery_stream_description["Destinations"] = ( + _drop_keys_in_destination_descriptions_not_in_output_types(destinations) + ) + return DescribeDeliveryStreamOutput(DeliveryStreamDescription=delivery_stream_description) def list_delivery_streams( From e2b9afc5082d5504d778fb0040b335dedfd9b163 Mon Sep 17 00:00:00 2001 From: Silvio Vasiljevic <silvio.vasiljevic@gmail.com> Date: Mon, 6 May 2024 14:02:19 +0200 Subject: [PATCH 115/169] Parametrize test selection scripts for usage in dependent repositories (#10757) --- .github/workflows/tests-pro-integration.yml | 15 +++++---- CODEOWNERS | 2 +- localstack/testing/testselection/matching.py | 33 ++++++++++++++----- .../generate_test_selection_from_commits.py | 18 ++++++---- .../generate_test_selection_from_pr.py | 18 ++++++---- 5 files changed, 57 insertions(+), 29 deletions(-) diff --git a/.github/workflows/tests-pro-integration.yml b/.github/workflows/tests-pro-integration.yml index 0a7cdda40b3a1..d2aa12a7117ac 100644 --- a/.github/workflows/tests-pro-integration.yml +++ b/.github/workflows/tests-pro-integration.yml @@ -7,11 +7,11 @@ on: required: false type: boolean default: false - disableTestSelection: - description: 'Disable Test Selection' + enableTestSelection: + description: 'Enable Test Selection' required: false type: boolean - default: false + default: true targetRef: description: 'LocalStack Pro Ref' required: false @@ -23,11 +23,11 @@ on: required: false type: boolean default: false - disableTestSelection: - description: 'Disable Test Selection' + enableTestSelection: + description: 'Enable Test Selection' required: false type: boolean - default: false + default: true targetRef: description: 'LocalStack Pro Ref' required: false @@ -91,7 +91,8 @@ env: CI_JOB_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }} # report to tinybird if executed on master TINYBIRD_PYTEST_ARGS: "${{ github.ref == 'refs/heads/master' && '--report-to-tinybird ' || '' }}" - TESTSELECTION_PYTEST_ARGS: "${{ github.event_name == 'pull_request' && inputs.disableTestSelection && '--path-filter=../localstack/target/testselection/test-selection.txt ' || '' }}" + # enable test selection if not running on master and test selection is not explicitly disabled + TESTSELECTION_PYTEST_ARGS: "${{ github.ref == 'refs/heads/master' && inputs.enableTestSelection != false && '--path-filter=../localstack/target/testselection/test-selection.txt ' || '' }}" jobs: test-pro: diff --git a/CODEOWNERS b/CODEOWNERS index ab2248f0ea357..d8bb2201ccdf5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -92,7 +92,7 @@ /localstack/testing/pytest/container.py @dominikschubert @simonrw # Test Selection -/localstack/testing/testselection @dominikschubert @alexrashed +/localstack/testing/testselection @dominikschubert @alexrashed @silv-io ###################### ### SERVICE OWNERS ### diff --git a/localstack/testing/testselection/matching.py b/localstack/testing/testselection/matching.py index 00e78c2b4af7d..278a25b3d3651 100644 --- a/localstack/testing/testselection/matching.py +++ b/localstack/testing/testselection/matching.py @@ -10,6 +10,12 @@ SENTINEL_NO_TEST = "SENTINEL_NO_TEST" # a line item which signals that we don't default to everything, we just don't want to actually want to run a test => useful to differentiate between empty / nothing SENTINEL_ALL_TESTS = "SENTINEL_ALL_TESTS" # a line item which signals that we don't default to everything, we just don't want to actually want to run a test => useful to differentiate between empty / nothing +DEFAULT_SEARCH_PATTERNS = ( + r"localstack/services/([^/]+)/.+", + r"localstack/aws/api/([^/]+)/__init__\.py", + r"tests/aws/services/([^/]+)/.+", +) + def _map_to_module_name(service_name: str) -> str: """sanitize a service name like we're doing when scaffolding, e.g. lambda => lambda_""" @@ -27,11 +33,13 @@ def _map_to_service_name(module_name: str) -> str: return module_name.replace("_", "-") -def resolve_dependencies(module_name: str, api_dependencies) -> set[str]: +def resolve_dependencies(module_name: str, api_dependencies: dict[str, Iterable[str]]) -> set[str]: """ Resolves dependencies for a given service module name :param module_name: the name of the service to resolve (e.g. lambda_) + :param api_dependencies: dict of API dependencies where each key is the service and its value a list of services it + depends on :return: set of resolved _service names_ that the service depends on (e.g. sts) """ svc_name = _map_to_service_name(module_name) @@ -39,7 +47,7 @@ def resolve_dependencies(module_name: str, api_dependencies) -> set[str]: # TODO: might want to cache that, but for now it shouldn't be too much overhead -def _reverse_dependency_map(dependency_map: dict[str, dict]) -> dict[str, set[str]]: +def _reverse_dependency_map(dependency_map: dict[str, Iterable[str]]) -> dict[str, Iterable[str]]: """ The current API_DEPENDENCIES actually maps the services to their own dependencies. In our case here we need the inverse of this, we need to of which other services this service is a dependency of. @@ -91,13 +99,18 @@ def prefix(prefix: str) -> Matcher: def generic_service_test_matching_rule( - changed_file_path: str, api_dependencies: Optional[dict] = None + changed_file_path: str, + api_dependencies: Optional[dict[str, Iterable[str]]] = None, + search_patterns: Iterable[str] = DEFAULT_SEARCH_PATTERNS, + test_dirs: Iterable[str] = ("tests/aws/services",), ) -> set[str]: """ Generic matching of changes in service files to their tests :param api_dependencies: dict of API dependencies where each key is the service and its value a list of services it depends on :param changed_file_path: the file path of the detected change + :param search_patterns: list of regex patterns to search for in the changed file path + :param test_dirs: list of test directories to match for a changed service :return: list of partial test file path filters for the matching service and all services it depends on """ # TODO: consider API_COMPOSITES @@ -113,11 +126,11 @@ def generic_service_test_matching_rule( for service, optional_dependencies in API_DEPENDENCIES_OPTIONAL.items(): api_dependencies[service].update(optional_dependencies) - match = re.findall("localstack/services/([^/]+)/.+", changed_file_path) - if not match: - match = re.findall(r"localstack/aws/api/([^/]+)/__init__\.py", changed_file_path) - if not match: - match = re.findall(r"tests/aws/services/([^/]+)/.+", changed_file_path) + match = None + for pattern in search_patterns: + match = re.findall(pattern, changed_file_path) + if match: + break if match: changed_service = match[0] @@ -125,7 +138,9 @@ def generic_service_test_matching_rule( service_dependencies = resolve_dependencies(changed_service, api_dependencies) changed_services.extend(service_dependencies) changed_service_module_names = [_map_to_module_name(svc) for svc in changed_services] - return {f"tests/aws/services/{svc}/" for svc in changed_service_module_names} + return { + f"{test_dir}/{svc}/" for test_dir in test_dirs for svc in changed_service_module_names + } return set() diff --git a/localstack/testing/testselection/scripts/generate_test_selection_from_commits.py b/localstack/testing/testselection/scripts/generate_test_selection_from_commits.py index 31e09576274a3..f6779337eb3dc 100644 --- a/localstack/testing/testselection/scripts/generate_test_selection_from_commits.py +++ b/localstack/testing/testselection/scripts/generate_test_selection_from_commits.py @@ -4,16 +4,22 @@ import sys from pathlib import Path +from typing import Iterable from localstack.testing.testselection.git import find_merge_base, get_changed_files_from_git_diff +from localstack.testing.testselection.matching import MatchingRule from localstack.testing.testselection.opt_in import complies_with_opt_in from localstack.testing.testselection.testselection import get_affected_tests_from_changes -def main(): +def generate_from_commits( + opt_in_rules: Iterable[str] | None = None, + matching_rules: list[MatchingRule] | None = None, + repo_name: str = "localstack", +): if len(sys.argv) != 5: print( - "Usage: python -m localstack.testing.testselection.scripts.generate_test_selection_from_commits <repo_root_path> <base-commit-sha> <head-commit-sha> <output_file_path>", + f"Usage: python -m {repo_name}.testing.testselection.scripts.generate_test_selection_from_commits <repo_root_path> <base-commit-sha> <head-commit-sha> <output_file_path>", file=sys.stderr, ) sys.exit(1) @@ -32,13 +38,13 @@ def main(): ) # opt-in guard, can be removed after initial testing phase print("Checking for confirming to opt-in guards") - if not complies_with_opt_in(changed_files): + if not complies_with_opt_in(changed_files, opt_in_rules=opt_in_rules): print( - "Change outside of opt-in guards. Extend the list at localstack/testing/testselection/opt_in.py" + f"Change outside of opt-in guards. Extend the list at {repo_name}/testing/testselection/opt_in.py" ) test_files = ["SENTINEL_ALL_TESTS"] else: - test_files = get_affected_tests_from_changes(changed_files) + test_files = get_affected_tests_from_changes(changed_files, matching_rules=matching_rules) print(f"Number of changed files detected: {len(changed_files)}") for cf in sorted(changed_files): @@ -63,4 +69,4 @@ def main(): if __name__ == "__main__": - main() + generate_from_commits() diff --git a/localstack/testing/testselection/scripts/generate_test_selection_from_pr.py b/localstack/testing/testselection/scripts/generate_test_selection_from_pr.py index 9a744a51dcc4a..136f50793a73b 100644 --- a/localstack/testing/testselection/scripts/generate_test_selection_from_pr.py +++ b/localstack/testing/testselection/scripts/generate_test_selection_from_pr.py @@ -5,17 +5,23 @@ import os import sys from pathlib import Path +from typing import Iterable from localstack.testing.testselection.git import find_merge_base, get_changed_files_from_git_diff from localstack.testing.testselection.github import get_pr_details_from_url +from localstack.testing.testselection.matching import MatchingRule from localstack.testing.testselection.opt_in import complies_with_opt_in from localstack.testing.testselection.testselection import get_affected_tests_from_changes -def main(): +def generate_from_pr( + opt_in_rules: Iterable[str] | None = None, + matching_rules: list[MatchingRule] | None = None, + repo_name: str = "localstack", +): if len(sys.argv) != 4: print( - "Usage: python -m localstack.testing.testselection.scripts.generate_test_selection_from_pr <git-root-dir> <pull-request-url> <output-file-path>", + f"Usage: python -m {repo_name}.testing.testselection.scripts.generate_test_selection_from_pr <git-root-dir> <pull-request-url> <output-file-path>", file=sys.stderr, ) sys.exit(1) @@ -37,13 +43,13 @@ def main(): ) # opt-in guard, can be removed after initial testing phase print("Checking for confirming to opt-in guards") - if not complies_with_opt_in(changed_files): + if not complies_with_opt_in(changed_files, opt_in_rules=opt_in_rules): print( - "Change outside of opt-in guards. Extend the list at localstack/testing/testselection/opt_in.py" + f"Change outside of opt-in guards. Extend the list at {repo_name}/testing/testselection/opt_in.py" ) test_files = ["SENTINEL_ALL_TESTS"] else: - test_files = get_affected_tests_from_changes(changed_files) + test_files = get_affected_tests_from_changes(changed_files, matching_rules=matching_rules) print(f"Number of changed files detected: {len(changed_files)}") for cf in sorted(changed_files): @@ -68,4 +74,4 @@ def main(): if __name__ == "__main__": - main() + generate_from_pr() From 95daed61b5a705d8b4c8e32653f2a2786a5bc740 Mon Sep 17 00:00:00 2001 From: Macwan Nevil <macnev2013@gmail.com> Date: Mon, 6 May 2024 19:14:37 +0530 Subject: [PATCH 116/169] fixed physical resource id for AWS::Glue::SchemaVersionMetadata (#10770) --- localstack/services/cloudformation/engine/quirks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/localstack/services/cloudformation/engine/quirks.py b/localstack/services/cloudformation/engine/quirks.py index f1bf1a7747c9a..46ae29e779777 100644 --- a/localstack/services/cloudformation/engine/quirks.py +++ b/localstack/services/cloudformation/engine/quirks.py @@ -30,6 +30,7 @@ "AWS::Logs::SubscriptionFilter": "/properties/LogGroupName", "AWS::SSM::Parameter": "/properties/Name", "AWS::RDS::DBProxyTargetGroup": "/properties/TargetGroupName", + "AWS::Glue::SchemaVersionMetadata": "</properties/SchemaVersionId>|</properties/Key>|</properties/Value>", # composite } # You can usually find the available GetAtt targets in the official resource documentation: From 5f0be1c9d5518e5c47afbe32f68e4fd39a3a4e50 Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Mon, 6 May 2024 20:40:09 +0200 Subject: [PATCH 117/169] StepFunctions: Fix Flaky Stop Execution Test (#10771) --- tests/aws/services/stepfunctions/utils.py | 8 ++++---- .../services/stepfunctions/v2/test_sfn_api.py | 17 +++++++++++++++++ .../stepfunctions/v2/test_sfn_api.snapshot.json | 2 +- .../v2/test_sfn_api.validation.json | 2 +- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/tests/aws/services/stepfunctions/utils.py b/tests/aws/services/stepfunctions/utils.py index d39f616c45bf7..6999f7e831eee 100644 --- a/tests/aws/services/stepfunctions/utils.py +++ b/tests/aws/services/stepfunctions/utils.py @@ -120,7 +120,7 @@ def await_state_machine_version_listed( ) -def _await_on_execution_events( +def await_on_execution_events( stepfunctions_client, execution_arn: str, check_func: Callable[[HistoryEventList], bool] ) -> None: def _run_check(): @@ -148,7 +148,7 @@ def _check_last_is_success(events: HistoryEventList) -> bool: return "executionSucceededEventDetails" in last_event return False - _await_on_execution_events( + await_on_execution_events( stepfunctions_client=stepfunctions_client, execution_arn=execution_arn, check_func=_check_last_is_success, @@ -190,7 +190,7 @@ def _check_last_is_terminal(events: HistoryEventList) -> bool: } return False - _await_on_execution_events( + await_on_execution_events( stepfunctions_client=stepfunctions_client, execution_arn=execution_arn, check_func=_check_last_is_terminal, @@ -221,7 +221,7 @@ def _check_stated_exists(events: HistoryEventList) -> bool: return "executionStartedEventDetails" in event return False - _await_on_execution_events( + await_on_execution_events( stepfunctions_client=stepfunctions_client, execution_arn=execution_arn, check_func=_check_stated_exists, diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api.py b/tests/aws/services/stepfunctions/v2/test_sfn_api.py index fd696d1bb42cc..3384ff235295c 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api.py +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api.py @@ -6,6 +6,7 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer from localstack.aws.api.lambda_ import Runtime +from localstack.aws.api.stepfunctions import HistoryEventList from localstack.testing.pytest import markers from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.lambda_functions import lambda_functions @@ -16,6 +17,7 @@ await_execution_success, await_execution_terminated, await_list_execution_status, + await_on_execution_events, await_state_machine_listed, await_state_machine_not_listed, ) @@ -491,6 +493,21 @@ def test_stop_execution( sfn_snapshot.match("exec_resp", exec_resp) execution_arn = exec_resp["executionArn"] + def _check_stated_entered(events: HistoryEventList) -> bool: + # Check the evaluation entered the wait state, called State_1. + for event in events: + event_details = event.get("stateEnteredEventDetails") + if event_details: + return event_details.get("name") == "State_1" + return False + + # Wait until the state machine enters the wait state. + await_on_execution_events( + stepfunctions_client=aws_client.stepfunctions, + execution_arn=execution_arn, + check_func=_check_stated_entered, + ) + await_execution_started( stepfunctions_client=aws_client.stepfunctions, execution_arn=execution_arn ) diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api.snapshot.json b/tests/aws/services/stepfunctions/v2/test_sfn_api.snapshot.json index 10c0db34669b4..48d104ca65939 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api.snapshot.json @@ -469,7 +469,7 @@ } }, "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_stop_execution": { - "recorded-date": "22-06-2023, 13:53:24", + "recorded-date": "06-05-2024, 12:59:34", "recorded-content": { "creation_resp": { "creationDate": "datetime", diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api.validation.json b/tests/aws/services/stepfunctions/v2/test_sfn_api.validation.json index 586bd546dacdd..ce1c6f417d464 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api.validation.json +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api.validation.json @@ -93,6 +93,6 @@ "last_validated_date": "2024-03-14T21:59:25+00:00" }, "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_stop_execution": { - "last_validated_date": "2023-06-22T11:53:24+00:00" + "last_validated_date": "2024-05-06T12:59:34+00:00" } } From 7298dfde0c14becd9b2f7dadcebaa2547b6aa651 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 May 2024 07:31:55 +0200 Subject: [PATCH 118/169] Bump cla-assistant/github-action from 2.3.2 to 2.4.0 (#10780) --- .github/workflows/pr-cla.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-cla.yml b/.github/workflows/pr-cla.yml index f9c3d2a10a53f..45df72952673f 100644 --- a/.github/workflows/pr-cla.yml +++ b/.github/workflows/pr-cla.yml @@ -16,7 +16,7 @@ jobs: steps: - name: "CLA Assistant" if: "(github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'" - uses: "cla-assistant/github-action@v2.3.2" + uses: "cla-assistant/github-action@v2.4.0" env: GITHUB_TOKEN: "${{ secrets.PRO_ACCESS_TOKEN }}" PERSONAL_ACCESS_TOKEN: "${{ secrets.PRO_ACCESS_TOKEN }}" From 0f7d49fd27179395a536827088231fc093f7ca72 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Tue, 7 May 2024 09:21:21 +0200 Subject: [PATCH 119/169] Upgrade pinned Python dependencies (#10781) --- .pre-commit-config.yaml | 2 +- requirements-base-runtime.txt | 10 ++++----- requirements-basic.txt | 4 ++-- requirements-dev.txt | 28 ++++++++++++------------ requirements-runtime.txt | 22 +++++++++---------- requirements-test.txt | 28 ++++++++++++------------ requirements-typehint.txt | 40 +++++++++++++++++------------------ 7 files changed, 67 insertions(+), 67 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9db53d18ac46e..b58e0898028a5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.4.2 + rev: v0.4.3 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index 7850a034b7b64..c1326e2c65ba9 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -10,7 +10,7 @@ attrs==23.2.0 # via localstack-twisted awscrt==0.20.9 # via localstack-core (pyproject.toml) -blinker==1.8.1 +blinker==1.8.2 # via # flask # quart @@ -40,7 +40,7 @@ click==8.1.7 # quart constantly==23.10.4 # via localstack-twisted -cryptography==42.0.5 +cryptography==42.0.7 # via # localstack-core (pyproject.toml) # pyopenssl @@ -85,7 +85,7 @@ itsdangerous==2.2.0 # via # flask # quart -jinja2==3.1.3 +jinja2==3.1.4 # via # flask # quart @@ -124,7 +124,7 @@ psutil==5.9.8 # via localstack-core (pyproject.toml) pycparser==2.22 # via cffi -pygments==2.17.2 +pygments==2.18.0 # via rich pyopenssl==24.1.0 # via @@ -180,7 +180,7 @@ urllib3==2.2.1 # requests websocket-client==1.8.0 # via docker -werkzeug==3.0.2 +werkzeug==3.0.3 # via # flask # localstack-core (pyproject.toml) diff --git a/requirements-basic.txt b/requirements-basic.txt index 05edf7f7d6c53..9e6d84eb302f7 100644 --- a/requirements-basic.txt +++ b/requirements-basic.txt @@ -16,7 +16,7 @@ charset-normalizer==3.3.2 # via requests click==8.1.7 # via localstack-core (pyproject.toml) -cryptography==42.0.5 +cryptography==42.0.7 # via localstack-core (pyproject.toml) dill==0.3.6 # via localstack-core (pyproject.toml) @@ -40,7 +40,7 @@ psutil==5.9.8 # via localstack-core (pyproject.toml) pycparser==2.22 # via cffi -pygments==2.17.2 +pygments==2.18.0 # via rich pyproject-hooks==1.1.0 # via build diff --git a/requirements-dev.txt b/requirements-dev.txt index 9cc0e643ba1ef..fa1ec8208ad07 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -37,9 +37,9 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.139.1 +aws-cdk-lib==2.140.0 # via localstack-core -aws-sam-translator==1.87.0 +aws-sam-translator==1.88.0 # via # cfn-lint # localstack-core @@ -49,7 +49,7 @@ awscli==1.32.98 # via localstack-core awscrt==0.20.9 # via localstack-core -blinker==1.8.1 +blinker==1.8.2 # via # flask # quart @@ -92,7 +92,7 @@ cffi==1.16.0 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==0.86.4 +cfn-lint==0.87.1 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -102,7 +102,7 @@ click==8.1.7 # localstack-core # localstack-core (pyproject.toml) # quart -colorama==0.4.4 +colorama==0.4.6 # via awscli constantly==23.10.4 # via localstack-twisted @@ -116,7 +116,7 @@ coveralls==4.0.0 # via localstack-core (pyproject.toml) crontab==1.0.1 # via localstack-core -cryptography==42.0.5 +cryptography==42.0.7 # via # joserfc # localstack-core @@ -205,7 +205,7 @@ itsdangerous==2.2.0 # via # flask # quart -jinja2==3.1.3 +jinja2==3.1.4 # via # flask # moto-ext @@ -220,7 +220,7 @@ jpype1==1.5.0 # via localstack-core jschema-to-python==1.2.3 # via cfn-lint -jsii==1.97.0 +jsii==1.98.0 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-kubectl-v20 @@ -246,7 +246,7 @@ jsonpickle==3.0.4 # via jschema-to-python jsonpointer==2.4 # via jsonpatch -jsonschema==4.21.1 +jsonschema==4.22.0 # via # aws-sam-translator # cfn-lint @@ -357,9 +357,9 @@ pydantic==2.7.1 # via aws-sam-translator pydantic-core==2.18.2 # via pydantic -pygments==2.17.2 +pygments==2.18.0 # via rich -pymongo==4.7.0 +pymongo==4.7.1 # via localstack-core pyopenssl==24.1.0 # via @@ -441,7 +441,7 @@ rich==13.7.1 # localstack-core (pyproject.toml) rolo==0.5.0 # via localstack-core -rpds-py==0.18.0 +rpds-py==0.18.1 # via # jsonschema # referencing @@ -449,7 +449,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core (pyproject.toml) -ruff==0.4.2 +ruff==0.4.3 # via localstack-core (pyproject.toml) s3transfer==0.10.1 # via @@ -515,7 +515,7 @@ websocket-client==1.8.0 # via # docker # localstack-core -werkzeug==3.0.2 +werkzeug==3.0.3 # via # flask # localstack-core diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 6fdbffb48abc8..1859af4951eae 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -27,7 +27,7 @@ attrs==23.2.0 # localstack-twisted # referencing # sarif-om -aws-sam-translator==1.87.0 +aws-sam-translator==1.88.0 # via # cfn-lint # localstack-core (pyproject.toml) @@ -37,7 +37,7 @@ awscli==1.32.98 # via localstack-core (pyproject.toml) awscrt==0.20.9 # via localstack-core -blinker==1.8.1 +blinker==1.8.2 # via # flask # quart @@ -73,7 +73,7 @@ certifi==2024.2.2 # requests cffi==1.16.0 # via cryptography -cfn-lint==0.86.4 +cfn-lint==0.87.1 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -83,13 +83,13 @@ click==8.1.7 # localstack-core # localstack-core (pyproject.toml) # quart -colorama==0.4.4 +colorama==0.4.6 # via awscli constantly==23.10.4 # via localstack-twisted crontab==1.0.1 # via localstack-core (pyproject.toml) -cryptography==42.0.5 +cryptography==42.0.7 # via # joserfc # localstack-core @@ -152,7 +152,7 @@ itsdangerous==2.2.0 # via # flask # quart -jinja2==3.1.3 +jinja2==3.1.4 # via # flask # moto-ext @@ -185,7 +185,7 @@ jsonpickle==3.0.4 # via jschema-to-python jsonpointer==2.4 # via jsonpatch -jsonschema==4.21.1 +jsonschema==4.22.0 # via # aws-sam-translator # cfn-lint @@ -265,9 +265,9 @@ pydantic==2.7.1 # via aws-sam-translator pydantic-core==2.18.2 # via pydantic -pygments==2.17.2 +pygments==2.18.0 # via rich -pymongo==4.7.0 +pymongo==4.7.1 # via localstack-core (pyproject.toml) pyopenssl==24.1.0 # via @@ -330,7 +330,7 @@ rich==13.7.1 # localstack-core (pyproject.toml) rolo==0.5.0 # via localstack-core -rpds-py==0.18.0 +rpds-py==0.18.1 # via # jsonschema # referencing @@ -383,7 +383,7 @@ urllib3==2.2.1 # responses websocket-client==1.8.0 # via docker -werkzeug==3.0.2 +werkzeug==3.0.3 # via # flask # localstack-core diff --git a/requirements-test.txt b/requirements-test.txt index 92a07069b2070..64250ce1a7494 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -37,9 +37,9 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.139.1 +aws-cdk-lib==2.140.0 # via localstack-core (pyproject.toml) -aws-sam-translator==1.87.0 +aws-sam-translator==1.88.0 # via # cfn-lint # localstack-core @@ -49,7 +49,7 @@ awscli==1.32.98 # via localstack-core awscrt==0.20.9 # via localstack-core -blinker==1.8.1 +blinker==1.8.2 # via # flask # quart @@ -90,7 +90,7 @@ certifi==2024.2.2 # requests cffi==1.16.0 # via cryptography -cfn-lint==0.86.4 +cfn-lint==0.87.1 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -100,17 +100,17 @@ click==8.1.7 # localstack-core # localstack-core (pyproject.toml) # quart -colorama==0.4.4 +colorama==0.4.6 # via awscli constantly==23.10.4 # via localstack-twisted constructs==10.3.0 # via aws-cdk-lib -coverage==7.5.0 +coverage==7.5.1 # via localstack-core (pyproject.toml) crontab==1.0.1 # via localstack-core -cryptography==42.0.5 +cryptography==42.0.7 # via # joserfc # localstack-core @@ -189,7 +189,7 @@ itsdangerous==2.2.0 # via # flask # quart -jinja2==3.1.3 +jinja2==3.1.4 # via # flask # moto-ext @@ -204,7 +204,7 @@ jpype1==1.5.0 # via localstack-core jschema-to-python==1.2.3 # via cfn-lint -jsii==1.97.0 +jsii==1.98.0 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-kubectl-v20 @@ -230,7 +230,7 @@ jsonpickle==3.0.4 # via jschema-to-python jsonpointer==2.4 # via jsonpatch -jsonschema==4.21.1 +jsonschema==4.22.0 # via # aws-sam-translator # cfn-lint @@ -328,9 +328,9 @@ pydantic==2.7.1 # via aws-sam-translator pydantic-core==2.18.2 # via pydantic -pygments==2.17.2 +pygments==2.18.0 # via rich -pymongo==4.7.0 +pymongo==4.7.1 # via localstack-core pyopenssl==24.1.0 # via @@ -408,7 +408,7 @@ rich==13.7.1 # localstack-core (pyproject.toml) rolo==0.5.0 # via localstack-core -rpds-py==0.18.0 +rpds-py==0.18.1 # via # jsonschema # referencing @@ -476,7 +476,7 @@ websocket-client==1.8.0 # via # docker # localstack-core (pyproject.toml) -werkzeug==3.0.2 +werkzeug==3.0.3 # via # flask # localstack-core diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 493acc613a122..70a136ba336c1 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -37,9 +37,9 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.139.1 +aws-cdk-lib==2.140.0 # via localstack-core -aws-sam-translator==1.87.0 +aws-sam-translator==1.88.0 # via # cfn-lint # localstack-core @@ -49,7 +49,7 @@ awscli==1.32.98 # via localstack-core awscrt==0.20.9 # via localstack-core -blinker==1.8.1 +blinker==1.8.2 # via # flask # quart @@ -96,7 +96,7 @@ cffi==1.16.0 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==0.86.4 +cfn-lint==0.87.1 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -106,7 +106,7 @@ click==8.1.7 # localstack-core # localstack-core (pyproject.toml) # quart -colorama==0.4.4 +colorama==0.4.6 # via awscli constantly==23.10.4 # via localstack-twisted @@ -120,7 +120,7 @@ coveralls==4.0.0 # via localstack-core crontab==1.0.1 # via localstack-core -cryptography==42.0.5 +cryptography==42.0.7 # via # joserfc # localstack-core @@ -209,7 +209,7 @@ itsdangerous==2.2.0 # via # flask # quart -jinja2==3.1.3 +jinja2==3.1.4 # via # flask # moto-ext @@ -224,7 +224,7 @@ jpype1==1.5.0 # via localstack-core jschema-to-python==1.2.3 # via cfn-lint -jsii==1.97.0 +jsii==1.98.0 # via # aws-cdk-asset-awscli-v1 # aws-cdk-asset-kubectl-v20 @@ -250,7 +250,7 @@ jsonpickle==3.0.4 # via jschema-to-python jsonpointer==2.4 # via jsonpatch -jsonschema==4.21.1 +jsonschema==4.22.0 # via # aws-sam-translator # cfn-lint @@ -333,11 +333,11 @@ mypy-boto3-dms==1.34.0 # via boto3-stubs mypy-boto3-docdb==1.34.77 # via boto3-stubs -mypy-boto3-dynamodb==1.34.91 +mypy-boto3-dynamodb==1.34.97 # via boto3-stubs mypy-boto3-dynamodbstreams==1.34.0 # via boto3-stubs -mypy-boto3-ec2==1.34.91 +mypy-boto3-ec2==1.34.97 # via boto3-stubs mypy-boto3-ecr==1.34.0 # via boto3-stubs @@ -409,7 +409,7 @@ mypy-boto3-mwaa==1.34.57 # via boto3-stubs mypy-boto3-neptune==1.34.0 # via boto3-stubs -mypy-boto3-opensearch==1.34.43 +mypy-boto3-opensearch==1.34.95 # via boto3-stubs mypy-boto3-organizations==1.34.56 # via boto3-stubs @@ -435,13 +435,13 @@ mypy-boto3-resourcegroupstaggingapi==1.34.0 # via boto3-stubs mypy-boto3-route53==1.34.31 # via boto3-stubs -mypy-boto3-route53resolver==1.34.15 +mypy-boto3-route53resolver==1.34.95 # via boto3-stubs mypy-boto3-s3==1.34.91 # via boto3-stubs mypy-boto3-s3control==1.34.83 # via boto3-stubs -mypy-boto3-sagemaker==1.34.89 +mypy-boto3-sagemaker==1.34.98 # via boto3-stubs mypy-boto3-sagemaker-runtime==1.34.0 # via boto3-stubs @@ -453,7 +453,7 @@ mypy-boto3-servicediscovery==1.34.89 # via boto3-stubs mypy-boto3-ses==1.34.0 # via boto3-stubs -mypy-boto3-sesv2==1.34.56 +mypy-boto3-sesv2==1.34.98 # via boto3-stubs mypy-boto3-sns==1.34.44 # via boto3-stubs @@ -553,9 +553,9 @@ pydantic==2.7.1 # via aws-sam-translator pydantic-core==2.18.2 # via pydantic -pygments==2.17.2 +pygments==2.18.0 # via rich -pymongo==4.7.0 +pymongo==4.7.1 # via localstack-core pyopenssl==24.1.0 # via @@ -637,7 +637,7 @@ rich==13.7.1 # localstack-core (pyproject.toml) rolo==0.5.0 # via localstack-core -rpds-py==0.18.0 +rpds-py==0.18.1 # via # jsonschema # referencing @@ -645,7 +645,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core -ruff==0.4.2 +ruff==0.4.3 # via localstack-core s3transfer==0.10.1 # via @@ -812,7 +812,7 @@ websocket-client==1.8.0 # via # docker # localstack-core -werkzeug==3.0.2 +werkzeug==3.0.3 # via # flask # localstack-core From 21b57abcc5aefe47e8e50448d92602b36a12a862 Mon Sep 17 00:00:00 2001 From: Max <max.hoheiser@gmail.com> Date: Tue, 7 May 2024 14:43:04 +0200 Subject: [PATCH 120/169] Feat: Eventbridge v2: Add pattern matching (#10664) --- localstack/services/events/event_ruler.py | 3 - localstack/services/events/provider_v2.py | 136 +- localstack/services/events/rule.py | 6 +- localstack/services/events/target.py | 6 +- .../services/events/test_event_patterns.py | 49 +- .../events/test_event_patterns.snapshot.json | 19 + .../test_event_patterns.validation.json | 3 + tests/aws/services/events/test_events.py | 463 +++-- .../services/events/test_events.snapshot.json | 1494 +++++++++-------- .../events/test_events.validation.json | 110 +- .../events/test_events_integrations.py | 36 +- .../test_events_integrations.snapshot.json | 44 + .../test_events_integrations.validation.json | 4 +- .../aws/services/events/test_events_rules.py | 24 +- 14 files changed, 1301 insertions(+), 1096 deletions(-) diff --git a/localstack/services/events/event_ruler.py b/localstack/services/events/event_ruler.py index a9210689566cd..0a7ef73eaf78c 100644 --- a/localstack/services/events/event_ruler.py +++ b/localstack/services/events/event_ruler.py @@ -3,7 +3,6 @@ from functools import cache from pathlib import Path -from localstack import config from localstack.services.events.packages import event_ruler_package from localstack.services.events.utils import InvalidEventPatternException from localstack.utils.objects import singleton_factory @@ -43,8 +42,6 @@ def matches_rule(event: str, rule: str) -> bool: There is a single static boolean method Ruler.matchesRule(event, rule) - both arguments are provided as JSON strings. """ - if config.EVENT_RULE_ENGINE != "java": - raise NotImplementedError("Set EVENT_RULE_ENGINE=java to enable the Java Event Ruler.") start_jvm() import jpype.imports # noqa F401: required for importing Java modules diff --git a/localstack/services/events/provider_v2.py b/localstack/services/events/provider_v2.py index c82497e2d5323..2c8544b09ee0e 100644 --- a/localstack/services/events/provider_v2.py +++ b/localstack/services/events/provider_v2.py @@ -1,5 +1,7 @@ import base64 +import json import logging +from datetime import datetime, timezone from typing import Optional from localstack.aws.api import RequestContext, handler @@ -16,14 +18,18 @@ EventPattern, EventsApi, EventSourceName, + InvalidEventPatternException, LimitMax100, ListEventBusesResponse, ListRuleNamesByTargetResponse, ListRulesResponse, ListTargetsByRuleResponse, NextToken, + PutEventsRequestEntry, PutEventsRequestEntryList, PutEventsResponse, + PutEventsResultEntry, + PutEventsResultEntryList, PutPartnerEventsRequestEntryList, PutPartnerEventsResponse, PutRuleResponse, @@ -43,10 +49,12 @@ TargetId, TargetIdList, TargetList, + TestEventPatternResponse, ) from localstack.aws.api.events import EventBus as ApiTypeEventBus from localstack.aws.api.events import Rule as ApiTypeRule from localstack.services.events.event_bus import EventBusService, EventBusServiceDict +from localstack.services.events.event_ruler import matches_rule from localstack.services.events.models_v2 import ( EventBus, EventBusDict, @@ -59,7 +67,11 @@ ) from localstack.services.events.rule import RuleService, RuleServiceDict from localstack.services.events.target import TargetSender, TargetSenderDict, TargetSenderFactory +from localstack.services.events.utils import ( + InvalidEventPatternException as InternalInvalidEventPatternException, +) from localstack.services.plugins import ServiceLifecycleHook +from localstack.utils.strings import long_uid LOG = logging.getLogger(__name__) @@ -79,6 +91,57 @@ def get_filtered_dict(name_prefix: str, input_dict: dict) -> dict: return {name: value for name, value in input_dict.items() if name.startswith(name_prefix)} +def get_event_time(event: PutEventsRequestEntry) -> str: + event_time = datetime.now(timezone.utc) + if event_timestamp := event.get("Time"): + try: + # use time from event if provided + event_time = event_timestamp.replace(tzinfo=timezone.utc) + except ValueError: + # use current time if event time is invalid + LOG.debug( + "Could not parse the `Time` parameter, falling back to current time for the following Event: '%s'", + event, + ) + formatted_time_string = event_time.strftime("%Y-%m-%dT%H:%M:%SZ") + return formatted_time_string + + +def validate_event(event: PutEventsRequestEntry) -> None | PutEventsResultEntry: + if not event.get("Source"): + return { + "ErrorCode": "InvalidArgument", + "ErrorMessage": "Parameter Source is not valid. Reason: Source is a required argument.", + } + elif not event.get("DetailType"): + return { + "ErrorCode": "InvalidArgument", + "ErrorMessage": "Parameter DetailType is not valid. Reason: DetailType is a required argument.", + } + elif not event.get("Detail"): + return { + "ErrorCode": "InvalidArgument", + "ErrorMessage": "Parameter Detail is not valid. Reason: Detail is a required argument.", + } + + +def format_event(event: PutEventsRequestEntry, region: str, account_id: str) -> dict: + # See https://docs.aws.amazon.com/AmazonS3/latest/userguide/ev-events.html + formatted_event = { + "version": "0", + "id": str(long_uid()), + "detail-type": event.get("DetailType"), + "source": event.get("Source"), + "account": account_id, + "time": get_event_time(event), + "region": region, + "resources": event.get("Resources", []), + "detail": json.loads(event.get("Detail", "{}")), + } + + return formatted_event + + class EventsProvider(EventsApi, ServiceLifecycleHook): # api methods are grouped by resource type and sorted in hierarchical order # each group is sorted alphabetically @@ -303,6 +366,20 @@ def put_rule( response = PutRuleResponse(RuleArn=rule_service.arn) return response + @handler("TestEventPattern") + def test_event_pattern( + self, context: RequestContext, event_pattern: EventPattern, event: str, **kwargs + ) -> TestEventPatternResponse: + """Test event pattern uses EventBridge event pattern matching: + https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html + """ + try: + result = matches_rule(event, event_pattern) + except InternalInvalidEventPatternException as e: + raise InvalidEventPatternException(e.message) from e + + return TestEventPatternResponse(Result=result) + ######### # Targets ######### @@ -345,7 +422,7 @@ def put_targets( rule_service = self.get_rule_service(context, rule, event_bus_name) failed_entries = rule_service.add_targets(targets) rule_arn = rule_service.arn - for target in targets: + for target in targets: # TODO only add successful targets self.create_target_sender(target, region, account_id, rule_arn) response = PutTargetsResponse( @@ -384,9 +461,12 @@ def put_events( endpoint_id: EndpointId = None, **kwargs, ) -> PutEventsResponse: - failed_entries = self._put_entries(context, entries) + entries, failed_entry_count = self._process_entries(context, entries) - response = PutEventsResponse(FailedEntryCount=len(failed_entries), Entries=failed_entries) + response = PutEventsResponse( + Entries=entries, + FailedEntryCount=failed_entry_count, + ) return response @handler("PutPartnerEvents") @@ -578,25 +658,41 @@ def _delete_target_sender(self, ids: TargetIdList, rule) -> None: except KeyError: LOG.error(f"Error deleting target service {target_arn}.") - def _put_entries(self, context: RequestContext, entries: PutEventsRequestEntryList) -> list: - failed_entries = [] + def _process_entries( + self, context: RequestContext, entries: PutEventsRequestEntryList + ) -> tuple[PutEventsResultEntryList, int]: + processed_entries = [] + failed_entry_count = 0 for event in entries: event_bus_name = event.get("EventBusName", "default") + if event_failed_validation := validate_event(event): + processed_entries.append(event_failed_validation) + failed_entry_count += 1 + continue + event = format_event(event, context.region, context.account_id) store = self.get_store(context) - event_bus = self.get_event_bus(event_bus_name, store) - # TODO add pattern matching + try: + event_bus = self.get_event_bus(event_bus_name, store) + except ResourceNotFoundException: + # ignore events for non-existing event buses but add processed event + processed_entries.append({"EventId": event["id"]}) + continue matching_rules = [rule for rule in event_bus.rules.values()] for rule in matching_rules: - for target in rule.targets.values(): - target_sender = self._target_sender_store[target["Arn"]] - try: - target_sender.send_event(event) - except Exception as error: - failed_entries.append( - { - "Entry": event, - "ErrorCode": "InternalException", - "ErrorMessage": str(error), - } - ) - return failed_entries + event_pattern = rule.event_pattern + event_str = json.dumps(event) + if matches_rule(event_str, event_pattern): + for target in rule.targets.values(): + target_sender = self._target_sender_store[target["Arn"]] + try: + target_sender.send_event(event) + processed_entries.append({"EventId": event["id"]}) + except Exception as error: + processed_entries.append( + { + "ErrorCode": "InternalException", + "ErrorMessage": str(error), + } + ) + failed_entry_count += 1 + return processed_entries, failed_entry_count diff --git a/localstack/services/events/rule.py b/localstack/services/events/rule.py index aad9d985b09a5..61011ccec6286 100644 --- a/localstack/services/events/rule.py +++ b/localstack/services/events/rule.py @@ -19,7 +19,11 @@ TargetIdList, TargetList, ) -from localstack.services.events.models_v2 import Rule, TargetDict, ValidationException +from localstack.services.events.models_v2 import ( + Rule, + TargetDict, + ValidationException, +) TARGET_ID_REGEX = re.compile(r"^[\.\-_A-Za-z0-9]+$") TARGET_ARN_REGEX = re.compile(r"arn:[\d\w:\-/]*") diff --git a/localstack/services/events/target.py b/localstack/services/events/target.py index bb2d423bacaf8..6a22f4e08d059 100644 --- a/localstack/services/events/target.py +++ b/localstack/services/events/target.py @@ -7,6 +7,7 @@ from localstack.aws.api.events import ( Arn, + PutEventsRequestEntry, Target, ) from localstack.aws.connect import connect_to @@ -53,7 +54,7 @@ def client(self): return self._client @abstractmethod - def send_event(self): + def send_event(self, event: PutEventsRequestEntry): pass def _validate_input(self, target: Target): @@ -83,6 +84,8 @@ def _initialize_client(self) -> BaseClient: TargetSenderDict = dict[Arn, TargetSender] +# Target Senders are ordered alphabetically by service name + class ApiGatewayTargetSender(TargetSender): def send_event(self, event): @@ -174,6 +177,7 @@ def send_event(self, event): def _validate_input(self, target: Target): super()._validate_input(target) + # TODO add validated test to check if RoleArn is mandatory if not collections.get_safe(target, "$.RoleArn"): raise ValueError("RoleArn is required for Kinesis target") if not collections.get_safe(target, "$.KinesisParameters.PartitionKeyPath"): diff --git a/tests/aws/services/events/test_event_patterns.py b/tests/aws/services/events/test_event_patterns.py index 7c31c3731bee2..a739af6e9c5f4 100644 --- a/tests/aws/services/events/test_event_patterns.py +++ b/tests/aws/services/events/test_event_patterns.py @@ -1,5 +1,6 @@ import json import os +from datetime import datetime from pathlib import Path from typing import List, Tuple @@ -8,7 +9,6 @@ from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers -from tests.aws.services.events.helper_functions import is_v2_provider THIS_FOLDER: str = os.path.dirname(os.path.realpath(__file__)) REQUEST_TEMPLATE_DIR = os.path.join(THIS_FOLDER, "event_pattern_templates") @@ -80,7 +80,6 @@ def list_files_with_suffix(directory_path: str, suffix: str) -> List[str]: # TODO: extend these test cases based on the open source docs + tests: https://github.com/aws/event-ruler # For example, "JSON Array Matching", "And and Or Relationship among fields with Ruler", rule validation, # and exception handling. -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") @pytest.mark.parametrize( "request_template,label", request_template_tuples, ids=[t[1] for t in request_template_tuples] ) @@ -118,7 +117,6 @@ def test_test_event_pattern(aws_client, snapshot, request_template, label): assert response["Result"] -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") @markers.aws.validated def test_test_event_pattern_with_multi_key(aws_client): """Test the special case of a duplicate JSON key separately because it requires working around the @@ -140,7 +138,6 @@ def test_test_event_pattern_with_multi_key(aws_client): assert response["Result"] -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") @markers.aws.validated def test_test_event_pattern_with_escape_characters(aws_client): r"""Test the special case of using escape characters separately because it requires working around JSON escaping. @@ -159,3 +156,47 @@ def test_test_event_pattern_with_escape_characters(aws_client): EventPattern=event_pattern, ) assert response["Result"] + + +@markers.aws.validated +def test_event_pattern_source(aws_client, snapshot, account_id, region_name): + response = aws_client.events.test_event_pattern( + Event=json.dumps( + { + "id": "1", + "source": "order", + "detail-type": "Test", + "account": account_id, + "region": region_name, + "time": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), + } + ), + EventPattern=json.dumps( + { + "source": ["order"], + "detail-type": ["Test"], + } + ), + ) + snapshot.match("eventbridge-test-event-pattern-response", response) + + # negative test, source is not matched + response = aws_client.events.test_event_pattern( + Event=json.dumps( + { + "id": "1", + "source": "order", + "detail-type": "Test", + "account": account_id, + "region": region_name, + "time": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), + } + ), + EventPattern=json.dumps( + { + "source": ["shipment"], + "detail-type": ["Test"], + } + ), + ) + snapshot.match("eventbridge-test-event-pattern-response-no-match", response) diff --git a/tests/aws/services/events/test_event_patterns.snapshot.json b/tests/aws/services/events/test_event_patterns.snapshot.json index 1f50547e900b6..39ed013c1b17e 100644 --- a/tests/aws/services/events/test_event_patterns.snapshot.json +++ b/tests/aws/services/events/test_event_patterns.snapshot.json @@ -430,5 +430,24 @@ "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[exists_dynamodb_NEG]": { "recorded-date": "09-04-2024, 16:51:59", "recorded-content": {} + }, + "tests/aws/services/events/test_event_patterns.py::test_event_pattern_source": { + "recorded-date": "29-04-2024, 14:12:14", + "recorded-content": { + "eventbridge-test-event-pattern-response": { + "Result": true, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "eventbridge-test-event-pattern-response-no-match": { + "Result": false, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/events/test_event_patterns.validation.json b/tests/aws/services/events/test_event_patterns.validation.json index ef63c055e6d26..d4bc0e6bd2f69 100644 --- a/tests/aws/services/events/test_event_patterns.validation.json +++ b/tests/aws/services/events/test_event_patterns.validation.json @@ -1,4 +1,7 @@ { + "tests/aws/services/events/test_event_patterns.py::test_event_pattern_source": { + "last_validated_date": "2024-04-29T14:12:14+00:00" + }, "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays]": { "last_validated_date": "2024-04-08T19:33:55+00:00" }, diff --git a/tests/aws/services/events/test_events.py b/tests/aws/services/events/test_events.py index 0dc10211621a9..6b3c8ee9ec244 100644 --- a/tests/aws/services/events/test_events.py +++ b/tests/aws/services/events/test_events.py @@ -7,7 +7,6 @@ import os import time import uuid -from datetime import datetime import pytest from botocore.exceptions import ClientError @@ -26,10 +25,6 @@ from tests.aws.services.events.conftest import assert_valid_event, sqs_collect_messages from tests.aws.services.events.helper_functions import is_v2_provider -THIS_FOLDER = os.path.dirname(os.path.realpath(__file__)) - -TEST_EVENT_BUS_NAME = "command-bus-dev" - EVENT_DETAIL = {"command": "update-account", "payload": {"acc_id": "0a787ecb-4015", "sf_id": "baz"}} TEST_EVENT_PATTERN = { @@ -38,6 +33,16 @@ "detail": {"command": ["update-account"]}, } +TEST_EVENT_PATTERN_NO_DETAIL = { + "source": ["core.update-account-command"], + "detail-type": ["core.update-account-command"], +} + +TEST_EVENT_PATTERN_NO_SOURCE = { + "detail-type": ["core.update-account-command"], + "detail": {"command": ["update-account"]}, +} + API_DESTINATION_AUTHS = [ { "type": "BASIC", @@ -76,6 +81,81 @@ class TestEvents: + @markers.aws.validated + @pytest.mark.skipif( + not is_v2_provider(), + reason="V1 provider does not support this feature", + ) + def test_put_events_without_source(self, snapshot, aws_client): + entries = [ + { + "DetailType": TEST_EVENT_PATTERN_NO_SOURCE["detail-type"][0], + "Detail": json.dumps(EVENT_DETAIL), + }, + ] + response = aws_client.events.put_events(Entries=entries) + snapshot.match("put-events", response) + + @markers.aws.unknown + @pytest.mark.skipif( + not is_v2_provider(), + reason="V1 provider does not support this feature", + ) + def test_put_event_without_detail(self, snapshot, aws_client): + entries = [ + { + "Source": TEST_EVENT_PATTERN_NO_DETAIL["source"][0], + "DetailType": TEST_EVENT_PATTERN_NO_DETAIL["detail-type"][0], + }, + ] + response = aws_client.events.put_events(Entries=entries) + snapshot.match("put-events", response) + + @markers.aws.validated + def test_put_events_time(self, put_events_with_filter_to_sqs, snapshot): + entries1 = [ + { + "Source": TEST_EVENT_PATTERN_NO_DETAIL["source"][0], + "DetailType": TEST_EVENT_PATTERN_NO_DETAIL["detail-type"][0], + "Detail": json.dumps({"message": "short time"}), + "Time": "2022-01-01", + }, + ] + entries2 = [ + { + "Source": TEST_EVENT_PATTERN_NO_DETAIL["source"][0], + "DetailType": TEST_EVENT_PATTERN_NO_DETAIL["detail-type"][0], + "Detail": json.dumps({"message": "new time"}), + "Time": "01-01-2022T00:00:00Z", + }, + ] + entries3 = [ + { + "Source": TEST_EVENT_PATTERN_NO_DETAIL["source"][0], + "DetailType": TEST_EVENT_PATTERN_NO_DETAIL["detail-type"][0], + "Detail": json.dumps({"message": "long time"}), + "Time": "2022-01-01 00:00:00Z", + }, + ] + entries_asserts = [(entries1, True), (entries2, True), (entries3, True)] + messages = put_events_with_filter_to_sqs( + pattern=TEST_EVENT_PATTERN_NO_DETAIL, + entries_asserts=entries_asserts, + ) + + snapshot.add_transformer( + [ + snapshot.transform.key_value("MD5OfBody"), + snapshot.transform.key_value("ReceiptHandle"), + ] + ) + snapshot.match("messages", messages) + + # check for correct time strings in the messages + for message in messages: + message_body = json.loads(message["Body"]) + assert message_body["time"] == "2022-01-01T00:00:00Z" + @markers.aws.unknown @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_events_written_to_disk_are_timestamp_prefixed_for_chronological_ordering( @@ -138,70 +218,6 @@ def test_list_tags_for_resource(self, aws_client, clean_up): # clean up clean_up(rule_name=rule_name) - @markers.aws.unknown - @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") - def test_put_events_with_values_in_array(self, put_events_with_filter_to_sqs): - pattern = {"detail": {"event": {"data": {"type": ["1", "2"]}}}} - entries1 = [ - { - "Source": "test", - "DetailType": "test", - "Detail": json.dumps({"event": {"data": {"type": ["3", "1"]}}}), - } - ] - entries2 = [ - { - "Source": "test", - "DetailType": "test", - "Detail": json.dumps({"event": {"data": {"type": ["2"]}}}), - } - ] - entries3 = [ - { - "Source": "test", - "DetailType": "test", - "Detail": json.dumps({"event": {"data": {"type": ["3"]}}}), - } - ] - entries_asserts = [(entries1, True), (entries2, True), (entries3, False)] - put_events_with_filter_to_sqs( - pattern=pattern, - entries_asserts=entries_asserts, - input_path="$.detail", - ) - - @markers.aws.validated - @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") - def test_put_events_with_nested_event_pattern(self, put_events_with_filter_to_sqs): - pattern = {"detail": {"event": {"data": {"type": ["1"]}}}} - entries1 = [ - { - "Source": "test", - "DetailType": "test", - "Detail": json.dumps({"event": {"data": {"type": "1"}}}), - } - ] - entries2 = [ - { - "Source": "test", - "DetailType": "test", - "Detail": json.dumps({"event": {"data": {"type": "2"}}}), - } - ] - entries3 = [ - { - "Source": "test", - "DetailType": "test", - "Detail": json.dumps({"hello": "world"}), - } - ] - entries_asserts = [(entries1, True), (entries2, False), (entries3, False)] - put_events_with_filter_to_sqs( - pattern=pattern, - entries_asserts=entries_asserts, - input_path="$.detail", - ) - @markers.aws.unknown @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_scheduled_expression_events( @@ -526,185 +542,6 @@ def test_create_connection_validations(self, aws_client): assert "must have length less than or equal to 64" in message assert "must satisfy enum value set: [BASIC, OAUTH_CLIENT_CREDENTIALS, API_KEY]" in message - @markers.aws.unknown - @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") - def test_put_event_without_source(self, aws_client_factory): - events_client = aws_client_factory(region_name="eu-west-1").events - - response = events_client.put_events(Entries=[{"DetailType": "Test", "Detail": "{}"}]) - assert response.get("Entries") - - @markers.aws.unknown - @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") - def test_put_event_without_detail(self, aws_client_factory): - events_client = aws_client_factory(region_name="eu-west-1").events - - response = events_client.put_events( - Entries=[ - { - "DetailType": "Test", - } - ] - ) - assert response.get("Entries") - - @markers.aws.validated - @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") - def test_put_target_id_validation( - self, sqs_create_queue, sqs_get_queue_arn, events_put_rule, snapshot, aws_client - ): - rule_name = f"rule-{short_uid()}" - queue_url = sqs_create_queue() - queue_arn = sqs_get_queue_arn(queue_url) - - events_put_rule( - Name=rule_name, EventPattern=json.dumps(TEST_EVENT_PATTERN), State="ENABLED" - ) - - target_id = "!@#$@!#$" - with pytest.raises(ClientError) as e: - aws_client.events.put_targets( - Rule=rule_name, - Targets=[ - {"Id": target_id, "Arn": queue_arn, "InputPath": "$.detail"}, - ], - ) - snapshot.add_transformer(snapshot.transform.regex(target_id, "invalid-target-id")) - snapshot.match("put-targets-invalid-id-error", e.value.response) - - target_id = f"{long_uid()}-{long_uid()}-extra" - with pytest.raises(ClientError) as e: - aws_client.events.put_targets( - Rule=rule_name, - Targets=[ - {"Id": target_id, "Arn": queue_arn, "InputPath": "$.detail"}, - ], - ) - snapshot.add_transformer(snapshot.transform.regex(target_id, "second-invalid-target-id")) - snapshot.match("put-targets-length-error", e.value.response) - - target_id = f"test-With_valid.Characters-{short_uid()}" - aws_client.events.put_targets( - Rule=rule_name, - Targets=[ - {"Id": target_id, "Arn": queue_arn, "InputPath": "$.detail"}, - ], - ) - - @markers.aws.validated - @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") - def test_event_pattern(self, aws_client, snapshot, account_id, region_name): - response = aws_client.events.test_event_pattern( - Event=json.dumps( - { - "id": "1", - "source": "order", - "detail-type": "Test", - "account": account_id, - "region": region_name, - "time": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), - } - ), - EventPattern=json.dumps( - { - "source": ["order"], - "detail-type": ["Test"], - } - ), - ) - snapshot.match("eventbridge-test-event-pattern-response", response) - - # negative test, source is not matched - response = aws_client.events.test_event_pattern( - Event=json.dumps( - { - "id": "1", - "source": "order", - "detail-type": "Test", - "account": account_id, - "region": region_name, - "time": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), - } - ), - EventPattern=json.dumps( - { - "source": ["shipment"], - "detail-type": ["Test"], - } - ), - ) - snapshot.match("eventbridge-test-event-pattern-response-no-match", response) - - @markers.aws.validated - @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") - def test_put_events_time( - self, - aws_client, - sqs_create_queue, - sqs_get_queue_arn, - events_put_rule, - snapshot, - ): - default_bus_rule_name = f"rule-{short_uid()}" - default_bus_target_id = f"test-target-default-b-{short_uid()}" - - snapshot.add_transformer( - [ - snapshot.transform.key_value("MD5OfBody"), # the event contains a timestamp - snapshot.transform.key_value("ReceiptHandle"), - ] - ) - - queue_url = sqs_create_queue() - queue_arn = sqs_get_queue_arn(queue_url) - - rule_on_default_bus = events_put_rule( - Name=default_bus_rule_name, - EventPattern=json.dumps({"detail-type": ["CustomType"], "source": ["MySource"]}), - State="ENABLED", - ) - - allow_event_rule_to_sqs_queue( - aws_client=aws_client, - event_rule_arn=rule_on_default_bus["RuleArn"], - sqs_queue_arn=queue_arn, - sqs_queue_url=queue_url, - ) - - aws_client.events.put_targets( - Rule=default_bus_rule_name, - Targets=[{"Id": default_bus_target_id, "Arn": queue_arn}], - ) - - # create an entry with a defined time - entries = [ - { - "Source": "MySource", - "DetailType": "CustomType", - "Detail": json.dumps({"message": "for the default event bus"}), - "Time": datetime(year=2022, day=1, month=1), - } - ] - response = aws_client.events.put_events(Entries=entries) - snapshot.match("put-events", response) - - def _get_sqs_messages(): - resp = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=1 - ) - msgs = resp.get("Messages") - assert len(msgs) == 1 - aws_client.sqs.delete_message( - QueueUrl=queue_url, ReceiptHandle=msgs[0]["ReceiptHandle"] - ) - return msgs - - messages = retry(_get_sqs_messages, retries=5, sleep=0.1) - snapshot.match("get-events", messages) - - message_body = json.loads(messages[0]["Body"]) - assert message_body["time"] == "2022-01-01T00:00:00Z" - class TestEventBus: @markers.aws.validated @@ -814,10 +651,9 @@ def test_list_event_buses_with_limit(self, create_event_bus, aws_client, snapsho ) snapshot.match("list-event-buses-limit-next-token", response) - @markers.aws.unknown + @markers.aws.needs_fixing # TODO use fixture setup_sqs_queue_as_event_target to simplify @pytest.mark.skipif(is_aws_cloud(), reason="not validated") @pytest.mark.parametrize("strategy", ["standard", "domain", "path"]) - @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_into_event_bus( self, monkeypatch, @@ -885,7 +721,6 @@ def test_put_events_into_event_bus( aws_client.sqs.delete_queue(QueueUrl=queue_url) @markers.aws.validated - @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") # TODO simplify and use sqs as target def test_put_events_to_default_eventbus_for_custom_eventbus( self, @@ -1019,7 +854,6 @@ def test_put_events_to_default_eventbus_for_custom_eventbus( assert_valid_event(received_event) @markers.aws.validated # TODO fix condition for this test, only succeeds if run on its own - @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_nonexistent_event_bus( self, aws_client, @@ -1302,6 +1136,88 @@ def test_update_rule_with_targets( snapshot.match("list-targets-after-update", response) +class TestEventPattern: + @markers.aws.validated + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") + def test_put_events_pattern_with_values_in_array(self, put_events_with_filter_to_sqs, snapshot): + pattern = {"detail": {"event": {"data": {"type": ["1", "2"]}}}} + entries1 = [ + { + "Source": "test", + "DetailType": "test", + "Detail": json.dumps({"event": {"data": {"type": ["3", "1"]}}}), + } + ] + entries2 = [ + { + "Source": "test", + "DetailType": "test", + "Detail": json.dumps({"event": {"data": {"type": ["2"]}}}), + } + ] + entries3 = [ + { + "Source": "test", + "DetailType": "test", + "Detail": json.dumps({"event": {"data": {"type": ["3"]}}}), + } + ] + entries_asserts = [(entries1, True), (entries2, True), (entries3, False)] + messages = put_events_with_filter_to_sqs( + pattern=pattern, + entries_asserts=entries_asserts, + input_path="$.detail", + ) + + snapshot.add_transformers_list( + [ + snapshot.transform.key_value("MD5OfBody"), + snapshot.transform.key_value("ReceiptHandle"), + ] + ) + snapshot.match("messages", messages) + + @markers.aws.validated + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") + def test_put_events_pattern_nested(self, put_events_with_filter_to_sqs, snapshot): + pattern = {"detail": {"event": {"data": {"type": ["1"]}}}} + entries1 = [ + { + "Source": "test", + "DetailType": "test", + "Detail": json.dumps({"event": {"data": {"type": "1"}}}), + } + ] + entries2 = [ + { + "Source": "test", + "DetailType": "test", + "Detail": json.dumps({"event": {"data": {"type": "2"}}}), + } + ] + entries3 = [ + { + "Source": "test", + "DetailType": "test", + "Detail": json.dumps({"hello": "world"}), + } + ] + entries_asserts = [(entries1, True), (entries2, False), (entries3, False)] + messages = put_events_with_filter_to_sqs( + pattern=pattern, + entries_asserts=entries_asserts, + input_path="$.detail", + ) + + snapshot.add_transformers_list( + [ + snapshot.transform.key_value("MD5OfBody"), + snapshot.transform.key_value("ReceiptHandle"), + ] + ) + snapshot.match("messages", messages) + + class TestEventTarget: @markers.aws.validated @pytest.mark.parametrize("bus_name", ["custom", "default"]) @@ -1411,3 +1327,46 @@ def test_list_target_by_rule_limit( Rule=rule_name, NextToken=response["NextToken"] ) snapshot.match("list-targets-limit-next-token", response) + + @markers.aws.validated + @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") + def test_put_target_id_validation( + self, sqs_create_queue, sqs_get_queue_arn, events_put_rule, snapshot, aws_client + ): + rule_name = f"rule-{short_uid()}" + queue_url = sqs_create_queue() + queue_arn = sqs_get_queue_arn(queue_url) + + events_put_rule( + Name=rule_name, EventPattern=json.dumps(TEST_EVENT_PATTERN), State="ENABLED" + ) + + target_id = "!@#$@!#$" + with pytest.raises(ClientError) as e: + aws_client.events.put_targets( + Rule=rule_name, + Targets=[ + {"Id": target_id, "Arn": queue_arn, "InputPath": "$.detail"}, + ], + ) + snapshot.add_transformer(snapshot.transform.regex(target_id, "invalid-target-id")) + snapshot.match("put-targets-invalid-id-error", e.value.response) + + target_id = f"{long_uid()}-{long_uid()}-extra" + with pytest.raises(ClientError) as e: + aws_client.events.put_targets( + Rule=rule_name, + Targets=[ + {"Id": target_id, "Arn": queue_arn, "InputPath": "$.detail"}, + ], + ) + snapshot.add_transformer(snapshot.transform.regex(target_id, "second-invalid-target-id")) + snapshot.match("put-targets-length-error", e.value.response) + + target_id = f"test-With_valid.Characters-{short_uid()}" + aws_client.events.put_targets( + Rule=rule_name, + Targets=[ + {"Id": target_id, "Arn": queue_arn, "InputPath": "$.detail"}, + ], + ) diff --git a/tests/aws/services/events/test_events.snapshot.json b/tests/aws/services/events/test_events.snapshot.json index 8c113ce47c615..424fa08196021 100644 --- a/tests/aws/services/events/test_events.snapshot.json +++ b/tests/aws/services/events/test_events.snapshot.json @@ -1,41 +1,15 @@ { - "tests/aws/services/events/test_events.py::TestEvents::test_put_target_id_validation": { - "recorded-date": "18-04-2024, 15:47:35", + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_without_source": { + "recorded-date": "29-04-2024, 13:15:31", "recorded-content": { - "put-targets-invalid-id-error": { - "Error": { - "Code": "ValidationException", - "Message": "1 validation error detected: Value '!@#$@!#$' at 'targets.1.member.id' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\.\\-_A-Za-z0-9]+" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - }, - "put-targets-length-error": { - "Error": { - "Code": "ValidationException", - "Message": "1 validation error detected: Value 'second-invalid-target-id' at 'targets.1.member.id' failed to satisfy constraint: Member must have length less than or equal to 64" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } - }, - "tests/aws/services/events/test_events.py::TestEvents::test_event_pattern": { - "recorded-date": "26-03-2024, 14:07:19", - "recorded-content": { - "eventbridge-test-event-pattern-response": { - "Result": true, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "eventbridge-test-event-pattern-response-no-match": { - "Result": false, + "put-events": { + "Entries": [ + { + "ErrorCode": "InvalidArgument", + "ErrorMessage": "Parameter Source is not valid. Reason: Source is a required argument." + } + ], + "FailedEntryCount": 1, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -43,60 +17,123 @@ } } }, - "tests/aws/services/events/test_events.py::TestEvents::test_put_events_time": { - "recorded-date": "26-03-2024, 14:09:45", + "tests/aws/services/events/test_events.py::TestEvents::test_put_event_without_detail": { + "recorded-date": "29-04-2024, 13:15:31", "recorded-content": { "put-events": { "Entries": [ { - "EventId": "<uuid:1>" + "ErrorCode": "InvalidArgument", + "ErrorMessage": "Parameter Detail is not valid. Reason: Detail is a required argument." } ], - "FailedEntryCount": 0, + "FailedEntryCount": 1, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } - }, - "get-events": [ + } + } + }, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_time": { + "recorded-date": "29-04-2024, 13:15:34", + "recorded-content": { + "messages": [ { - "MessageId": "<uuid:2>", + "MessageId": "<uuid:1>", "ReceiptHandle": "<receipt-handle:1>", "MD5OfBody": "<m-d5-of-body:1>", "Body": { "version": "0", - "id": "<uuid:1>", - "detail-type": "CustomType", - "source": "MySource", + "id": "<uuid:2>", + "detail-type": "core.update-account-command", + "source": "core.update-account-command", "account": "111111111111", "time": "date", "region": "<region>", "resources": [], "detail": { - "message": "for the default event bus" + "message": "short time" + } + } + }, + { + "MessageId": "<uuid:3>", + "ReceiptHandle": "<receipt-handle:2>", + "MD5OfBody": "<m-d5-of-body:2>", + "Body": { + "version": "0", + "id": "<uuid:4>", + "detail-type": "core.update-account-command", + "source": "core.update-account-command", + "account": "111111111111", + "time": "date", + "region": "<region>", + "resources": [], + "detail": { + "message": "new time" + } + } + }, + { + "MessageId": "<uuid:5>", + "ReceiptHandle": "<receipt-handle:3>", + "MD5OfBody": "<m-d5-of-body:3>", + "Body": { + "version": "0", + "id": "<uuid:6>", + "detail-type": "core.update-account-command", + "source": "core.update-account-command", + "account": "111111111111", + "time": "date", + "region": "<region>", + "resources": [], + "detail": { + "message": "long time" } } } ] } }, - "tests/aws/services/events/test_events.py::TestEventRule::test_put_rule[custom]": { - "recorded-date": "04-04-2024, 10:47:20", + "tests/aws/services/events/test_events.py::TestEventBus::test_create_list_describe_delete_custom_event_buses[regions0]": { + "recorded-date": "29-04-2024, 13:15:44", "recorded-content": { - "put-rule": { - "RuleArn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name>", + "create-custom-event-bus-us-east-1": { + "EventBusArn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } - } - } - }, - "tests/aws/services/events/test_events.py::TestEventRule::test_put_rule[default]": { - "recorded-date": "04-04-2024, 10:47:21", - "recorded-content": { - "put-rule": { - "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + }, + "list-event-buses-after-create-us-east-1": { + "EventBuses": [ + { + "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", + "Name": "<bus-name>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "describe-custom-event-bus-us-east-1": { + "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", + "Name": "<bus-name>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "delete-custom-event-bus-us-east-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-event-buses-after-delete-us-east-1": { + "EventBuses": [], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -104,36 +141,21 @@ } } }, - "tests/aws/services/events/test_events.py::TestEventRule::test_put_list_with_prefix_describe_delete_rule[custom]": { - "recorded-date": "22-04-2024, 13:07:35", + "tests/aws/services/events/test_events.py::TestEventBus::test_create_list_describe_delete_custom_event_buses[regions1]": { + "recorded-date": "29-04-2024, 13:15:47", "recorded-content": { - "put-rule": { - "RuleArn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name>", + "create-custom-event-bus-us-east-1": { + "EventBusArn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "list-rules": { - "Rules": [ + "list-event-buses-after-create-us-east-1": { + "EventBuses": [ { - "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name>", - "EventBusName": "<bus-name>", - "EventPattern": { - "source": [ - "core.update-account-command" - ], - "detail-type": [ - "core.update-account-command" - ], - "detail": { - "command": [ - "update-account" - ] - } - }, - "Name": "<rule-name>", - "State": "ENABLED" + "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", + "Name": "<bus-name>" } ], "ResponseMetadata": { @@ -141,75 +163,53 @@ "HTTPStatusCode": 200 } }, - "describe-rule": { - "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name>", - "CreatedBy": "111111111111", - "EventBusName": "<bus-name>", - "EventPattern": { - "source": [ - "core.update-account-command" - ], - "detail-type": [ - "core.update-account-command" - ], - "detail": { - "command": [ - "update-account" - ] - } - }, - "Name": "<rule-name>", - "State": "ENABLED", + "describe-custom-event-bus-us-east-1": { + "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", + "Name": "<bus-name>", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "delete-rule": { + "create-custom-event-bus-us-west-1": { + "EventBusArn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "list-rules-after-delete": { - "Rules": [], + "list-event-buses-after-create-us-west-1": { + "EventBuses": [ + { + "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", + "Name": "<bus-name>" + } + ], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } - } - } - }, - "tests/aws/services/events/test_events.py::TestEventRule::test_put_list_with_prefix_describe_delete_rule[default]": { - "recorded-date": "22-04-2024, 13:07:36", - "recorded-content": { - "put-rule": { - "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + }, + "describe-custom-event-bus-us-west-1": { + "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", + "Name": "<bus-name>", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "list-rules": { - "Rules": [ + "create-custom-event-bus-eu-central-1": { + "EventBusArn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-event-buses-after-create-eu-central-1": { + "EventBuses": [ { - "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", - "EventBusName": "default", - "EventPattern": { - "source": [ - "core.update-account-command" - ], - "detail-type": [ - "core.update-account-command" - ], - "detail": { - "command": [ - "update-account" - ] - } - }, - "Name": "<rule-name>", - "State": "ENABLED" + "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", + "Name": "<bus-name>" } ], "ResponseMetadata": { @@ -217,82 +217,94 @@ "HTTPStatusCode": 200 } }, - "describe-rule": { - "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", - "CreatedBy": "111111111111", - "EventBusName": "default", - "EventPattern": { - "source": [ - "core.update-account-command" - ], - "detail-type": [ - "core.update-account-command" - ], - "detail": { - "command": [ - "update-account" - ] - } - }, - "Name": "<rule-name>", - "State": "ENABLED", + "describe-custom-event-bus-eu-central-1": { + "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", + "Name": "<bus-name>", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "delete-rule": { + "delete-custom-event-bus-us-east-1": { "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "list-rules-after-delete": { - "Rules": [], + "list-event-buses-after-delete-us-east-1": { + "EventBuses": [], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } - } - } - }, - "tests/aws/services/events/test_events.py::TestEventRule::test_put_multiple_rules_with_same_name": { - "recorded-date": "22-04-2024, 13:07:38", - "recorded-content": { - "put-rule": { - "RuleArn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name>", + }, + "delete-custom-event-bus-us-west-1": { "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "re-put-rule": { - "RuleArn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name>", + "list-event-buses-after-delete-us-west-1": { + "EventBuses": [], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "list-rules": { - "Rules": [ + "delete-custom-event-bus-eu-central-1": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-event-buses-after-delete-eu-central-1": { + "EventBuses": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_create_multiple_event_buses_same_name": { + "recorded-date": "29-04-2024, 13:15:47", + "recorded-content": { + "create-multiple-event-buses-same-name": "<ExceptionInfo ResourceAlreadyExistsException('An error occurred (ResourceAlreadyExistsException) when calling the CreateEventBus operation: Event bus <bus-name> already exists.') tblen=4>" + } + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_describe_delete_not_existing_event_bus": { + "recorded-date": "29-04-2024, 13:15:49", + "recorded-content": { + "describe-not-existing-event-bus-error": "<ExceptionInfo ResourceNotFoundException('An error occurred (ResourceNotFoundException) when calling the DescribeEventBus operation: Event bus <bus-name> does not exist.') tblen=3>", + "delete-not-existing-event-bus": "<ExceptionInfo ResourceNotFoundException('An error occurred (ResourceNotFoundException) when calling the DescribeEventBus operation: Event bus <bus-name> does not exist.') tblen=3>" + } + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_delete_default_event_bus": { + "recorded-date": "29-04-2024, 13:15:49", + "recorded-content": { + "delete-default-event-bus-error": "<ExceptionInfo ClientError('An error occurred (ValidationException) when calling the DeleteEventBus operation: Cannot delete event bus default.') tblen=3>" + } + }, + "tests/aws/services/events/test_events.py::TestEventBus::test_list_event_buses_with_prefix": { + "recorded-date": "29-04-2024, 13:15:50", + "recorded-content": { + "list-event-buses-prefix-complete-name": { + "EventBuses": [ { - "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name>", - "EventBusName": "<bus-name>", - "EventPattern": { - "source": [ - "core.update-account-command" - ], - "detail-type": [ - "core.update-account-command" - ], - "detail": { - "command": [ - "update-account" - ] - } - }, - "Name": "<rule-name>", - "State": "ENABLED" + "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", + "Name": "<bus-name>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-event-buses-prefix": { + "EventBuses": [ + { + "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", + "Name": "<bus-name>" } ], "ResponseMetadata": { @@ -302,133 +314,43 @@ } } }, - "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_with_limit": { - "recorded-date": "22-04-2024, 13:07:40", + "tests/aws/services/events/test_events.py::TestEventBus::test_list_event_buses_with_limit": { + "recorded-date": "29-04-2024, 13:15:52", "recorded-content": { - "list-rules-limit": { - "NextToken": "<next_token:1>", - "Rules": [ + "list-event-buses-limit": { + "EventBuses": [ { - "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name-prefix>-0", - "EventBusName": "<bus-name>", - "EventPattern": { - "source": [ - "core.update-account-command" - ], - "detail-type": [ - "core.update-account-command" - ], - "detail": { - "command": [ - "update-account" - ] - } - }, - "Name": "<rule-name-prefix>-0", - "State": "ENABLED" + "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name-prefix>-0", + "Name": "<bus-name-prefix>-0" }, { - "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name-prefix>-1", - "EventBusName": "<bus-name>", - "EventPattern": { - "source": [ - "core.update-account-command" - ], - "detail-type": [ - "core.update-account-command" - ], - "detail": { - "command": [ - "update-account" - ] - } - }, - "Name": "<rule-name-prefix>-1", - "State": "ENABLED" + "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name-prefix>-1", + "Name": "<bus-name-prefix>-1" }, { - "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name-prefix>-2", - "EventBusName": "<bus-name>", - "EventPattern": { - "source": [ - "core.update-account-command" - ], - "detail-type": [ - "core.update-account-command" - ], - "detail": { - "command": [ - "update-account" - ] - } - }, - "Name": "<rule-name-prefix>-2", - "State": "ENABLED" + "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name-prefix>-2", + "Name": "<bus-name-prefix>-2" } ], + "NextToken": "<next_token:1>", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "list-rules-limit-next-token": { - "Rules": [ + "list-event-buses-limit-next-token": { + "EventBuses": [ { - "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name-prefix>-3", - "EventBusName": "<bus-name>", - "EventPattern": { - "source": [ - "core.update-account-command" - ], - "detail-type": [ - "core.update-account-command" - ], - "detail": { - "command": [ - "update-account" - ] - } - }, - "Name": "<rule-name-prefix>-3", - "State": "ENABLED" + "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name-prefix>-3", + "Name": "<bus-name-prefix>-3" }, { - "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name-prefix>-4", - "EventBusName": "<bus-name>", - "EventPattern": { - "source": [ - "core.update-account-command" - ], - "detail-type": [ - "core.update-account-command" - ], - "detail": { - "command": [ - "update-account" - ] - } - }, - "Name": "<rule-name-prefix>-4", - "State": "ENABLED" + "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name-prefix>-4", + "Name": "<bus-name-prefix>-4" }, { - "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name-prefix>-5", - "EventBusName": "<bus-name>", - "EventPattern": { - "source": [ - "core.update-account-command" - ], - "detail-type": [ - "core.update-account-command" - ], - "detail": { - "command": [ - "update-account" - ] - } - }, - "Name": "<rule-name-prefix>-5", - "State": "ENABLED" + "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name-prefix>-5", + "Name": "<bus-name-prefix>-5" } ], "ResponseMetadata": { @@ -438,52 +360,173 @@ } } }, - "tests/aws/services/events/test_events.py::TestEventRule::test_describe_nonexistent_rule": { - "recorded-date": "22-04-2024, 13:07:42", + "tests/aws/services/events/test_events.py::TestEventBus::test_put_events_to_default_eventbus_for_custom_eventbus": { + "recorded-date": "29-04-2024, 13:16:20", "recorded-content": { - "describe-not-existing-rule-error": "<ExceptionInfo ResourceNotFoundException('An error occurred (ResourceNotFoundException) when calling the DescribeRule operation: Rule <rule-name> does not exist on EventBus default.') tblen=3>" + "create-custom-event-bus": { + "EventBusArn": "arn:aws:events:<region>:111111111111:event-bus/<resource:1>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "create-rule-1": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<resource:2>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "create-rule-2": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<resource:1>/<resource:3>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "put-target-1": { + "FailedEntries": [], + "FailedEntryCount": 0, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "put-target-2": { + "FailedEntries": [], + "FailedEntryCount": 0, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "get-events": { + "Messages": [ + { + "Body": { + "version": "0", + "id": "<uuid:1>", + "detail-type": "Object Created", + "source": "aws.s3", + "account": "111111111111", + "time": "date", + "region": "<region>", + "resources": [ + "arn:aws:s3:::<bucket-name:1>" + ], + "detail": { + "version": "0", + "bucket": { + "name": "<bucket-name:1>" + }, + "object": { + "key": "<key-name:1>", + "size": 4, + "etag": "8d777f385d3dfec8815d20f7496026dc", + "sequencer": "object-sequencer" + }, + "request-id": "request-id", + "requester": "<requester>", + "source-ip-address": "<ip-address:1>", + "reason": "PutObject" + } + }, + "MD5OfBody": "<m-d5-of-body:1>", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:1>" + } + ] + } } }, - "tests/aws/services/events/test_events.py::TestEventRule::test_disable_re_enable_rule[custom]": { - "recorded-date": "22-04-2024, 13:07:43", + "tests/aws/services/events/test_events.py::TestEventBus::test_put_events_nonexistent_event_bus": { + "recorded-date": "29-04-2024, 13:18:57", "recorded-content": { - "disable-rule": { + "put-events": { + "Entries": [ + { + "EventId": "<uuid:1>" + }, + { + "EventId": "<uuid:2>" + } + ], + "FailedEntryCount": 0, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "describe-rule-disabled": { - "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name>", - "CreatedBy": "111111111111", - "EventBusName": "<bus-name>", - "EventPattern": { - "source": [ - "core.update-account-command" - ], - "detail-type": [ - "core.update-account-command" - ], - "detail": { - "command": [ - "update-account" - ] + "get-events": [ + { + "MessageId": "<uuid:3>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": { + "version": "0", + "id": "<uuid:1>", + "detail-type": "CustomType", + "source": "MySource", + "account": "111111111111", + "time": "date", + "region": "<region>", + "resources": [], + "detail": { + "message": "for the default event bus" + } } + } + ], + "non-existent-bus-error": { + "Error": { + "Code": "ResourceNotFoundException", + "Message": "Event bus <custom-event-bus> does not exist." }, - "Name": "<rule-name>", - "State": "DISABLED", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_put_list_with_prefix_describe_delete_rule[custom]": { + "recorded-date": "29-04-2024, 13:16:41", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name>", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "enable-rule": { + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name>", + "EventBusName": "<bus-name>", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "<rule-name>", + "State": "ENABLED" + } + ], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "describe-rule-enabled": { + "describe-rule": { "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name>", "CreatedBy": "111111111111", "EventBusName": "<bus-name>", @@ -506,49 +549,60 @@ "HTTPHeaders": {}, "HTTPStatusCode": 200 } - } - } - }, - "tests/aws/services/events/test_events.py::TestEventRule::test_disable_re_enable_rule[default]": { - "recorded-date": "22-04-2024, 13:07:45", - "recorded-content": { - "disable-rule": { + }, + "delete-rule": { "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "describe-rule-disabled": { - "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", - "CreatedBy": "111111111111", - "EventBusName": "default", - "EventPattern": { - "source": [ - "core.update-account-command" - ], - "detail-type": [ - "core.update-account-command" - ], - "detail": { - "command": [ - "update-account" - ] - } - }, - "Name": "<rule-name>", - "State": "DISABLED", + "list-rules-after-delete": { + "Rules": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_put_list_with_prefix_describe_delete_rule[default]": { + "recorded-date": "29-04-2024, 13:16:43", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "enable-rule": { + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "EventBusName": "default", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "<rule-name>", + "State": "ENABLED" + } + ], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "describe-rule-enabled": { + "describe-rule": { "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", "CreatedBy": "111111111111", "EventBusName": "default", @@ -571,42 +625,15 @@ "HTTPHeaders": {}, "HTTPStatusCode": 200 } - } - } - }, - "tests/aws/services/events/test_events.py::TestEventTarget::test_put_list_remove_target[custom]": { - "recorded-date": "22-04-2024, 13:07:17", - "recorded-content": { - "put-target": { - "FailedEntries": [], - "FailedEntryCount": 0, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } }, - "list-targets": { - "Targets": [ - { - "Arn": "<queue-arn>", - "Id": "<target-id>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "remove-target": { - "FailedEntries": [], - "FailedEntryCount": 0, + "delete-rule": { "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "list-targets-after-delete": { - "Targets": [], + "list-rules-after-delete": { + "Rules": [], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -614,39 +641,45 @@ } } }, - "tests/aws/services/events/test_events.py::TestEventTarget::test_put_list_remove_target[default]": { - "recorded-date": "22-04-2024, 13:07:18", + "tests/aws/services/events/test_events.py::TestEventRule::test_put_multiple_rules_with_same_name": { + "recorded-date": "29-04-2024, 13:16:44", "recorded-content": { - "put-target": { - "FailedEntries": [], - "FailedEntryCount": 0, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "list-targets": { - "Targets": [ - { - "Arn": "<queue-arn>", - "Id": "<target-id>" - } - ], + "put-rule": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name>", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "remove-target": { - "FailedEntries": [], - "FailedEntryCount": 0, + "re-put-rule": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name>", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "list-targets-after-delete": { - "Targets": [], + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name>", + "EventBusName": "<bus-name>", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "<rule-name>", + "State": "ENABLED" + } + ], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -654,29 +687,68 @@ } } }, - "tests/aws/services/events/test_events.py::TestEventTarget::test_add_exceed_fife_targets_per_rule": { - "recorded-date": "22-04-2024, 13:07:20", - "recorded-content": { - "put-targets-client-error": "<ExceptionInfo LimitExceededException('An error occurred (LimitExceededException) when calling the PutTargets operation: The requested resource exceeds the maximum number allowed.') tblen=3>" - } - }, - "tests/aws/services/events/test_events.py::TestEventTarget::test_list_target_by_rule_limit": { - "recorded-date": "22-04-2024, 13:07:22", + "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_with_limit": { + "recorded-date": "29-04-2024, 13:16:47", "recorded-content": { - "list-targets-limit": { + "list-rules-limit": { "NextToken": "<next_token:1>", - "Targets": [ + "Rules": [ { - "Arn": "<queue-arn>", - "Id": "<target-id>0" + "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name-prefix>-0", + "EventBusName": "<bus-name>", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "<rule-name-prefix>-0", + "State": "ENABLED" }, { - "Arn": "<queue-arn>", - "Id": "<target-id>1" + "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name-prefix>-1", + "EventBusName": "<bus-name>", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "<rule-name-prefix>-1", + "State": "ENABLED" }, { - "Arn": "<queue-arn>", - "Id": "<target-id>2" + "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name-prefix>-2", + "EventBusName": "<bus-name>", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "<rule-name-prefix>-2", + "State": "ENABLED" } ], "ResponseMetadata": { @@ -684,15 +756,64 @@ "HTTPStatusCode": 200 } }, - "list-targets-limit-next-token": { - "Targets": [ + "list-rules-limit-next-token": { + "Rules": [ { - "Arn": "<queue-arn>", - "Id": "<target-id>3" + "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name-prefix>-3", + "EventBusName": "<bus-name>", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "<rule-name-prefix>-3", + "State": "ENABLED" + }, + { + "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name-prefix>-4", + "EventBusName": "<bus-name>", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "<rule-name-prefix>-4", + "State": "ENABLED" }, { - "Arn": "<queue-arn>", - "Id": "<target-id>4" + "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name-prefix>-5", + "EventBusName": "<bus-name>", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "<rule-name-prefix>-5", + "State": "ENABLED" } ], "ResponseMetadata": { @@ -702,92 +823,70 @@ } } }, - "tests/aws/services/events/test_events.py::TestEventRule::test_delete_rule_with_targets": { - "recorded-date": "22-04-2024, 13:07:46", - "recorded-content": { - "delete-rule-with-targets-error": "<ExceptionInfo ClientError(\"An error occurred (ValidationException) when calling the DeleteRule operation: Rule can't be deleted since it has targets.\") tblen=3>" - } - }, - "tests/aws/services/events/test_events.py::TestEventsEventBus::test_delete_default_event_bus": { - "recorded-date": "16-04-2024, 15:10:07", - "recorded-content": { - "delete-default-event-bus-error": "<ExceptionInfo ClientError('An error occurred (ValidationException) when calling the DeleteEventBus operation: Cannot delete event bus default.') tblen=3>" - } - }, - "tests/aws/services/events/test_events.py::TestEventRule::test_update_rule_with_targets": { - "recorded-date": "22-04-2024, 13:07:48", + "tests/aws/services/events/test_events.py::TestEventRule::test_describe_nonexistent_rule": { + "recorded-date": "29-04-2024, 13:16:49", "recorded-content": { - "list-targets": { - "Targets": [ - { - "Arn": "<queue-arn>", - "Id": "<target-id>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "update-rule": { - "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "list-targets-after-update": { - "Targets": [ - { - "Arn": "<queue-arn>", - "Id": "<target-id>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } + "describe-not-existing-rule-error": "<ExceptionInfo ResourceNotFoundException('An error occurred (ResourceNotFoundException) when calling the DescribeRule operation: Rule <rule-name> does not exist on EventBus default.') tblen=3>" } }, - "tests/aws/services/events/test_events.py::TestEventBus::test_create_list_describe_delete_custom_event_buses[regions0]": { - "recorded-date": "23-04-2024, 06:11:32", + "tests/aws/services/events/test_events.py::TestEventRule::test_disable_re_enable_rule[custom]": { + "recorded-date": "29-04-2024, 13:16:50", "recorded-content": { - "create-custom-event-bus-us-east-1": { - "EventBusArn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", + "disable-rule": { "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "list-event-buses-after-create-us-east-1": { - "EventBuses": [ - { - "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", - "Name": "<bus-name>" + "describe-rule-disabled": { + "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name>", + "CreatedBy": "111111111111", + "EventBusName": "<bus-name>", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-custom-event-bus-us-east-1": { - "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", - "Name": "<bus-name>", + }, + "Name": "<rule-name>", + "State": "DISABLED", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "delete-custom-event-bus-us-east-1": { + "enable-rule": { "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "list-event-buses-after-delete-us-east-1": { - "EventBuses": [], + "describe-rule-enabled": { + "Arn": "arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name>", + "CreatedBy": "111111111111", + "EventBusName": "<bus-name>", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] + } + }, + "Name": "<rule-name>", + "State": "ENABLED", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -795,124 +894,106 @@ } } }, - "tests/aws/services/events/test_events.py::TestEventBus::test_create_list_describe_delete_custom_event_buses[regions1]": { - "recorded-date": "23-04-2024, 06:11:34", + "tests/aws/services/events/test_events.py::TestEventRule::test_disable_re_enable_rule[default]": { + "recorded-date": "29-04-2024, 13:16:52", "recorded-content": { - "create-custom-event-bus-us-east-1": { - "EventBusArn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", + "disable-rule": { "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "list-event-buses-after-create-us-east-1": { - "EventBuses": [ - { - "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", - "Name": "<bus-name>" + "describe-rule-disabled": { + "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "CreatedBy": "111111111111", + "EventBusName": "default", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-custom-event-bus-us-east-1": { - "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", - "Name": "<bus-name>", + }, + "Name": "<rule-name>", + "State": "DISABLED", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "create-custom-event-bus-us-west-1": { - "EventBusArn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", + "enable-rule": { "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "list-event-buses-after-create-us-west-1": { - "EventBuses": [ - { - "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", - "Name": "<bus-name>" + "describe-rule-enabled": { + "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "CreatedBy": "111111111111", + "EventBusName": "default", + "EventPattern": { + "source": [ + "core.update-account-command" + ], + "detail-type": [ + "core.update-account-command" + ], + "detail": { + "command": [ + "update-account" + ] } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-custom-event-bus-us-west-1": { - "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", - "Name": "<bus-name>", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "create-custom-event-bus-eu-central-1": { - "EventBusArn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", + }, + "Name": "<rule-name>", + "State": "ENABLED", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } - }, - "list-event-buses-after-create-eu-central-1": { - "EventBuses": [ + } + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_delete_rule_with_targets": { + "recorded-date": "29-04-2024, 13:16:53", + "recorded-content": { + "delete-rule-with-targets-error": "<ExceptionInfo ClientError(\"An error occurred (ValidationException) when calling the DeleteRule operation: Rule can't be deleted since it has targets.\") tblen=3>" + } + }, + "tests/aws/services/events/test_events.py::TestEventRule::test_update_rule_with_targets": { + "recorded-date": "29-04-2024, 13:16:55", + "recorded-content": { + "list-targets": { + "Targets": [ { - "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", - "Name": "<bus-name>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "describe-custom-event-bus-eu-central-1": { - "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", - "Name": "<bus-name>", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "delete-custom-event-bus-us-east-1": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "list-event-buses-after-delete-us-east-1": { - "EventBuses": [], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "delete-custom-event-bus-us-west-1": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "list-event-buses-after-delete-us-west-1": { - "EventBuses": [], + "Arn": "<queue-arn>", + "Id": "<target-id>" + } + ], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "delete-custom-event-bus-eu-central-1": { + "update-rule": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "list-event-buses-after-delete-eu-central-1": { - "EventBuses": [], + "list-targets-after-update": { + "Targets": [ + { + "Arn": "<queue-arn>", + "Id": "<target-id>" + } + ], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -920,93 +1001,94 @@ } } }, - "tests/aws/services/events/test_events.py::TestEventBus::test_create_multiple_event_buses_same_name": { - "recorded-date": "22-04-2024, 13:11:10", - "recorded-content": { - "create-multiple-event-buses-same-name": "<ExceptionInfo ResourceAlreadyExistsException('An error occurred (ResourceAlreadyExistsException) when calling the CreateEventBus operation: Event bus <bus-name> already exists.') tblen=4>" - } - }, - "tests/aws/services/events/test_events.py::TestEventBus::test_describe_delete_not_existing_event_bus": { - "recorded-date": "22-04-2024, 13:11:12", + "tests/aws/services/events/test_events.py::TestEventPattern::test_put_events_pattern_with_values_in_array": { + "recorded-date": "29-04-2024, 13:17:04", "recorded-content": { - "describe-not-existing-event-bus-error": "<ExceptionInfo ResourceNotFoundException('An error occurred (ResourceNotFoundException) when calling the DescribeEventBus operation: Event bus <bus-name> does not exist.') tblen=3>", - "delete-not-existing-event-bus": "<ExceptionInfo ResourceNotFoundException('An error occurred (ResourceNotFoundException) when calling the DescribeEventBus operation: Event bus <bus-name> does not exist.') tblen=3>" + "messages": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": { + "event": { + "data": { + "type": [ + "3", + "1" + ] + } + } + } + }, + { + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>", + "MD5OfBody": "<m-d5-of-body:2>", + "Body": { + "event": { + "data": { + "type": [ + "2" + ] + } + } + } + } + ] } }, - "tests/aws/services/events/test_events.py::TestEventBus::test_delete_default_event_bus": { - "recorded-date": "22-04-2024, 13:11:12", + "tests/aws/services/events/test_events.py::TestEventPattern::test_put_events_pattern_nested": { + "recorded-date": "29-04-2024, 13:17:16", "recorded-content": { - "delete-default-event-bus-error": "<ExceptionInfo ClientError('An error occurred (ValidationException) when calling the DeleteEventBus operation: Cannot delete event bus default.') tblen=3>" + "messages": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": { + "event": { + "data": { + "type": "1" + } + } + } + } + ] } }, - "tests/aws/services/events/test_events.py::TestEventBus::test_list_event_buses_with_prefix": { - "recorded-date": "22-04-2024, 13:19:19", + "tests/aws/services/events/test_events.py::TestEventTarget::test_put_list_remove_target[custom]": { + "recorded-date": "29-04-2024, 13:17:18", "recorded-content": { - "list-event-buses-prefix-complete-name": { - "EventBuses": [ - { - "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", - "Name": "<bus-name>" - } - ], + "put-target": { + "FailedEntries": [], + "FailedEntryCount": 0, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "list-event-buses-prefix": { - "EventBuses": [ + "list-targets": { + "Targets": [ { - "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name>", - "Name": "<bus-name>" + "Arn": "<queue-arn>", + "Id": "<target-id>" } ], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } - } - } - }, - "tests/aws/services/events/test_events.py::TestEventBus::test_list_event_buses_with_limit": { - "recorded-date": "22-04-2024, 13:11:15", - "recorded-content": { - "list-event-buses-limit": { - "EventBuses": [ - { - "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name-prefix>-0", - "Name": "<bus-name-prefix>-0" - }, - { - "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name-prefix>-1", - "Name": "<bus-name-prefix>-1" - }, - { - "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name-prefix>-2", - "Name": "<bus-name-prefix>-2" - } - ], - "NextToken": "<next_token:1>", + }, + "remove-target": { + "FailedEntries": [], + "FailedEntryCount": 0, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "list-event-buses-limit-next-token": { - "EventBuses": [ - { - "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name-prefix>-3", - "Name": "<bus-name-prefix>-3" - }, - { - "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name-prefix>-4", - "Name": "<bus-name-prefix>-4" - }, - { - "Arn": "arn:aws:events:<region>:111111111111:event-bus/<bus-name-prefix>-5", - "Name": "<bus-name-prefix>-5" - } - ], + "list-targets-after-delete": { + "Targets": [], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -1014,31 +1096,30 @@ } } }, - "tests/aws/services/events/test_events.py::TestEventBus::test_put_events_to_default_eventbus_for_custom_eventbus": { - "recorded-date": "22-04-2024, 13:11:43", + "tests/aws/services/events/test_events.py::TestEventTarget::test_put_list_remove_target[default]": { + "recorded-date": "29-04-2024, 13:17:20", "recorded-content": { - "create-custom-event-bus": { - "EventBusArn": "arn:aws:events:<region>:111111111111:event-bus/<resource:1>", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "create-rule-1": { - "RuleArn": "arn:aws:events:<region>:111111111111:rule/<resource:2>", + "put-target": { + "FailedEntries": [], + "FailedEntryCount": 0, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "create-rule-2": { - "RuleArn": "arn:aws:events:<region>:111111111111:rule/<resource:1>/<resource:3>", + "list-targets": { + "Targets": [ + { + "Arn": "<queue-arn>", + "Id": "<target-id>" + } + ], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "put-target-1": { + "remove-target": { "FailedEntries": [], "FailedEntryCount": 0, "ResponseMetadata": { @@ -1046,95 +1127,80 @@ "HTTPStatusCode": 200 } }, - "put-target-2": { - "FailedEntries": [], - "FailedEntryCount": 0, + "list-targets-after-delete": { + "Targets": [], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } - }, - "get-events": { - "Messages": [ - { - "Body": { - "version": "0", - "id": "<uuid:1>", - "detail-type": "Object Created", - "source": "aws.s3", - "account": "111111111111", - "time": "date", - "region": "<region>", - "resources": [ - "arn:aws:s3:::<bucket-name:1>" - ], - "detail": { - "version": "0", - "bucket": { - "name": "<bucket-name:1>" - }, - "object": { - "key": "<key-name:1>", - "size": 4, - "etag": "8d777f385d3dfec8815d20f7496026dc", - "sequencer": "object-sequencer" - }, - "request-id": "request-id", - "requester": "<requester>", - "source-ip-address": "<ip-address:1>", - "reason": "PutObject" - } - }, - "MD5OfBody": "<m-d5-of-body:1>", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:1>" - } - ] } } }, - "tests/aws/services/events/test_events.py::TestEventBus::test_put_events_nonexistent_event_bus": { - "recorded-date": "22-04-2024, 13:12:23", + "tests/aws/services/events/test_events.py::TestEventTarget::test_add_exceed_fife_targets_per_rule": { + "recorded-date": "29-04-2024, 13:17:21", "recorded-content": { - "put-events": { - "Entries": [ + "put-targets-client-error": "<ExceptionInfo LimitExceededException('An error occurred (LimitExceededException) when calling the PutTargets operation: The requested resource exceeds the maximum number allowed.') tblen=3>" + } + }, + "tests/aws/services/events/test_events.py::TestEventTarget::test_list_target_by_rule_limit": { + "recorded-date": "29-04-2024, 13:17:23", + "recorded-content": { + "list-targets-limit": { + "NextToken": "<next_token:1>", + "Targets": [ { - "EventId": "<uuid:1>" + "Arn": "<queue-arn>", + "Id": "<target-id>0" }, { - "EventId": "<uuid:2>" + "Arn": "<queue-arn>", + "Id": "<target-id>1" + }, + { + "Arn": "<queue-arn>", + "Id": "<target-id>2" } ], - "FailedEntryCount": 0, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "get-events": [ - { - "MessageId": "<uuid:3>", - "ReceiptHandle": "<receipt-handle:1>", - "MD5OfBody": "<m-d5-of-body:1>", - "Body": { - "version": "0", - "id": "<uuid:1>", - "detail-type": "CustomType", - "source": "MySource", - "account": "111111111111", - "time": "date", - "region": "<region>", - "resources": [], - "detail": { - "message": "for the default event bus" - } + "list-targets-limit-next-token": { + "Targets": [ + { + "Arn": "<queue-arn>", + "Id": "<target-id>3" + }, + { + "Arn": "<queue-arn>", + "Id": "<target-id>4" } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 } - ], - "non-existent-bus-error": { + } + } + }, + "tests/aws/services/events/test_events.py::TestEventTarget::test_put_target_id_validation": { + "recorded-date": "29-04-2024, 13:17:26", + "recorded-content": { + "put-targets-invalid-id-error": { "Error": { - "Code": "ResourceNotFoundException", - "Message": "Event bus <custom-event-bus> does not exist." + "Code": "ValidationException", + "Message": "1 validation error detected: Value '!@#$@!#$' at 'targets.1.member.id' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\.\\-_A-Za-z0-9]+" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "put-targets-length-error": { + "Error": { + "Code": "ValidationException", + "Message": "1 validation error detected: Value 'second-invalid-target-id' at 'targets.1.member.id' failed to satisfy constraint: Member must have length less than or equal to 64" }, "ResponseMetadata": { "HTTPHeaders": {}, diff --git a/tests/aws/services/events/test_events.validation.json b/tests/aws/services/events/test_events.validation.json index 9c1bb67593892..dd22b7d3333c7 100644 --- a/tests/aws/services/events/test_events.validation.json +++ b/tests/aws/services/events/test_events.validation.json @@ -1,128 +1,92 @@ { "tests/aws/services/events/test_events.py::TestEventBus::test_create_list_describe_delete_custom_event_buses[regions0]": { - "last_validated_date": "2024-04-23T06:11:32+00:00" + "last_validated_date": "2024-04-29T13:15:44+00:00" }, "tests/aws/services/events/test_events.py::TestEventBus::test_create_list_describe_delete_custom_event_buses[regions1]": { - "last_validated_date": "2024-04-23T06:11:34+00:00" + "last_validated_date": "2024-04-29T13:15:47+00:00" }, "tests/aws/services/events/test_events.py::TestEventBus::test_create_multiple_event_buses_same_name": { - "last_validated_date": "2024-04-22T13:11:10+00:00" + "last_validated_date": "2024-04-29T13:15:47+00:00" }, "tests/aws/services/events/test_events.py::TestEventBus::test_delete_default_event_bus": { - "last_validated_date": "2024-04-22T13:11:12+00:00" + "last_validated_date": "2024-04-29T13:15:49+00:00" }, "tests/aws/services/events/test_events.py::TestEventBus::test_describe_delete_not_existing_event_bus": { - "last_validated_date": "2024-04-22T13:11:12+00:00" + "last_validated_date": "2024-04-29T13:15:49+00:00" }, "tests/aws/services/events/test_events.py::TestEventBus::test_list_event_buses_with_limit": { - "last_validated_date": "2024-04-22T13:11:15+00:00" + "last_validated_date": "2024-04-29T13:15:52+00:00" }, "tests/aws/services/events/test_events.py::TestEventBus::test_list_event_buses_with_prefix": { - "last_validated_date": "2024-04-22T13:19:19+00:00" + "last_validated_date": "2024-04-29T13:15:50+00:00" }, "tests/aws/services/events/test_events.py::TestEventBus::test_put_events_nonexistent_event_bus": { - "last_validated_date": "2024-04-22T13:12:23+00:00" + "last_validated_date": "2024-04-29T13:18:57+00:00" }, "tests/aws/services/events/test_events.py::TestEventBus::test_put_events_to_default_eventbus_for_custom_eventbus": { - "last_validated_date": "2024-04-22T13:11:43+00:00" + "last_validated_date": "2024-04-29T13:16:20+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventPattern::test_put_events_pattern_nested": { + "last_validated_date": "2024-04-29T13:17:16+00:00" + }, + "tests/aws/services/events/test_events.py::TestEventPattern::test_put_events_pattern_with_values_in_array": { + "last_validated_date": "2024-04-29T13:17:04+00:00" }, "tests/aws/services/events/test_events.py::TestEventRule::test_delete_rule_with_targets": { - "last_validated_date": "2024-04-22T13:07:46+00:00" + "last_validated_date": "2024-04-29T13:16:53+00:00" }, "tests/aws/services/events/test_events.py::TestEventRule::test_describe_nonexistent_rule": { - "last_validated_date": "2024-04-22T13:07:42+00:00" + "last_validated_date": "2024-04-29T13:16:49+00:00" }, "tests/aws/services/events/test_events.py::TestEventRule::test_disable_re_enable_rule[custom]": { - "last_validated_date": "2024-04-22T13:07:43+00:00" + "last_validated_date": "2024-04-29T13:16:50+00:00" }, "tests/aws/services/events/test_events.py::TestEventRule::test_disable_re_enable_rule[default]": { - "last_validated_date": "2024-04-22T13:07:45+00:00" + "last_validated_date": "2024-04-29T13:16:52+00:00" }, "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_with_limit": { - "last_validated_date": "2024-04-22T13:07:40+00:00" + "last_validated_date": "2024-04-29T13:16:47+00:00" }, "tests/aws/services/events/test_events.py::TestEventRule::test_put_list_with_prefix_describe_delete_rule[custom]": { - "last_validated_date": "2024-04-22T13:07:35+00:00" + "last_validated_date": "2024-04-29T13:16:41+00:00" }, "tests/aws/services/events/test_events.py::TestEventRule::test_put_list_with_prefix_describe_delete_rule[default]": { - "last_validated_date": "2024-04-22T13:07:36+00:00" + "last_validated_date": "2024-04-29T13:16:43+00:00" }, "tests/aws/services/events/test_events.py::TestEventRule::test_put_multiple_rules_with_same_name": { - "last_validated_date": "2024-04-22T13:07:38+00:00" - }, - "tests/aws/services/events/test_events.py::TestEventRule::test_put_rule[custom]": { - "last_validated_date": "2024-04-04T10:47:20+00:00" - }, - "tests/aws/services/events/test_events.py::TestEventRule::test_put_rule[default]": { - "last_validated_date": "2024-04-04T10:47:21+00:00" + "last_validated_date": "2024-04-29T13:16:44+00:00" }, "tests/aws/services/events/test_events.py::TestEventRule::test_update_rule_with_targets": { - "last_validated_date": "2024-04-22T13:07:48+00:00" + "last_validated_date": "2024-04-29T13:16:55+00:00" }, "tests/aws/services/events/test_events.py::TestEventTarget::test_add_exceed_fife_targets_per_rule": { - "last_validated_date": "2024-04-22T13:07:20+00:00" + "last_validated_date": "2024-04-29T13:17:21+00:00" }, "tests/aws/services/events/test_events.py::TestEventTarget::test_list_target_by_rule_limit": { - "last_validated_date": "2024-04-22T13:07:22+00:00" + "last_validated_date": "2024-04-29T13:17:23+00:00" }, "tests/aws/services/events/test_events.py::TestEventTarget::test_put_list_remove_target[custom]": { - "last_validated_date": "2024-04-22T13:07:17+00:00" + "last_validated_date": "2024-04-29T13:17:18+00:00" }, "tests/aws/services/events/test_events.py::TestEventTarget::test_put_list_remove_target[default]": { - "last_validated_date": "2024-04-22T13:07:18+00:00" + "last_validated_date": "2024-04-29T13:17:20+00:00" }, - "tests/aws/services/events/test_events.py::TestEvents::test_create_connection_validations": { - "last_validated_date": "2024-03-26T14:07:16+00:00" + "tests/aws/services/events/test_events.py::TestEventTarget::test_put_target_id_validation": { + "last_validated_date": "2024-04-29T13:17:26+00:00" }, - "tests/aws/services/events/test_events.py::TestEvents::test_event_pattern": { - "last_validated_date": "2024-03-26T14:07:19+00:00" + "tests/aws/services/events/test_events.py::TestEvents::test_create_connection_validations": { + "last_validated_date": "2024-04-29T13:15:43+00:00" }, "tests/aws/services/events/test_events.py::TestEvents::test_list_tags_for_resource": { - "last_validated_date": "2024-03-26T14:06:51+00:00" + "last_validated_date": "2024-04-29T13:15:38+00:00" }, "tests/aws/services/events/test_events.py::TestEvents::test_put_event_without_detail": { - "last_validated_date": "2024-03-26T14:07:16+00:00" - }, - "tests/aws/services/events/test_events.py::TestEvents::test_put_event_without_source": { - "last_validated_date": "2024-03-26T14:07:16+00:00" + "last_validated_date": "2024-04-29T13:15:31+00:00" }, "tests/aws/services/events/test_events.py::TestEvents::test_put_events_time": { - "last_validated_date": "2024-03-26T14:09:45+00:00" - }, - "tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_nested_event_pattern": { - "last_validated_date": "2024-03-26T14:07:10+00:00" - }, - "tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_values_in_array": { - "last_validated_date": "2024-03-26T14:06:58+00:00" - }, - "tests/aws/services/events/test_events.py::TestEvents::test_put_target_id_validation": { - "last_validated_date": "2024-04-18T15:47:35+00:00" - }, - "tests/aws/services/events/test_events.py::TestEventsEventBus::test_create_custom_event_bus": { - "last_validated_date": "2024-03-27T09:15:34+00:00" - }, - "tests/aws/services/events/test_events.py::TestEventsEventBus::test_create_list_describe_delete_custom_event_buses[regions0]": { - "last_validated_date": "2024-04-03T13:49:07+00:00" - }, - "tests/aws/services/events/test_events.py::TestEventsEventBus::test_create_list_describe_delete_custom_event_buses[regions1]": { - "last_validated_date": "2024-04-03T13:49:10+00:00" - }, - "tests/aws/services/events/test_events.py::TestEventsEventBus::test_create_multiple_event_buses_same_name": { - "last_validated_date": "2024-04-16T15:09:51+00:00" - }, - "tests/aws/services/events/test_events.py::TestEventsEventBus::test_delete_default_event_bus": { - "last_validated_date": "2024-04-16T15:10:07+00:00" - }, - "tests/aws/services/events/test_events.py::TestEventsEventBus::test_describe_delete_not_existing_event_bus": { - "last_validated_date": "2024-04-17T07:32:19+00:00" - }, - "tests/aws/services/events/test_events.py::TestEventsEventBus::test_list_event_buses_with_limit": { - "last_validated_date": "2024-04-03T14:53:31+00:00" - }, - "tests/aws/services/events/test_events.py::TestEventsEventBus::test_put_events_nonexistent_event_bus": { - "last_validated_date": "2024-04-16T15:10:30+00:00" + "last_validated_date": "2024-04-29T13:15:34+00:00" }, - "tests/aws/services/events/test_events.py::TestEventsEventBus::test_put_events_to_default_eventbus_for_custom_eventbus": { - "last_validated_date": "2024-03-26T14:07:59+00:00" + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_without_source": { + "last_validated_date": "2024-04-29T13:15:31+00:00" } } diff --git a/tests/aws/services/events/test_events_integrations.py b/tests/aws/services/events/test_events_integrations.py index 2124fdca6204c..9c8659322d498 100644 --- a/tests/aws/services/events/test_events_integrations.py +++ b/tests/aws/services/events/test_events_integrations.py @@ -20,7 +20,7 @@ @markers.aws.validated -def test_put_events_with_target_sqs(put_events_with_filter_to_sqs): +def test_put_events_with_target_sqs(put_events_with_filter_to_sqs, snapshot): entries = [ { "Source": TEST_EVENT_PATTERN["source"][0], @@ -28,10 +28,17 @@ def test_put_events_with_target_sqs(put_events_with_filter_to_sqs): "Detail": json.dumps(EVENT_DETAIL), } ] - put_events_with_filter_to_sqs( + message = put_events_with_filter_to_sqs( pattern=TEST_EVENT_PATTERN, entries_asserts=[(entries, True)], ) + snapshot.add_transformers_list( + [ + snapshot.transform.key_value("ReceiptHandle", reference_replacement=False), + snapshot.transform.key_value("MD5OfBody", reference_replacement=False), + ], + ) + snapshot.match("message", message) @markers.aws.unknown @@ -76,9 +83,9 @@ def test_put_events_with_target_sqs_new_region(aws_client_factory): assert "EventId" in response.get("Entries")[0] -@markers.aws.unknown +@markers.aws.validated @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") -def test_put_events_with_target_sqs_event_detail_match(put_events_with_filter_to_sqs): +def test_put_events_with_target_sqs_event_detail_match(put_events_with_filter_to_sqs, snapshot): entries1 = [ { "Source": TEST_EVENT_PATTERN["source"][0], @@ -94,19 +101,26 @@ def test_put_events_with_target_sqs_event_detail_match(put_events_with_filter_to } ] entries_asserts = [(entries1, True), (entries2, False)] - put_events_with_filter_to_sqs( + messages = put_events_with_filter_to_sqs( pattern={"detail": {"EventType": ["0", "1"]}}, entries_asserts=entries_asserts, input_path="$.detail", ) + snapshot.add_transformers_list( + [ + snapshot.transform.key_value("ReceiptHandle", reference_replacement=False), + snapshot.transform.key_value("MD5OfBody", reference_replacement=False), + ], + ) + snapshot.match("messages", messages) + # TODO: further unify/parameterize the tests for the different target types below -@markers.aws.unknown +@markers.aws.needs_fixing @pytest.mark.parametrize("strategy", ["standard", "domain", "path"]) -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_target_sns( monkeypatch, sns_subscription, @@ -174,8 +188,7 @@ def test_put_events_with_target_sns( ) -@markers.aws.unknown -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") +@markers.aws.needs_fixing def test_put_events_with_target_lambda(create_lambda_function, cleanups, aws_client, clean_up): rule_name = f"rule-{short_uid()}" function_name = f"lambda-func-{short_uid()}" @@ -237,7 +250,6 @@ def test_put_events_with_target_lambda(create_lambda_function, cleanups, aws_cli @markers.aws.validated -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_target_lambda_list_entry( create_lambda_function, cleanups, aws_client, clean_up, snapshot ): @@ -334,7 +346,6 @@ def test_put_events_with_target_lambda_list_entry( @markers.aws.validated -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_target_lambda_list_entries_partial_match( create_lambda_function, cleanups, aws_client, clean_up, snapshot ): @@ -482,8 +493,7 @@ def check_invocation(): retry(check_invocation, sleep=5, retries=15) -@markers.aws.unknown -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") +@markers.aws.needs_fixing def test_put_events_with_target_firehose(aws_client, clean_up): s3_bucket = "s3-{}".format(short_uid()) s3_prefix = "testeventdata" diff --git a/tests/aws/services/events/test_events_integrations.snapshot.json b/tests/aws/services/events/test_events_integrations.snapshot.json index a52a6f84d34b3..e27cdb2a10e9e 100644 --- a/tests/aws/services/events/test_events_integrations.snapshot.json +++ b/tests/aws/services/events/test_events_integrations.snapshot.json @@ -100,5 +100,49 @@ } ] } + }, + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_sqs": { + "recorded-date": "26-04-2024, 08:43:27", + "recorded-content": { + "message": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "receipt-handle", + "MD5OfBody": "m-d5-of-body", + "Body": { + "version": "0", + "id": "<uuid:2>", + "detail-type": "core.update-account-command", + "source": "core.update-account-command", + "account": "111111111111", + "time": "date", + "region": "<region>", + "resources": [], + "detail": { + "command": "update-account", + "payload": { + "acc_id": "0a787ecb-4015", + "sf_id": "baz" + } + } + } + } + ] + } + }, + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_sqs_event_detail_match": { + "recorded-date": "07-05-2024, 10:40:38", + "recorded-content": { + "messages": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "receipt-handle", + "MD5OfBody": "m-d5-of-body", + "Body": { + "EventType": "1" + } + } + ] + } } } diff --git a/tests/aws/services/events/test_events_integrations.validation.json b/tests/aws/services/events/test_events_integrations.validation.json index 6d701cbe30a64..bf2860707799f 100644 --- a/tests/aws/services/events/test_events_integrations.validation.json +++ b/tests/aws/services/events/test_events_integrations.validation.json @@ -6,10 +6,10 @@ "last_validated_date": "2024-04-08T17:33:44+00:00" }, "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_sqs": { - "last_validated_date": "2024-03-26T15:49:59+00:00" + "last_validated_date": "2024-04-26T08:43:27+00:00" }, "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_sqs_event_detail_match": { - "last_validated_date": "2024-03-26T15:50:07+00:00" + "last_validated_date": "2024-05-07T10:40:38+00:00" }, "tests/aws/services/events/test_events_integrations.py::test_should_ignore_schedules_for_put_event": { "last_validated_date": "2024-03-26T15:51:47+00:00" diff --git a/tests/aws/services/events/test_events_rules.py b/tests/aws/services/events/test_events_rules.py index 63758ca8db630..1926910cc8261 100644 --- a/tests/aws/services/events/test_events_rules.py +++ b/tests/aws/services/events/test_events_rules.py @@ -14,11 +14,10 @@ from localstack.utils.sync import poll_condition from tests.aws.services.events.conftest import assert_valid_event, sqs_collect_messages from tests.aws.services.events.helper_functions import is_v2_provider -from tests.aws.services.events.test_events import TEST_EVENT_BUS_NAME, TEST_EVENT_PATTERN +from tests.aws.services.events.test_events import TEST_EVENT_PATTERN @markers.aws.validated -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_rule(aws_client, snapshot, clean_up): rule_name = f"rule-{short_uid()}" snapshot.add_transformer(snapshot.transform.regex(rule_name, "<rule-name>")) @@ -38,7 +37,6 @@ def test_put_rule(aws_client, snapshot, clean_up): @markers.aws.validated -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_rule_disable(aws_client, clean_up): rule_name = f"rule-{short_uid()}" aws_client.events.put_rule(Name=rule_name, ScheduleExpression="rate(1 minute)") @@ -54,6 +52,8 @@ def test_rule_disable(aws_client, clean_up): @markers.aws.validated +# TODO move to test_events_schedules.py +@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") @pytest.mark.parametrize( "expression", [ @@ -76,7 +76,6 @@ def test_rule_disable(aws_client, clean_up): " rate(10 minutes)", ], ) -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_rule_invalid_rate_schedule_expression(expression, aws_client): with pytest.raises(ClientError) as e: aws_client.events.put_rule(Name=f"rule-{short_uid()}", ScheduleExpression=expression) @@ -88,7 +87,6 @@ def test_put_rule_invalid_rate_schedule_expression(expression, aws_client): @markers.aws.validated -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_rule_anything_but_to_sqs(put_events_with_filter_to_sqs, snapshot): snapshot.add_transformer( [ @@ -142,7 +140,6 @@ def test_put_events_with_rule_anything_but_to_sqs(put_events_with_filter_to_sqs, @markers.aws.validated -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_rule_exists_true_to_sqs(put_events_with_filter_to_sqs, snapshot): """ Exists matching True condition: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-exists-matching @@ -190,7 +187,6 @@ def test_put_events_with_rule_exists_true_to_sqs(put_events_with_filter_to_sqs, @markers.aws.validated -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_put_events_with_rule_exists_false_to_sqs(put_events_with_filter_to_sqs, snapshot): """ Exists matching False condition: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns-content-based-filtering.html#eb-filtering-exists-matching @@ -243,6 +239,7 @@ def test_put_event_with_content_base_rule_in_pattern(aws_client, clean_up): queue_name = f"queue-{short_uid()}" rule_name = f"rule-{short_uid()}" target_id = f"target-{short_uid()}" + event_bus_name = f"event-bus-{short_uid()}" queue_url = aws_client.sqs.create_queue(QueueName=queue_name)["QueueUrl"] queue_arn = arns.sqs_queue_arn(queue_name, TEST_AWS_ACCOUNT_ID, TEST_AWS_REGION_NAME) @@ -276,7 +273,7 @@ def test_put_event_with_content_base_rule_in_pattern(aws_client, clean_up): } event = { - "EventBusName": TEST_EVENT_BUS_NAME, + "EventBusName": event_bus_name, "Source": "core.update-account-command", "DetailType": "core.app.backend", "Detail": json.dumps( @@ -303,16 +300,16 @@ def test_put_event_with_content_base_rule_in_pattern(aws_client, clean_up): ), } - aws_client.events.create_event_bus(Name=TEST_EVENT_BUS_NAME) + aws_client.events.create_event_bus(Name=event_bus_name) aws_client.events.put_rule( Name=rule_name, - EventBusName=TEST_EVENT_BUS_NAME, + EventBusName=event_bus_name, EventPattern=json.dumps(pattern), ) aws_client.events.put_targets( Rule=rule_name, - EventBusName=TEST_EVENT_BUS_NAME, + EventBusName=event_bus_name, Targets=[{"Id": target_id, "Arn": queue_arn, "InputPath": "$.detail"}], ) aws_client.events.put_events(Entries=[event]) @@ -331,7 +328,7 @@ def test_put_event_with_content_base_rule_in_pattern(aws_client, clean_up): # clean up clean_up( - bus_name=TEST_EVENT_BUS_NAME, + bus_name=event_bus_name, rule_name=rule_name, target_ids=target_id, queue_url=queue_url, @@ -339,8 +336,9 @@ def test_put_event_with_content_base_rule_in_pattern(aws_client, clean_up): @markers.aws.validated -@pytest.mark.parametrize("schedule_expression", ["rate(1 minute)", "rate(1 day)", "rate(1 hour)"]) +# TODO move to test_events_schedules.py @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") +@pytest.mark.parametrize("schedule_expression", ["rate(1 minute)", "rate(1 day)", "rate(1 hour)"]) def test_create_rule_with_one_unit_in_singular_should_succeed( schedule_expression, aws_client, clean_up ): From c64eda9a44ed63ce332f62ba0bd67ae7572ca9b6 Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Tue, 7 May 2024 15:44:23 +0200 Subject: [PATCH 121/169] decrease test logging verbosity (#10761) --- .circleci/config.yml | 17 ++++++++++++++++ .github/workflows/tests-cli.yml | 22 ++++++++++++++++++--- .github/workflows/tests-podman.yml | 15 ++++++++++++++ .github/workflows/tests-pro-integration.yml | 18 +++++++++++++++-- .github/workflows/tests-s3-image.yml | 15 +++++++++++++- Makefile | 21 +++++++++----------- pyproject.toml | 2 +- tests/conftest.py | 1 - 8 files changed, 91 insertions(+), 20 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 51c238d067410..113e17260ca3e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,6 +7,9 @@ parameters: ubuntu-arm64-machine-image: type: string default: "ubuntu-2204:2023.02.1" + PYTEST_LOGLEVEL: + type: string + default: "WARNING" skip_test_selection: type: boolean default: false @@ -166,6 +169,8 @@ jobs: acceptance-tests: executor: ubuntu-machine-amd64 working_directory: /tmp/workspace/repo + environment: + PYTEST_LOGLEVEL: << pipeline.parameters.PYTEST_LOGLEVEL >> steps: - attach_workspace: at: /tmp/workspace @@ -194,6 +199,8 @@ jobs: itest-sfn-legacy-provider: executor: ubuntu-machine-amd64 working_directory: /tmp/workspace/repo + environment: + PYTEST_LOGLEVEL: << pipeline.parameters.PYTEST_LOGLEVEL >> steps: - attach_workspace: at: /tmp/workspace @@ -220,6 +227,8 @@ jobs: itest-s3-v2-legacy-provider: executor: ubuntu-machine-amd64 working_directory: /tmp/workspace/repo + environment: + PYTEST_LOGLEVEL: << pipeline.parameters.PYTEST_LOGLEVEL >> steps: - attach_workspace: at: /tmp/workspace @@ -246,6 +255,8 @@ jobs: itest-cloudwatch-v2-provider: executor: ubuntu-machine-amd64 working_directory: /tmp/workspace/repo + environment: + PYTEST_LOGLEVEL: << pipeline.parameters.PYTEST_LOGLEVEL >> steps: - attach_workspace: at: /tmp/workspace @@ -272,6 +283,8 @@ jobs: itest-events-v2-provider: executor: ubuntu-machine-amd64 working_directory: /tmp/workspace/repo + environment: + PYTEST_LOGLEVEL: << pipeline.parameters.PYTEST_LOGLEVEL >> steps: - attach_workspace: at: /tmp/workspace @@ -347,6 +360,8 @@ jobs: resource_class: << parameters.resource_class >> working_directory: /tmp/workspace/repo parallelism: 4 + environment: + PYTEST_LOGLEVEL: << pipeline.parameters.PYTEST_LOGLEVEL >> steps: - attach_workspace: at: /tmp/workspace @@ -402,6 +417,8 @@ jobs: bootstrap-tests: executor: ubuntu-machine-amd64 working_directory: /tmp/workspace/repo + environment: + PYTEST_LOGLEVEL: << pipeline.parameters.PYTEST_LOGLEVEL >> steps: - attach_workspace: at: /tmp/workspace diff --git a/.github/workflows/tests-cli.yml b/.github/workflows/tests-cli.yml index d103b11159815..e690a27a5a2ce 100644 --- a/.github/workflows/tests-cli.yml +++ b/.github/workflows/tests-cli.yml @@ -1,6 +1,17 @@ name: CLI Tests on: workflow_dispatch: + inputs: + PYTEST_LOGLEVEL: + type: choice + description: Loglevel for PyTest + options: + - DEBUG + - INFO + - WARNING + - ERROR + - CRITICAL + default: WARNING pull_request: paths: - '**' @@ -47,6 +58,10 @@ concurrency: cancel-in-progress: true env: + # Enable colors in tests running outside of docker (not in a tty - https://github.com/pytest-dev/pytest/pull/7462) + PY_COLORS: "1" + # Configure PyTest log level + PYTEST_LOGLEVEL: "${{ inputs.PYTEST_LOGLEVEL || 'WARNING' }}" # Set non-job-specific environment variables for pytest-tinybird TINYBIRD_URL: https://api.tinybird.co TINYBIRD_DATASOURCE: community_tests_cli @@ -79,14 +94,15 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install CLI test dependencies run: | - python -m pip install --upgrade pip wheel setuptools + make venv + source .venv/bin/activate pip install -e . pip install pytest pytest-tinybird - name: Run CLI tests env: PYTEST_ADDOPTS: "${{ env.TINYBIRD_PYTEST_ARGS }}-p no:localstack.testing.pytest.fixtures -p no:localstack_snapshot.pytest.snapshot -p no:localstack.testing.pytest.filters -p no:localstack.testing.pytest.fixture_conflicts -p no:localstack.testing.pytest.validation_tracking -p no:localstack.testing.pytest.path_filter -p no:tests.fixtures -s" - run: | - python -m pytest tests/cli/ + TEST_PATH: "tests/cli/" + run: make test push-to-tinybird: if: always() && github.ref == 'refs/heads/master' diff --git a/.github/workflows/tests-podman.yml b/.github/workflows/tests-podman.yml index 0d9675f765633..d5165502e85e4 100644 --- a/.github/workflows/tests-podman.yml +++ b/.github/workflows/tests-podman.yml @@ -2,8 +2,23 @@ name: Podman Docker Client Tests on: workflow_dispatch: + inputs: + PYTEST_LOGLEVEL: + type: choice + description: Loglevel for PyTest + options: + - DEBUG + - INFO + - WARNING + - ERROR + - CRITICAL + default: WARNING env: + # Enable colors in tests running outside of docker (not in a tty - https://github.com/pytest-dev/pytest/pull/7462) + PY_COLORS: "1" + # Configure PyTest log level + PYTEST_LOGLEVEL: "${{ inputs.PYTEST_LOGLEVEL || 'WARNING' }}" # Set non-job-specific environment variables for pytest-tinybird TINYBIRD_URL: https://api.tinybird.co TINYBIRD_DATASOURCE: community_tests_podman diff --git a/.github/workflows/tests-pro-integration.yml b/.github/workflows/tests-pro-integration.yml index d2aa12a7117ac..f47388e4b4b72 100644 --- a/.github/workflows/tests-pro-integration.yml +++ b/.github/workflows/tests-pro-integration.yml @@ -32,6 +32,16 @@ on: description: 'LocalStack Pro Ref' required: false type: string + PYTEST_LOGLEVEL: + type: choice + description: Loglevel for PyTest + options: + - DEBUG + - INFO + - WARNING + - ERROR + - CRITICAL + default: WARNING pull_request: paths: - '**' @@ -82,6 +92,10 @@ concurrency: cancel-in-progress: true env: + # Enable colors in tests running outside of docker (not in a tty - https://github.com/pytest-dev/pytest/pull/7462) + PY_COLORS: "1" + # Configure PyTest log level + PYTEST_LOGLEVEL: "${{ inputs.PYTEST_LOGLEVEL || 'WARNING' }}" # Set non-job-specific environment variables for pytest-tinybird TINYBIRD_URL: https://api.tinybird.co TINYBIRD_DATASOURCE: community_tests_pro_integration @@ -316,9 +330,9 @@ jobs: AWS_SECRET_ACCESS_KEY: "test" AWS_ACCESS_KEY_ID: "test" AWS_DEFAULT_REGION: "us-east-1" - PYTEST_LOGLEVEL: debug TEST_PATH: "../localstack/tests/aws/" # TODO: also include tests/integration/ - PYTEST_ARGS: "${{ env.TINYBIRD_PYTEST_ARGS }}${{ env.TESTSELECTION_PYTEST_ARGS }}--splits 2 --group ${{ matrix.group }} --durations-path ../localstack/.test_durations --capture=no --reruns 2 --junitxml=pytest-junit-community-${{ matrix.group }}.xml" + JUNIT_REPORTS_FILE: "pytest-junit-community-${{ matrix.group }}.xml" + PYTEST_ARGS: "${{ env.TINYBIRD_PYTEST_ARGS }}${{ env.TESTSELECTION_PYTEST_ARGS }}--splits 2 --group ${{ matrix.group }} --durations-path ../localstack/.test_durations" working-directory: localstack-ext run: | # Remove the host tmp folder (might contain remnant files with different permissions) diff --git a/.github/workflows/tests-s3-image.yml b/.github/workflows/tests-s3-image.yml index 117e534ec2a21..65b44a47a0b22 100644 --- a/.github/workflows/tests-s3-image.yml +++ b/.github/workflows/tests-s3-image.yml @@ -44,6 +44,16 @@ on: required: false type: boolean default: false + PYTEST_LOGLEVEL: + type: choice + description: Loglevel for PyTest + options: + - DEBUG + - INFO + - WARNING + - ERROR + - CRITICAL + default: WARNING # Only one pull-request triggered run should be executed at a time # (head_ref is only set for PR events, otherwise fallback to run_id which differs for every run). @@ -52,6 +62,10 @@ concurrency: cancel-in-progress: true env: + # Enable colors in tests running outside of docker (not in a tty - https://github.com/pytest-dev/pytest/pull/7462) + PY_COLORS: "1" + # Configure PyTest log level + PYTEST_LOGLEVEL: "${{ inputs.PYTEST_LOGLEVEL || 'WARNING' }}" # Set non-job-specific environment variables for pytest-tinybird TINYBIRD_URL: https://api.tinybird.co TINYBIRD_DATASOURCE: community_tests_s3_image @@ -108,7 +122,6 @@ jobs: PYTEST_ARGS: "${{ env.TINYBIRD_PYTEST_ARGS }}-o junit_family=legacy --junitxml=target/pytest-junit-s3-image-${{ matrix.arch }}.xml" TEST_PATH: "tests/aws/services/s3" DEBUG: 1 - run: | mkdir target make docker-run-tests-s3-only diff --git a/Makefile b/Makefile index 7c668725e8968..6fe566fb0f0a5 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,8 @@ VENV_BIN ?= python3 -m venv VENV_DIR ?= .venv PIP_CMD ?= pip3 TEST_PATH ?= . -PYTEST_LOGLEVEL ?= +TEST_EXEC ?= python -m +PYTEST_LOGLEVEL ?= warning DISABLE_BOTO_RETRIES ?= 1 MAIN_CONTAINER_NAME ?= localstack-main @@ -187,14 +188,14 @@ docker-create-push-manifests: ## Create and push manifests for a docker image (d docker-run-tests: ## Initializes the test environment and runs the tests in a docker container docker run -e LOCALSTACK_INTERNAL_TEST_COLLECT_METRIC=1 --entrypoint= -v `pwd`/requirements-test.txt:/opt/code/localstack/requirements-test.txt -v `pwd`/tests/:/opt/code/localstack/tests/ -v `pwd`/target/:/opt/code/localstack/target/ -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/localstack:/var/lib/localstack \ $(IMAGE_NAME) \ - bash -c "make install-test && DEBUG=$(DEBUG) PYTEST_LOGLEVEL=debug PYTEST_ARGS='$(PYTEST_ARGS)' COVERAGE_FILE='$(COVERAGE_FILE)' TEST_PATH='$(TEST_PATH)' LAMBDA_IGNORE_ARCHITECTURE=1 LAMBDA_INIT_POST_INVOKE_WAIT_MS=50 TINYBIRD_PYTEST_ARGS='$(TINYBIRD_PYTEST_ARGS)' TINYBIRD_DATASOURCE='$(TINYBIRD_DATASOURCE)' TINYBIRD_TOKEN='$(TINYBIRD_TOKEN)' TINYBIRD_URL='$(TINYBIRD_URL)' CI_COMMIT_BRANCH='$(CI_COMMIT_BRANCH)' CI_COMMIT_SHA='$(CI_COMMIT_SHA)' CI_JOB_URL='$(CI_JOB_URL)' CI_JOB_NAME='$(CI_JOB_NAME)' CI_JOB_ID='$(CI_JOB_ID)' CI='$(CI)' TEST_AWS_REGION_NAME='${TEST_AWS_REGION_NAME}' TEST_AWS_ACCESS_KEY_ID='${TEST_AWS_ACCESS_KEY_ID}' TEST_AWS_ACCOUNT_ID='${TEST_AWS_ACCOUNT_ID}' make test-coverage" + bash -c "make install-test && DEBUG=$(DEBUG) PY_COLORS=1 PYTEST_LOGLEVEL=$(PYTEST_LOGLEVEL) PYTEST_ARGS='$(PYTEST_ARGS)' COVERAGE_FILE='$(COVERAGE_FILE)' TEST_PATH='$(TEST_PATH)' LAMBDA_IGNORE_ARCHITECTURE=1 LAMBDA_INIT_POST_INVOKE_WAIT_MS=50 TINYBIRD_PYTEST_ARGS='$(TINYBIRD_PYTEST_ARGS)' TINYBIRD_DATASOURCE='$(TINYBIRD_DATASOURCE)' TINYBIRD_TOKEN='$(TINYBIRD_TOKEN)' TINYBIRD_URL='$(TINYBIRD_URL)' CI_COMMIT_BRANCH='$(CI_COMMIT_BRANCH)' CI_COMMIT_SHA='$(CI_COMMIT_SHA)' CI_JOB_URL='$(CI_JOB_URL)' CI_JOB_NAME='$(CI_JOB_NAME)' CI_JOB_ID='$(CI_JOB_ID)' CI='$(CI)' TEST_AWS_REGION_NAME='${TEST_AWS_REGION_NAME}' TEST_AWS_ACCESS_KEY_ID='${TEST_AWS_ACCESS_KEY_ID}' TEST_AWS_ACCOUNT_ID='${TEST_AWS_ACCOUNT_ID}' make test-coverage" docker-run-tests-s3-only: ## Initializes the test environment and runs the tests in a docker container for the S3 only image # TODO: We need node as it's a dependency of the InfraProvisioner at import time, remove when we do not need it anymore # g++ is a workaround to fix the JPype1 compile error on ARM Linux "gcc: fatal error: cannot execute ‘cc1plus’" because the test dependencies include the runtime dependencies. docker run -e LOCALSTACK_INTERNAL_TEST_COLLECT_METRIC=1 --entrypoint= -v `pwd`/requirements-test.txt:/opt/code/localstack/requirements-test.txt -v `pwd`/tests/:/opt/code/localstack/tests/ -v `pwd`/target/:/opt/code/localstack/target/ -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/localstack:/var/lib/localstack \ $(IMAGE_NAME) \ - bash -c "apt-get update && apt-get install -y g++ && make install-test && apt-get install -y --no-install-recommends gnupg && mkdir -p /etc/apt/keyrings && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main\" > /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install -y --no-install-recommends nodejs && DEBUG=$(DEBUG) PYTEST_LOGLEVEL=debug PYTEST_ARGS='$(PYTEST_ARGS)' TEST_PATH='$(TEST_PATH)' TINYBIRD_PYTEST_ARGS='$(TINYBIRD_PYTEST_ARGS)' TINYBIRD_DATASOURCE='$(TINYBIRD_DATASOURCE)' TINYBIRD_TOKEN='$(TINYBIRD_TOKEN)' TINYBIRD_URL='$(TINYBIRD_URL)' CI_COMMIT_BRANCH='$(CI_COMMIT_BRANCH)' CI_COMMIT_SHA='$(CI_COMMIT_SHA)' CI_JOB_URL='$(CI_JOB_URL)' CI_JOB_NAME='$(CI_JOB_NAME)' CI_JOB_ID='$(CI_JOB_ID)' CI='$(CI)' make test" + bash -c "apt-get update && apt-get install -y g++ && make install-test && apt-get install -y --no-install-recommends gnupg && mkdir -p /etc/apt/keyrings && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main\" > /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install -y --no-install-recommends nodejs && DEBUG=$(DEBUG) PY_COLORS=1 PYTEST_LOGLEVEL=$(PYTEST_LOGLEVEL) PYTEST_ARGS='$(PYTEST_ARGS)' TEST_PATH='$(TEST_PATH)' TINYBIRD_PYTEST_ARGS='$(TINYBIRD_PYTEST_ARGS)' TINYBIRD_DATASOURCE='$(TINYBIRD_DATASOURCE)' TINYBIRD_TOKEN='$(TINYBIRD_TOKEN)' TINYBIRD_URL='$(TINYBIRD_URL)' CI_COMMIT_BRANCH='$(CI_COMMIT_BRANCH)' CI_COMMIT_SHA='$(CI_COMMIT_SHA)' CI_JOB_URL='$(CI_JOB_URL)' CI_JOB_NAME='$(CI_JOB_NAME)' CI_JOB_ID='$(CI_JOB_ID)' CI='$(CI)' make test" docker-run: ## Run Docker image locally @@ -211,15 +212,11 @@ docker-cp-coverage: docker rm -v $$id test: ## Run automated tests - ($(VENV_RUN); DEBUG=$(DEBUG) DISABLE_BOTO_RETRIES=$(DISABLE_BOTO_RETRIES) pytest --durations=10 --log-cli-level=$(PYTEST_LOGLEVEL) -s $(PYTEST_ARGS) $(TEST_PATH)) - -test-coverage: ## Run automated tests and create coverage report - ($(VENV_RUN); python -m coverage --version; \ - DEBUG=$(DEBUG) \ - DISABLE_BOTO_RETRIES=$(DISABLE_BOTO_RETRIES) \ - LOCALSTACK_INTERNAL_TEST_COLLECT_METRIC=1 \ - python -m coverage run $(COVERAGE_ARGS) -m \ - pytest --durations=10 --log-cli-level=$(PYTEST_LOGLEVEL) -s $(PYTEST_ARGS) $(TEST_PATH)) + ($(VENV_RUN); $(TEST_EXEC) pytest --durations=10 --log-cli-level=$(PYTEST_LOGLEVEL) $(PYTEST_ARGS) $(TEST_PATH)) + +test-coverage: LOCALSTACK_INTERNAL_TEST_COLLECT_METRIC = 1 +test-coverage: TEST_EXEC = python -m coverage run $(COVERAGE_ARGS) -m +test-coverage: test ## Run automated tests and create coverage report test-docker: DOCKER_FLAGS="--entrypoint= $(DOCKER_FLAGS)" CMD="make test" make docker-run diff --git a/pyproject.toml b/pyproject.toml index 246449ec8cf67..66506fad12b9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -225,7 +225,7 @@ exclude_lines = [ ] [tool.pytest.ini_options] -log_cli = true +log_cli = false log_level = "DEBUG" log_cli_format = "%(asctime)s.%(msecs)03d %(levelname)5s --- [%(threadName)12s] %(name)-26s : %(message)s" log_cli_date_format = "%Y-%m-%dT%H:%M:%S" diff --git a/tests/conftest.py b/tests/conftest.py index f8c1b1fa2fe27..86d0442815a12 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,6 @@ "localstack_snapshot.pytest.snapshot", "localstack.testing.pytest.filters", "localstack.testing.pytest.fixture_conflicts", - "localstack.testing.pytest.detect_thread_leakage", "localstack.testing.pytest.marking", "localstack.testing.pytest.marker_report", "localstack.testing.pytest.in_memory_localstack", From 9d0093aa1ac32f1959cf1386d5e58a7bfb97f7c3 Mon Sep 17 00:00:00 2001 From: Bernhard Matyas <90144234+baermat@users.noreply.github.com> Date: Tue, 7 May 2024 17:12:10 +0200 Subject: [PATCH 122/169] fix serialization for sqs http calls (#10732) --- localstack/aws/protocol/serializer.py | 27 ++++++---- tests/aws/services/sqs/test_sqs.py | 50 +++++++++++++++++++ .../aws/services/sqs/test_sqs.validation.json | 9 ++++ 3 files changed, 75 insertions(+), 11 deletions(-) diff --git a/localstack/aws/protocol/serializer.py b/localstack/aws/protocol/serializer.py index f7631360175ef..c2b80a9ab38d9 100644 --- a/localstack/aws/protocol/serializer.py +++ b/localstack/aws/protocol/serializer.py @@ -1642,23 +1642,28 @@ def _default_serialize(self, xmlnode: ETree.Element, params: str, _, name: str, node.text = ( str(params) .replace('"', '__marker__"__marker__') - .replace("\r", "__marker__\r__marker__") + .replace("\r", "__marker__-r__marker__") ) def _node_to_string(self, root: Optional[ETree.ElementTree], mime_type: str) -> Optional[str]: """Replaces the previously "marked" characters with their encoded value.""" generated_string = super()._node_to_string(root, mime_type) - return ( - to_bytes( - to_str(generated_string) - # Undo the second escaping of the & - .replace('__marker__"__marker__', """) - # Undo the second escaping of the carriage return (\r) - .replace("__marker__\r__marker__", " ") + if generated_string is None: + return None + generated_string = to_str(generated_string) + # Undo the second escaping of the & + # Undo the second escaping of the carriage return (\r) + if mime_type == APPLICATION_JSON: + # At this point the json was already dumped and escaped, so we replace directly. + generated_string = generated_string.replace(r"__marker__\"__marker__", r"\"").replace( + "__marker__-r__marker__", r"\r" ) - if generated_string is not None - else None - ) + else: + generated_string = generated_string.replace('__marker__"__marker__', """).replace( + "__marker__-r__marker__", " " + ) + + return to_bytes(generated_string) def _add_error_tags( self, error: ServiceException, error_tag: ETree.Element, mime_type: str diff --git a/tests/aws/services/sqs/test_sqs.py b/tests/aws/services/sqs/test_sqs.py index 0f14951160182..8a1fbe0cc586b 100644 --- a/tests/aws/services/sqs/test_sqs.py +++ b/tests/aws/services/sqs/test_sqs.py @@ -1334,6 +1334,56 @@ def test_external_hostname_via_host_header(self, monkeypatch, sqs_create_queue, kwargs = {"flags": re.MULTILINE | re.DOTALL} assert re.match(rf".*<QueueUrl>\s*{url}/[^<]+</QueueUrl>.*", content, **kwargs) + @pytest.mark.parametrize( + argnames="json_body", + argvalues=['{"foo": "ba\rr", "foo2": "ba"r""}', json.dumps('{"foo": "ba\rr"}')], + ) + @markers.aws.validated + def test_marker_serialization_json_protocol( + self, sqs_create_queue, aws_client, aws_http_client_factory, json_body + ): + queue_name = f"queue-{short_uid()}" + queue_url = sqs_create_queue(QueueName=queue_name) + message_body = json_body + aws_client.sqs.send_message(QueueUrl=queue_name, MessageBody=message_body) + + client = aws_http_client_factory("sqs", region="us-east-1") + + if is_aws_cloud(): + endpoint_url = "https://queue.amazonaws.com" + else: + endpoint_url = config.internal_service_url() + + response = client.get( + endpoint_url, + params={ + "Action": "ReceiveMessage", + "QueueUrl": queue_url, + "Version": "2012-11-05", + "VisibilityTimeout": "0", + }, + headers={"Accept": "application/json"}, + ) + + parsed_content = json.loads(response.content.decode("utf-8")) + + if is_aws_cloud(): + assert ( + parsed_content["ReceiveMessageResponse"]["ReceiveMessageResult"]["messages"][0][ + "Body" + ] + == message_body + ) + + # TODO: this is an error in LocalStack. Usually it should be messages[0]['Body'] + else: + assert ( + parsed_content["ReceiveMessageResponse"]["ReceiveMessageResult"]["Message"]["Body"] + == message_body + ) + client_receive_response = aws_client.sqs.receive_message(QueueUrl=queue_url) + assert client_receive_response["Messages"][0]["Body"] == message_body + @markers.aws.only_localstack def test_external_host_via_header_complete_message_lifecycle( self, monkeypatch, account_id, region_name diff --git a/tests/aws/services/sqs/test_sqs.validation.json b/tests/aws/services/sqs/test_sqs.validation.json index 81fbf2255d88a..0b7e911c76c25 100644 --- a/tests/aws/services/sqs/test_sqs.validation.json +++ b/tests/aws/services/sqs/test_sqs.validation.json @@ -143,6 +143,15 @@ "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_list_queues": { "last_validated_date": "2024-04-30T13:39:55+00:00" }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_marker_serialization_json_protocol[\"{\\\\\"foo\\\\\": \\\\\"ba\\\\rr\\\\\"}\"]": { + "last_validated_date": "2024-05-07T13:33:39+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_marker_serialization_json_protocol[{\"foo\": \"ba\\rr\", \"foo2\": \"ba"r"\"}]": { + "last_validated_date": "2024-05-07T13:33:34+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_marker_serialization_query_protocol": { + "last_validated_date": "2024-04-29T06:07:04+00:00" + }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_deduplication_id_too_long": { "last_validated_date": "2024-04-30T13:35:34+00:00" }, From a51874c58af2738c29d11640ff5d7eee31a9d22c Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Tue, 7 May 2024 17:30:57 +0200 Subject: [PATCH 123/169] fix CircleCI test error reporting by disabling colored reporting (#10786) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6fe566fb0f0a5..a3c90aa492113 100644 --- a/Makefile +++ b/Makefile @@ -188,7 +188,7 @@ docker-create-push-manifests: ## Create and push manifests for a docker image (d docker-run-tests: ## Initializes the test environment and runs the tests in a docker container docker run -e LOCALSTACK_INTERNAL_TEST_COLLECT_METRIC=1 --entrypoint= -v `pwd`/requirements-test.txt:/opt/code/localstack/requirements-test.txt -v `pwd`/tests/:/opt/code/localstack/tests/ -v `pwd`/target/:/opt/code/localstack/target/ -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/localstack:/var/lib/localstack \ $(IMAGE_NAME) \ - bash -c "make install-test && DEBUG=$(DEBUG) PY_COLORS=1 PYTEST_LOGLEVEL=$(PYTEST_LOGLEVEL) PYTEST_ARGS='$(PYTEST_ARGS)' COVERAGE_FILE='$(COVERAGE_FILE)' TEST_PATH='$(TEST_PATH)' LAMBDA_IGNORE_ARCHITECTURE=1 LAMBDA_INIT_POST_INVOKE_WAIT_MS=50 TINYBIRD_PYTEST_ARGS='$(TINYBIRD_PYTEST_ARGS)' TINYBIRD_DATASOURCE='$(TINYBIRD_DATASOURCE)' TINYBIRD_TOKEN='$(TINYBIRD_TOKEN)' TINYBIRD_URL='$(TINYBIRD_URL)' CI_COMMIT_BRANCH='$(CI_COMMIT_BRANCH)' CI_COMMIT_SHA='$(CI_COMMIT_SHA)' CI_JOB_URL='$(CI_JOB_URL)' CI_JOB_NAME='$(CI_JOB_NAME)' CI_JOB_ID='$(CI_JOB_ID)' CI='$(CI)' TEST_AWS_REGION_NAME='${TEST_AWS_REGION_NAME}' TEST_AWS_ACCESS_KEY_ID='${TEST_AWS_ACCESS_KEY_ID}' TEST_AWS_ACCOUNT_ID='${TEST_AWS_ACCOUNT_ID}' make test-coverage" + bash -c "make install-test && DEBUG=$(DEBUG) PYTEST_LOGLEVEL=$(PYTEST_LOGLEVEL) PYTEST_ARGS='$(PYTEST_ARGS)' COVERAGE_FILE='$(COVERAGE_FILE)' TEST_PATH='$(TEST_PATH)' LAMBDA_IGNORE_ARCHITECTURE=1 LAMBDA_INIT_POST_INVOKE_WAIT_MS=50 TINYBIRD_PYTEST_ARGS='$(TINYBIRD_PYTEST_ARGS)' TINYBIRD_DATASOURCE='$(TINYBIRD_DATASOURCE)' TINYBIRD_TOKEN='$(TINYBIRD_TOKEN)' TINYBIRD_URL='$(TINYBIRD_URL)' CI_COMMIT_BRANCH='$(CI_COMMIT_BRANCH)' CI_COMMIT_SHA='$(CI_COMMIT_SHA)' CI_JOB_URL='$(CI_JOB_URL)' CI_JOB_NAME='$(CI_JOB_NAME)' CI_JOB_ID='$(CI_JOB_ID)' CI='$(CI)' TEST_AWS_REGION_NAME='${TEST_AWS_REGION_NAME}' TEST_AWS_ACCESS_KEY_ID='${TEST_AWS_ACCESS_KEY_ID}' TEST_AWS_ACCOUNT_ID='${TEST_AWS_ACCOUNT_ID}' make test-coverage" docker-run-tests-s3-only: ## Initializes the test environment and runs the tests in a docker container for the S3 only image # TODO: We need node as it's a dependency of the InfraProvisioner at import time, remove when we do not need it anymore From 375bc7162bcf635d607f83ef129bc84a38418415 Mon Sep 17 00:00:00 2001 From: Harsh Mishra <erbeusgriffincasper@gmail.com> Date: Wed, 8 May 2024 12:52:49 +0530 Subject: [PATCH 124/169] migrate contributing docs to core (#10775) Co-authored-by: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> --- .github/workflows/tests-cli.yml | 4 +- .github/workflows/tests-pro-integration.yml | 4 +- CODEOWNERS | 2 +- README.md | 6 +- CONTRIBUTING.md => docs/CONTRIBUTING.md | 14 + docs/development-environment-setup/index.md | 105 ++++++++ .../end_user_license_agreement/README.md | 0 docs/integration-tests/index.md | 168 ++++++++++++ .../asf-code-generation.png | Bin 0 -> 325785 bytes docs/localstack-concepts/gateway-overview.png | Bin 0 -> 88077 bytes docs/localstack-concepts/index.md | 248 ++++++++++++++++++ .../localstack-handler-chain.png | Bin 0 -> 266044 bytes .../service-implementation.png | Bin 0 -> 260436 bytes {doc => docs}/localstack-readme-banner.svg | 0 docs/multi-account-region-testing/index.md | 66 +++++ .../randomize-aws-credentials.png | Bin 0 -> 30971 bytes docs/parity-testing/index.md | 248 ++++++++++++++++++ docs/terraform-tests/index.md | 3 + 18 files changed, 860 insertions(+), 8 deletions(-) rename CONTRIBUTING.md => docs/CONTRIBUTING.md (72%) create mode 100644 docs/development-environment-setup/index.md rename {doc => docs}/end_user_license_agreement/README.md (100%) create mode 100644 docs/integration-tests/index.md create mode 100644 docs/localstack-concepts/asf-code-generation.png create mode 100644 docs/localstack-concepts/gateway-overview.png create mode 100644 docs/localstack-concepts/index.md create mode 100644 docs/localstack-concepts/localstack-handler-chain.png create mode 100644 docs/localstack-concepts/service-implementation.png rename {doc => docs}/localstack-readme-banner.svg (100%) create mode 100644 docs/multi-account-region-testing/index.md create mode 100644 docs/multi-account-region-testing/randomize-aws-credentials.png create mode 100644 docs/parity-testing/index.md create mode 100644 docs/terraform-tests/index.md diff --git a/.github/workflows/tests-cli.yml b/.github/workflows/tests-cli.yml index e690a27a5a2ce..1fbf8f274650c 100644 --- a/.github/workflows/tests-cli.yml +++ b/.github/workflows/tests-cli.yml @@ -17,7 +17,7 @@ on: - '**' - '!.github/**' - '.github/workflows/tests-cli.yml' - - '!doc/**' + - '!docs/**' - '!scripts/**' - '!.dockerignore' - '!.git-blame-ignore-revs' @@ -37,7 +37,7 @@ on: - '**' - '!.github/**' - '.github/workflows/tests-cli.yml' - - '!doc/**' + - '!docs/**' - '!scripts/**' - '!.dockerignore' - '!.git-blame-ignore-revs' diff --git a/.github/workflows/tests-pro-integration.yml b/.github/workflows/tests-pro-integration.yml index f47388e4b4b72..9e526a7a477f1 100644 --- a/.github/workflows/tests-pro-integration.yml +++ b/.github/workflows/tests-pro-integration.yml @@ -47,7 +47,7 @@ on: - '**' - '!.github/**' - '.github/workflows/tests-pro-integration.yml' - - '!doc/**' + - '!docs/**' - '!scripts/**' - './scripts/build_common_test_functions.sh' - '!.dockerignore' @@ -71,7 +71,7 @@ on: - '**' - '!.github/**' - '.github/workflows/tests-pro-integration.yml' - - '!doc/**' + - '!docs/**' - '!scripts/**' - './scripts/build_common_test_functions.sh' - '!.dockerignore' diff --git a/CODEOWNERS b/CODEOWNERS index d8bb2201ccdf5..b30ed8ef1503d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -6,7 +6,7 @@ /CODEOWNERS @thrau @dominikschubert @alexrashed # README / Docs -/doc/ @HarshCasper +/docs/ @thrau @HarshCasper /README.md @HarshCasper /CODE_OF_CONDUCT.md @HarshCasper /CONTRIBUTING.md @HarshCasper diff --git a/README.md b/README.md index c85ad0e4628c3..e141a7789e1d7 100644 --- a/README.md +++ b/README.md @@ -164,8 +164,8 @@ Please refer to [GitHub releases](https://github.com/localstack/localstack/relea If you are interested in contributing to LocalStack: -- Start by reading our [contributing guide](CONTRIBUTING.md). -- Check out our [developer guide](https://docs.localstack.cloud/contributing/). +- Start by reading our [contributing guide](docs/CONTRIBUTING.md). +- Check out our [development environment setup guide](docs/development-environment-setup.md). - Navigate our codebase and [open issues](https://github.com/localstack/localstack/issues). We are thankful for all the contributions and feedback we receive. @@ -215,4 +215,4 @@ Copyright (c) 2017-2024 LocalStack maintainers and contributors. Copyright (c) 2016 Atlassian and others. -This version of LocalStack is released under the Apache License, Version 2.0 (see [LICENSE](LICENSE.txt)). By downloading and using this software you agree to the [End-User License Agreement (EULA)](doc/end_user_license_agreement). To know about the external software we use, look at our [third party software tools](doc/third-party-software-tools/README.md) page. +This version of LocalStack is released under the Apache License, Version 2.0 (see [LICENSE](LICENSE.txt)). By downloading and using this software you agree to the [End-User License Agreement (EULA)](docs/end_user_license_agreement). diff --git a/CONTRIBUTING.md b/docs/CONTRIBUTING.md similarity index 72% rename from CONTRIBUTING.md rename to docs/CONTRIBUTING.md index 978960d692573..d4683b64937f7 100644 --- a/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,5 +1,19 @@ # Contributing +We welcome contributions to LocalStack! Please refer to the following sections to better understand how LocalStack works internally, how to set up local development environments, and how to contribute to the codebase. + +## Sections + +- [Contribution Guidelines](#contribution-guidelines) +- [Development Environment Setup](development-environment-setup/index.md) +- [LocalStack Concepts](localstack-concepts/index.md) +- [Integration Tests](integration-tests/index.md) +- [Parity Testing](parity-testing/index.md) +- [Multi-account and Multi-region Testing](multi-account-region-testing/index.md) +- [Terraform Tests](terraform-tests/index.md) + +## Contribution Guidelines + We welcome feedback, bug reports, and pull requests! For pull requests (PRs), please stick to the following guidelines: diff --git a/docs/development-environment-setup/index.md b/docs/development-environment-setup/index.md new file mode 100644 index 0000000000000..19d3127701981 --- /dev/null +++ b/docs/development-environment-setup/index.md @@ -0,0 +1,105 @@ +# Development Environment Setup + +Before you get started with contributing to LocalStack, make sure you’ve familiarized yourself with LocalStack from the perspective of a user. +You can follow our [getting started guide](https://docs.localstack.cloud/get-started/). +Once LocalStack runs in your Docker environment and you’ve played around with the LocalStack and `awslocal` CLI, you can move forward to set up your developer environment. + +## Development requirements + +You will need the following tools for the local development of LocalStack. + +* [Python 3.11+](https://www.python.org/downloads/) and `pip` +* [Node.js & npm](https://nodejs.org/en/download/) +* [Docker](https://docs.docker.com/desktop/) + +We recommend you to individually install the above tools using your favorite package manager. +For example, on macOS, you can use [Homebrew](https://brew.sh/) to install the above tools. + +### Setting up the Development Environment + +To make contributions to LocalStack, you need to be able to run LocalStack in host mode from your IDE, and be able to attach a debugger to the running LocalStack instance. +We have a basic tutorial to cover how you can do that. + +The basic steps include: + +1. Fork the localstack repository on GitHub [https://github.com/localstack/localstack/](https://github.com/localstack/localstack/) +2. Clone the forked localstack repository `git clone git@github.com:<GITHUB_USERNAME>/localstack.git` +3. Ensure you have `python`, `pip`, `node`, and `npm` installed. +> [!NOTE] +> You might also need `java` for some emulated services. +4. Install the Python dependencies using `make install`. +> [!NOTE] +> This will install the required pip dependencies in a local Python 3 `venv` directory called `.venv` (your global Python packages will remain untouched). +> Depending on your system, some `pip` modules may require additional native libs installed. +5. Start localstack in host mode using `make start` + +<div align="left"> + <a href="https://youtu.be/XHLBy6VKuCM"> + <img src="https://img.youtube.com/vi/XHLBy6VKuCM/0.jpg" style="width:100%;"> + </a> +</div> + +### Building the Docker image for Development + +We generally recommend using this command to build the `localstack/localstack` Docker image locally (works on Linux/MacOS): + +```bash +make docker-build +``` + +### Additional Dependencies for running LocalStack in Host Mode + +In host mode, additional dependencies (e.g., Java) are required for developing certain AWS-emulated services (e.g., DynamoDB). +The required dependencies vary depending on the service, [Configuration](https://docs.localstack.cloud/references/configuration/), operating system, and system architecture (i.e., x86 vs ARM). +Refer to our official [Dockerfile](https://github.com/localstack/localstack/blob/master/Dockerfile) and our [package installer LPM](Concepts/index.md#packages-and-installers) for more details. + +#### Python Dependencies + +* [JPype1](https://pypi.org/project/JPype1/) might require `g++` to fix a compile error on ARM Linux `gcc: fatal error: cannot execute ‘cc1plus’` + * Used in EventBridge, EventBridge Pipes, and Lambda Event Source Mapping for a Java-based event ruler via the opt-in configuration `EVENT_RULE_ENGINE=java` + * Introduced in [#10615](https://github.com/localstack/localstack/pull/10615) + +#### Test Dependencies + +* Node.js is required for running LocalStack tests because the test fixture for CDK-based tests needs Node.js + +#### DynamoDB + +* [OpenJDK](https://openjdk.org/install/) + +#### Kinesis + +* [NodeJS & npm](https://nodejs.org/en/download/) + +#### Lambda + +* macOS users need to configure `LAMBDA_DEV_PORT_EXPOSE=1` such that the host can reach Lambda containers via IPv4 in bridge mode (see [#7367](https://github.com/localstack/localstack/pull/7367)). + +#### EVENT_RULE_ENGINE=java + +* Requires Java to execute to invoke the AWS [event-ruler](https://github.com/aws/event-ruler) using [JPype](https://github.com/jpype-project/jpype), a Python to Java bridge. +* Set `JAVA_HOME` to a JDK installation. For example: `JAVA_HOME=/opt/homebrew/Cellar/openjdk/21.0.2` + +### Changing our fork of moto + +1. Fork our moto repository on GitHub [https://github.com/localstack/moto](https://github.com/localstack/moto) +2. Clone the forked moto repository `git clone git@github.com:<GITHUB_USERNAME>/moto.git` (using the `localstack` branch) +3. Within the localstack repository, install moto in **editable** mode: + +```sh +# Assuming the following directory structure: +#. +#├── localstack +#└── moto + +cd localstack +source .venv/bin/activate + +pip install -e ../moto +``` + +### Tips + +* If `virtualenv` chooses system python installations before your pyenv installations, manually initialize `virtualenv` before running `make install`: `virtualenv -p ~/.pyenv/shims/python3.10 .venv` . +* Terraform needs version <0.14 to work currently. Use [`tfenv`](https://github.com/tfutils/tfenv) to manage Terraform versions comfortable. Quick start: `tfenv install 0.13.7 && tfenv use 0.13.7` +* Set env variable `LS_LOG='trace'` to print every `http` request sent to localstack and their responses. It is useful for debugging certain issues. diff --git a/doc/end_user_license_agreement/README.md b/docs/end_user_license_agreement/README.md similarity index 100% rename from doc/end_user_license_agreement/README.md rename to docs/end_user_license_agreement/README.md diff --git a/docs/integration-tests/index.md b/docs/integration-tests/index.md new file mode 100644 index 0000000000000..eb4ab9ea8d1df --- /dev/null +++ b/docs/integration-tests/index.md @@ -0,0 +1,168 @@ +# Integration tests + +LocalStack has an extensive set of [integration tests](https://github.com/localstack/localstack/tree/master/tests/integration). This document describes how to run and write integration tests. + +## Writing integration tests + +The following guiding principles apply to writing integration tests: + +- Tests should pass when running against AWS: + - Don't make assumptions about the time it takes to create resources. If you do asserts after creating resources, use `poll_condition`, `retry` or one of the waiters included in the boto3 library to wait for the resource to be created. + - Make sure your tests always clean up AWS resources, even if your test fails! Prefer existing factory fixtures (like `sqs_create_queue`). Introduce try/finally blocks if necessary. +- Tests should be runnable concurrently: + - Protect your tests against side effects. Example: never assert on global state that could be modified by a concurrently running test (like `assert len(sqs.list_queues()) == 1`; may not hold!). + - Make sure your tests are side-effect free. Avoid creating top-level resources with constant names. Prefer using generated unique names (like `short_uid`). +- Tests should not be clever. It should be plain to see what they are doing by looking at the test. This means avoiding creating functions, loops, or abstractions, even for repeated behavior (like groups of asserts) and instead preferring a bit of code duplication: +- Group tests logically using classes. +- Avoid injecting more than 2-3 fixtures in a test (unless you are testing complex integrations where your tests requires several different clients). +- Create factory fixtures only for top-level resources (like Queues, Topics, Lambdas, Tables). +- Avoid sleeps! Use `poll_condition`, `retry`, or `threading.Event` internally to control concurrent flows. + +We use [pytest](https://docs.pytest.org) for our testing framework. +Older tests were written using the unittest framework, but its use is discouraged. + +If your test matches the pattern `tests/integration/**/test_*.py` or `tests/aws/**/test_*.py` it will be picked up by the integration test suite. +Any test targeting one or more AWS services should go into `tests/aws/**` in the corresponding service package. +Every test in `tests/aws/**/test_*.py` must be marked by exactly one pytest marker, e.g. `@markers.aws.validated`. + +### Functional-style tests + +You can write functional style tests by defining a function with the prefix `test_` with basic asserts: + +```python +def test_something(): + assert True is not False +``` + +### Class-style tests + +Or you can write class-style tests by grouping tests that logically belong together in a class: + +```python +class TestMyThing: + def test_something(self): + assert True is not False +``` + +### Fixtures + +We use the pytest fixture concept, and provide several fixtures you can use when writing AWS tests. For example, to inject a Boto client for SQS, you can specify the `sqs_client` in your test method: + +```python +class TestMyThing: + def test_something(self, sqs_client): + assert len(sqs_client.list_queues()["QueueUrls"]) == 0 +``` + +We also provide fixtures for certain disposable resources, like buckets: + +```bash +def test_something_on_a_bucket(s3_bucket): + s3_bucket + # s3_bucket is a boto s3 bucket object that is created before + # the test runs, and removed after it returns. +``` + +Another pattern we use is the [factory as fixture](https://docs.pytest.org/en/6.2.x/fixture.html#factories-as-fixtures) pattern. + +```bash +def test_something_on_multiple_buckets(s3_create_bucket): + bucket1 = s3_create_bucket() + bucket2 = s3_create_bucket() + # both buckets will be deleted after the test returns +``` + +You can find the list of available fixtures in the [fixtures.py](https://github.com/localstack/localstack/blob/master/localstack/testing/pytest/fixtures.py). + + +## Running the test suite + +To run the tests you can use the make target and set the `TEST_PATH` variable. + +```bash +TEST_PATH="tests/integration" make test +``` + +or run it manually within the virtual environment: + +```bash +python -m pytest --log-cli-level=INFO tests/integration +``` + +### Running individual tests + +You can further specify the file and test class you want to run in the test path: + +```bash +TEST_PATH="tests/integration/docker/test_docker.py::TestDockerClient" make test +``` + +### Test against a running LocalStack instance + +When you run the integration tests, LocalStack is automatically started (via the pytest conftest mechanism in [tests/integration/conftest.py](https://github.com/localstack/localstack/blob/master/tests/integration/conftest.py)). +You can disable this behavior by setting the environment variable `TEST_SKIP_LOCALSTACK_START=1`. + +### Test against Amazon Web Services + +Ideally every integration is tested against real AWS. To run the integration tests, we prefer you to use an AWS sandbox account, so that you don't accidentally run tests against your production account. + +#### Creating an AWS sandbox account + +1. Login with your credentials into your AWS Sandbox Account with `AWSAdministratorAccess`. +2. Type in **IAM** in the top bar and navigate to the **IAM** service +3. Navigate to `Users` and create a new user (**Add Users**) + 1. Add the username as `localstack-testing`. + 2. Keep the **Provide user access to the AWS Management Console - optional** box unchecked. +4. Attach existing policies directly. +5. Check **AdministratorAccess** and click **Next** before **Next/Create User** until done. +6. Go to the newly created user under `IAM/Users`, go to the `Security Credentials` tab, and click on **Create access key** within the `Access Keys` section. +7. Pick the **Local code** option and check the **I understand the above recommendation and want to proceed to create an access key** box. +8. Click on **Create access key** and copy the Access Key ID and the Secret access key immediately. +9. Run `aws configure —-profile ls-sandbox` and enter the Access Key ID, and the Secret access key when prompted. +10. Verify that the profile is set up correctly by running: `aws sts get-caller-identity --profile ls-sandbox`. + +Here is how `~/.aws/credentials` should look like: + +```bash +[ls-sandbox] +aws_access_key_id = <your-key-id> +aws_secret_access_key = <your-secret-key> +``` + +The `~/.aws/config` file should look like: + +```bash +[ls-sandbox] +region=eu-central-1 +# .... you can add additional configuration options for AWS clients here +``` + +#### Running integration tests against AWS + +- Set the environment variable: `TEST_TARGET=AWS_CLOUD`. +- Use the client `fixtures` and other fixtures for resource creation instead of methods from `aws_stack.py` + - While using the environment variable `TEST_TARGET=AWS_CLOUD`, the boto client will be automatically configured to target AWS instead of LocalStack. +- Configure your AWS profile/credentials: + - When running the test, set the environment variable `AWS_PROFILE` to the profile name you chose in the previous step. Example: `AWS_PROFILE=ls-sandbox` +- Ensure that all resources are cleaned up even when the test fails and even when other fixture cleanup operations fail! +- Testing against AWS might require additional roles and policies. + +Here is how a useful environment configuration for testing against AWS could look like: + +```bash +DEBUG=1; # enables debug logging +TEST_DISABLE_RETRIES_AND_TIMEOUTS=1; +TEST_TARGET=AWS_CLOUD; +AWS_DEFAULT_REGION=us-east-1; +AWS_PROFILE=ls-sandbox +``` + +Once you're confident your test is reliably working against AWS you can add the pytest marker `@markers.aws.validated`. + +#### Create a snapshot test + +Once you verified that your test is running against AWS, you can record snapshots for the test run. A snapshot records the response from AWS and can be later on used to compare the response of LocalStack. + +Snapshot tests helps to increase the parity with AWS and to raise the confidence in the service implementations. Therefore, snapshot tests are preferred over normal integrations tests. + +Please check our subsequent guide on [Parity Testing](parity-testing.md) for a detailed explanation on how to write AWS validated snapshot tests. diff --git a/docs/localstack-concepts/asf-code-generation.png b/docs/localstack-concepts/asf-code-generation.png new file mode 100644 index 0000000000000000000000000000000000000000..c93e4301fd2af57ddf44202b5767678396aaf69b GIT binary patch literal 325785 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%0Ar}H`Fi^<DfzetE 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≫ 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 `6@ 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�OlI24ezPHzKOh!DwXVLp9*tgb(tjQzu|fE)mQ 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Ð(KZJk0a#feD*nfi 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 Pf2K^(9)C&8X#4*G4X@-^ literal 0 HcmV?d00001 diff --git a/docs/localstack-concepts/gateway-overview.png b/docs/localstack-concepts/gateway-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..4cb50fa64a24aa6225272573125dbba99e7734ff GIT binary patch literal 88077 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 OJr#W}lKxop-G2k4*tUTH literal 0 HcmV?d00001 diff --git a/docs/localstack-concepts/index.md b/docs/localstack-concepts/index.md new file mode 100644 index 0000000000000..10eac81da35d1 --- /dev/null +++ b/docs/localstack-concepts/index.md @@ -0,0 +1,248 @@ +# LocalStack Concepts + +When you first start working on LocalStack, you will most likely start working on AWS providers, either fixing bugs or adding features. In that case, you probably care mostly about [Services](#service), and, depending on the service and how it interacts with the [Gateway](#gateway), also **custom request handlers** and edge **routes**. + +If you are adding new service providers, then you’ll want to know how [Plugins](#plugins) work, and how to expose a service provider as a [service plugin](#service-plugin). This guide will give you a comprehensive overview about various core architectural concepts of LocalStack. + +## AWS Server Framework (ASF) + +AWS is essentially a Remote Procedure Call (RPC) system, and ASF is our server-side implementation of that system. The principal components of which are: + +- Service specifications +- Stub generation +- Remote objects (service implementations) +- Marshalling +- Skeleton + +### Service specifications + +AWS developed a specification language, [Smithy](https://awslabs.github.io/smithy/), which they use internally to define their APIs in a declarative way. They use these specs to generate client SDKs and client documentation. All these specifications are available, among other repositories, in the [botocore repository](https://github.com/boto/botocore/tree/develop/botocore/data). Botocore are the internals of the AWS Python SDK, which allows ASF to interpret and operate on the service specifications. Take a look at an example, [the `Invoke` operation of the `lambda` API](https://github.com/boto/botocore/blob/474e7a23d0fd178790579638cec9123d7e92d10b/botocore/data/lambda/2015-03-31/service-2.json#L564-L573): + +```bash + "Invoke":{ + "name":"Invoke", + "http":{ + "method":"POST", + "requestUri":"/2015-03-31/functions/{FunctionName}/invocations" + }, + "input":{"shape":"InvocationRequest"}, + "output":{"shape":"InvocationResponse"}, + "errors":[ + {"shape":"ServiceException"}, + ... +``` + +### Scaffold - Generating AWS API stubs + +We use these specifications to generate server-side API stubs using our scaffold script. The stubs comprise Python representations of _Shapes_ (type definitions), and an `<Service>Api` class that contains all the operations as function definitions. Notice the `@handler` decorator, which binds the function to the particular AWS operation. This is how we know where to dispatch the request to. + +<img src="asf-code-generation.png" width="800px" alt="Generating AWS API stubs via ASF" /> + +You can try it using this command in the LocalStack repository: + +```bash +python -m localstack.aws.scaffold generate <service> --save [--doc] +``` + +### Service providers + +A service provider is an implementation of an AWS service API. Service providers are the remote object in the RPC terminology. You will find the modern ASF provider implementations in `localstack/services/<service>/provider.py`. + +### Marshalling + +A server-side protocol implementation requires a marshaller (a parser for incoming requests, and a serializer for outgoing responses). + +- Our [protocol parser](https://github.com/localstack/localstack/blob/master/localstack/aws/protocol/parser.py) translates AWS HTTP requests into objects that can be used to call the respective function of the service provider. +- Our [protocol serializer](https://github.com/localstack/localstack/blob/master/localstack/aws/protocol/serializer.py) translates response objects coming from service provider functions into HTTP responses. + +## Service + +Most services are AWS providers, i.e, implementations of AWS APIs. But don’t necessarily have to be. + +### Provider + +Here’s the anatomy of an AWS service implementation. It implements the API stub generated by the scaffold. + +<img src="service-implementation.png" width="800px" alt="Anatomy of an AWS service implementation" /> + +### Stores + +All data processed by the providers are retained by in-memory structures called Stores. Think of them as an in-memory database for the providers to store state. Stores are written in a declarative manner similar to how one would write SQLAlchemy models. + +Stores support namespacing based on AWS Account ID and Regions, which allows emulation of multi-tenant setups and data isolation between regions, respectively. + +LocalStack has a feature called persistence, where the states of all providers are restored when the LocalStack instance is restarted. This is achieved by pickling and unpickling the provider stores. + +### `call_moto` + +Many LocalStack service providers use [`moto`](https://github.com/spulec/moto) as a backend. Moto is an open-source library that provides mocking for Python tests that use Boto, the Python AWS SDK. We re-use a lot of moto’s internal functionality, which provides mostly CRUD and some basic emulation for AWS services. We often extend services in Moto with additional functionality. Moto plays such a fundamental role for many LocalStack services, that we have introduced our own tooling around it, specifically to make requests directly to moto. + +To add functionality on top of `moto`, you can use `call_moto(context: RequestContext)` to forward the given request to `moto`. When used in a service provider `@handler` method, it will dispatch the request to the correct `moto` implementation of the operation, if it exists, and return the parsed AWS response. + +The `MotoFallbackDispatcher` generalizes the behavior for an entire API. You can wrap any provider with it, and it will forward any request that returns a `NotImplementedError` to moto instead and hope for the best. + +Sometimes we also use `moto` code directly, for example importing and accessing `moto` backend dicts (state storage for services). + +## `@patch` + +[The patch utility](https://github.com/localstack/localstack/blob/master/localstack/utils/patch.py) enables easy [monkey patching](https://en.wikipedia.org/wiki/Monkey_patch) of external functionality. We often use this to modify internal moto functionality. Sometimes it is easier to patch internals than to wrap the entire API method with the custom functionality. + +### Server + +[Server](<https://github.com/localstack/localstack/blob/master/localstack/utils/serving.py>) is an abstract class that provides a basis for serving other backends that run in a separate process. For example, our Kinesis implementation uses [kinesis-mock](https://github.com/etspaceman/kinesis-mock/) as a backend that implements the Kinesis AWS API and also emulates its behavior. + +The provider [starts the kinesis-mock binary in a `Server`](https://github.com/localstack/localstack/blob/2e1e8b4e3e98965a7e99cd58ccdeaa6350a2a414/localstack/services/kinesis/kinesis_mock_server.py), and then forwards all incoming requests to it using `forward_request`. This is a similar construct to `call_moto`, only generalized to arbitrary HTTP AWS backends. + +A server is reachable through some URL (not necessarily HTTP), and the abstract class implements the lifecycle of the process (start, stop, is_started, is_running, etc). To create a new server, you only need to overwrite either `do_run`, or `do_start_thread`, with custom logic to start the binary. + +There are some existing useful utilities and specializations of `Server` which can be found across the codebase. For example, `DockerContainerServer` spins up a Docker container on a specific port, and `ProxiedDockerContainerServer` adds an additional TCP/HTTP proxy server (running inside the LocalStack container) that tunnels requests to the container. + +### External service ports + +Some services create additional user-facing resources. For example, the RDS service starts a PostgreSQL server, and the ElastiCache service starts a Redis server, that users then directly connect to. + +These resources are not hidden behind the service API, and need to be exposed through an available network port. This is what the [external service port range](https://docs.localstack.cloud/references/external-ports/) is for. We expose this port range by default in the docker-compose template, or via the CLI. + +### Service plugin + +A service provider has to be exposed as a service plugin for our code loading framework to pick it up. + +## Gateway + +The Gateway is a simple interface: `process(Request, Response)`. It receives an HTTP request and a response that it should populate. To that end, the Gateway uses a `HandlerChain` to process the request. + +An adapter exposes the gateway as something that can be served by a web server. By default, we use Hypercorn, an ASGI web server, and expose the Gateway as an ASGI app through our WSGI/ASGI bridge. + +The gateway creates a `RequestContext` object for each request, which is passed through the handler chain. + +All components of our HTTP framework build heavily on the Werkzeug HTTP server library [Werkzeug](https://github.com/pallets/werkzeug/), which makes our app WSGI compatible. + +<img src="gateway-overview.png" width="800px" alt="LocalStack Gateway overview" /> + +### Handler Chain + +The handler chain implements a variant of the [chain-of-responsibility pattern](https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern), not unlike [the javax.servlet API](https://docs.oracle.com/javaee/7/api/javax/servlet/package-summary.html). The handler chain knows about three different handlers: Request Handlers, Response Handlers, and Exception Handlers. Request and response handlers have the same interface, they only differ in how they are invoked by the handler chain. + +A handler chain can be _running_, _stopped_ or _terminated_. If a request handler stops the chain using `chain.stop()`, the chain stops invoking the remaining request handlers, and jumps straight to the response handlers. If the chain is _terminated_, then even response handlers are skipped. + +If an exception occurs during the execution of a request handler, no other request handlers are executed, and instead the chain calls the exception handlers, and then all response handlers. Exceptions during response handlers are logged, but they do not interrupt the handler chain flow. + +### LocalStack AWS Gateway + +Here is a figure of the handler chain underlying the `LocalstackAwsGateway`, which every HTTP request to `:4566` goes through. + +Some handlers are designed to be extended dynamically at runtime by other services. For example, a service can add HTTP routes to the edge router, which can then process the request differently. OpenSearch, for example, uses this to register HTTP routes to [cluster endpoints](https://docs.localstack.cloud/user-guide/aws/opensearch/#interact-with-the-cluster), that are proxied through `:4566` to the cluster backend. + +<img src="localstack-handler-chain.png" width="800px" alt="LocalStack Handler chain" /> + +## Plugins + +Plugins provided by [https://github.com/localstack/plux](https://github.com/localstack/plux) are how we load: + +- Service providers +- Hooks +- Extensions + +Key points to understand are that plugins use [Python entry points, which are part of the PyPA specification](https://packaging.python.org/en/latest/specifications/entry-points/). Entry points are discovered from the code during a build step rather than defined manually (this is the main differentiator of Plux to other code loading tools). In LocalStack, the `make entrypoints` make target does that, which is also part of `make install`. + +When you add new hooks or service providers, or any other plugin, make sure to run `make entrypoints`. + +When writing plugins, it is important to understand that any code that sits in the same module as the plugin, will be imported when the plugin is _resolved_. That is, _before_ it is loaded. Resolving a plugin simply means discovering the entry points and loading the code the underlying entry point points to. This is why many times you will see imports deferred to the actual loading of the plugin. + +## Config + +The LocalStack configuration is currently simply a set of well-known environment variables that we parse into python values in `localstack/config.py`. When LocalStack is started via the CLI, we also need to pass those environment variables to the container, which is why we keep [a list of the environment variables we consider to be LocalStack configuration](https://github.com/localstack/localstack/blob/7e3045dcdca255e01c0fbd5dbf0228e500e8f42e/localstack/config.py#L693-L700). + +## Hooks + +Hooks are functions exposed as plugins that are collected and executed at specific points during the LocalStack lifecycle. This can be both in the runtime (executed in the container) and the CLI (executed on the host). + +### **Host/CLI hooks** + +These hooks are relevant only to invocations of the CLI. If you use, for example, a docker-compose file to start LocalStack, these are not used. + +- `@hooks.prepare_host` Hooks to prepare the host that's starting LocalStack. Executed on the host when invoking the CLI. +- `@hooks.onfigure_localstack_container` Hooks to configure the LocalStack container before it starts. Executed on the host when invoking the CLI. This hook receives the `LocalstackContainer` object, which can be used to instrument the `docker run` command that starts LocalStack. + +### **Runtime hooks** + +- `@hooks.on_infra_start` Executed when LocalStack runtime components (previously known as _infrastructure_) are started. +- `@hooks.on_infra_ready` Executed when LocalStack is ready to server HTTP requests. + +## Runtime + +The components necessary to run the LocalStack server application are collectively referred to as the _runtime_. This includes the Gateway, scheduled worker threads, etc. The runtime is distinct from the CLI, which runs on the host. Currently, there is no clear separation between the two, you will notice this, for example, in the configuration, where some config variables are used for both the CLI and the runtime. Similarly, there is code used by both. Separating the two is an ongoing process. + + +## Packages and installers + +Whenever we rely on certain third party software, we install it using our package installation framework, which consists of packages and installers. + +A package defines a specific kind of software we need for certain services, for example [dynamodb-local](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html). +It also encapsulates general information like name, available versions, etc., and manages the access to the actual installer that is used. + +The installer manages all installation-related information: the destination, the actual installation routine, etc. +There are various types of installers available as base classes that try to minimize the required effort to install software, depending on what we need to install (executables, jar files, GitHub assets,...). +So before you start reinventing the wheel, please check if there is a suitable class to extend. + +Packages and installers can usually be found in `packages.py` in the `localstack.services.<service>` module of the service that requires the dependency. +Dependencies that are required by multiple services are located in `localstack.packages`. + +Additionally, there is the _LocalStack Package Manager (LPM)_. +`lpm` is a module located in `localstack.cli` that provides a [Click](https://click.palletsprojects.com/)-powered CLI interface to trigger installations. +It uses the [Plugin mechanism](#plugins) to discover packages. +_LPM_ can be used directly as a module, and if called without a specific command it prints an extensive description of its available commands: + +```python +source .venv/bin/activate +python -m localstack.cli.lpm +``` + +### Versions +As dependencies exist in different versions, we need to reflect this in our process. +Every version of a package needs to be explicitly supported by an installer implementation. +The package needs to manage the different installers for the different versions. +Each installer for a specific version should only have one instance (due to lock handling). +Resources that do not use versions (e.g. because there is only a link to the newest one) generally use `latest` as version name. + +### Installation targets +To keep things nice and clean, packages are installed in two locations, `static_libs` and `var_libs`. + +`static_libs` is used for packages installed at build time. +When building the docker container, the packages are installed to a folder which will not be overwritten by a host-mounted volume. +The `static_libs` directory should not be modified at container runtime, as it will be reset when the container is recreated. +This is the default target if a package is installed in the aforementioned way via `python -m localstack.cli.lpm install`. + +`var_libs` is the main and default location used for packages installed at runtime. +When starting the docker container, a host-volume is mounted at `var_libs`. +The content of the directory will persist across multiple containers. + +### Installation life-cycle +The installer base class provides two methods to manage potentially necessary side tasks for the installation: `_prepare_installation` and `_post_process`. +These methods simply `pass` by default and need to be overwritten should they be needed. + +### Package discovery +For LPM to be able to discover a package, we expose it via the package plugin mechanism. +This is usually done by writing a function in `plugins.py` that loads a package instance by using the `@package` decorator. + +### `lpm` commands +The available `lpm` commands are: + +- `python -m localstack.cli.lpm list` +- `python -m localstack.cli.lpm install [OPTIONS] PACKAGE...` + +For help with the specific commands, use `python -m localstack.cli.lpm <command> --help`. + +## Utilities + +The codebase contains a wealth of utility functions for various common tasks like handling strings, JSON/XML, threads/processes, collections, date/time conversions, and much more. + +The utilities are grouped into multiple util modules inside the [localstack.utils](<https://github.com/localstack/localstack/tree/master/localstack/utils>) package. Some of the most commonly used utils modules include: + +- `.files` - file handling utilities (e.g., `load_file`, `save_file`, or `mkdir`) +- `.json` - handle JSON content (e.g., `json_safe`, or `canonical_json`) +- `.net` - network ports (e.g., `wait_for_port_open`, or `is_ip_address`) +- `.run` - run external commands (e.g., `run`, or `ShellCommandThread`) +- `.strings` - string/bytes manipulation (e.g., `to_str`, `to_bytes`, or `short_uid`) +- `.sync` - concurrency synchronization (e.g., `poll_condition`, or `retry`) +- `.threads` - manage threads and processes (e.g., `FuncThread`) diff --git a/docs/localstack-concepts/localstack-handler-chain.png b/docs/localstack-concepts/localstack-handler-chain.png new file mode 100644 index 0000000000000000000000000000000000000000..ea79a7dfacffa61e32e42cca81ac1f95149503e1 GIT binary patch literal 266044 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�$qB{e>4@;No(*ajmf7ieJAzXo}hPthd}Md_uEygoz^G{o-AQ zw8Dy~Q)kRnRBuO!#ajp)YrWWL?mPPH&AoI3m+lJRiBtJb)B{;ciD9g$wfi1^8yV^A zNQ<CdRsX8&*%@tFtVlNb!Ly7yL<=NKA2ryK!+d3}6^#MNQzzT6Q5lv~vv2o_+4s^} z`rWw0K4&kIpIfmlj&{xES+Bht;wqRDVHxC+k?{`EWV$v<o{onfMi7x(hvSmJ<nOi# zkAg^DfP_++lac#GMW2-DTNS#HmDKTGb5LnrVNGBH)y1c$6t3fw*ecQ3Y*(&%yk`48 zyYBp8A0K%No6TwS@$L`9o#6UH3rsz*b_%cZUw356*zKGC_!!NvH$#aMUgObCOK&)A zP<FO!FWgF~ejPs4<QJUu0hh`0j2(;kr4^J?D%Ji>vj-^#Q|nc3-G1RTz`MfRUr|kM zMVwyp;J49t3TfovG7u4*T>T@uxbaKzrv(X5zttdPIznjIY>{=J(8^rKmeD+!tewAi zPh&MU+rW6|?M#l1G@a|2o?Loro$PLBZ2YXQc>3{&vL36R3RM?2hmn=|s1_q>12}o& zHn&Liu+7;mPzo>Mclli1QA{(v-*#fdyQS#*Z(n2>!0edHX`E%0b#DR=v;AW4-huH^ z8XL-OX=?$)@{)s3^%lK?E{Gs9kk`&koifzf(%7ur+ohtQ;D08l$`Kn`x8UOIG7ZVJ zw7RTp(Q5pYu4`eobC*<p`UCD#VkufTu7#&mcfb6m7JE_l`SEjuP{yPGmcKGDcn>gn ztf-C6dInDRdiAab_UNWUxn}LM!QsJxgCmtT1&jH1JId_3cF9#=N;3{N*$}0mj=$OJ zGCfIM&nnUA_L&ZQEN6At>qRLu;bTD&FLSH}sO3<euncGYtIUQWmROCOaD4lP?l;9l zmiV9gi}MYoRi-aW>9;#)=Vn(m*4&pqRB(d1P(E{M2TsG|e^&;o!|lo-qhWv8jZ4g- zBuwlW<#~Gh<n1;2xhrGr-Xx(-H#>xf%Ff5+j203d`7Z1ve4|}tpZm~rTISk2QzFya zZ{iO-{XF0|A4<341?OA{e!G!g7QeEVZHu*t550Zg>GwBYyg0jYj4phS0}CobyqdJL zQrZ<*p(kVN{xVwbarph;K4xTTNc`5zyZz?ff!76{P8@+5Kg+$UNAT|_wHRehPuLBu zMG=1JjdHsZbD_G5Q+T$9<)N`h<VCwbCdGhHX{pM0VV@fP<MUUvX>9oUQz+P>D3PB@ zso7w|vK+Y5#751{q>*x3CA#qJZ%30CbZh<mvICB-MXu<Hb+Q4$YUIfP71qaI?%!=y z+8UYuDPwb!f#B2Ok&(vuykzoG)`fCoL#sqVAQZk?OE`+pH&6UOH%&N~3^!B8vB#+i zNyq0Z7^>!%dLap>=6;}_(qf%g$yMoPQPrtgx$|hyp9qIHQ%UDI{Ik?(#kO?S)-y?} zRo*Uc*RuW7*_P>V7g-Tq6MDfXokzHZwkJD*hn740h>FU7bMrw4pBk}+Rqu^VDT}&V zp?l=-@C7lEcN4{v-qso9_{=-0Ai6G{*XOF?`$say)q|HA<b`>Ufn=O@QZyzy{2KWL zk7An*E7sJ9s7L9611ZxIeyC9X(=MRI3ooWpULnh9dNVb4mCPn=Af3lv%S)4Ey@psj z{Rc&6bnkw$hCc1g`}DmO??YmW%-7dAdYUx7_@XlB;^RXW357o`P;k?>ZAuW2lLZ=K z@I8;0>=@3Goc99iaq62eJ8ZY{I#;@d723f`3z9Yr<tI6Ic;h7ZQu*7t$nz?X=B+(p zk8?Z8vb~?R;gJmg(RGtkQ&BnXv4cW~_KX%>ijlmmBD+a8U^#?TN3sEeA+5@)uBp`A zqH>v@Wo**UC$H1rRNTjuV|kep@ywkc3%Jtjgvg;sxsZur{)59z_KFClo!q7&V_p~Q zL~L5jqP#+UVtph1E_|ulH+O7T!H87ZUe8T>+_Kp<w`~R2|4#Q{q?R?kDK$5tyly*t zbx{Tb9?3%WWcFxa7qZ!Z{19?h517e?nVkB|j=g*LSMz2*TphZ;<L$b}bE8$SgYd`u z5`tW(H?LO~`1^Pt+v=Sa04fjhTjZf#m2Na3T*Xca9d%!ww05iesa(I+s4yq?{Mi^n z_Nh#?o_OZ(ysmq+EK>sMoz9_P!TWZEO8!ThTEpwf_D7nvP53UC{J~>E9qKu1(#JRa zd^<QLKnmvB6WAl<(BhUCyENA)sU@Pvbu?hIynXoF9=o431VFRov|pK@b`Pv=pNadD z(rR+9;uNJ*o9dQy`A)z01DfI!M2sA@zOC>3NZ_NgGIFMKdjEX^WM%QjjJU*UrBc(j z3mOM#H#Ik#{A{}Y!gYVd9`aE`{$jBBnH}<TN@wO4f|MBko}z&v6}n=15Ni`fJfc5( zJ6qqqgIO)&XY8Fj8;PI~-YFg)p8kMyP`#W5{Rt|Fm=%T%=n$Yw%n%4=Bq}N}Vz8*d zPxHR%Hw_(~1$uEoz5MS9hB@SmJ4DvIsWp|DQQ<AN|2Y#h=P|)d-b41aNAe-k8`uwq z!!5oK(yX%CzMz>-f8BHf<w6Fz2IL&t?0Y{PUBJjd&c5dFh#x&Yfy@mE-iNy*UMn?X z2-1JR?C0UWBiZ^GhGhEIg&n;naJ4XK{gC7O>qCal--j9?v?L%|a(JEGjvcO(EWB9y z^1NXjHiZH3=K=9176*JEA5gZ4Ygr$%Q#wUvCqY>N7~pJXV`nE}Rm3KPgn6Kij_6l` zHS^gskT<q2^y|WKK~&L{^z4s&p@K_}f3M!CT?$OCh@1}q7uwKp2eC=u!~9@9BEh&S zF%qHpf6rT6lko-(nEH7_O|7-F8c&%_ZwEI~3A7(PEm6sfiD`8rJCz;HGcc;~Aq+)$ zMq4NaB4T0zc;_VthYDycfzvd@hXeIy-km~lnE23H(7j@YF-)HRprDDW_nJ~-AH=N` zDA^kkCt}tGo@)<=p0}py+!dyS7XVBi#JiY}b`wfTL@-gm1alB^1<xoEm;>JXR=`l! zt!Zz`fb1V*b{!b{dG0NSd7bDGPS=M(wON10-;_)(AVX7gk$<<cd~upJZukq85de7v z<s-wrpA6B!_rp_PMW(RBQ;=ZG$;=!8O(@o5GGy0KT@hPK`fq5@jwfnBrsj`FhqX!2 zdE%u5vb72$-BTxbxxu7?eihIy1px687?SzVz1Rb)CjopMglrlvbW!*3$7!T|YfZ-o zd1!8I?2X5OnXxWk53I)v+-?zslUSCX<-Cz)&BB15)%O;^%K}>!q1p-!bT}5AH#>V3 z_NKSk0nm-Eh=Vem%zpqls|nd21d1f(4CLVHd{$Tm@m~iZHk0yPxDU*1&&AI#XJmrm z0VSjvvT2V1iVVDU%M{-kbG4PDSOuVR!CNGj4;;Ag{PzKy1CXBuUjaHW7OAUyAjqS9 z%rxW#O*6nuLdszYWZBj_2P7GB!|C6txWvS^Qc+PYE&M)-pL+u=7+zK)HYy;ouXh`g zOy7n9O+40-jI3{{OyJhO2%E|g&47<yR>R|<WPz3)h46E(^Rvi?|7rmy238sUQygE5 zi{pYps{#Im_B<OVX!XD>i}uXXv5MUS67M+LDg>`&4fxA&DVQQ|K#>#2U%3G~Y<N0R z^(cWTBEi8}_@p;8d$$#T>NS8M5;W?-$pmj60%ur|iv_U<p_DObrOn{q8Im9OQ17XU z;q{=Q_a~UQzfM}py0zuOv?AGccsIsEkTXD7n*}o`2_(lLiL)3UP*hX|KMDzMMxOub zmpDr>#*DD93rq836e5rY>IN{iX^WZY?t=%nL1axKtS!*O7tUiG*ms0V!Y18x4Te6P zuq6B!SS*wk;Yh^a2-^sD%ng`a@JKLEo2F4v<rkExX5ZKlU;ixc>QRhsBjR|Vht)*$ zj+J-`usj6i$}m|$J7@~o^R=X*76>yAci4i1O8!n|1Pc&JPQy_#AbIFeu~T(hbMxz} zs<nufCmDR;o}iyw5eMA~3Dg7f-IQ&n{L%UVoOE2sp0?qHla|cc02Ro8EEhsu%6~+W z6zJkf@YMM?H}^uQ0^Y?ElcO<7fzV;Zdxgh=WJ?Meym$!_=t$xQ>pTH*@~)W__yRIZ z&B;O6Zws(*71u7K93Spafxd|mK9FWb?b6=d3XcMr$N`Qu0U4SYvcPl=YEH6iF^U3Q z=_y{1-cmPle6XF+R^vaBXg@6X6y3bth&;TS=_ujxC%5VMZYT%vGB7DZ7EdtTWM%Dh z2xX^FC9AO$Jv)Tv#F7Ob&tvowfNS;C7QAP(*=~$Tfl5EkBsB6i9LIR5m@uQw!okAC zRE;H(tuKb9SKEeVVJ{sE4dE|j#9?m{_5pY}0`2+b1c@NTZ)PQ-B1lj*7S#ugH%ROm zB}*1a_mSp=35@12f}<6z2pQCO9P_vEBL&aBpoiK4P6~K6iE;^|(YML>$O;YGB6!+| zCL%S<qD_oi&OIc-e_D|!pjSA&c}8NUTcR0%VDDc``ZW$mRFcgy7#<_UtzLt=Rl{i| zvR}WuJ(TO*d$hUh+nMUsBug6(%!=OLSjcT0a6V%4-7#>z_c1k*Xy(U2*08}pu^Qf{ zPypW0xQY|4Ju<{G5<|NI#_F#izZN2fT#$li8=(p)KRgqXsOylzw~m5XuVuRk-$m^d z#3FQXLnrEE_}kw7`*{$x2qB8ihC=sCWfG2#zY7e=98j<Ulr+O~nVwPWJl&|1=@2gw zr42Utk({CwJOOSW<r~NAVnidWgE_(=hohD+$$8_|V6+f?<Gz^vn%mKNVWg^(?!U=l zV^w;s2|0eW6eLc3w7y*Ag^@@~4-j3o`*-#VX!C#MRbvR$nKCscCD(zyw{P7taH#_| zyMl21P{vfDs%uBIF_3y*+!5q2l)CvOx?CE+j^nr)8jG7-g=k)aP|R&`g!wH~>OD9f z(_&k&^|Q!gUs12FnWvjR^#`skj3|)=70CxD4ffuVoW>)kPlq@C#LbDah_cJ>lQvnM z@i`D;xC~szE7^9fe0k}CUDwj0&E(0$i{U@F{jCe;_uIFG?i{_Md`h*XU}Nu8Rm!77 z(5{^TTS)q-5~_#YS3i(cI5rvg`yiPp7vbtAaf%Q+kim-`9W0>Zh!YrUF;ob!|L#LP zxOmS}6K@Gd(F)+)hLZdNSY*q*6wJVddl5r@b6lsaDTtl2>M1&a^XoKkN+$Lhgb~E_ z2~)i&YA*OxHdxX~i)_t4(%dz2%wBiP<QeeGlzJ%(5%U~~i*Vp?wA89bbnbSTz`!+` zORyrAJTdt*&UwhZ_;LQ2V|8#!KY;Rq68-2Z)H}$6@ByjuM9(x1#|9z}jf{<t0?ER1 zMY^mnC>z*2S6jL!V_xQ$(ob&4^F)0Gmo1?LC})eDQd(UQG7<`P!^-G0M&-znfPKvh z3H~_2h{g%psS<}}k#f$z&)0E_h_P~gr!a%hReL}Kf@iZfpiP(@>3@^;`V|8-vn2`V zbk3$?g%SxK43Xfs$dn$Xd&2c3lMg}8a1{>Zyq8J9N}@mzvleDKI3#b~QTtQV#G6`< zGaS#2nDikRx6VUF)t4OWxpIH1y4JJPfpCsJ2VI7!*0j2JXK+YCYeK3FQp8{^3Di5R zjVe!K#T3rMs6v^=tTr<F#UaL7XUk?dO(;kBA8!Cj1p$u(hI|p@GoCi&burRzTOnzO zIO+Zu+ta%vM5P=NRgB`03KItLJF*2I;8*V&`T*gJ*%~rYrWtcIDPne=@|fg9gh!C# z^uyi;sY3wDfE6LIEStUZ!RI1YQPc-;kRMIdfUBG~kt^zy)r^3cSR@oKWOg#IYP1i8 zAYKdOREbIvk5N023lo=8BGo4>Lkak<;3%$pB(2B4awCEY-i}{x`!@wb{&?MzjdU*^ zvtr_ILnUS$ba$K_Mt$e!=l5Zj7x6dH(Iu0ghsY7!PG+B;gE_qcnFk9I%?JsB>dn!( z=n@(n9IO#i3%NJ;panEf!<{(X0&pY|Z4PK`3^^g8RBY0&;V>kuB#wMo2JmjrAcOu3 zzBZvYjVo5C8(c(Sy#TCt+%htc;5#jI-e@JqvufqatGGp=_wn)ZAv0`ndtvxf93tWo zA;F(|qfA*0lCA_C&?rVU5u8s_pOI^jEy#-MmjoH0E^?Xt7TQYx5o|3{7J?`CgY64H z6H*6Ne)EW3r(~_VknM=#>KHUydqhPmy1Qer`pI3=nQu4WAc^5g%?OadZzq$Y`1$!U zTH`Jz2YntKBv%VU&#If7??QwR@=PAReOL-gU?@=uuZb&k6C-27X7OI9`hR_6$?f>i zO^g1wUw!j+NkN*n=`E#)8~AJ-u3TBWKUL1tmP{_gm4M3*1FiYSxZd)1Ib6ELEq1D2 zbU7~AwflcmZP&Tb-%X7|l}RI?CtcRNG*gELeJ_8(!T9iWS)P5EK*r4RcBMXz`bMEc zN*9(e0JN+JH>7$&nZFOWsH%$7W&p$3k0MLn7JEdmc?zYXY^!b#{f~^brc6t4>4sr7 zZ7681z`#3wHZz%L7lvpF)b>eMB-x;y^fC1VQ}K{}9q!D^CS$M5L?7V&IIZ6j5DXi* z9*e9%WpR<~@HWI9^?y&v=`Ov@r0J-6bE}BbjpG@)(-#&S9BIuX`Aw3aTQ;1hy-h30 zvMFq5wZw}Vdg)H<L5Vbt9***QPhA1!G3s_XPWyWI+a}3(5|0&U9)9a~w)~mE%-E-s zGww(^=U>#ZDw-a?ncyholCwTkPNL;E*B+yN1s4pGuTtw#%e(Jk*Vj)=`z^XueUzD* zxv8Nz*#{kZW~j+}8_ZFCI~o5NOf&pFV4kVd4Rf>m&UOR>4uMg7I2(49s{`)VeP6p7 z0pQGBl9=5i-8I#uYbFl9*?(E8->Jr`RmkxD$h9-~qaF;*3aYWwnY}9#+HcpfzxrHr z0#)$U$<LdrH?o`5mb~3>+p7L)QnueGc0S_~W%U}{s$#dp<O4D?YDQ*ssRr9W*z5Wi zfBOq_f@01L!G}aaR-f&movQUCfnDGbVhUSp$><GR3Ex=y!p<7c!+`T-%T~_K{+w?( z{Y)#cedv;{Zcd=xQdhu>ZLjJA_l+-h%p}C>z1t|1@<W3)qM`c18>5l;%};xu%Osg6 zbxe3}714?LaQ8r}FebCpyGn(x*~b(HL2_<vva+#i0NZ2r(DUI!+K(J9)RdUFH#4)a z<`}&Z`!i!1<n0|#(2h)QM*@LTJ|oC)!Eu?BQZ~BC{?PTxW2LFx5(nGPH^l^=PPst$ z@~ug7ZvG)Q_xl&pi%%81v&ZcIm>BGA*x>i5#rmsO&%+7d-Fv#L@2xjxnoT;Y>^H^H z@S2J)<?IuNf%RvMYTNmByJ&|GgyT>BI@7U@2Z6d*I&_P2zbj=<m*#dJlMd<|`f|9r zyJNk-MvIkJf#g2EdZhx{3*UdA^<<wCFc3EUYNYp&S4~xCkK>9!>cGtU{-gb?2Gt(U zw|?I~c1+a#+gT~={ZFjlT1#kOiFfPbU_0b~&dPD;0u6qBoO?V?nog0Q!H@FfDvJYF z>OHr|ckqFztov!_ojYZ#Fx6z()ZGzut$u}WH3%+2z2~>4Zy!=GoxHdI>CdVcU%rOV zPm3ZA%KBr<(c1?PJRQ}&=UKLR@sjh3&kgB^<Q0ysqQ5dCSXw?6ZxHe4bXLkuz01zW zCt2Ut2k6z7iDi6x60=-CDjp?zh30@vs$yl!x+D91u3ij&_2S_Exajm%x7k>K=ZuNS ze+qIRn*Wt=Le1Tz_rihou7wcU#F%GfF>N?%+4=Y(K_fHc`9SLXP#RmB&y;;Cu;Kiv z+P&-alaPt->j&0DgwV8&rrh<db=8Jtz|*(UK_Q;xJ9Yc(SEW=@rA}M(My%g|-Lfcn zb4h>g#rZ)yTjhH^hD+`eADuq8$D4eolY04?lWJA;%BEhbrL}R7{MWDbTbn60grv&m zrqV4lV+)63+arDnoTDzUc$>g2c&-3S3-(9VQppi+Hi>%@k~6FGl-ur4zjIoWnH6`` zt~&-xtW9s;Xv^T|M>hl?j#an)D67h^OC1ur>A|7grlntaWv*zV`^GGlRDk=-EV=5l zc^*4swP(-SJfKeSJVT@S;nW8~!xu5l-0?Ao9laq)pa>h5)FF<L|AJ=BM8vyQdnV_N zZF`^V7y1<!{LHo<x4yn=CdW1>wsp~!-EqBRdi6cwa~1P&NPX>#82Z^=^!j0o^Eyd} z3O|pf;lUQYd?U$sOv2hfTYfw!7;em6b?Rh7xY#D`9c2G`$E`~1DBPxO^KPbyPJ~l= z-MBT<OYhR7HVRn2HVHCj-HIcxD%PY7pJu#f;(zChZu*MbHsZ!7GUFE{zn*M(DC3_M zm?c`?*<5`x3eN<sq+My(#lFr<*I!Sc%5axtU}RvCzDf(!L$|uU;1F}fotqo=yT101 ztUmJEoP%<%)G6Xn!A}akr*Mn{9p{SAt(A*;LsAyCY(IMogF~}6HE&hf@vHK}pN(yT z!WX~)y0o%@YQ^)5t+LxQjvHCoz4a?yiYdAj_CfRbs*4h{>y`w>pL99vYxBj7914vI zuk$%oUN+4(P4z?jruE^IoYnEasdS#3%eek=<=M%*Y4dZw9<fNR$OS16ww%a1o|B)X zC@~wP6&C%b&Su$3_WZfBiVMoYRc;$sUNl|al=xK4szF}X{?YlKpLsZDE{#n~CoM_+ zxIH7zzc<jtKbSFB?ylmO^S*lv_E=1XwNzHWF|j>g7yU4mdSh30+^HLs7aqUGEacuN zcC|fwlWN)cVnwe<FUxv?_4#Q%vLBmwO>UUf!SAikv>v>u+@+xVzIdka?@Iv;J~|t^ zXxm$&1^hZ-`7Q?$3vJ%F6y31U%k;d(OJL?g@7F&Oqvy&e>`$gGtoglfx?rzF;lV6> z=RH_nP_#C-6^Q&<`=|e1Z1A0)gTLgKPjwew_J4lY`1InlXW1U!vTJ_P=>`W5mrKWO z+i(3+WJeV=&2`>!H*&`>)PMQBgMtg>%h{Q!RPCSL+3P96NgOW@T%a5YUg|0I95{HA z<1qI~R1#H7>r@v6XwO(;J3Q9D6#t>O$E(U6D^l)d5=HmQ>~XiD|BtTo0PFeh+kRy4 zm94T{L?M~UtfW*@q@)rmk&%|j&Pb7zQAx^Z5~77l!wh9cOA=bB6g{tx|Npx0=eVEe zzK-KQj_bZqzu))!`Mk$@p6~OmAayUZTUpl5;e%hh?N6R+F+F&7qH@moO*K`YK7B5J z8!}ewfEHxuxkWF&N4tLiV0p`7*)_+VO9v@Se0KY08x{Pq-|>`tO$L|j<#U#feH`DI ziyXh?>8|<93RW(CdSsr3nnB7|$KuQj6Hj$~-E$5W%6#dZjbqOyFKE=1GIuF_*J6=m z_kD}iV76EtQ|?}-7*Ou=U{jRkwU{X_v%=ctU9^qp_j=8v@#^w{XO?gE_4A`3=xUN+ zHBw6}yN}%QmgK5?x%Ycfm%guUvYXUud45<}-kmV5&T#6!mH)6ccb`8v{%82Fr=D|W zu798OFym^!#2#CB4sY^t!uP-XFqv_&O|n}Dzj{Q#l+ca26jWxuqWRXw-+PTMqvB`V zM4cPBV~e?6%B9~U_x&95G3~6Rcmq!*gYGWlT$)eqGGU5B5WTSE%B73lBFkc~j*_#$ zz1<1hNr@AWmgnTT=vcU%jWLMdtd;UL-*?Z|mQ}?*y(`p){a)vG-NycahjeaGxZO#W z)t%I0-alWq<j0bn<BC!44)*qKm<((co^v9vc|-oQ+3z)i;n+5pZyWmMuHr$PnvFGc z3VqI-98l}t{+Zg7@}^nI4If+DUb$ppY*~>1PU(-;c*}@KPA%Vl-$>D0r*8MYYTlTE zMXBo~JJxhm3cS60<fed?tJh~PO-Q|a+jg|KzueEl`l2!tfq7Nt55+EDUijXbc_?DF zL%)u5tPjXv{k`8q`reMSU!SO%%=f4tMI$zB>9N0LgvQkl8p|Gs4a@M>zcOb$KWOje z2w3+YMUMw~s#x8vVqHj2*WwAtrgmJ=FEM&w;otP&n6gj4)$rkAORsNo{Cp?WV_#!a z+Ix~(@7f!yZEmY|mDF9NZJd(ta!)&yK$VkZQ<l<jXw{^QX9+HeFYC`qOS;Coyz9IE z-OY!7i4_NDVX+)xmm4i=*0NlMYn6FNynjmde{0p@hGXHQ4S$p$t*BS%*z)+cMrdoy zRBQYlHXFTGPr9ifx8~zgMZ@SWd*`)8f1Bj|dfMtmr&&-S-F0B1K}1ULJ>GqbZ})wl z{L`$9#VwyDmzG+-35~LU6MM#EYCT7%%lN0`+kEXXY|Z>0R@(>7CVEzFk?CSlcebbI z`K2K%dp_EC#=0sx%D|*4>rbNRspK~WPKw2~iWV)F&Gu3E@9catIo8(gx<j4IfI-m~ zWAE0cu6`Q1xMc9A6?voTUQbTwzkBD|WsR>==|oV%UwHA635F-GN)9Ti4{d`z<}tUx zzQ3F9`nE&=63P-E>_3^_O8C|-&8PSLr?(85p2+oAuGid|dB`u^UP3?OQlmp*ADa&F z6AF*5&PuMdJ{@;%|7BZ`%x;5pFv700eK2~)Sm{lj>(shQCOon`yvM&SiRosKt+Of< zWZM79dN%rTWcgyBk5Q+5V>D(+|EW9b+O@Jo_56*zv12D4DJ?Bk>-!PU&jAhhB;1=0 z$$l|1xm8f`D{3Sdf#%#!MK{*UgXpL4o+I5{Cu>-xmu)bK?0UA>^^px8PrH;go=+c9 z40hlB{6o+B;>3bT!;-^ID-yOzNk~7Oc5?D^)quA({ft!~?!R2Lba7bbW!+7;er+zw z4jYX~G2r4%y|Rju2z%2TdmLZiXb5SN-Mwupy5)8G7w4}`TRrd4Z{6073m$Yee43Cq z+hTNE=lZ2Bv*cppes+59|HXd%Q|HI|4p@M&?e1Qd-1zw`0<LABo+O<xar&oce|!w$ zt+O}3msuMl-%{W^DX>tuzv2E6jDsbsYnzWP8DUC|wBtg-;$@-hYER0~nScL-e%L_z zX|r1!0!@$EwhA(zEA>T@{T&;-bm_Wa;R5T-7}ZZxuP6)K;ug!+%L^PcvR>{#<Jtab zj7G{0={I>fe#;gmyp*{nliD^H{c?!%qkcaX#(o=XzbyGc)SovSHg7!tIbg}Fq^+uz zOB*Dsx7?2L-)i?ezc}@7aJ$21@=Y$LM`YAz1uyd0CS7;h(ak+KcKmD$x6`BEPq6&q zH*xpb4!ZTrR(V|*{VHsq&*bf;ZLWvwsJP_}4&P_GdgE4S%cjgBr>AW7Ua@>@(d#V# zH)>T+3T`V0D4S<SpBqxSHMPk)uVc&1mV>s+3Uw8_O*X%#ezn^0cqrfYK&PKlCu<5e z*Y<nh<B^b~-F>c(V(!Ak?wb2=TrST)88`gb#*xj_7;_xD)7rP8!Q{$=Lva%p&e3bH zRJ$DNCAf<tF!2Cbrr{3f4`1`0WcoVu-fY!X0{O&YMBwF2SHipacduPI0!7m99uoae z^{;!AB>(+$^>iNRLbnWsW8k+7gH(H}Zc=^l;Nd6rkkP3vzos7isFdmWN5#O;5P+HV zJI<+je;272$7PxyPUA@jr?+`%G3(u_Td7-4{Ym*f`DVGSkMy)v`t^5~#i-v_H>___ z^!Yj9<Lv5hE2;f!w<Px!MzqHxP2-;PP^LH4Z!6h|FmU+xnsD8F*K>~Uov<i+OUJj- zrjtYJ3eNZ|={{WO!n?@2?HlOiInc!=e&dUF-P+4oYTVdKqNHwm`93oVo2Og(q8+5v z)hu}(<l@AGj7d7{TUYhGZuz{QO*^lK+FrDHAw~T=a`beM=}9fA4Qcxd+Ye2DlQ?0` zgQ!8kC;>}0U(I=CIRDjct1+|pb(!~gvJX91R$mH1$N?MF)SGdvT9#lf{pNw+n9g=> zJN1zWVTj17{hn>z!D9k?q$|F-wEUpMzU%R89u1>GEu&47=p%2wJLiSMv%~<kk@h-r zU){b}9dQ`2{DiXEqwu-_;peMaR_k8SH1*Pp$e)YuFIpZcJAP<Sis5zY$-noBa^~et zXGx0E^5->%Z*O}96V2B&>spZ#7YE)@a!g6Ge_8VQn#F~U#cMkFIuDnxG3_`ld0NHG zC`p=5$Ci|fW@lsGxW=t-bd4%cU0<28E35sL<tayAmd6=wjJ2)S+ciLSvl2an?fY*l z*PEvX%Ga0slzCE_TC`c;z4TXEWgn%*M+YuqG1iUB<^9HN8}dGQUszb!u*n-{W{%2y zesSr(!r3YJqi#(lKQ=tH*z2rZrS>h%r4pN>Q~RV28ap)>+epuJKjfbvwN+|H#PZbj zMi<RTKKr(-NAW`@WbQaW=+kl7L9@hdK|8wYh*cUcwF?&OcJNIWV|>h*UPaRY7#oZF zS6^kPc9ikut8cV)X6f$lE=ECtq37G53$pn$OXkN*`GEVDS9<T#&A6U8tTH@w`F`CO zcPciS9x>nl*19F*$H`CUYok2AnT)CWzfhZ$thnM5tC~})(uTc<Gwd-8K0H1OdJ{*f zwC*#TcYy07zK^%@nONcQj~ab<!%6A4w{<n$m4@IYguLKY5Oxlj`-({gCSTgTijTdH zN?(ExH(JOtM!cU%l{@E=)S#x*P0o&nmlmpb9DeNOafRH5n%)g9N|Mkc4$R-|#pt6a z(^wsc_s+WY^B*XybX;olxE<FYe%9_0m}R28#2W$3U~<D;&8#*;VkRj4Vf!{-D!+4V zes}dvpHJ@g)%w2W1$vKR^IcpL+-kP}m-eDtiP2-p6)RT!{PKE~rFE|r{z5yUt~1Fj zxAxdSQGuW4B<5BwZLi2WyJGpsLkl<je7^W&%XmW}z>x#jp9*q#LUzW{vvZ)k{(dRd zl`s2WsVhAut8(Y+=<bPIzj_PZ&0;1s?n38}o#ih3H_ZwPNK*ZI<nW>56PqsF`>)Xh z{|_~&^nX4ix~-LK&vak@CJ%kau=(<gmW#N>ZxrIaf4;2u!NX-SiH318#*#t^ljU|v zKfK+egiEdCz0UR+R{rVD4P}Ovd!@HW)$sS9XqM_Dwo+=gQosV@7%vRPj_jTU`%wVB z@X{Nwt5cdfkD(+qkgHgxD6#q5+dv3;F=xiwRxt&RUmqB8&%uLF+mt_L5Es<)ROof4 z7t(Wc53;XQbya$Yd58M@hSxJZ(uz0(^_KzCUwT+x{5m~9+-e?0pKa65eqPjxLgSWj zlpJhlKmP5#+I#nVs1i74+0RSf-Q2hfJ%Z0x#%iO^fy2vO6F#so=Nb}^POI<2*br<q z#-Zx#*Tbo)o=04K#OmCIK@;TPob|-vjA<TyY32`0N=xT3CE+fyf3R*RyH^jOX4wS( z5%R}0z6H*7JUWLH$`*?lDU|5Yp@Zy*5xZGB70Wc5Ui?uutqYG(7d8sA{qF-KZ>Fx( zuuK(eRYCquFvLn4|93}W_9e0t<9?b)hWL$}(F(bN6<qLxpos^0dd~plg=Gp%g36D{ zz<eP2la|}mW%RRPoLJ7$&;B!0Bj^`u4rk~ObvkD@_5LzUZ6Ww;yKT$H(y!GT?BU=f zB}61(6x|QtzN{wVW5%W?lX9$Y<^##cU#tJ?>6pIn((=@y;W@{LkDX+Ecbgv82W5E` z_t5A!?W$kG7Fx_@j^8I-moYZVs^RS_1%vS24t{VOj5^A!@sGjf7w}nGYC#R3RzEs@ zAS}!(`gdWjQsRaC%h0;qoZc?rKUx5s4abOu0ELBTEw(qd-66PV7&YNhg<6DBf7BOI z4l>iWX)vZG<QE*V^MH%ZP#%58nwhM%a&S2FEoLE$i85cmURIe1A5;9-mQS58v<jI5 zBmIgo^XJZe3XuU3B3zVFpy1Wh?nxJCCEPV7aHT>PVe@X#?DF?F_9EOt$-pFAD`-ju zqsWk?zP)?zLf-&+FMNL)4FdX!N{B(bAT$Z#;Uj}Rehz?4E`tSG=72C@Hzx~wJ08Yi zSTebo>7IWEi^aPO1J5Nqgx_UPEviixMr$w;4>p>4=gv-!xcpb#eNm7MW5iHr6Wt_Y zuRejxO?qvp9=yfu?TAD5-@jzf79u2@CbfwZGjOD-Y<UUmELK2^(J=__sF@#e9Xdt| zwflMxD{KA>Gw2arLgBb%c}VR(@cspTgj=PMo&bVhyY38C1p#<yu(}`43KAQ>yd2M* z{}I#{EcKlTW8qeLv>4*28lQ$dwmR?aHY5kyx7C7$3)8c+wd_iDwY9zTHz6P~*z1+@ z1?s;G0uCtTr>t}5)Yjyib(fj3koBCHVBSZc1Jz=eEG!l96XEVIiQdR$aLn5RsI$X7 zJ4~ip%c7oOf|4Y1?wV=p_UV{Ljez05d2=!+N(<Axjn2-tX*&detGgD%Ehpy2vrz#F zt0p8odd?3Jkd)xG3Dwhz)!kZZ$8YkOw42o4mEgHt^!ZZqj%=LruAOE5stx)w_qvB_ z*X&>UAsA7`M*zdYANf~&2CI8P8abVXkX>vJ^E(ii!yJ!oHT7R_lzorq(`;}0szz87 zRXdNH$yoWA`Sa!pyJz{B`cF@1S8n4<tU`c6T<Hk)2A`9Oc!m9`)&v7cZNU=}jp_=^ zPo7}{Zmx@7XZb5OS<uPCiWM2vc|r`>hr!j=x<9|Y`@40H@k^#1u@~CNh@3`UK@dMR z-uAIH6l^9+uF0$o7vhKV5@qZbaHwWkXxl%(DwWZm+(#1jDKGqB-UXCA!i@nvh6(}9 zyN|pk(ON8*W{P{sj1a+m!=MFL3`qXmz6|LQ&ZZg`7QtLFh3T&(u*AMgAUrwEMTOzY zF_02C*UomCE-Nd6#-Cwde8tA{R;@VV@Dyu*AB8X)MGcK&<>>|8L0F6*x0}diWI1}p z_8w$7KUx~xAboJJ#jC2laK<K{EnaJDWyOxZ`6tE<SE}<DNlSBoy)P&fFA6F<Wc%-5 zIdv&1DPr9%N*dLzUrdBw3VIyX3ASRfCHvMysm(OKE33!8@H&<e7Itw~u6i_WMV)gw z;VUb_8yHR&7988Yf9i>yNgC2RMndyA!Nm4Q1Ud`1FL+4e_NGIM8!&NQAJ&OqaD1O4 z^LCITEFd>Ws6T{xIBF8p>-K>L8(jSfUyM9n#2$@!9!B;R!;`3i)+L<6IzZ5nynFOo z2RZsH?9RqcfD-RNaG(=9KnX_Dnfw~5q_nU?PN;Ahnna7zB;#f)eZuh9%s&N&@~;$h zhRyCMzu469{FU+k;|8qDob<Ejs|cl6ek%L>-tE$Rpw$(t0Xo+Y%vpHKag<IcDXY$l zd%vji*?z_+VtVe%TNVXdqA!=9ZhBkpky_QHu2=3+*qMRH54;IssX>Gfy?i+rQ-cw+ zho|vj)m^{K2?CQ~=TtyK9U}aE3km{AaFVfB_q)MKW)Po*Vh2asP~n_dcD5w%CC+gq zzm@b<gqeMaA)UBzPG{cmaCsYTOQ-Ip9nhNcv83c4OBGEM)`|6w98RMt#?sT1h50`O zuUI692(%0J+O%oYN>iqaPJ#@{|8@w8mJ06bPMbD`Pr`{M%UT5y4^{5y$&*haB3Q|> zg5uQH)`o{ojyvR)Ge5jj+rvkWFhM@A_GT@!=#-Ztr}&LNiFFs6yL1>*UY>B}!C@Gg z7Bt)f2edtko4<bfVvi};$q8%yN?&~>*<8Oqi6~6A8iHJhHPK!$aggm2FA)<%xw^Ve z5xEjEfTRXCZ?M71n}#H_Igf9mu6G4#s*dOK<;!yu9Pwu(x6K`Otd&@HA#O8v1V|Yy zIV4z6#$t|h5vtW8mTjfnNOwl89|X?~emtY;5S?q-xpTvQZ%AJ6z6rZBwn9))Jt11F zsHzeQyYt=@hn+$ba{3VY4FyF~z{GG2PFJtKaPwG)Hm!GR$oA-vGuJX^XjXTz>1Tul z3yyg{d*O*IT3!=HC^QzVStC4|ZOmtznhpqDxYy6G7Z26`-7P7;0E4ux*!?ko$r68l zVcm<R7FzO73w3pwZ<r}Tyt1~n755uxi;5Cc?bzqn6qPMftkHU4!7BL#9b-y!tqyh@ zTHK)bCbl?a3h}J)3yhBLrUBdCf4b}D1Ld&mtoInBKAxLW%bnZ)r_mNwYWP;X7LIM6 z0zpp_o&&_B8#KYUQg80uwq#!v$70C=LO_I8yLew4(9KcE>_2e863v|O=;V4MdmNMe zRt;eiR~p5S%Esb3vNKf_(4jtBK3-YbhwvBgR==Kep*(v2ojQy4>&H@!@%eQM_t-q# zXqx2u!<$q&&f)chTU)<&N~20dX}KZs;{tqpoznD;jZe6pMJhForMMD84~@;T-<dPQ z>^t%M_bF7ae5qAy*LKukox^?~pV4Y+{qT=-t^anIB^%%0EjBV5frSlqlD1t0_l<ix zgg+O4jFgmdSFhefHJGJ$(CFy=+ew?`d+bfn-o;Bd>t8|}wia=oO5h>J9TH`(6J#ZY zO{iGQm>t<ppVhp`5oDJ<RO*gLCL74|!sd5Rw3B+f67{Qa@St2BHDZKV$AcJ*J@Njf z2bP!%1p}c}XWI4;X{~0oG}o<iydWxh?6y_B{4=olD}6m>>Qq%;3JPM19if&X!(=B= z)pi$i;5;F5kn!6UdJ8@Y?kUO@54-Fj5pZv8XeEH0g}T8!Y1__Y$DXxcVaOA<M!3?9 zUGAE7>%6CWi^JQd^&QV16A#~FSS0ya3V~(h09m#&WK))jZA8MNFhBnUx-SE^4kQL- zV8Jn)ga%-%b_=D88WNy|agRGtsvPfPN2#2*ojRSV`+Mvet9ThVoa*o7sZ+VXeaufH zw^%T5;ts-29?@~t^fU#9RC17Y|DEnuC6&PkRnw5RXs!=tcz)0%`=hxx-XW;0*v8i_ zJGZIRGvc1Sw`iDV?P}*66r@e1uArcBJ|aT=3F5IiD4yajx|!>ZKF>q2HIfuvQ>RXy ztZi&?dDt~<k)#ANm63R!Q%QxLKW`MRKK3AQl?OD7{C3`h@L81}Sn|WRqF__~yCj#Z zX0EP75b}yj1ywh0a>$Wbyft!mVaTjq9at`ofG_WDp9}c^8n_Xc(2&nVAX9d>{{6g6 zy^hnTC)f9VwIndoXzxVcf8t{Raj|XO@5bxOK1l}{89ULmQ$FP8Chf2f^J71B)wg73 zpw&yBIz{0yTq1b~V%Gzeqc<iZ>(;F+yLAF193=LNHQTh}mCbG*Wfqb!XZUSn@}GMh z8yywcA)%$CQ$pq~{rEBG-T6*yUd9fkbu8j%p_jd85@_DiQ)E_@E5bRN>DG#_Msv%H zR;c21f?U=cqhNGw#YcvTv$69-SXscBK_*w-huT_Ib+9q&G2Bq~%7Ztp9fMn+-%#Vl zUGhC~B9q9a^JCtzY-^{)z|MxR;{Z*>*-dmeYn!xw0p?Oc-*0$(cS1sl9!{(#;UAci z2`Pf2PTyZ@O6|TNGuS(;d5X8Ucg8o*-x=;MHuCQ83d+wZA;>we{}jCpMiUFkD#S!N zMa5@?dQ#hL4u?Q&=yMbow+ngMx+jM}gY6+y3&oX{xoB)kKYqf877=U!A}95#JMkYI zxO#JcrkW>T)fl~Jh>~`m$6UF!XLBw`6o|G0yD35%5?0~FjfulbWVDfyk)jl;t*!lp zFBZC9lGHelU*A=1l(OSWb*-C^Q_|J8mp7`l2s0=%QB10)NMjOU7>>?a#w%aMSfr+$ z-e=mX?F&_%5)@HR1zye595YtLr0uz3p6p6~<v=4lUMkkL2q!B{C*<lUV>JJ?pg?)! zW&TcFdElv2$IhI2h)_GnBkHcycw9;r=;(wof3arm+R%^?efuvi%a#o!@Si7JMqa+$ z?{djqM`L0<Mz$rq5+S#Ca(O<Z4;~ytQEH>*^}c=JZxHbE8Mq?3@7tE9v7V<V4?AAt zyO_lsRA-$^Q_Z#`tvVgBboJ^{EMW0>t1){bUAyV1{|Uz{RHZmghh4d%<j^)TSzASp zJy3BBJ798?vA^f=>bg2BYIuA?BA?stDhTe5EPVm)M`!~xa(=t!HRfSCUc?9UQ*B-9 zX7Zc3G&06{#k*mJ+9aGcYt|h9Twy$rr8&ZFnCc?r;>80OE{sBZ$QA=3Cl^P<Zl>p- z(<m9W-m9yt)A#FmA5u6#CJPA<-@_7I_Aq?_1UljNQ|t;7n*-=)q|TP`RceY8KBvW0 zrs5)xo5<XpiRR-xGX|6vzM5Xq^?M)eSjmOl5`iUP8P74Fht&fg%q(^L8FubT)BlsK zmRXim-afimzcKRpNMQv+vX4C7N=a3<H?nl<<j$o_)2D6vc<8O}=;6b6(E#Qu8qC^> z5Kd?P`%3?H<WhQ%dA`<xREFVvwztV{0oSfgpk@;F2YH8<LfEyU5-f;bp;&*D+_7Wf zD8-G6^1Bl#r2jD)GWllb@G5@pw>l~^m!Dq-thlv#78%^is$`hdL58Ipo2pX7cb~w1 zW;I<PHHD6Y7*9y_o+Yt$>sG%09%?3HVmeUE%!YxlQSKt1bLyzt_MZ}G#=O^CK6_nS zH*t~lbXjpc@CDBP@Em-FP*0=Lp8C-&#eDz`GIN2SNK!{zhW%z5i3GLb_pdF&J!am# zy=K9DA@~5=r@geS{l=QDL|ZE~vJ`1JK%s3`T0Dx)xk5Who69#Y0YDMk&c|+!ABVk; z=maQ*d$CXnGZ}=4A;fgzfgy5FMhHaiXHA0u!*yoT%?V=}F}%WIA(65~=xm=yrh^%o z5Q~|DxBogoT!|-1A{P%IZ0+ccaVzx`*{Pib3iSA^S1F+by4Z<3#fcF=2~&CT<oH4l z$bMy-V|2m@|H7(}6MB?e%!H0GYvD~t(52&}qyL%;JNL8|DS-bX!X6U`rKCrvc~hmN zqy#QNO2o-*u<(p>b<J^6<q7oc(L>gD@R;U>T^e1oa}3v%=zc#A9!!(Lyo#u^7-krL zEL%9#kQfN|tSRUzOxR6KPF~CW_SQvJ@167b0I!xA+Ws!p^ijj*<T?P(n5|f`*w}a! zB|gQ*#k4fmLRQz;M^Hfv<7?VwnuFPk79Bag+@8)>3|dh#gi$}+7bXQVdA2~UiR(Sy z`J35AZ7skF*4|{&%m82LSY8t0vbHq)e0ccc)a@R4f#-rphY!-+dD)2?n5TIB;8Nue zy0d4sVwsInKy#cDb!#@sv_Eq-)K45{jKcQq+0#}+_z+Rxh<$|$Gh){~9fmMG_Ucv1 z+RIlc>ZxQOl6<xF^bloUU2yk_*9JBW)EAeQ8jX;drPciNjiZ=C1NUNwikbZ>wt<~s zYs^)GlWdD_5UmD5+^@1)Jb2a)A*K%~u3#sJ&O_CqfAgM3bzDef<br;cg@dOib@#E^ zt~peuCr`nKhhmnr&6j(oqkKC#DQR0{?MPI<Y&4KqvSf(_E1?epd(k(PfCGdQI7IdH zLml{3Y$c=lZ}@o1QTTRp53R^xkryxG!1%;qxP~a2IC(;sj1$@gN-^Pl1R(0aZgbw# zr()Lw<6VQPL3oGZ<rnCSg*y;KZ40QUnMEn0She{aA*=L)aSQE$I&Au!7(nXvSX8h) zPw@un&V+>|dB(e||CLBf2d-G&rnyvg0t2Fuc6z)T#A`K80T^X1zMF7&rl6O5lFF#x z(g_naKYrMNAx+y>y@RtZCZ!U$G9tt_oQUV@>)+lS&zGd+-Oc+D`}I<OeO46LYdctz zywp6RT|2AHLqo@m(Ou@uD{lrtVBg$=vE5Bui4KwlgJd+ey)3amb=Sy9Pkb1Xyg>qA zdd0<~vu9bRQOEYcG?@oz8gJcEAVryn21w=j{+XoGNkJx_axF<u_mkJUAsEps)T^W6 zEu|S|G$s419y>EI?E$&ii){!qiD_VYr8$z=Sm1))hv~7!OPAV;$)78xy;&ZR{i2Z~ zmxWzB`5b<}zB?JoqzkeJpK+4jIJ3E05}-u1z@*@!FJB(;gGcmxf#!Z3WwAK_{4-27 ztynf;5V__~_X^GpBRxlN_e|-0L}un9%5)bxs*5$>0oAci-w#L-$5yUK!83hs-I_wr zDH{4mWj_fQOtu^aJ7fRC$j3JeW*hKt4}vx_CPMA5LKlGTQ=(ga^xIR%MPvhysV3?} z<`_ngk1|glh_;SQP?mEtbrFw_t0UTXjg&W=0Gt|y2T;s3szIVK`@L?JK3WgFjce=q zyu9FExVj}}J+i9&R?_l$%ckft^Og+VH7aVKvG*za4d1t$mny7$<<z6~&XC&b`xdt& zFC5Js*Za2i_`=eg!PyU@cCM;(SyemJNwa&)yy73J{im)yp+B$f&B0Q4UYy)uW|s0h zvNR_*clh+_L%Mc7tfw7JcRNB+@$y^coVHaba9j+bv>9<DAD2~p&(BkHXl$PlK$S`d zdClQX-z?=6#+ObSB)4rF+zmHCqC=nY4>~K%3m<<a*$&+@=XH#qRhJJ`eXK^SI<|1N ziTS<P`&JR?*nY!)<l4nqqr2EiMW_tS_X|FMz~Yqlg8VxBZ=+^tTwc54lJU~Moi6U4 zu-Hzviyghv?yIA_ZTR*)RIk=^#?Fu=QbHY@4KC;fFCq#w0EO*8d>D=Y7|;{}9t!Z$ z`Mt;J<3D69x$XEpYCpC1Qk$(6`Qe=*o^g7{KfUyyF+u2I6dh%1YDx!Li*c_C(cCBV zQ$5MA(~@4jjAk54SoUO2@=pCZBS-uyUUK4=V`lyRwI};u@4;t_Tq<?8zsS=Kn??P| zFoEw~=dNA4z_*<6mSI#e7)-9y+>UMb^w>Lb=U_hPIbde@nv8xFo>HgoeVzTGxL7!8 zvDahl(!|pGhh#-Sg9)M!xIb54A#>R;&5r*5QyW`$2d$}irW<kQ$-aNf#vstqs|=U) z^6^|Z^nU9*se8Qi@0_aPM&J@w*i!m%0)02l*>Mxsrr969eS1XtdXw{I^r>jJu#=T9 zIP-588j<dw`!v1T#a!;(>BzOuk%H>UY0CC!ju57_3~_1pbUaXP;x>EWB>M$4SuMw` zRL76Mlb&80=BI^=3X42^$`6K}PlwUT=r`-1&USS6$(S$WC#YEd&VRpKC1<Da#R0}2 z9d=F&?Xpv&I@pp_HP_pugTj$&livRUa})+7|C+Mlr;k3#e~-#vglAjl-Ilt4e<05| z(t2~1LvHRD`@4&zd*(C*WIcMcnjp)I+6OUd9JEo{G`@|DOt8}paO-l;TwXba9Pa+# zja6V(j`{m9DXyVne*5m7=;cA}NgZO4#{FnbKQW&N!qBqjBPk)2tsglKbQzW~|I|@Q z&+MekyUVBs`(5#JJ~-}5$A96cHvWgA^OF}ZiJIqbc3eL*HJ#r_ARjtzT#qeVwuBz- z)gYXrS@Ssc>!$SmJt@-?(@Q{5i>M{8thjCbKv!MtUBUJ>ZxV3erKK@PD6J3z%lvR{ zIG!K<;h9GN{699#cOT8R7K=2)P8i|?6u2-`tODcX`es{m+KF~evr5JC8)`f;gTxxW z_SPx`NIa_+|NB{!THPPkYeV383%PTH&5n4l{JpH@#^(|MXfCFhckUx4Rd%GTtjz!O z;ve$0@`o$LA8~$Z<IaW1O8e&SqW)+1q3<Lr8GN}Pb5L(G+A}(#U5nlOWN+4X&EK}| zrpXh>12^sd^G4hF*W9_eT{3hx_;8J6uii1KHH|nRaP~J+nKI~-=-D!Zv~EQseVR=h z{7za_Eo0-pl$JKU2i$Ob!>S0B>j&_rM(_*|R<O(jbQWzOl@M(^+f(io7D|H_aN`wi z&0b<OPU1PwB@pvE^mLLtcvMzv*UIm?FvIO<7dA}5$;h8cwJR9=>*v>8zDZVAmYBl^ z-oW70htR%u?OKC3#hFGY>0AY51eWAI{W|N#=uTn!M$lyJfX@;$h2XE$8|nm}5;lzF zxd`N#mn|HN*>f6;y#Sz&7Cjsk8-g!^w6$*C+S_asl_81V@90t4h=T=8^krmZ?13F% zZJ&r+_bFq;OGKSc1`|JwDBO$I$lSbU&sgn2GfM}#u<9&(M=MFOmz7+|&(2B^xHD{l zxD#ZdSiWIm3Eu@6TTD!`^R@c>_YuQ~mmDsQ>cFR+KkC?Gvba;WYth}du0OxX7u<)Y z2=~Z{+LxEV$)#W=v@-PwrV_qv1z`u?UKN{%j~-<*X3s46S_`VxYgM||4<9@bTQP)j zDA!Jvex1?$UonMm*dJzoQS;gRes^PkDyy<ujQ7QGXhUR~J)>+Af{46y>5@UmLa*O4 z24{<BbpFr9l2K${L?nOCpBS75OO?3kQa1_`$cDjj;aEvcyWyY7&kPz_fgex|b8fPy zA8Mi97CY*2brifc5ESusHJ^}GwXw_uW*?1*N}0sw5^f64uJ<8NAh3E#OCJFN1KJW; zkp!)7D1|61C#`RKA31V?fp$50`63W<o(eF*N<0wg=Tw1Q)qR5RTDhJ9C)@}b34^kh zz|nI$;Oh}y0}C}7Tax3ON_=7M4mfqZ{VNs5%hGwMlieG}NmyE13MT@9ME)&{d<O&m zt)j|^ymX0g@9pK)8XgaW_2{gL`#{;5K3l`8UiziHO?s^W0IjKD{~e46>1X)MlGY&) z%F&rFR}6wvF4}U@7>d4<m5?)VW*V`&k!*KzoMmg24nv_T5iDrmhkz5i(cs~)EKBIb zog(&#vbYsDM3_Lp5qD+!UKnQZ_hAu-GoP(lYrGr(P!t67poK?1bTf@VF?VBS=(Qha zXD6DZVP6jMC5$>@06lt`vRYGb+qTK{jW%p66OXj0Xz2Qr!Lt6Du_ullyT__ero_&H zqyST^Qod7NmAzh(=+u@YJaEDq4XP&L2e^HW1T_XB5UN8gYJ={C<LE^<4<o^wle4W^ zfoF$GU>64m2l_wZu>JY-XZ+uCSEvCD6Uf*@vnKC+N1*Fr6DKZ-en_*j7lNF&&i&^r zTh3om^2qs{H*BC!YIia)Ff?R@>TA3IXaPdRqB$|?P1UTCa{K#vtz$y8z-z`Qv<n5@ zpFd5GiQ5Iio|vdUaNs~zVIRa8RsPC%FyykcgLu3W?Ya$?^Ds6rz#3zb`@w&E{x&ib zLF0HbU`spV<KwR}j^0GQL&O+lwB&EUAT2E|ejKgJc+@9GMn=4J-%3X+D@fs9n93qj zVRWb!*Jy%c;c1!&CYy)<wn$+nSxl9SEsY!@G$40j^M&?^h8U^Ues6ETLu=^YWv5O( zMP&2~2$=tmFoW!ceMEm={Q^W0)V`JwA)J3!xGgI_Df`}TJ)_hD*o4-v2s}3qq9x<A z*8KVVV6D$ZM9gOg8;y?t`QEtAgAMGV6GZMw9OrtBn64wQs7O<NKIP!4LG<9(DuHh& zT6i8|v~dF8`rgZz{drv?LjCZ>Wq}HT6L3E(Yd;U>>Z*)ROwOL4ku&uVpfUwZ2Jv&g z_=c!8giQp$>=CN@+ow!&e|>NGq`Le8`l!(sWNo;)~$+iGyqS`!K^rEq|(7NN%eV zOUHEQ)*Mqb7oHu25<ENR%$^-)_Fw1w^+2TuGh3SP1z2o9K<^BT*eDwh3&pak{Tw8k z&<sQ&bdtWOPARSr3huA%ZzFsQ#K}jt`ixHPC1BI&MW;V-BR^GC^c*teFsB9X1fqb^ z<$HWVb@*}aI>IE=&&rC5omo4FbHOJde!3JwgkNA_HZOQ16OKofT3}B`W>&hnx~h;L zB{<<Xw^a81XBok1@Ee1$d0n~G{h@Ru5Ph<7Y@Y~Q?1g11vfR6*LaDK1#}dgk{9FX- z`RyLb_ViTeZoCvx{{bRWu81y9tVLjim`>(C&{g)shjVAm+KJ=9CjOaP&?%r95epG* zbWG#=P)fR$m-PLoGKgH5KG(wfU69STsMia|O!U2$JmUF;&*v^*u7A|T%B2&kL1M`W zhYbz)Jf@~SfB)zQRio!yM+e<9^VRd`kp|)3-y467OG18ijOGx+y{FzuDq_hef~+VV z^S_W4w3!!I+@6GwmVwNF^C`XVw`t611@M@alT%C);^c9>JzPrS<CsBdhYlTzTynWD z91(7!d(baK%qh4Z_IvXDIeVP<62)+noWqBtUpnrzdeFdudE8RK2pz~~Oj|N36CMqZ zN-EJ?xNr`f+N0b{X$_`5E^(K#Kx#`LQA?9oyINaYALR$}d<k!gc`az5U{d7`3u{GM zz&29ZV-d;^m_{Yj_}zdBpnW;V6V4sCogQ$l-9a&=>vT(5H^2B$w4>r>R6%eu#9MYO z?`5+G^YAU}&d!7v8D<{j<!!ijT0YOVT^iDkn;uP*#JY7(0ktH~ED~vy3)v4Iq%#1? z92Yyj55nrO3S}j2l+$n6meBC<4E}k4)zw|Vozn92`(}CSNK^w^WGhy(rdG)~8dOWG zV?%OsvgmesFTzTKtuaCqMq^ImkgR>T51JFoB#hvSV5Io#*A>k(wKr}A0Jxg|FfT9u zxy(_1i*_(gz0&WHk)uX|si;fPyg#BGyXoGj0@W}IQVG)z!h0YM7%-)XinIe_Qv|uM zaLsz2Q)zJ(8zN`nPmr!=@=rVQVT``<;-Z(K$){)hxvf7>zJFlQ`1R*<r&iq{g*gR- z(`WN+L|q6g0p1gO;ezq%nJR%-UjF9X96LKyH1yZOLnQE<9W~%Q1&=r44G?mWcrLLn zaYx&u`UJ#G<D*VK7gt*7>mTj&`WKadrwXmPbHmfEL@h&Y_cqDp7y)WLli>hug|7w+ z9U#}aci+Bysku1^<#=M8IQ9^3g>m%|T+V4T#QeNj2TPax7B?g3x@2}=u=gY|>EdM9 zEmOGm)+rocwkD1VNGc26P}3UohSiVHKBT17TDVXt;7Z2g$-!oG3pQ!l#a{bUXq4~w zGhnGPN#oxwXIz6-U|ynpCa*W!)n5nSvI6Q_Uwb#!cn0d7c4?R&jHu5QtKu7KtVBkk z7CgRSoTB1EwpPTiywkd3{%0)0VZMwW#R`y_1!yBApHpzP5+x6mfdGQ}%uiYR4xQlb zU&+L?_3G8!ue(5_BTTywo}7l*fIB2;ec^M|*iq-*+qVr=cTb-`ZzDHedWM=>02j2^ z4KM2usc?#C)`v=F)u#_a?gC!`yp7X`m5>pE7$<I@KJ(6HbQKf<1lPXHx8J;y*z@0B zB}wLc*JWWZVtbB0BHbgWAq~9mG>^UnXD_NP!NkLcxL)?&BdTUp5d^2>{{G<)u^{0v zwr=b>{PpYC_OIgGvP~YGXgp96)5|DIv>uNq+Dvo(zMt4}vE{*og&dV-7o1Hf@mSb> zm!co_<?*RW(bM|$S$dMiF`}D5ap2f|+Hf#uBpjv>bQa9OLTIm;S7at0R``Ub^Y7+n z;M>?z`^COBYHRa$6KWET=3sC>3>TbVS1w=HVMFy>mwzZ7Gzmx0G6)7rO>JpV6(1+% z_s`{cHkgfdd4C){w)KL&q4CyZ`oAYloG%F<S*85gb-Q8Ca79_$-rJU&?(*_lpsSlf z&|@=HoWq+*d3kx>=P&niZ#D|je*!OambSVMhF<7CNDJ+EU_ry(orAyv2};FBvy(|l z)7X)u>_I9X+biZ>jKxgylTa}6A5wjv9R%NihQ1d+?9&2l{ieIy-{1ec^MVEYzt_X? zdVgO$ImWaVymr<3TRI!n1e<)7G%e)D^a(=71L-VsJjJW_xAr&EHne+!cC^!D-MJ;d z+m@BTI=1=j46S~lGJmNRqg6^#O&dPlu%O|Ve`+<a`VR!>EIuqX+Ki?z7@evJC6IWW zW5z@oRh`W1Kgqs7?j&=xv<^^s_3Ya>6C$tltRlx(WO0&tyV`q-Eas&RyPuuiaR<j& z)NIqHO=|!bU%X_=$jOuY6F>n%4cFhx1(AIjYbhy2bEJT|uOVZrfwXY?2;Y#Kr*CLD zoP$rJMR_N%a%wJ-?fv})XiVKpLSpBEMRbol1z@L`wCtg>2#t!;rJ#Zj(=as+#3-B2 zUR1smltS4|Kt~>-Z-0J~X-PqTJ|a|h-zqc5Lq4;nu5-#9Ov4STH;Z*ApIA8nPz+Yb zGkq`d1c3$gP&)Cs{|xuZ_%f7Z1f(&0_AU+&!|cm0C3&{|Rufhe^lN^d{gt}6y*2je z4JFfrYuB<KJ{0>@M2`sO-*5bszUJM_OdmUHn#L<AD(WP@6ryQLDZ|nrDgTMs>_7zz zfE@%{-R-B6O5nn8ji%LW^~qe6wqgllA`VqpDix7%h`D>n#Q3y9Q(icJ>H}p>X<3=n zD|5B#KDKg(ix*phn`>;Jyk_TUQ!UGowrYA=%I&Ie>1?18p>G(5uIkbbgj_FHmE8ge zPK|zyT~aR@na8A<>K{Kc(CRAeN*3+(=g^qjt^1YtHOqh7p-^V&ALYG@Eu&jje{^qH zyKIeownO<nz1(1D^`MycKaXAyuZ!4JzSYm{)YKct8rI$$ICgjNiR%%1d3*ByR{9ND zpYp>PXpLgUmt*Lm?)%Xv35!7j)*+dhXTcuOlVVBKo9jMm#*9CQf5A&<uxNqai!EH* zIF)f#Pv|tQTD^K6;hCF}w`o3wEC*}v>77})8A?h@UYv4)1`>RB<f#41M@Y6cuWW-^ zy}tZQjn}wG^rwAJSwxRBys$8oLyf$rxTt6+i3edFc522>C;KDC6{?WV{gjkhFep6n zV0Z95)Mnd`@K4dw5=zm3NffbLffY|Hd2js(4Ju(r!MqaZO`C*I2|I*Tk#4Rlh#u3= z6{6D(S1#bUE)<;2zba#d>kOr>#3iGPUT7`Ib=H)U`0jW?#EOivL#50E@2}M6+Jk0= zNwp*jFQjY`Ao2lMXth@ZDvH9C+<6s&EsxINv#T5-tapx|)JjkT;aWyBBIpKgY^l@s zrAGOr(q|#FB$11yBg9X{<_2)v@32O7g>0^y%vjy3z}Ak@94-IL9;}Lll$H=1u3#u- z-yi!z4-syWx?TACBO`QTVt|F-8Y@?Zd@t$QtCttDf8qJZ4|K18=Pbm!1dl$6`7m<= zfP_Mo9W&-Ik;|zhvunk+Sv^}rR1#Ea<w615>8-AAt}vjXCj+(P-HM!qx(yUZ>_G+| z5W7I!*$IV+s}r1faY>1A$@{CEV>;37Voe^m!Jxrzu0Y34Vpq0WwrrU&iy?v6()$n_ zH^EyTCM6Q3X6xzskQ30H!+7j7^0*L^w1ERgvMdW4bm#W{*RO+a*UMbpQom8%CxP?) zvQ8(~>`g0+ZedStr#S7M__*;|*^Kt<3w1}=t#@j7OPv2o+uUy@*JA41U+Oeq!rJM_ z4J`v=|CptGY0Ta=%VdkgXpbXDbXQ(_lha(aJ)rsby)*Vt*Vd-0xmQ#?-k+#c<?Ueq zy!DnJL95QSl{xebTkIE(gCn}NeHx`RHMGYa&!GS2I5Y=%wTuV|8+LPG?;{6uZ)gr^ zeaGzsKo;@z_|%!N_m#p*Tie(ydD&EBH3GVjR?^q@>zIpuLPMwv<`cj`3zpqjuS*A; zos}g<c_jSYB6u!DEGt_4!PrufT<OO!8S^$uh*v_JD=rDxnnp$8x}#Nblc1eA&tNX) za6jcpO$b1i_Vy3@S-9`KbV-4R9bit9;L6_;i@k&#m#FXLIL<sm_Wm-hq_y8#0Zj^o zOk11kQcUaMMxOQH!5m%P!^$ZQt!cqdj9rnl)WRc!GcNiV>Rmys9R4`3dV6H}1@jSI zuJJN%J~(Od*`?jQQTFdVlAcdG^WgTz@kKjN&p$m&(W;@zGt=x$X4mQsgA|?5E-E=U zwxul-(B_q`KIMm>Ic+&+%C~ia?QY$jH|^M@U-q3&`)CdN-SVUNskFN`gT}4>J@baM zW%@seef2+(Zs)lj+Eg8g@6^$Kq0n+f_o|9`Kwf5dZ@9@0N3HO2SLEg7Ho%aB)ieT! zQ$ox|jH#-s3Ul*_&asuqGGv~6G|JRVLyu*DDu|IGlZEqt$gWU{($cnR(?-H(P`<<o zB~wifYsXHVic#$d%RZ{UI7sU3C|9y7Ao&g|wB9l@K704J1Kn7KLy^F8GJZCtI9<U_ z+3DB!wa}D8Lq?5))a420M17S^c5qvd%0xt8x|AcBejlF*1l_EyU$7{k?98A$5n5v= zZc$gBwBvNaxrLj9CRooJdT*KK!q=ZReb(2na9DQZL+PE6VNK={u|1WiObUIOvVK%V ze0`dHMctc$HJyIO)CP^RP1n(0R2I>ran4hl0X0GH`5NDD{=>jPZuICdL&<+wAEW9T zCX%MJUw%ik{u7{#j^XaIsMB41&n&%A{_6Iq^4F{6`+PSXG5z;|ryFaf?M6kFP0#o9 zGkn$Da(H#b-Lb7c++J81YkYRx%i8M$K1`ea=xD?BdDD7cZ&=?WG{h@scZasWyZ?^a zs^<S|S@Fn4mhA3IOElb9IAzu?K7;$hg=eM+X)GaTw@P8_k)fhclAHPY^Te_)%QJ?S zA1F6=A9OkTYnGd9-1F_7+=}PkAK9m)|BJP6cNEQbIM~0*d~}6zP@j3r(@t$GPCi=| zy(9k6wkf&;^HMF;)bClj{!;Z@AD(k&G(^8w_s9Khr&5lU2S{3K4%nmrEb?G<bfl4T zj}Cv91B3{x!_fUV=Jd}OTDaliu{~oZ?>f8a*xm+LJ>`U8?}0Uo&ScleD~@@;LaUE$ zy5g!3x9)!rZRF(_JsQ7>e>7&C%KG^q6!fI0yK8)%vSp8^dBNfThlUECb7#NL5KEKZ z&6kBg?7@C@GSzea=Is1XQnY2bxt6p`S+?`im-_>bpZ2~LloXnKeoN<{yWN$`ZR%4F zZIg<cc1l@i_Oz5i!^@xYS8`HjN0e!<-_uhM`m7-K+Yj|tiUAvHU$$!3b->U*e|3Gc z)S&|Jq~88;{P>!JMWq9Kbx`P>A+=n8Y0QCLQBvKm8;?A@bn$TWSz~X<dmH!pqWGY4 zUefgTi~45I3fQdJwcwRv=_C2<{pCfIu0J?8nAcN1J}>i{<iIVTor)u2rV6{$S>e`I zQdOk~=u`NsnM`8I>U0!A?*k73AIwIbv}MZ=wQp9S4kWW+auGtZ2XtTRa9_)FS;BUg zHtiJ-rzG*q)r|uN3=k_1`43UAW>&t&+>c*YJjl46cSb}1(wW(je-Cl+l#wp4k}v06 zHD2+irXgQTUHP`(p>4+J^f^HdyG_5<_kOsne}}mp3Ohqm!DXauToiF*dg?Sb4W|0X zeWUP4mhDerA;>FIdwRNONFrxp%)bnvmy^D6k#}wX8u4b~BHUy`cdz=tsO+WD0RaK` zDIn`+ZIVguJ^(ZaOS|7I%TpH_8|S+{tpWni&dn9G0K=U9KZFT8Qt|+z(mg7L9}7nr zRj-d+8Y4}zmPt4SW=tv;tx@UAe`vPm>U-T+sr(0@^(xp_+tBL3fr9s!d|(tAvdHCQ zSG>A98i5lS=<+F8>`%g_@E&!l$jOMbBm}A8-t_f`klC|R6MICX;)#%vfz#ctzfHkc zj*aVL^QMc7i_lC$*XG@Jc4V;d4)E;aOI-vfhVt9V12Lb7dNri*KE6X$RgW>XHI7AL zh^`+3Lf>@#;VJzdwh4Tybu2`O7}Fw`LF9yaw*0Ywd(#(mW;{F;xxRtIEBxo4$I1_Q zC;jp7rHNTht$c+67l;b&`;=E&_V!AU1a~qr!m}DtkQ8xL=n0@e`peFA&-<UmS%&#c z1qJOXF?b~-98X$sir*$6{t8<P88fU6-~Mtt14>`SK$(U{+SKKhHiwzP2|zm0k@Uzc z-QOot1FS<4Ba#6Ueo;Xo(7W-{&VN#V@yByY?^>On;SofpMrLsjLDIp&p24@8`k<hg z0wK~ztx<%I!k#>a32XC8!vgAHrbxiEDbW5Ul&h5?wAyu(o|Z+qEObG1rEDd2`ucj} z#fL&){om6g012vC1aeqR`Fmo3NDwp>2N~wLZAxPbi9%)Q*Qpze+n|#V0sFLnpK^wu zAx5*%b)a(>gGKCD43HD9#Bi4EL)Kfjskvbzz0+QAZ|$A~#O%iZ<V6;rGe<o~uLoxe zHK~U(kK&r~Lus~tf@~r5R5VQe-{>Q!udO8|k%C*rskCj^F7LX7n0e@kEh_G;x<UJZ z41JNcvMeh*Cr8041?t*5Qsij1ua3>j#KHBpbCvMOr2d}Amu02a3@Q$(y!XI9Csk}Z zK@)s<<M{r}XdBa#t~ihbxl_dNMetl#Tc%UdAOv}n90pPj+@}@c9aGPkAasi>P;j_1 z^5NXIw}4d#)A5`~h9NvT`9&%ozc#a5_m9-Ojta{Dlln|RyNT~#($zoUAU}-DQ>k@F zhS$#=0(sEPrsc^~rwSC^`E`Bj#CCrZDZ2Wm;lFE!9~u1i@RG;dpBJo?>r;D#0RSmz zEh1d29aQ^b{D)7&PEB=*`l|P%<>dAUO{}}T$nk%h4Uuv=8y6d^LF>;=6*o=5{>TkS zC|^fBB}7NrAA~d-eB&{uBXWDZq)vn)M-df?;GpSjh$mtS%rw!-CawgoC7t!lRD2S` z)(}dOM=K}S=SO8Eg&pHD^MTXh2xGAjCmqMe$F~+!rwHNvFIxOdp+rSReSa%tU?2u% zuLFaE@=`rpRy6^RgM)OEQ&12~w8vVc@VmOeyE9g{U<$)FBPL8Rx@FvoAZKG^BLTSJ zJ;ae$hnwWFUxxoRuW>Fq+WgX=PSk5Q5Iqb$T{L-oTP^%b%`#KdI6f2#Tg9lMOgmH0 zBCihx!#pHCeIOhiP))m1w>f8~xy0YK3$HdiKHr(&F1#r5T$TW@+eR0M67xKebJY@O zuN>X*8k0ec@CeKB`uCpq)6?7N={;`t9Gv%T00a;;1zNqQ%#aNGc22ZCpnRylo-(7r zSe{@qC=d-!A(G45<23`J9^6}yn&S~<TwH&r=rj~wK--ji^Xc`)(}Ab41bLw${GME4 z30p6A3_`}jq?oRI6Q3y|S|c$>2Xzc66bct97zx_9tb{k{3sC3^k4jp%_zgvxXv9aR z?EH%<b!a0qAQ3h6sqtsS+B<i)Y4-e=*f93s2MpnPgJ=u$dpZrpUfLG{)(2dZeeY(^ z^G9@|{YF6V4<C-1<bD}EM0_evf@F`LJ$F(D^IPAhdIlq(15hgc)wyFw;l({NN8$<g z#up#cr3&phFWY-a1Ox!8OF*at^wGlbJ~7cHZr&&{0-}ANz!7O2g<k#7<B;Wo|F0;3 zN(0&vhs`*~{2^jU?=gdcnk~Mm--dPv1v$}0UE;qP6SZaSDXxb>8_R5O*vX*=hb!(> z>4+XD$-U8!U*LS}QSQJhuu&n?qz%p62r|mg7FKtRdyoV-Am}AN3CS*V^CQh;&e?@e zc!vfBx~yyB7Wre@V&wuYchw5#U<IV>RSEqU*n}D+_FIwc>W{EWKp+$}*^eGwpjDt< zuETnKan!Pj{8U65rNGbE*1hN?0s2jw`w(|oeSQ5j2IU})@QHojE9OOno(x>yA0J?d z5P&nb9}`PHlFPqW)TN$}CAeoked<e9B0emKEz&Te8xPD?c?v&jB?T0Uf)C&&Y<^ho zbLY*QL1ZHan(qNeVB#xJHIUccl%S0u#hN)T&=rO~ci|X8tPTIT{~y6CwPMrr=;b~q zq7JEV{4QbPsj4N{W}M2zdpSvCj@D;-h+GUN`Oh?X2A_wbA~!+)^p_OQr4UJg=}1+? zq#cYH2f{CwPXMC}BVG8?1JK8%v#ud*r0F+4cRFL_+Z*iGhOaE5hh(ff4|d5~#Wb#* zvR`b?rsaos62X|T6pP9pgf=Jqvfr<<hj|+D$XP_|8yY&4;4imTsjcg_LDnj-2B~+X z!5;tro78Zsh>>XDzC8yo6OA0$f9&a^PagW?3cbb$SU21P+Mu7i_vFb@;`>uTAy}IZ zUAuZzCK#Of#@I(zcJ{2ri{qZhHNUyl^fHi`A{SrNjlrITPoLJXNeZbz>oib!R%q0L z$KVDvT!O(1`p=Q*pBTMk@jxqRKlX+DU3m`=W+0OhhwdEFln$Z{zQy0MHnfXEPoy4Z z#EA+&Sp%?(9AxZB1rG}W-2BUFfGLTo<M->ja1|aJ_xZ0K0VNz6gCl#W1;8M)Q9Mv; z-Xnn`4TwW<q6K>LTG;42JJ)wgp4jU@q=V<JEjKK!KGa*MY}@&H>bN;ZTg7?-C|^^+ z8Zxfr2at=5j}@+GgbrrNkca%5a1RtlvPw!<((kT4GiA#;?0!Wh#0}7Dj1m$umdt|` zLvw3?1?)dS6hz;ku(%sgpb#K4(2v`k6~*bu>m==Sl$C;nx3U^urgQMfkrSDJW{Bkm z)XDe-%jc!U58IrhIe-2>z8hoAN@dR;ft}G8Ad&uY`0RWJY<QGzPo6xHkTcjHI^nle z|2bd3e!WO7ZQ)+i7GV-H?0`2{s~j@l!1Pd-^V{#;z1un~#%l_LPZYrFI4KihGIMiv zK40B1;@PQa|M<-jAt4$B$pZ%u9@ErmB?u<KuJ8#9s&xJ)3<dKLgjcpbrn=OQK#z}* z370!fXjqEBbPR^6+9eyc#{FQ)hM6|$lJj75pg{a4p@r{aaMZEPnE`JK)o7K&(3Ecz zp<Iwq9e-Jmqzbv+$o0@NN&#cx%?_iZc<)}P8|S!OL1iz4(^(_OPE=80#K;Fwh+>5t zDYk<69WgX8IRCa6qOBh9HUFtaynGI=pKi|~E^>M>H}sJ64u-K%h#-mfW!*G|HEifd z5DSF+{-1vH0n+hpFo|2$mJ$)MP<KyHPxX5ymI4z+Pr%cpp3wM~XhZfAGJ6aU{2>S+ z^k3o^alrCS48GPGr~@_OA4@gcb{0-H|8sId`-h)fKQb!D$+M|D@C{jsT!>{D)g<45 zwQ24{0vFC>o{2{(QnZ-Kp!cH=O@wm=$3mErtv=9bj-a*~AHK`;XU<71mDkv=`P=)m zV`-MJ7;K^LlWbg~h0S!S_tHN1DH<6P33%h@=a(}$hGWV63q*}Kp-s$nWT)A!T)Bh) znQV>V;0PT;9Duu^B$2f*(9?^lUgqL4HYe<MOv*2B0^L>q6zWHF<w{v17PI$p3*U8Y z<2XB9<qEJ|wg>+B#R%chb}v}pMDG=@{D*V|&!LpJlwL?DiND}&X1=2}lY3A=zKZkB zo^D&s14lg^1En}3*hYAyqw&+=Jz?>)&CyCOYx<K^UdfPt{qwzV-|6(PUTucb*N3-y z)p$BrJb7gxS284oc^<M4hByOJM2tpeI&}8l54>C??LbS)Kc`VQi9}1DILTnW&_E)0 zbpQRMAly8Lqr_}wKdlv3c6J9@1;as12lR&!VfMw$zKxD0h>)lfo`p<6;)WzDjoc=F z4h;8Ql8}SL*ZNB(nBj{dS=9T&3c=9u7-lj(1`S#ss=0}zDn=<dYOOz%mF?kJ8I>7O zZ^AgBFH%$r<HQTGJrV{~XGcK&V{u{<ybJX`hxy{sKNNYHcyqi$j>%EQKBXES=6J=$ zde$tD+upn_F)Hn9+?AR|fhv2bkBX|Qe2})nrHRkSxCXkX{m_HLhk>sK?u>G27boL* zNzyR8vLA_ZKHtbQcc7XkZ}=bqIWbg4#ZL^74H~+{#JTmIH?q0>r|J3bvE!tPf|uTw z3p)^)C1~?eUn{WzBBHG3xsE)mMn@r3DjdhMpMwSruw<QeOzNLbrTvsa2y95#tP%*I zu$I!=f0KkKgn7^^q7{epEoQ>sY33rjE-XGolud5RK9Mar=(1G}5eozFGT@+27ek37 zI%~(2#tUXv`qYew287Vt>01hfMQ4UYhQf`Lv!EX6&@UN%T~NJI4AUI^GVS`e8GmBT z)xh?Qq*T`AxkjFBbH~%`e5q02ZBe#6-<WI;YOdOoUZENg`&sRX;XW?`)tWW!$({6} z=3W29pRMmiEOdJN*!p9>MqDEb>pPx)xr=+69qxEPqwJ0LWdUxJJ~E*wXf3Xr^g&C% zgEx)Z;QXaD6!D=d-BSQNDFy_Wzt#dl12Z~2lztMxw9J*vLIe4H)X<@!Mwhp8B(!vO z#~2t-MnZ>xBQZAHyEa;LR%eu@b2wxaZ^;4Uk>u$kG|HAR6%@Wunjy!P5<#(A%W&o3 zXATaJ_t#*9ylBkz6$VZU!!wmNZX`?x`M4W7ilQjE2VShQ<7-^Q2fHm=)D5Ols27F3 zM*kOWyjpr7AymG8H#TII+>(!Xz_|`%Zo}8%b%HGi!jTZH!sj%J1VqpG^39u)qM}wJ z4z49`9*~+(LnTmW@#iFp`3yM{8OZmKVEqcUMGT-5H7>648rG#OJmj<3P4&k-Q`{!H z9x?jCw+SaeF@srBS*e3O@8Irog2A1LtkyT;n)^6uyq-7tjpegvU3T2AmmcSIS96l% z8|4CXfjoHWmE93Y1OYg1-3x`#9wZ96IpLG8phFX*l>!#$IwL(13ziT`w5Fy6XQ>sP z5o!V8x@RO((VZ|s19UMj?sty03IoJB$lms`;)H(oKEph`Qmq#Y3Xj+Oo(h4bLJR~c zRon9NcHPaO+LoOqG3%Q&`}-yMC1m&|?@u>T&3yFIHvYJQ>9ER|C6}))n7CS{r;6!l z)4c<lcJICWJ>k4{cK<n<Yf^vz`Bcz&YuU)vpX%JFxlVh#>9Kn7<8qHj(-W&^<W60? zi)&8H0nM@lHDzBMCI~<v2PqA0I(bzu9ycK}x8bQ|!}jGPBv>kZwAb3<qaV#TZS)+H ze&0u0mOXl2)pt5{ewFpDT7T=1UrXAA`K^QOf&dj{3Ddy)cXaTQO}dcxAkNqvZd$DQ zVS59`d)HFs!z*IUrwi0gD2P^!a6g(p*v?BoZvE(pmDbTW^RC9nA3`B<_xgForn#$h z8$Rt<&!1ub-m@gvd(Zm8GkW{@ZhWRaXy4_4bBWS!?QF{{iZAqNQxrz*(i765!FG;X zqcrukcUY^SWf7}pc+lk_P+}4VyaVn}w)F@;okUAz!q0V8*Onezmy@INbh&Pi-Q5@Y z6mFGhb{Y|HaR1H~-O(2&Pd+e6v&U{9Z`1+{E-jJpHBp>Pnf8*YKR}lYz<Xfe8__p& zk}sNGz0l#PEI-*)#nCBLdR<$mO=YXrPn^|b#^kO82R^-dvEzLOmsPqw+RW<VJ-nBl z%{Wje=FsmWU=btfsV%=_CJbI8{<u@07j|9eIEsI2Uov{C>W0{pcZ(c`s5`Z>v#Zpc z$$`tj(+nxwe%X$l5J0eN<%Bu?IOU<?#}Y(EfitFiHgJ&wgf)Vi+6}WU4AYMqK3r0; z`j}hkVi<9Afnzo*5hOA__jf;i(jy)RhvSjUm%0z_#YfOKj@A5h`Jc~VJv`HWq?i6q zxFN(5ZDmJ~-p8%NX>Fma>n)SFI{F)E1;vG+qVbB4SAn1}t^@X*|5=40cj0YAL!Z8< z=k9|2=|SQF8HIS6+bzErmi5p-?23}rgZ<mj-QW-Iw*T@ue9ijwGa1?0HBpak)~`Q8 z5BmM}-Md~WM!7n_V(i__&83Cxa?P4%yBIxUAZ1#XwM_jEB?@P7vyAozk>i-7H!(Fm z;F4eHx8O=gFWC;7ZQT+If<8r!>flvfq~rGMWP18;=dIgO98<ClOJ1a>XG?n{3UnYY z%T=ocn@?70{ekxbAHJz9M+zMg$Wj_<ke0ty<Gr6~z(wUi%K#)G#>*y5n1CcpY&&CM zfS#weCH1lsHIMK0J$v@_l#$T`6!~{e4x`W4?O)xcw-Xj_Bqh8k^qI!%bg`}y+<1Y8 zhVV)Gh*~F;mY+uQ8K|HH*v(uJ&27vt1J>HW#xd9%V6yL=s1n2kCvQd6LShsGtTnuz zi2RtGBD903o5XkmKZ+^lIhe<wG!Wl6l|?}OmEtd77V-pg`}9VQPVQT<V8PM7lFODa zR|PT?P94OHqchFWhYDdZWOoC@lR16DibXbM=#E}K+=0(_7j-{E8B|Qs8^mnaAmQ5t zI6jN-{@2-y9*`(u0BywWJRh%P*Ho!}&}OZ}yEv=%^h4r0fwofK2-A^AkGdgQfH1#^ zwv!V40!UrirIN~0Cv_Uha@s5uTOFYJ#qRmo*rT6;Vn*T(^NQtNOv$Aq3Zx|z#vFS# z%_RvG72Tn0LulpudYb*qag)}+-GA|7#Z;G{6h;C|syXVZv+4-fuFb5-m2H#rCB}<m zDRcO^oAb7yUvt`=Yh6}ax&h=_sKtTxu6bZ}W(LdwI)X6ru&-}V9D1<+kPrj8Sma>X zGyqJ#2oe-1UQ$>&;c|kYY!#j9zfN1^u%Gj`=uie@!yslD>9BkD>5~o;j(XbmpYa7& z$;dvu{bSOWu{lVk090q51du`beSt#(|Jfn2+4Eo<sApfq$kaMd^YRoDy2Tvy@v)_3 z<ACxIchQOglNsEt;1mdtFmO;fR)Nd`O*emkrYFIN-1PH{DY-x(=!{q>nJ3+RP|%aK z-G%WZjyxZ!96A^&kJvXxMEI){L}974^pH1WH=<RcBRMWamxn;u<wv}bKxrpXdKe{A z&Ny@(70KeiP=507F`u$3%kg)7{&T5+-`x0XKd8vXv_W~f5G;4>+SLZh3RBKU`6nT0 zNrHnd5-S^SA+5#9sy_-s6twiNLIOyce%=GkwP>8-#CR<GIVOyj8GSNC9x$7_2ZIAS zvts}<$yd(@>2I9llhFMwcN|yAbE34|z8(~c+4no(R$#db-?!7wiuGD3n0ofpB+pFb zSo{+e+f^uAta=mIv0HTA#-M}+ZA!=V`To<zpZ<@Uyv+xC2<ncjxcba!EbT2JMEkr9 z07S@Je@6WG6wVTOUEi6Qwuy=ibS~_iU&|JDU#*)wWeUpJcbd~}yv6V&eWQRw(e}#X zN<n|H;8-~d=6DqK;MhK9V?8fV=~sq1TsYt&h(hZSh(f->wm;2@&vSJVDYS3DPbDL2 zW$ID~*gNK(s*x#52x}x>4O$2>j3gxHkYu9bM<Gi+CU*M)E=e>_zs`q;Y-lH3(coBy z2jnK=+#UGY3i*7P)t??zK33d_bh<q_kUTSYCNt)C+cZY!kar%p3QYPh!G<gkvI&83 zJ6u18)h{(PoQ)=lfrg5+twmczgW7&2O)0VPDC#*Ur`kal2mMxoFy=hZ=aUHZo_V=; z?b<Qyhtz-cm}zt!8n_zdc%&vNkd?svxn6Kl!JFOD0YPVpZFgB!U2ma3#AT&Z8WGjJ zbX8kUHVI-QerOl>eSqRiXG@lXbdU@Qz9}v7_HBlsvYh=)b~UR@5n9k28HA>qckt4` z_2i|D)*?6A){C`t&D3{{xiP%x`u@2#)*;LHZH)RlM|QW5*M$>(hbY~ysq@X!4m5cd zW!{@Pu1@L&mk;&>`rL#xnuS=5Z>)m7LmB8B5U{j##1D|}S)-1PcW>Ch6>FpmCYptS z?c`No<)}btN%fNDl?Ywo>C<K()0d&#J707W-<qLQr!Fs=x(~RP*&~fV-Vu(5<>$o8 zCBmf>%6`}ru7je=ETfQXH>HHR4^XujJ=Lt5?oumS17X-mi&*g_RFcV8v6~f(u-4<& zJds6cmH{DM@%&<MW(x1*<@E|&X#DZlwKY#QYs{a$7*m&QbfUjh1X)Ljgem+)=w{oh zFj9Q~$$Wdlx0pwFmY%g+Y@n3;Iv-?HyXkUdB*r7*u@aWEoGvGAaeskEjv89-`}8F4 zA4H^?>x$Iv9($a0Zl%!vif~9GtgSAZO)SZvqIq9&^&ym_wo<3E(Y@?scA!-y8|38V zR3(35LRSDO1lvZi0mbic;c8$MC~MN^_MNeA+2<wW{tsR69nf>%_Wx&{$V&EJl@ua- zR3ubLI~7HQgea>tY$8G>DbYftlo<_DN=8JbP(}%rR9afU$BXm2uJ0fB@1Og;FXyQ~ zpZEJUj^nwGy3qX{G-O|^dq`ZpadL=d4=Swb@11_FsLpZNQ8!=mX0>8Yo99%1cg@Wt z9PL~pbNVO=yNO6i0rN)|DM8Hu5|Imz6ST6^$$#*6h{H*soGVv~GzjpMu@wvMER-8D za^%Id+QWIx>w-f+G=1vb{?ga!YgVly7H5md$$1&lp8Wz_qKYZrV1?SEMbD;Oo}*VC z2o*;gI_g)TuqZ{0Bgq2lZ4;_B?%2&+wydHz0ttM9U{(0p!FqrOx!lCe+*7ftQF5~i zG)z|->PwHDIWquV_$E9foOjia3Z5J;*`fF6ZDeI-J$aw~DbFx)`l<bVQc)r1+-AKA zxC#7{HftP1%RmImf8~BDEp3dlI9)hLFI?J|i-HM?fmD$pq`XnFKolNLvYfs#KhB+o zi>no{eI^fwTW5sIjTRi<Leee@9S8<^9)tu7mgx_38~uy6=wEjLrK74avhipoK<}3j zTEL$eU9x073OyVYwzz=cix6D-VIC8Z7avZXq_CKZ5l@AZ5CXT;*{I>dM<q;^G;OTo z15ico4Gq74ga2_*wB{kL#vmuDm_xf+#dVCH>=_FNYBd<Ww7~FT*tHv|VLbTX!W#o? zCw2g=(&(+jb!OL1$uz!lSSwy<xYQn9_nFNlszdwimQq#7ZaKH4p-59^M5_CWAHBl4 zmNVmsbeSj8#AM{;v<nQ-j1jOh7&=0X`UMEv3zwx#6Ce^!P&oRra)87*z(wK;(QTnh z)qpmz1Xf6eed-(Q9ljMrJ@07b%@-T)_4tl`(cy`6MR2fSc|rW_mKEphtUynITlGF@ z?<IntL>)Oc`dC<2(53JVg2DtOqk}nlYwo;x_OK;F=7yY-GF1TI$hn0!lJb*6LBy@n zv6JDWjiZ;$8qbO6O!({M-#LE%9p_$|{MBfT*^TqmE2ZyZy?8)aIZGw)oqOUiM*DEs z-MhcdZ{7_4UUzx=*6-(8ElgtI?9QyAaZxDzCtrSANU%B`+f?Xykb*Uw{GUF`OWU@L z4GIbp!de<e;k)OA_*qf00%Kz4!Ajg+H*9mdGS`mEMHH#VgQxNL?PCXWaW~g}?uO}k zF&bO2GTf&YX4Zv*B`PmCN8Q(>Z{IX{BLUkY`1DWG*e58{PJH3}rMcH_2#P6isU3u% z_WskS9wPma;^7sX9WVsN*7F%h-cy<|2JOR@sDw}ajg-|tRc6ivP{D#Df_<<@?-hwk z7(FquX1d~qAU5LwkB}2+$-Ql32mt*6#PvFInLjrz4v>X-sKKh{;#2_X0WJjjjyoNv zCwjoPABViS<JU2N_lhkgxr^I|?6Fz9R?|Haf<|ybP>Y+kZM#4#!d-|XX&>$(G@wG` z%C8ks*Q_?klwh_AJJJec9KTs4cER?K95bd1*&T$f4wsb>)d_6pJPK0WMBZ|bm2~Le zNRnZ8-GJDo$I$MgQa>&|kygyks5l34-f-V1(=dmCJ~7SH-0Z7Jj+py=h_Ttb!^%#- z)}jYb<70}DZ|LJy&;$CxivWCt8_LIrsfiN=cC6-R(Z97L26Ij^5<g~v?(mR-WHtB; zPmIpx^l~o0OB_wmszr`gzwT0`^%y)@haP3YqJQ}Nn;U922(IkA<C9l4C#~2VRkt_V zI)2{)kHn7cH)GrS6I$0ecX|vQ=mA{KP81*nM4)t^B6$YZc0qtUN6t&Ds-NWW=$a^a zi6mkL;k112?xlVa0tfty^t;v|<^m@dK+Suz5{en1_B#kzSVRNp!DYsd73=ZaBRZWm zRBcRz-_NNe5Ps;XP<!rIE%uK9Na^&I9L{1{_*q$LMOmA<cQH|c9PdBTdINawLV?PA z5LT=5(c5^R)Z@q|wsW-gays8jSTImNQMzj?c7-763|C79y?N`_Fpe7uqA~2bfWPey zi;5DxCev~yDKSNq5nzQ7%s?zDoxK9&DdgI?9^Ds5igvt1lG^!}^WBGcC>QaNgdhn+ z1G`%IEn!9J6h*EMr2-(~NLeW+p3c_Ql|m0rh8ljO=e&ndnI#+=^xX{F9Dpc`)Br#5 zUc-F04EZ7{4<59NUHY5`2c$zW`WPqrtoifXgWw^g9!E^Yt5*j7bE_tAbzKG{vT4(% z<utb-omTvGF8zv{n$=t=q)nes`>cKCVF{T#oQxvDfzTE)c6`!0Z*NV(CenWi5g310 zWb1HtDO~mV_T$Gf$b4k0viLG-7diC8KJK(LJC`$w+k!G@Ja3zSAcMUYH{MCb{W`i6 zo0!Ivod?f64OV=Bz%h1<{`Y3PJQkZN0Quj=Mga*Let2O-D-py@XDqsU2w3ROq$f|V zEPR~$iDPIm4}`Z}HLt3yW6S6;POi9aS2NpPaf+KhYPo3<(Nx&rxS+(zBn}O58iFa% zM(EHkglSD}MTNtK7{+d=XB4xSd!Lc70Ab|$qY3nYO60ubLQ^aaxp*-Zfu!l(EjE4! z_Z&m!15z-`<I2XD!TTphkC=sBr0+EQRnbq@08;u)?Pv`$a|dxmbT>1&4%fzt`4W(L zNgN2E-wGVpl2`Wh_IQ#A|0`lopI3;)oYkuzadU|Z2G=PyTL2xscEe2PIx$$9MnHp) z-A{Q{2mFe|$Qce&73>-P#k;#(7dYE1GA_D{()G$W5XC#kOsV~%JkeTd+_)h|qdpDn z-!MX{NIAm4TlXnvhZ+ta8C9mfKrU+Hkv=k(xjJ{3wl9+Fl#<d%Rog^quEU5QqcO7$ zF}lY#MGk(v_b4d^U*0j+e*vP0`slQq3&p$U!Jc;ISL3kQpgcj^)gHojXK~SnH`iK$ z!mv(LQd~RvTu9CPR|W-*BYY~*3q8|M6~sEsUy(%z;c9+Od!Al-o6M=gkyU8#c%nh{ zM?Bp4&G7}>_8+6&b0{;b=pmo@SYtnSi;p<0R?+!zA~QM|DZ_#?XQSErKeKN6W?$f9 zabR?9`0cza$!#qEoc6PK6l4af5@r1{#MYO}?}mD~t|O^sDAr~#S<)B1np8pb;ms(t zxx|0vr$0$c8_M)?VidVY8ZFWnYHI49cCe5?r|q?508&OUGU3(`$_kuJf}R7M8(tQI zFA<bHeA7|@d;HAz-@O~?;?gwSw^{{ugtG^VUkY9kTX>KQlbm%|C6#3zd<%hTw6@`G z<mvFp$Y$eZdX`}%I8%l6C4@F8e^tAVHvs7N54NRFmgw1IW7tjnq@G}|eH7;W{dRT5 zO||Przn!z!^sd~jwsWLZXMqZ{VneQ6G5_A^<YP+901~j%uE+%$IGvKfo%Z|e-S4Jm zYQ1(9AC?1QB@!Ss5@y<^U@k^05)ocUlTXh358I&D^s5I&TOq$x41u9VAk|`Qd<zyX zQs<xZ$C$4soqGS@837jw!rU*d4vSX=R2j<YUO(ISaUGkhr}QT1lMxdpTu*vCxKE#h zCr|1}tI?dgg;cqS#I?aQ`%;Pu`&Sv7GL2=+&QdQcln%iOikh98Rl%Vj@9P5_L~@Sx zt5>hS<;OSdEIXhmj@ms#dv0q=G|BDW|7qB1%emv)er)p^o<O8rP_f|{iSD2|K7WmV z7bJU$*0}NQCMw<gS*|<EDI)yRC0wkJ=yJl%Z;ur&DE>8-SC6#Cfv{$tLX}J4plZ`E zh&Vq{?cxU#wqH)}Gm|$QCU%YTXEjp$v?6;j1!BOhsj6l*hYAm+2=zi4c7`P#%~6NS z_1g6(=hmbEzCs`iwhfk6jI*cEl=RX&<#Xmte<*Z8xA7i&IVBE1xTm*B%7gwB_3p}O zSbf3Vaeaym+E|A*C=iYys-fZu<FK_>O{8PMYfUHq@zbY0{5%Rj5oNfdd4_=CD1n+v za@P!HEjBtAN^Hp68rgG0_MeV=<>re0o<Pl&I|qG|_g<uDFf85{>y4dCleeyxQ$#C@ z>o%P87j(`Z2J`~^T$<=Q2puswXb;ImUql%p*g)=iBCDmS3xS$MnlHbK9$Q$1#G=9L zFA8=-dnp|jY7eX~d+W<7Q3cOb3H&K^sp1DxWQ5%nU*hxUhupWqh{d@`1>1boX=^W$ zI7M|0?-`R;+v&ib-n9nT3Tym@M2W_2%ncX7uL$$o$N0e_WnK_O;CCd-rT90c&NcBx z#(ETpFot)6&X%O-FZ}wkCLZGX%$C}BIK&`KVZ)4VAALuh@VW48^@i!=Qyyq7b*8aZ zAXUQZ;~q)fZ^rGKZj1hlLm{&@5O~5NquB-Bq7^O;HPo>((*3xycG>wU7hR_<)T|5A ze)Z+d8PiKw3f+$=bx4{|iR5G3)BmU0z_}foO>rg-4+lgS>)2UWZqe4OhFUid3s@7> zT0VZ;_U%YCzgk+BvmUP#equk!-{1cTQd+u@=1Y-9EFo@5mo>%bNHn1+8Cw}o+s}z@ z{KY}Vx$ZdFdHnUecVlCt>5x2sUuT5--lInsMUO9ge{Tv(xjx-uwHL}{|NZX#-~4D+ z?sW{k+F7eP=Kkq6xbfw)kFS~a!R^`X>Ae9cpFk<U+FAm2h7VVFmMBnXgVr&TW$wcL zR^dpy{(Kj)EzAGQJ?ORl+y&f#$DO~x6t<HNEWvlh2FmxkR4auq51m?2KT&=+%fJ0n zTznAs>z%0oFb#ixocGYOZ!3|IrMon8NMd4Xe;;e2*_(x9eT79+_-sVl5Yd8u3|&xA zsJDE@S-T56o$&o4SYb!e{dC^4Wy|5w^T+yLCk}+!<xgQju`G)OB3dd5M(%o$hs95s zV{SeGi7LxoK{f<Fh<Py|r~>#7$h46l7|DM|3@%jdT){^eoGx_ek35H1u73-PO|;HM zfOR%$4=JhploeqqpJ7sLC|ORVChVx#BQkuN8yz`k#c$8eHC?xuD~SS(;3qE{7ye@= z%?~HikX|jA3cjVe3H?NBQj)sa&oL+h&_sYky~xhKz>=qg3A%h)vanlO)z6>H31p%& zXixFMi~}K@+TGZ$P)HbX(~003Ik^kE?@<BjfOu&i%|T<on{y09x+r;5#%i00!8o*@ z2qaQibfjyn=D;Dg>l7YZ>RnF)Oi*NU)i}^=z{-dmQ6UowA3sL>Z%l>=X$N#ASaW2` zr>$Wr+qi0I?+#LmiR+RNB8=q{U}io)wxbRGOJRrcSFC@}^xo#Y8xCavBjIU1#h3{R zYOGJRn^db3fSUrhLc5I>3XZT1J%TG%z~L(;e<Fj629^|~J`nHq{Nd|%9UYuoL~T*3 z29uQjUE@JZ=XL&?o<4WN+@adzfzIhxcM-zMwKdnkpvNvS8zBxy=;f#CGF@DPkT47q z1(rg4E12tq=HIqL(hO|?JEag4KtBpGbA*6EO6}n@W{7|r)(otYUs64!3=b$E>E~<0 zmc_gXAxWUOf<FAh8JXF@TPN_h6RQoRW)mu9(q!-7zptD!j5dQh54C4Hp9c6q1ka@W zP{T?Z{*>4z4AHVTFU}&MnyDu%P(RYgA47XD(yo!KoJO?{|07vtiB9A&Sii{AKx>~t z$bf)L>1QOZcRD(b2bUz6>l$->pqfx(sc&6+V{LcJSSvOH&F~ADGHyF<wz>9ElX>(0 zLEVXLD;i~t+~UBev@lRYI)3F>@%YnM`C;X!rxK#;oQZ!;hY59`I`J#g2fi{95Gjx6 zS9LXDP#2U?7~JSC)OzOKzVRjy(37n(j%Hh!0+2p%YPWN1tT@>72|t`e`w>Q0P#Q^e z)z-~3y|*-80XiH%WlAx-F*BBHnU1@V`&<IdMju5ijnsur>-EOB!^|AUtNmZv6fWw( z9d$<1!Kr@;RB>t*>0h*H6I|1>D>ZZL@V>Gx-}LoFq%rdo=+tk#r;g*3OL!bTIt^1g z9C-Vo3JQ|A6@d<P_4Qky=NlpRMqOsG_G>nn3^VJ=DQuUiQ9>Cb8bpfLD$1eY@bJfc z-mt-&MR^E!#<>mpB*wu|=LSJJNSr=%Mj7ln+=D_$nWIHK!EjYR2An&s-f63ynZ3kV z9~9%nS4gle7ef;P+V~NTpNg&y{osg+6MMp3OYrW}X~d^O7`6GQvxIPYwr$r=(3&7( zZ@8oB@DMc`Y2S*=JHSc<G#L182hUP89!SO1>rst))d)ueix4huIZoee1y|yO=*`sG z@w^ORPM`i!XuiD2W@ozBh_e?dNSv#)Oso@b(`+GB|KmVFB{Lpl1yBk}78=A=(QIA5 zyv}ds@5>x%tO}uumH?O+YHFMY*Iy)rrA#J<jT-&M-`E5ZT*XtFXJphXcBu}lpXDf0 zId|b6ui~*PyA0+mH~dr)RYFpbFd#9mXFsh9!pS*+ZE|vQERIb2E%n+k`U3TZNA6m} zNswJ2q68=vj<jEHz2ni5|K4P}gR@nfdfc=dIVEygn2p1A&s%5jX4wKEF8x?MY5aJ- z9Zl;p%K(EY(lYsE>ZyopFTJ>h$KyB?BXFaIZa~G&(v!G`I|}Y0EYxiFNf8f0=L2$$ z>XNOi<77$U%Wm@W^=)5R+jRybekdKs7&e1PYA=ewHhqW+dpvj$-%mq~7rAj`w0E4m z{1vh_^;>rkLrXZq1&(85&fzW?F5&SgMzz3tQb<0}E0Np~NjXjIgizG;>j}yX)$MwI z+)N#G(8JvIy?OiB0Eze<^I`a&^o)Wo5xeMkfC%0W3OW+zc+<ouGIC>dgU_T3QQDfC zVj!gCTQ;3ZAusC|2dVI<2*w0#@zrG`MJnIW8gAQxBxH#hARPB=$p_{V;kZv*6+@Ee zGWr7yI)yY2g}@U?kEPjI5CCF;4<XgiX(3C`746aZeenL9H+3QAOAF5pmA3s)so}j~ z`&(CuM(4Uz&*{;tvoG{`KW5CXI7t=MmQK;-Hnz4D%f-V@q>YS?$KDt-a%B7SX);Dy zP+yn^CAH#nN|uaUs0&_3jf7X@xO#)nsU5AE-V(&ygL8DKue3&Vfl9cm$mAJ6?3V(y zQTWweNmlLJz3t>Qu~)Asl<wS^`?;=IV1%yrj{PuubA`M%3zQrAA%+AVsp8UyCqEgz z-a5K0K|bC8_07@aU5wa$(bdk&Y@#QP8@C*CR7ekKx}!0$F&syroQSt6s>M7!kUatu zMIyAwwBWt^eyh^WtcaR0d9p}v7Jn7#0Bc<XwpMWV^n}7a&*C&mTT@@~G&i?Ceso<$ zbPtC=Dh~1dbjMCwmz|WqdGdxCOWc<mj)Qt^ZYXF0lGKAw#DdK#L0ImHe>kX#{^!s_ zxzFJHnYW-qgy4qwtV77+DIknA&qHQ<`T4m++k<q_6xrmDN?U9g@07q2?LJ_DV%B^R zwlf8#)vJv4XqbfKU+^_d8$u0B#fIW^eUJ;V4_Z5;(>bG6RL=6mdwySLET~dCiAvxS z#pDlk2$>~>or`I4&>@SckFfEO=o|ko=Nnj|Amd5x;7Nr3w6LjAo;Xq53Nv@sES&y@ zN@8x;>k%D_+{5|Y0?pqt=>7ZmoyyG3*3;q$^c%`a40~fUXA+Vi)-2=X_8~E&@##Eh zx;Iar8d+J4z@dN-KX)+mn36V2*!|eCFC0SxBLKj}g23^5@pXfaRJ!`FB|ZLAfV^9+ zv=sHvKhXD-RMc_WOLs@_V&z~55sv~7eHEg&DjaG01RKHsnXYXCsuDrvlxm#v@Hzkx zt!7bgtR+wq^kWS*TIV6E0pMz^8ZkL+qTOjg*&Ek(!(_7GSq4iLR^r8gl{kUASv)y7 zrpG)M_L!lcl?cuMzmVfjWwskP>ad1Q<93Ya94A6ZWHX}YruQ9N*$AjAvgq&MpG9%a zc!gmyG8#eik6|lk-?AP;A|s!}0Ku6Gxd1MxDU5x=vI<+JOY$|&P6k<MI7Ka&eR=Ye z_1_Rsra87}&Y-M)Y=9u)ria!nLczdHl{SGh9rYtmcZEoyPx)L{wup{hh=xSDLJr3M znlWQ;At(fvTA-CeZM+#*xtM0c)&++u<k5<BC$KB5w!-&{$12QqF!#}dnF;bJEX0%} zBr{qMw=Y__P{c=Q2NeIvI(>MBck4rg#d(voGbU-J?1kT}_q&=+TDz#=dDD`~+xvO8 zI5$2|Zz-v_JpJ82y2$@(d4xyotKwd{k4)Sq+r4Y_yp?KL;IXTv?V$nsbm1!3W75mB zrl93Q(z4*aC3hLfEy}4RYS?E${lcGzMtD@s>3$Pdi^7e<3N<rAvG(Y9&=`c{V**AN zLEr))Cd(%8wp|9;aM<VKXa4i4+-O3CB?ix!V8yh{(KjGjdB~auLtt8&2MWqjr3TW- zlZ2jt)*RTo=yrkYL!3hJo4sON&^n8}O^iQQ#E8)Rpa~y(^|F~r6A>O=P+yV(3z?V| zLhYV8p33Ar>lC?xrk1cYZ{pkp^{$?mBixMW;^^QA@ahX*7W>PA*aH2}VWcy$uZ%7m zu0WTA4OMx^&*l3+hT2`>ToBTHzM!w-1?c<-2%VU^K@Ta!f}l%d?04AOL=WPEz(G5T zhp`YrEP@4)6@iyhU<lSofT(~=<R|P|I(7|u|GzDU9v#t<g>oUZX_+G0ml{P#ZX6c2 zn>{LOxm<cTO^An9lw^1A?AfEM19KktG@Ur|mE||zz4L$a!aM%{eP_e4Lk??8J>$He zT-&G`npxnWIMX<$?A`|FX-x}mY`xJ^doJ_WmdHmIMWybRHhICLJ|5h5{z9bj$D+i# zl6L2|4%qi_bo&DygBx}7rf&Z|)77J{`$5yLDLQ6v!)@oA-P};^d6QMsIn}i5)Tj1R z-IBI{ExfsG?c+iZ+YKh8^8B5xXMD3C<n2GYXCKQt`DO7vpB=q(onG+p^2R4{k#uJ2 zkPo{WgM^NqIceA5xR<sIipe8+38HDgalhUK1xqSW`No-izTgqI`fAdgB{Kt5-rROj zdwDT9`iIT3k44`NUw(X(5@P2P7!WWJvS~rUf=Bt^u1wMCda5X{oke4Ha8jPZOUdtY z9n_0X`ki?2@?pcbmfWA7F)lWXhj^VnJ#_4^_8;i|unH>!Kq47Q=V)6@Y0=AgL9e#M zEY;QbBuU;;uRlag1G_6x{dQpelZyqhF7sD3gs;4Kdg?0av;L-;@jhKTE$v<Tp?R-* z>cM~PR&4Tou*7f61Dib)YRmI48Z~8?c7EScM6MngI6c<(?Z$+@;|_E^Iz#!z?+M0Z zmZb16dew~2nlhrXVbiI*N@2@FYHn<N@z0Es-KB41Tdw9UJypdmYjxS~QT|Vt(k=xD zmzm5^Z>@fKLq^T<K^~<u4vU`^Q+~<n;Nf}u%B&0%U*52NJ@{VaHvU_awdXReESJ7V z;^yqC($p!^U|!Xe#qBl5?Q;{6evDd!84ww+oLze^UR>kXyh)a8cktiN<7d>*iId#s zM&xb%6}I7H!NKe6f7nmo_ARZa@8eKaouY$Ry9YEEO;j70t93Q+ro>OvqB++xru-W6 z?%eK&|JYZ)88z=$KP^fKcWbApB^h#wi<WFF@p$;u=3T7I!9o5Hr@4J;J;QTf2sBvP z0BI@pZQHs|_%c1@YRICsKYM@4IPDn}{lL6S+2|5S(jv5;Mvd=WZ#C|{TA4**?94un z(*5K1=WmPuR_mx1x%6(*_x)F5y|x^zS5ygY>RReP9N+ACOV`~S3I><v-STfYw$s|z z$%jt$9#bG+V|~>hnctKJw&oKj-K?wEl$jj=`^c^iEvKpDy{C3Ni-2@)!}1=s0OnF| zTc!5he*GZE9~{kdft%lRpEp<5R~BeET>rAA+{evQb%D#H-#)6&lN%=(*NyhA88Xpc zYK~J^+2!x&^^P)4e%QWS?!Y<~R`A27l4%ki{>sOq4!m{v@hjHqNxWOFYC(IK#Vrq- zOnZD7wZYCVH_+5z^>xMdQ$~#RIDSj*ch&e47u`a2;v0S5<!xJ+I;&)NfSN;su2tE+ zT~daIN1A&z)ThmM*01uKqFP>Y?wG@eEizlb7nj#MMcqyIci-o7@2Gp%9;a`vJ$0kW z^t(Y)Vz*Ss$R0+Qjaz;2^j4d?Nlv=yZoSq*|HT{nzA2k9wz2=51xHR<A9LBfwMW~a zN(!upm(4S{9L{X4KGU~fzayQ-fDopDwMVQsYo(f&oW~f@&~}~Mew@>%siBR1q4w#C zml@F;nR9>EQi+f8C95rdRoydsYut82Hw~jPiN0_4Tdv#@SQD_>pr=WH+t;DpH_W(I zl+$AkCt2ZCQ$JIWz+Z{&##8u?c+^E{eDt>8#jjSav99uY<tzENp>yfO-@Q}=>b~t$ zx4ODB>W6-1r~7woMzHH2Yi7uA*{HTS(dtQ=)vLL~qK&hwwv}haCnskuZh!0~^F}ld zx=X$-9_ycK_i}uiLy=^$g68E`6W5(ld*JY;#n&|ZjqlBk$dtR@a{0&p*9+Gl`C7rV z=Y)7(-T=P(@_lCOv>tuTkcA%rTp}@exRl+uy^A6`CZ#47A6yW+=AqQvQ^WhJhyDD0 z`8*>Fl2ei^N*-Hw{MFT}=2K(7@ohWVR^#r>f2nx6_rl|DQ|iZ}Mtg0myZEC^o0Six zOExRj561Kz^60Q7LAl;*c`UU*-Wk2Ih*}Y^UMwG3{41>Rh_yxB`H14HmdUPBcXu|7 z&01$@pqV$@HTZd5OOG|}`uCkI9~~F-C?P_;IQy?E%R9uk?>u|{#lsyAh#J}kNj>G= zzS_%ojAjo?oi1Ioq3^3TGlIjz+|QV~S}BZMKO%2#gtF=JPU+9n4-Bs~S2lh8s$HSa zi;`J#20%vhSNLE4ey`rgG`niu+Ea}=$pi=T+@EY2J84pue!h}<=@YXlv!xW;pU_pz zryt}B52J`<fR4+i>cu)b7r16oaBo6ZEsT3;i@C9Uf}3PTtP{m@-@b*OZBbeb1yd4< zg@^+qr__Qjr7_TozdIz^#h&_)HW*#$rC%+F4{w6_hE^qZz9n&TSL8_}Cr$DJb!uL@ zSvp@yAl+=&s28^kX2e{NaculL{%4|vQI=ZxvAG8)ORCvqWTvG*ZGApSZ;pM8gJ!I6 z@TP<rTHCcpN9~^7hP(E*-TSe>@~`^4_nr9r+5zn;6W3{e|7;j|QN`p<;3k#bDHHm5 zc^^0Y;@~r>hFd{B*SYfXNQ6#3`t(Vq_Ex*|dolK3#Ja7Q!)H@*GrmTSutNPCi88|0 z2V0T3M+;TIH8o*lv06*`3;UY#f(*htRZll)NNv8nP~(qppw2|sAftS4^{4tyL*2UX zzMOSg`sX<bd;8{`ouyAJt}h%i<?Mn!R;{jm-?*QG_oM&AFc%v3H~sEjPp%w&&pbHz za?<VQf>G<$;#$USw<`!UUtH2n!8Q9<HX&FQF1nYJYBm&XSebL{{p-5Wd43s2*>5_r z3wp!0_K17ga`kz|<B{h+;H05lQ>)v7>h?P5roKr*^OCogXE5am6?{xg@{r(tZc`AL z!1pM&t~iyft1Gx1%h^%421|^p)nB_Je~*IEl@AlwOs4`-{#e!V*VmO#xbt!c$DIA$ zrDTG=b8}Mb1CI`PoSk`~ytTTfOQrXh)z7AFEtOGfd2Tt$z%=Zu<<>5p`;U9>I=Z0U zzMrMvCPdsd-#qeL*BQ4<Lv4;+=saLX#JdfnY>af{^KSN?x_)_z+hZH-C*AjU_$66r zQK@2f({`@&(wRFS)+%SacD|dn^@($2%p(h_wn1rYKFm|fd}aCR>#YwVHU@dGQo%{$ zsvQv-yv%AUs9FLVPp>QFuK)7<q<m13NeGIS1;-?Ck^T!HB#oE~ACmc(VY_Jz5WkRN zBBBH71V*Z=ioe6apN{v@{nnE@iK4c*C@YbL3hfgmKj{@+cEON|vB582b{Cld2~{;% zga#2JCwxULB>GN~r#f}Uj0Fwe7eJ`7uE+e%%-|vyl%5zJls9tPw0@u&u;K@h^Ps{* zIcId{brZnu`n((NG-M&5O;n*6x0TWG0o%2~OR|@tiu9KMoW~z+*VCWRByuGo2*d<6 za09A0VdNIp%N;wKhe*LC2we~@7L3e(!IU9cLyk{pHLB|8D=~C{IGbC5`Am+(UV-0x zFJxde8fj!8Vsao`e4d<Fe1%IUabFVD-?RnAQ0tHx?FpTs3O2t`(ye}uZ7nGg{1DoM z3*e-ZWdayPeD;x!iR5_Pa8v0CjK17qYPNLu?z3?m<I?~(?8Q&PTo5+ZamS8puqq@W zFvzqkuW~ic&RNa6AK)`!Ur<Fp%+9{`W}k!OXK&g+lss5>hk#FAWXTJ$hi%F2mJypE zS;4UTB;NUVgZ7;`$wqQlhJxk#hP>9}oHEz0%L#v0J3FXtLt9bC)LOjXY=2JYX8)|v zm21WZXz~B##&4Li=GeKQHJ0Y811kEBXgC(E-Q8iIR_d5DT601Ul;$V2FUU&xnsfD8 zTE&RdAIH!6-Ob3+`-Z~WE4QxZYgu(!UjN&A;>OiacdO_PJTiUW+26nKjNN4Yt}xo} z-zEKV>vk`%H@6i%!Y6$3ar}{*t^i{K7$Gz-=x9W2Ic^P+$2xJtnU<a73B;v%7PeOa zQZbDUiWVvTrG?gFJ{CjKaDND}4oL44_^W>9a3rdL$rVtd*hrqj0lm4idBnGG--JGs zCtFEh8iT!_^;(<%_8fo<L>IuF1_&Tzguuz6zSEZq9Z}t<BTBr8Ht4Eo#@hp}(04s# zS)d8J4;2k~Ck73oHDWM~m1vki=+WMhye*g>=B<gmA8M1AETF&ID4&zdif)4b^E)1a zSFpT4!pz?pwJYoOe`W;4#xhM#OnSq1%yvHjkrZ;_f?(RGk&_9H%7_CY2|`@{hU^1B z6URoJr2_or_7^kR7)OFlmLqH=bU4OFq<rYyss<E{<P)8=Qe)W*(J(o+P*tmFcdhR> z1Eb4fzeAQG4f-O)gLIa8dG$w@pZ!ZLi0n?dq(7;U5DQfNEN$PsW)OEV*GzEyyn{Q0 z6t>i7xLvYLRAtNg)+ioYgtRS}{1ax!3iT1jC^3VCn+Tnq82AU=K>T|<p=i1^6Wy_W ziVbKGe~QawmjK5im#%=7#>TgtaV&#Fher#_*RXe$iPJc@u=&w6=L0GmhU_o)zgA|e zD4Tb<^5=s&I*nWQsJ7WzmGw(0tc-)T)r&ihD<z1?J3O=HTJ%Hhva`moH%n};?Riim zvc@uHU5k<RF)h>QS0W;o7P`*8IiJWLYE!?j<0_6m8NIml$Q4R*AOD*hh9ph4E4=M+ z@!Rs;MQua-0)b}w7IEgaT29Bv_WM`lzYCFq7@`PAQFSgSu|)^Zg5Q(>@ddqhp-UJg z)8*F2_vu6jh;=3*W&{40r05GC1TUfvh0?2+Pi?*{+DEhl2stE{lZYjzTyg+{Wjrx7 zX6-T9y|HBkSD+9P<3tf79YODbn_?jK!d?Eyl?b;k*qV?9AW>jGg@}Jy_xjTAg>qia zQWuG*tS)sjKq)E1+CdUt#U-m5{jXUBBvqJR7Rm`|lTAU!3>;0T&~`FxT|qZ-BH<#v zv#^N3b&KySa=^)WDx0=%Coi)N;HYce$MztVKaFc2bBKmntOogIWXgU7sN{)+=coa# zpxoE8h8;Nx2OvhGwW*Jf;{(oS{1)lPVsHkko4Zfl1~MgWI(8;;N$J-^G!B=YyE#9f zm!Z`l&<q^Rz=OD|KjJ>4mps5h+G_c_b(eqEJDojqCT~Cr_tjC6!-OCHo7t2NdEUV9 z6~w`~C!hIi)JuB%>ID;Jb6Os3i5gXPDkCgt#hm7yMTzT|-ahf;+_sB)Uh{@)Qe%~$ z@Y%Zf_3Nzq`ipIbEV5d3+i4M4o#L+m6i7osZ@8%B*W10i&;=6L*8+L5rS|1kl5<8a zaA;|Ev8h|z3O5tjk8J$E@dHG;-F{JlV2vq##b=XmM3gCMe)7K9)K2oi>t5nO;&Q=i z;s382<K)2qx-k|ChY-R;%G?W-Z~{z)9AyZ@k`>XK+S($10-7N1#m>21%Hr6oxw*NL z(TIW`rluAjO?TNkRnTg}JNo+VA_AH|CMcG>Wa2DtX*yTt6SyU8SqraS<V{aU9r87= zmrS;B+>>3!l<zsh9u3i%MA+}Ttn}>b`6!ox!MO4sP-Y0y7N874xk72}cU>)T*ioB( zPoD-8H~9J(DVLaYM+C~w5L>;a0bs_$P`?D+lz$jcy#NSq=ThEpz<>phmj*Tdd%;~x zEH!+UG9~%o+zEXq+)0(P*_q*bYqZ7nC2chYQfOdca*;owxObhnLD}||`;Sw-XXVdO zS|H~&>gmg0ljD;md!N^1mAq;GF13sgB=}{%h~%J_$*+bR8a50|^v!s?BfbeGPFhi= zbRSFTCRkn(i_Y-~`;0(nH<t&ZokMI-#PnB=Pli+sxq9^(m-OL?`I~cY#;Fvho2OR3 z8)-Lv=B*FE`{dj*@zMd}7jbWVH+x)yR3YLAg1M%=dl&aQhJ=hgXp_jWLn6G~*4Dgm zT;8W2iJuPKy>jJ>*NGDq^U6qT2R@g;+?k0cKr%6si@|#E^wHj%3J(8a--g_Mo_%&& zoo&yC#Xr{Ud_DR3dyhxOGlr~t9WiXr2>Oq1KU-?rFMXHTd$;^-SZ=~thVnC4G*AL| zsy;m`3NlYXAJsp1H|Eb3COV5LOgyB$MjsG*j*WY5)5j$Bj^W*|HnkJ7f&DoMu+F3K z=5a44JQ5<cm+=Fnt|KQ*z;-kOwoF7>Alk4_+Sie6I$<yZNmht({eO%JIYUYHBUxz^ zwUBzll=W}W|2=~J?-^Nw93z&*s2Q;qy<E<DZ)+kTT*RL7TXlD|u(C1}|Nj_>V&$wA zTywurOo{B~L^n5KDqXx-$zcH6nd482B*$kweyn$Atla{%E67H%?e(-OFE7u4cS_m& z%kp0aLiL#CS_Or^Q+hpAZAZh}t++OuqzC$^dCQg!;+GSXGw*L~S}*+re=~F#3@(M7 zgyXzJ$BuemgZ^(xIwXL0p15=<eqUjP1I^Z7WhydzXdFSHn{D$fKak1Cye3PtxaG(X zD6LzI0W<%Xj(!hG99TEdZ(zM!h<-%uWUA}T7x8-njX2v0k-PRLUk&b=xb2m;<~x35 zQqTWS$$#p=NI6+qt%p0TmHR;mKwD!69)$CN8HMx4`Yt%q=YGq9p8p^R57p{nV-b0~ z`a-SN+@&qB<DZ%7#t~xk{_Z3cQWPL&(?w~_y$A^*GDXp(-9bKZ!>XA6WGX4`pwUKs zv(09inE1lK0kb2G6sArwN-I2Ei?tE15=&`!5TPz;dJ-A=9GCdkFcdnvr^agq40W@Z z4Tw)D9sN!b7mEnNv9q(Q7}|7uW(HZqf5d70-~2>w4`h80sPcq-ou5s;<Wt{iyQ%P- zaL3YPKvSX5se&)D`Q(NM0A)#s&YcNU=>@CGDHL{msF1Xx+UdeW&KxcgCGv!L#MU|y z@liEx=tIOT4^kP=IahYX-^Qe{u4$H+hN9=74HXar66wEfPp`Cd;|hp;5`k#Uh6mx* zW}W(BpgFGAV$8QajgE$YoRmB7_MV36E9>!u`y>f?2Zpp_-~Iq!CkB*ld_UbLfk7II zY$!3G3$?@=)PM*{K9EFUX4anuiDAXsIa~SpFs#N)tjZRZdj%V(IKL`s!Q$ry4kg-( z80$`4iL1EQiSrt`3!b9_(JBHIp+yy4TxO^i*6jhpx{n3oIj1Fp^Jk~W@8!j^ZT7&J zp_EvT6<OT5RQ6g&<h)lwfgn~9p9RM}@QTNv8K>&%T2_`n|GX}}rTwu)Gbx|m8#Jud zgFBcG=xHX^<3PWDBPQI^@;Dn|?br8_a(3UnX_HTTykBLRYG%6NRq~}$bq&KrwclG4 zBGpeV{MJOO#HRA500-4`wbQomEXc5L4tpDC69CBqh$AxBge#fC7Qxc_h*HJoo$LJU zre6t^`V@br`mK%rIrW2ICLPw;`ATWg`A{dxQ#EIgXzZLbd~${SoU?L9GtyGVYIKij z`Q)S>zUJr6IsU_6=090ueBw;d-g!mi?JM^m_V#X%ii0r<2AAG$uKA^BHN4xaD9ZwM zlUn*Q1GF@%^)gpXwzCoNIf#g?@}?v8=&(pC+_HOJB9IfEiLvqArYPZIF>PnyJXP>P z2oURUb(p_m3Mpe~p;m2uxm^bGdDuQF^$x@G@73K|@H%qU{pVv6`~1H3?%vKLckaAV zH?0+|E|zR@ub?`94uQa#`z*Tu=X-fDswd-mOydoYUL5b0+#qz{AnU`mZkjL%wTQz+ z`p|ZnIkWeRU$9TA{H%3~c3<!DR<+L;-6(l&)VeZI=%L_!;$s#<LkL+tLX!!+a3(sm zK?Ok@P?_IHPV`5~$)WdeTAG_ToHaIfv824;!%?#I&XD8$L)kY_s$NUl_RnuoKUB+< zqN86nZg?Yo>wqY0Cr*@+FW2K2RL%6Bz`iLcsT$_B&Mvc=!mrn%Dp{?Y-7?@z_Y76- z&`(m~&(qkK)@SU-z|ae-pVD@}-+y<I(?9bJBi4=?Y1pAckJt%5m$LY3D%pSkYpMFA zaGOqFn(rQ4e0Eu<tusp3O}Zbew_#ScOAmGDnWBGVgSP!NNXv85->0pSAaA5SAjjB3 zT0EE`W3>MB8Z?F#!3-j-%y(*_<UBLcM%~Hjx9yYWzmt+Ly-1NK@sQo}Bt?IhNM`BN z$9JZ0w~iejWMq_IJvRWC3J10G@mngcji>3^ieuwg4s<|XM}noilF$5o6Y7U@I2`&T z*V!MLkEp9X*+R#AEZN2R{fcv;+qO8`LjzRU-RvXa3UQj8xwz_P-BeAhMdI_ScQ_Fh ztTSZI=esY|&0DTt^o-fm9N_sQZl-Hv-O+S$uLB`DAHQpjwsdoo>K7;H^>`7!6(qK_ zpOH@({cI#Lhc(t06gr(%**Td}NvAqh4$}JH&#pOOXI*$cFjUpaP`<>i{KuG2pR5XO zh7grGY@*?$*ssOK7vJ5c=|0eHhz@y)nB+lU$0N${8Ro%(v=ZSrMg19Gub2_z)5PbH z?%jJH(A8!K2Wr!U)Q^gu=hWN+QEGJW(`P=OmnApWo=C4jgeqLC3bh-krudf?g^zo{ zR|a=(ca%}*@86#VHh9H&FS8qKMV>JpQT<ykq%`rz%px}kKf=SN36FgwKXIbB$i+dz zk3=XL=;lwB04o0E_wNUhoQtY1YQ2LPPj+K%P|P`O_42jP2!VoYpWCmJ&h2;J)cA<O z89U!^zW#)XQX;z+URlKN`uVk(OMK>Hq5|myK0xuD2ApbGbDQWW91|V{c5zY)>A!Q) zq+3Xcgq)E982sVFbE;?gh6y(suY8|E#Uw`AQwfP8A1Zj7*nV6?=`~zHLP~&FM$8fu zJTCIH${k1!g@gw89Nf2f{Ag+<_<6?tB%J`QSv4ZxRQxXRI_9Y=XJka>s7y&{Y2Ilq zPyeWZiqp$}G#3|~sA1$E-3VdZpe~}vMAPPYxSG^y?_pPPU=1V_Mu<SE1G*vX<G)yZ z?3N0X$C0j)ai~Re0)%rm{d3e^V~g52S0AcLQZ~of>6dH%_b_nI3iX?@_*LfAYika+ z{oD(ks=J5BDzNCkcg7AbzTFWP^62V5gW++j2#OyU*aNCW?=qw`&!`23uaDkPXpn>f zOF>0HP}L<JZSS}2t75o@LhAO-n?+5e@Hmc#{PKp4fws5&iI4=Et`(pk!fJ(j0NGoJ za-lss0GjT-F!z-RrpF<|!*5~PZ?9W({Xrl}WkRh{`Ank>V9uQg@8z!|XqwBb$ChUv zXq5-y&nv~pI07Fu>Wwd>bd<6`FM6Ng;?L8wLb-t$U3@mG6K(raKA6yeQEdpt-){!a za`BVzxclTG;mZ{nRCFDobhLnf?z9a>PH_;`KRNb=<P~7)8xnP_@FE)5?aJ>~LP5!| z6~kbG3($bCL69avVJ84|=2LV)5Ik<&asCF$MNu*ZqPpnRDTE71JayK~r;-wXTTca) z792N^kVKwltx!LSe@(BO^JuzK#?j(2IOK(OfSrxJ)dMsE`=r6(#pD4Kv4ntTFF_E( zMkhcIKAPo#650$AlPwVI*}PK$aS6NZ+LwFyPlam^8GXdb<yc+L38M)!&XY*JVd?up zVSM>!=J>wKtWa%H$K8Jq%He8R5zB_W&vYuykL`CmyG&-N(uu&p=d?)j%kEB;pcwwl zy(=L=C)6#3I`2zJP(40ru@R5Oy9`0ob2gVx;4IMZk8~@-wL}<ThcxX2Dr%$04jVQ* zH-S#wdyM%hj-*$Fjxg%(2@8Wl%_*|_839~&1qta1Lgy@kKiNRw%@Fu8>~z3qpOuLl zBd_C7lg0>&%trzQP5>Yx_A2d6aT(dXBHoZjBB8lc)CD<?6>N0|jDwz;`YpvNE}Z}V zIFKVMHZi@H8MO9A>6FzSR>@wa*dZ^xlLY0J_h{3TFjrMsCYGF*2SL&3%Y$j23?rBJ zLx{_`)KeuD^gd!b6_Kaf0RjiNvifA6)s2gLA1z04xS!8tdD9pf>hmzphSI#7ov6EE z3EcTmnFqi$HW3#KGV_4mf%)S%q}Nb?a`1%OKeMLP624J<D6KGCKS0BD$E_v7K0a}w zMc{ypBS(%1(F-B#ZFiwqbfA#;k+A^r7pIy8;{iXwvwcBMv+~9H*-;)h5gOSaJKRS} zq%))Q!tr`Q0F@HR+{NSP48)hwMS}0BUy}pWis)+&+FCG1x3Zfj-&UD9FFCoS$KPV3 zP!X3}fccH#-+xu8o4TpA8KGc&<9Y38ZzW3$t;?;@FO6!+XqIkvJ{d$xYI+&(3R7hg z3&_x^Ktkd1$<i{53&JtaR9;?F%=U5%);|T$4x9SY<oide5snEGTI;g)$etFd0D-c8 zy?6(no}Sv`(X-@2l}pD(RrbHwr=;Qn_z1gvlF;<SV}*c+isc3!Q{)U#cKq74YXjfr zb4u_paG}Ue-|}@9#RdqC9JPSbB4q#^J?xbn5A%T^S-)R6NVJ2$FDhxTOKQ4o_&>S; zd?=-`KtS{a(qf@$K4a?<MW`9)Gg(Hb>=T(_%*JXRb6}5Z6tK}ih6QjcinLKiOa`uO zx?B9{2!tl51W}A4G+V^lAxGGE_jk)bzp}y&<`-38;r0d&`FM2YS!Rn0UlW(DVNIXD zeNTUOme)GdviKjhf&`OgijIyJi5@p^PNEJzF(rJvjQf8}tO4D}^;+5Ey1ME1duClW zZ8RK^<{Z`Ff)(Jq?}TT*ZYr-{yhy8VE**9zOxvfmkcS&yHvHz3`fRNK0AxzM^@kYa z#2mG(ETeBHy!}fpmdD;KODPLnyfhA9YPRi@RprGgb=Etx)_FJ{HL&<zaryHpdA&V= z0%8q{DUtAoBIJQfER2N@(k}9$i|+Q-zybyQxRTyBfD%~5mSATPX%bxP#1LgfKSQzW zN4rMFe^D%MyZlC<4Lg=KS@gp0vjxCi<OoKpNKKkJ5mUp`z>Iookcz4*tbJmZ<Hyoc z50rcG70(UcS=PQwE9)Oc&J>WOET#s|LiB|XxfgttW0&2_&3WseJe|MD3|rsqimhAQ zS?s*JYGk8Db)2go8qLJ4n`13@Y}D-VpKAB&HMy+(i*$uoWO`UkZg02|UGX8N&QF_W z8mq+MG=!q^mI=({CjTdtb0WPUV44FC;JJaGB2W}Rzqn+$SE$?Kjm!x)P2+p|{GOe? zRL}1Exjs9U=8}RVrdVkbAl=V1(7f-6ox^|o%8tsPA7HWlop@dV{32?YDhvkY0=xmk zJ9_O}tXjVh`MS^aRT<xi9(%;0OPt9NtPH|Z!ITQ!AoX?(b3bdlO!yioX8KLvA`jKZ z;rGL*DmOmTRm4LmJWeW|J5cxpNvcN5$GVlTP9%?nH(7*TpR8t3yXUYZ-esK7&#<jU z#@wG<Q;#J<WIBo1bc&%w<(I!MWE}0&u6=t1SciBa>^95*-axHD4&KTcIU@3&;_oNG zc--a)!b?cU(RFXLYRrXe&h?cuZ<cS(Ib2uXD}Asnh#aTH;^%>Nm+D&Dr$*#l8l^b7 z^yG(5q9QMxz!|yi$2%=fEezULG|YAry`!<i|Cwt>FsaB;Bez``c|=zI7aH)@@J|$F zLfOjD)%$#4pU1BY3L8Y64J=stf`iftI`mCqc7$luaYXUX72Wqu(fS7ozyz}Nsq0yY z!>D;_JLDZAapA!fu$MZm&50a@H<0^BVZQxmRx{+2K0th}^u?S(CJeknl_-)kcp^*$ zEd-7NNKh#I4pS<uJ~WkuR9;Wdsh#8Rw%Jttd=NWwFGflfb3$FseO<{dUTii*;5Uec z309WX_7<iqE=c{DRa!WM0zynOOTM=^RotLMWkW;iMP3eT5z$5p7Za=FI_w$k);zeZ z9T-4CP#@jDF9G@{!E1advS!oLM1BMKV;j@QZ~6C^U#kGZBT*zxR}4)c;-KvZ5T&TI zE60ecg}fREa%l)VV&uCBa;KNq3)hz|n%0k7nj!}JiOQ-OeDoNlZSkn=-1uL>nM|7O zW$9AgORe!|8xVNroFAoKkm}WIe^iuR;JG$bxyf!(FS@n^seC#<+oHpX?Mh>d$Nbro zm5XU%L{f^FjY0w%F&yT8@@h&k2Y);ZnC8U=%sA*YTCWG9x>n|nW<^tW{Ub`2*w|_G ze<JF{&d`LDPekP3yVo$|rXw$1Gz@SkD{O4s@#B%W?-Y`+$pR9UB9NO%W2X>d?#xBy z(a#|<IWDgAXRt;j!F?i41nZneE~CGDPz|QgdohXPbA0l*t*KY_Ld1BiPh89+U~PHX zB2-i;16EbXumXFNR<iuV{oUN5FEQBCgi6k$;EF}+3<vm`+6KO<f(knBSAcU%Q>|cR ziT{E+L)te|D01;W5a?s!SEu@xMIg*mU;&H__OM^ZwbnkZ|DLHBY*s%H&?3MYzpSXW z%M%)O!gN6Gg!~)RhY0f*Y$O}X=sT755uO5vmn_k<=m<p|2y3J-$OS2Cj>W}~s_2TI zgH?&-IXWrBh$SaTK}H)B8S#1=ZYtNNYLn)sT1z&LINmvto*G0X;weH~?ha@VAuUpV zD3wKOJB2frDxu6)sOpiBUJ}`>qQC$H*3;4JeQ<33vz#i&{-6Cj7SJxu)PB<VK_xr$ zKcLgx&wWlEKeT9Oy6#1-tgiMiJL>H#O}rDR|Gl4c*^3LBH)<>Wj6zp`J+J5J5#U^5 z_CE3F{n)vRcdJV+*M4{YAL_I{-(>Y_Int>K)b07EhQ{X%rC2n?*uYLh3XK@R2-*9H zE7G*B@MQ{3zSz4@;&iBi)t4=6+MCg@+H%kowJvDlDbGN%5{Fm+?g^*~@SIDvw_s<S z%R-TA>*e)ofNcF-otPbz--XxxyGbu`Yh2S&qkC1|ivBsDHm;~~?{1ctB0F^M@h}V1 zj(enHHMQf?Bg@=+UQW)7zjqs9yNgl-&6MIbt()AE8~_Um*ryWOSrXcR)idi`TpBo8 zL|x15yVYp2QG*nlxBk9C&Q9rRYOhbrQZTF0C3H-laZ-RKg&}8F8(n+ttCTZWFWJ4X zYu{LJzr?OdL1w)=YIYBe3R9ddHN4NWr7EV<GxvJ8uYLJ)%$QcAmRrWasJRCQ<`2kt zQWFf5U%`G?W~pJk76Wj(jSLS+AKr6y)#H;&%POi@USQCPjMHWJ+6AI-J>j|TOs=NX z;mcP{drX*dI`_rv%g2)EbQsobIpKD8qq1H9oeyt+%KfpRqh>38&J_mo9cJ+RnKR|h zM$vQ=EZq66%{#Pf+qUQ9Z6=Y@Jq}*@j}~C5skOD?uuAp+cE{ZwMbBQD@HMYTiNdY@ z7oPuxxtd;{8-O@Lgy+O+?w219;m;EG5ct)9Fq(?li%w5Z)XmC?G;R|xAnPCHqQZ{D zRQuD!v|Q@_zAoz0f)0`*gg}@;pPy4}$&29{f#w~PdXAzSyOCHXxK^BdZjN<pQRP%! zjoP2-N>fq^viwNQ@(GMBWbkR?s}PP~0F-*OD3ZtoU_rm3y2JlpRFUVUCKGL}$wS5B z?{tlwb31=NOSYxCK?yJXN+Pj_%w3AXLW>0{f;|{~q2PJ6@A%e0Dx%WGiAG0a4RQqi z^@)gdTK~$c7MK)5sh3DV`?>d)=^xyyXJ_^ScKi5QSvH(OIDe)U<Pj$FeY-`(2U<^= zkt18yj6XAV^KeeNkfrz45BZ)<HtYTg119@~zsooD<;&_*3hi?Kre$<|{o7kddAP?B zj))_YS+bxCj8|`qut7+5Zr{GWROU~sF+}%i%JsPyuKowEo175DUldTiV@!u(k`dUg zCgY)nPP=yVrYv+jxYLK?;`Xz^Y{X<$AxpHi1=KUb?IR4a>8g0Dwb>lP_{_m9LK-hz zn8!iqql}<&E+8sCZTZ?JLcY?EKS9hC9+xYcEbdzc!%O#xdiMS7dxdY>2H_EgK|W4r zJR)+$neF1;o~Qic)@;Rqs1SgY^jks{g6I)s|I49++=DprINO;Y>`A*t-O~C4Z$mp4 zlM|P~B5ui-Sy^G+J+TRX@na{$85&w$BXHmPUa37|gK2q9W219PR`pRZ(ofIJ&8;9L zvX!YyqXfQiO7L!?N2PMc>?gt%C4_|%g?o$(R0_#nc-Kw?ntl55<2g5p(RTw12A<s# zQlvz}?vf?T*RNm5lEAIjRRSCav|*0>b;=LES=$ekgcu!9@dNN4YS2DU+xp$xz6&2k zl75hywd1O49Yr7lZWU+1hN$(|1)!iY`<8RXz?o+U-En9z-W^qv`)a=FR7>OAqqTv} z)#uGS&{o~~wdexqwq>QtFKPZ-vXenY)Eb2xt(e%`{G<mT&ox59!^I#JIb7&CDCb+> z<7-SH_?7LJ0gAMQbXIX&mpEbats;VHGvQHC1rO2xh|wN&iTYn2<t4bx06rBRnmDH5 zW)Sje0^!x+c!I-S4iOLQ^Jf?l5~qs;SF${~J4Y@~?D?Ll@$||Dx!vwVH*6xkkMoqU zS?c!t)HMwK8bKGR@FE)j{3ilGjB399&1n@0St|b3wwJi>P&pY)yM>hs_8U>u%2lh{ zib(?0XJS9%7&*?WiNVwi5}rhNYya8}bXTP1Gw0ctl7dD`Qg$%Qj$vV4(zKJi^iyt& zIFagFq)?zC;z{l0E&yN<;adU_V#r#$<5Gh75FZpWKAH(j?YZ0-zVgoZ??w+kZ=3nF z%`Uau`+L>h&Hqb<+|GA}L~aC!7x}O-mE1G1K|`tL!LXQE{D2K{vGM*He@-bziinBV z&@(CM0C^f-0Rc~``7~K76_u6xu3Mfr51#1O>@0%PIO1SJS5pWpsfcmj;>+Wuk|08J z{JPij2m=@JJx-Gi$dhy2xbhZ48j+mEFX=~WRJvy|9Sh?TJpru6?3Y8`M=(U7zl4a4 z0i6<sx!OcEJBo7QEsk|zA&95R0qfHMs~6Ik76>GPLQH5u#7D;7LxU1l3L7U(j}*9} z^zXzG%jayn#SXN*c%u8KIoKcnP&^&9BH}HmgQ;0brV`>tAt2>lv3^9bB-Wu#UtbK9 zpd>_KHUj{wk%$TjF)V@Om7LPY1GW6(oIugpX6XZF5v2$EEHE+91xl~hBJ>h7xd65V ze8Q8&*1Q*>jXI2=058$aMn!d7HD=*Ys=>nWuKZXd(>Q@V3G!mgw<OArRwAo}H8X|7 z{R>C6a3aH#wo3c%xRt81H4%FQ23V`*+IetRaeSJ^W@Jw;j`;yPMZ&O^jg6MOw5-sh z0cf;4PhrUPJTa^o@LiM32DsF){=5Y9sxm0H(Z=YXTnqc&UN~~e?CB_?HN|m@H&8U3 zkXE8a#6-o~IY7C})ujK$YQVVV64cfrQiicmv}#cSBMl5*{dZMfk66;NlcH@+>AS|4 zw{LQ3akxR-&*w;^o1&VK6>orInbgT+gdp&#=(@gyHK0urR@FalV9GG$7B65i@ITk1 zUlfsK^j6xHw)nn;x>cltA$laZCW-ea5@L_J3;{k8ldJ6uHAMmeqEi%j-U>#CQ8^Oc zDI~tUKEeo!!jDR{G*)%v*NIFcF&v8bB<ABvh!I%lXYYkpmz9_A0QgHTn0I-9Zqg8L zC-Fe&SBs{u2DpuNu8&dv4az$K{l_K@uS0EhzM;=#(#Uz|bE4NHM|1^sF~5;P;#Xph z=W=-n?XEzw(K}3&b_7o!$^S|wsv|I2{ZK@Jy58E*zs6xhi+9&YexIVOtS_xCfp|vT z%baCi`}Va(N_ha&EU2+)jfvOhGP){rCSPOKs?f|T1y+U@x$>vUB}@i37U{A+D_mF) zVXzWZldqr;dXHUt=)i$Tww@xo(`Rx~3g_dC*H7>EjOjB6(%U4j9yvhK(MEbhh=35? zlrQyT-#NN}<oP0I7>w?c*i<hLWW9(b<6VD$8z}@af_&vNGITmn|NFN-D4YZxQ98Y` z<+N#-+NaZ*w~ukv8pyOd32q$;kqF42835-|GkHKgR6Yqpa*c%4%S1_F20fh?`QKhu zUAdoUs0SwOS-ZgZ|G|hi3sEV37<mmw7gh_GqiE+%ug|kS?AM2Ct26klFp=@x3BC)# zC@rz2B>O0blLL|YNK`~bCsuh~)r-bN407!g-8qYE-N$9{G<yLGD|208BHR_x){Cl` zMQNxSlJ__Mm7MC?SmrEp9~G+|@g(Yv_OK12(RBWrcSbU{{_~tv+R}>5hp*Ss=Q+L3 z%6cgPz4^}cW#D7eFzuuLB{ky%p=lheD0Bp5$(|K6sHhMig1D~Q@-<`!bH&OKIR>jG zWS!zGPt##ri@7Nnt(SANaSEh+nRKW~%t;J>p$XdJGwo%}y)uhL*RwR-#qw@^X?|@h zq5up}>g-A%@WOvb!j+$NhxRJu)+V@6c2dlA8OTNgmJ=+GsIbM{=POrA%u^qbK7nOY z0=2l17>mOjN<)~5VgD9{i*eq_nRi51Bfqx~It(s+lt~(ImT;5@SMGqFQ*IyN<>i&B z3U*P{Mq<`}$zJk$!EIrnKC({rv?5g0ECV7Vf7rfthN&y^O`s4c-Z68+1OvatR{@Rj zX<q{p9%{SQKrD_!o)3I2YeTcv4C>^pzbNDWp~Jnd7QAf+yxu`Y0yV53o5+2Gwy@Uc zEr%r2W1O56I$kVRyVrW6ZMFw{l|Wl9P0bGkK?qxM*i+C{Kg1+}G`fCvQ~#jHcb3kd z^A6>3#)dJL331x-ux3Gx1w?5;YZBx_4$Dk@^YGkzD7C(nJ&Cv=hyU~=#Spp}F{(sl zRPpsdkUEQDgTy%VW<=o(^L5g&jl9w2o)8#R7K*h-bn>a2g;9YPg!b<X3s<2*DtO%W z{}TF!G6(joZb_<aJTgNJ1pv&N){Gk=(wn%+!^%qHZhE)Cv#POgBOKNjr-k@jmU`C1 zF0|IYu&|$C4f&3=w2Hulp!TP^z&JgcbT;pBt!Ry4Rm`meZP}GsH1l2{oqlX~6@}55 zEzzTv-MvxyPwesst-;of24jR!p)5m91lttZ*qU;D3~A_5-uD>WQ#reEnOm4LX&0(( z8Ji)Kv(4OmjmwS(06p|JM`Fq#7wm7$77a`AxXbGe&Q7lmkaSilTQ}+aZM($A7JItQ z%KG0M5=v&;0RTdI9%pVD)iTIPXKHc<Yc{@6p~KQ!v0c+W^YIr<T>s|y&!0aBy5+{- ztNc{2k$0rIxxtR^{Sj;oDVHCy9vk>g7^5U+L1Ckqh89bpSeJBLA><Ib;D-+B9@w3r z;F_Vdyw>@k$9nXJl%|SvY%Cj9M`*0b!rnA6esPL<+_pX`^JYx5j&~eFS^DNpgqCIx zY3a2u&bK0Y43(p>h_KmX843w(^?{8{K^6^uKd|9R;7?a>6JECi4hiI!7xabFlaN>; zROXHgiF0>hA`*%$MX%QsnEteqSm?C@4Ta(z&}f!%aB<3%ygnnlA#*+-`}l|bS(SGa zW=uwMQA|}nEBpItJ_gcdO#0O`Q!jAUqz`z-k6})U9C1)!Vy-{mkm^D!QbEa_N_|cg zwEeNU{|L9pmMx*&b6oHOJjLtI$+;3I%Piyai8E&0yM4EY7FL6UhaH7FZ^$mJH9k&x zZ{La;8AEL+1&ga06qjV@qqtp=^rdY*=Ke0Sz1XrH^nOw`^(K&$n%C%u8ariYNtbH! z#VLN1;Yh#2;&LPkZpvD9eA@R`(Az4i4W$$4@I_Y8@hq>RzhJj++>XsKWY6KezP-I2 zrF`^>Q2UG|Jcn;q0qkl3-Z({PG#I*Yf(zgoKlkp4KsQ*L;ascrPdAVeOCRF>a@6rj zSvdHg+OD+V)&jc{^OS*G)3=VFFihj6kxUO;z<=ZuUUb}IWD^+1eh3}6rs_WYLc!gV z>Lst3L3bX^giL;so=HZ(n9|4JS6W$JS`t0HmxAH)s?}LqORB4@`%T$6@c2zhYx)~j z<qBX28tVD=))%;*Sr@M;Pr>NJb%R>7N8827Wo879;4^jJ#UVZcK}6A1yBG!e^IPs^ z=md8eG`*)dP6DRxX7T~A9lByOBP8f5_lJku1g@lCzR)>?jIA+zP}anyEjL{YqKSIa z`_3w}l(%zY7n9IDWETxeCUznig|B%x73nk{pk@aAlR(>gY4qu9lt*;X<H;T269X7S z)JF{L;g_IOQ_rE%o%8k+=cuUPdMX-wAkz~{`sF-gVrPlEFn0Q|h(Zhbib1eON3gUj zGOgx&^bJa`u-?#H-Fe5(ZUHjm)(#tcb!U}z?FnfqmP<>+r?5!MhwX$HX#mG_Xhsv3 zDu{CqQ+`r0y#okGef+>E4R>k50aH$5;Gb`3*b`R1n2kq=68<YQJ+gonO6V1UZUlIV zG(cSKWO6(P4)y~M;Q-N2-D7fdywZr#d{1#L@WDk5&MkBuGTXZ1T4{Mt>VBnVu`k}d z>5J+a2Nj+Ui5O{HbneBx9_^TCj30(49ESx;uxoq*@w(Y70>&G40Re%q`iRM)EJ_K= zuuD%nj@&07992H|=0|al6J8?_SvoY{gn($N(M`T|@h6}Q5+<2ENx%(Ix=5L&fyKy- zWI{z6Z)N2tuHp>6jqFqkY~Y+UK@w9+Qiq5r%=9Q33idUermrs>2`h!LUXdcg5w8&c zA!cM%#^B<XTQLVUser}Yalx^Q#1c**n3G{#g&d?YqFCJDv9iBECL5U9UFdEFQbqzl zA!N=+jzaK5bBTL}Pkp8H?6Y9xG(=xGc^@*_fC`C!JxJECx-p#zk-t{Z)^HPd<~+dQ zN1}u^(JXe=D^d|4A!nsPBV&9hu`bXbjD+Zp2nSYORpigpv<O0)3$jP|?m@|~8B~w% zUxas18PkMn+gBN}uHr1a>%Cn6F2?iz`Tbj3#unS==I<TZv4ffBYPlsQqm^3qP(L)& z?7DOJ#H)?fAsbiLq)8!svNlXY<n)-9>>K9*WDyL*GHcfl+l4$A9G=wlU=c(^i!WGt zJ}CfaIxF5`JvZnaL_b2a@~^OllU<sGOQu|CK&AS0AK_^8xkUJaxFZBM&!H!B_<36s zSS7;RCP8JvhCWfU{m^>W-bixms23$rBh#*)XA>wmylB_1-Kc^g<b0qVUc;-5?b_n( zM^h`f!>~04bdL^a{mnY)ZMkIKJL&NmA_A``+>cvrmEr98%JTXjF#!)9e4t43<eQ#m zf3f*hDF>Df`E}=;ubi&FO1lLO&+Vl(c6$dj?n=8peqz&&W9dz;W_Rk?+3eE0TQ}qv zBtP)^T2h>_{BgqGk()PW`Nx~@btqkM$n#mzge#iQUd#@u?A!6c4n@mbHc24|M>R$_ z819ZJ08VmupSh436Gf{891CQ}v?;AGraR}Kk#5bNpZh+)&B%@camVA^>nPNoRqcCt z&e4|Y{y!^fk{={U{&u=Mb;>CXKPmON#n0ZU_)Xp{lM^Ltwaz&5epY$@dvhA_ba!cW zXG0)#hh-g2;`}@(M+2#64s>vgR~$0rT(@lzNy+5!<-FmfEnaeImUCYQcf@qQBh{ib zt-jS0;|+5BV`sNOnPzS21z(ODXdU}j^6j`^#{##Ueu__GroMT*+&buY%JcM?fySqb zWX^-_b~_WhS~k1+UGGOG?_Fev0-{MC6~C4~wN};eHR<%AxV+Gyn24>e1FwuDB=J#d z>d5))k$ziF>$4}Vz+t%iBt35pjkyl>1-AU-)w8z-vaNc%$)0HS(5?0PZx^>-vN7qW zcqB&Q?TpvgR+MKi?-(kj-Z)<&*YIKfgYH`=ZS8n~dRV<EWKl#>ar$Vk)%xYB3JcM@ z7+QTUEnPS)AZc+2y;-s&)g*NNUX5<HAKt6%?PYI#Fe`RN-L`)lbf8O1K>Zvi|Bk~n zTHSVSc-h)~%3q<#DKD9Qb(fZyn*-4H6P&{|At>#+<b7f%9#ODzZSK?-i?(&O`h#!B zj85hMk2>41@Z>rkLjL|`SO1wh<#Lxa&qvhi^V!;rMLIbvZp0rcY2D(|89PoMnlL{7 z-J17tDiz-{=Y>5DY%}s&jQqu?E?=_prlq<DF23=v_BZ+9U)4nY>VgMeG6f4}N8Qjl zmcQ!#@3x!jwWW_wsx@tFX=!n+STjXS>R;5o-6<LIQRBP{3)?GH1-9C;b@0<iV;a2Q zE>gStF#plzBW?Cw4LFbx7}@z%PxTH#QyD7lTCmNEJzy9UZhZdxDfj-$JyFw+cXZX8 zS5YGqEw@kV)6vmg-Y4@@G!trfu|ikNKC@V&_bPVtpn>b&d{}PTj(0Tm^|i;*=idE5 zexrB8=&FU+&d1x|G!=ho?X%?G<}!tn+fiY2b}9jE6lruECK*pZFYJLFIG|O|W8M1v z5$*&XN(@LVa`QSgwa1*sA{k0<ziHOjwEg&<_R|&mD;|m1-zH*o`j<BuRafjK**A`1 zA?9c9cIkh8O>u9h`Ywv&su@9*qP96&48RaS(z&OohVk?=Z0Y3kzb`?fySGYh?wYIq zm*1&5<SVRxW*PQw<8PbiMw5NVb~rA<&rO?x$arDbzUgP*-X1sFU|hmapX@22Hv(IM z=K-{9=$w=aesSdR;Uq>=D_f^)j>|tGo_g@1CH?QZ-+z3BuW{*}uVc$T?SALR2JUd2 z{&~5jY-*eh>m&K#i<oD{YfHWc2L<V!|DJWpbil2a!Rk;QVALmj$nKNBVYTwQ(}!&( z*^Bm64E`hRRc-z6asGGRD9JthRlUXLyP^Na{6N-`(n-e*UU_5A({!JU2EKNkmd2xV z{;<xuGsTbn$$e7$&1jQ^?~s5_X^-0Tx^meV6(zpD4)+R}JuF`xszJ8H{-;MA&K(`_ z(jaWUX`_!!>pi`#Z1jx%8rU|$>4*I-0-y6e7W{vFy$3kff8RfRsx)Z*qEaa(L}pY% zC{kIODO(B^l2zGCnOPZSN7*8zkf^NeWQ9;>cJ|Km`uJVf^*{c{@jTCc9QS?QS3~Fd z{eH&#{aSB6EHn3a-L)ge{Kx9<8*STo;7qguaAh#q2kzffcv%W=j(-2bW=OOn2KXxR z;yS<OzLUwh)&9!->$2`BKd(X`qi8)o?)tI6qK<K=$LWqu%AO3;!|@S;LTYDr^z<X` zXt@+=vpoljN|~7thu;5Nm+5+&O`M`v6Ra1$XW6Wlg7sGU?Bx1&c6&k=QYht-nIm)U z_vg~R@oyj7u&l#&ymys+m>qp~uBe>KA<-8nICm<<%k-4v)jdQBR4z>?L4N{B-%v+E z6x2Ti%RPE5)~R7Zjs0LQguxVaxSU3c?(bM?q86XxCh*pr`Nv7<i1GTqAeQ{)5n21( zH%jG~)dLR-(1{s0&~sK;oN&@ORQkgRha<z!L2fGU=P6tjR}7lgJ}vQiyr*^|s4Qc) z!eh+^og(aYck%bq4-P%tn#OmfekR;oxcHMhCv}FS;H@^+;gH=ZA&hXB26A57j4Ku! zCWs$ZDKR%5Zf$s_dC4I1ILASQmyZL>eEHFWli@K%MJY^HsG`2W4v2Iyf5B%na>_w? z!kaVt#9HlsvS9DO_*N&UtGhEwjEYkKChb9fB#+vTB<Y<F4%*YVHU*#BVL|ox!|%V^ z`(-5sf7KNSm>;z5$-c1a5bw1czYQtDl}v|DqiATlZ9Z{rrn(CGFQGT5vuL!IWCr() z_q=n-j<R<Xh~I2^Ow4t#Ore&a3!QnmRTP@;WD_H;cGt=$hRSEl=|lKobqnO2DX95~ zVnw-kbzp5%Q#?8~cvd=p{v^;yw>N840(9YqJKoAb_qY06*ID<w^REXwKfib*r)Khf z>a=f|<Be6U4JxnSjA|uxPIXF9C^uvGUcBWyF)HO;a8rVn`LKps3l}q2vW-R0z%g{B zU;{qBkdl1s#-#MOD!)L&MkO`8gPDqsDd_*$JGN~cYeyiE*V=}v-{Mw`jxIed+hX{d zx9)qSW<HyLm3xEh?C%wqz?b5W|I~S$pnFy6&{)+P5KFsCwH%A_eE*~AjGP*lL^Z25 zJ_GVIqpR?*QuO*p3RLlLIxQxMZR5hhvbvD}Y<<E-UrVKU76x)>a*=DnGu%DlI&+PV za<5osiehkx%G%_>xju^r*xWj=!g{~^R@$*e_SMC1>SCh~@UsGKvXRw4cqz@*Rk){o zcXDMqR#Rs~FORxgVhGnHGftwPgeC8MK@K|cr*xJq{A(XSt47IkKP9Jzi!-n)-Qjyu z6pMSOx511?g}kk;>!cdquON#w5$RD!kkgE5I~EI<W{!G}sewArIepXK?$ge=HRf(F zefVn39Y+2J!7@6#$R%|q`k%Kpyx92o4y&{644{JSE3tl^!Q~btrEV~ixl>v~zB~Sh z?JH@s|L6e4E|3G7etlWl7Wb2TByr~8k9AzSpu$0C&gRM*?etU6>_w%+G;{wyEPsu8 zr!@lFalww=O4XIU)^f%v*KzQiw}$n<l?Od}ied@0PgDJ;fAa)SC(YD^7kHxjEgufe znLJAS2TQ|=cS8*OpUBZHwhs;X)NKsA!ZIu?Mj65agNUNQebqT_iTw}1m^s~Q{bIz; zn5Z8oy<qlW?wc7mPT#`@_IaYsnXJ#8suxf9F43J~)Zn;J_b6E5>G8{ff~%inP4&OR zRXGZSj7uE1M#e%-6%I!{OCOd-nh+2l=?FsvNH;#rfEM61QV1Z%pnL^eL}V`9TSJuo zs|7$8hgS0-)PT4o!F-bG?fDHPg^keK3I=!42!aSCOcIdUYu#V`=WO?ZrhEYO49#^X zv{O>^p?OZcA<cCq0-XzH{Vn-IKF7~4T3?>ZW!L?*ZDM@K<R1QDrbY3qQ$rVodWAAG zxZ*2)*@{Z{-&QrF{QEubdnF$-f66nCbg6$AD*Y}RjW2-XccN852gsrMcF3+{8Nm1Y z&6^E@T@$C`YlM^#m>vuVW_H2*sYGa^?N4WC?}21`hICv4jLdWeL5ztSm<-Lpc?@y# z_j7htZ3Ry&i!J?G7VSckmrLqCzu<m*XLEVi&T1)syOw)avV&KQ`%b;TpnFy4ld=I5 zey>t9m&$okmX19!Qu$hBrhik6wBzC_ojlLyo)-+vZ>LB|#b*q8i65Zicrck|m3V{A zD|Y>u*ok-79~pD<jvVTs_VQe5xT(+j%csP?d<hr8!Al=$LHw*e1(JkAEro%C8x;K8 zTKEHd?wp>mZxd5p0*jpp2TA_)zC}NtaPN<K54OinO<o*XNO|-^MreX2_^?OTym#m} z%V7kY=rE6KY<xP(D!tz`VY(~4*=a6tO^ka+JtrsjOgkNpgUTiAh&9+qXYQ+9+hz=q zo&d^Z+B-(T)@2yTk_gZX7YxDoLYnObvplrYtBJuLiG(jq>?V5U$>dc?A2|RjBjt=} zrSS1Mq_;z<Kn#5djR+bB@=$?(7#giq3ti->qalUflm?9(K?*S`8Z?OTcKG^@z^9Y( zs-zi)h6Ny<6p$l`0M}sz0|l7RDp)Wbq3W{tcnVeU*$pDX!sxr*BO>+!VIxX)XiO>U z8)UFd2xI~E2tpb4pxHx9c(`>G-vMDKurGpaA`~oGDuDAr{A7$P=uM#0z?6yrTLhKv z6SF5%At9eN!nZG9!aI3^1~^>IVfVRn=KzfXl|e|2KVq+Ae}MmB07>^R&~dV(Q<cHs zP&Hl;<#~r+I+nYd&5y?F`;L+&%4td}m7JHA^B)A|-OQ)Hs%;snI=uh>)|mCpPUfP1 z2mUn}%zoLPxX&1X5$W<FUL#{GK>%Ebq!QvWl8>nzD{}-f<AksU^+6~VK-X~~5(qaA zKoUbnDu}oibT;4*Gpj`)h5#^#_=<c`^w&V_pxQ45fKDL*AmQaep#zvD9#LW!g8BlS zXAb}w0`dZs0%0=Ues$7b;&3Y{V0PI1K+_+@w4462wSPzyDmqL;W?ESt#S89%C6H0r zYW<hpXIn6$gAAR6{F98>Az|_0$7DhZ{?5~)BPZdO;6upBUT7?!K>Uh@N2JWq=>uQA zB%J~j+7nXBuAhDlb~nLBQV3%myBF>`&=cLQe;R)1t6hF6Zs%%aL#uJ`O>y1>)~Vgc zs+4-?&V>Ez*uWD18-Jd)ks!{G9~Z$a>kR+*A7Jzra3_{O<=t`$Zn#9KQt{~syiLY1 z;@}_|CCK<uj?*s|3~N2W&AkcyB=TFI9WxHjU;+vVJ?Ac<?O+qgbUWxZVL#pC3`s9& zPw0ML#KuaYuApGHU?F0WKadeZ=fEH)O%`Rmdw1#YXlG}PGBJBReDo+fE^h59)Zw$R zgEWH@$C8KHiUukgOzlkw#I|3HUm{E_5EwEzgUD^bU=cotg5vtHM<Kid7|L4(eGP|h z{z(*>B(w$3mhD^{kS*1L-v~g)_|1GR6|kEap0%T;?_q??3;r2&_c#Qu-0rg7#ujJ5 zO8y(oL*}f*37GT%!sI)IGhm>BK>CBMg9iZP3F5HNNxCV86taL?RAFog<k1cVXZxWx z9mFEomzQ8}%hSQ<Ccsrg!4qDc@#HeegMm38%afdJkeZ%>9&5rr1%DBbVES9ZP=lB* zRK)`Lnr;yh5l`0GMWhw{)rd6#2!x3Wy{JY(9T7n{xXvVH81VMuC}AzZJ=X)eL+!~; z3h-=*b=cnlw?hN0?3#ZIp-=ezU&F&oQ93H<Ij%xxSpM}gE=j-%PKI*(yxyxE+kpS( zzUsyKsmlUjz;fWotAUIlu>EFDMI>bz;MDi;-Qo96yff8dMV^&2CgyQ9m4aTQx{S~u za#X;A471V>d@pRF%9qjs7$03xmrh2%W6A5z&sGfVV_-NBW*(?^rR956*cWdhZUHmJ zh-nVUA9y+!*ll{grve8hgO*{0v_K!PH8^QsBLJ5_Ja5qWuLjR>73&mHu6biN_c}l* z5pTc^Ozy<j47x;5zL>kcYFqw}Eh_tN-&^L{ee4Dg)jz0VqBxaB8+F33w}Vhak&iyt za<MJnVGjiY8N4csAx^ug%1btmwW|;@zu!3W-47q6iHMV2{SX*4L0FPNiUX3afAFxA zD!HeHk<)|uoOzR@b5g4#n*eANwi4>2NZ+YWPsnjYSXfz!R2Wknkd#HV2f5m43h^$_ z>gtL&F&IU;m=HA+G2|B&kqmac*!d`36~rzOAQfrj$~yAoO&u6lzq<3T=XT!ek%%zX zl^S-<RLqZAi0%H_w=eYxMOI!O<2pjH%T}v%>JxVh#2&G75GL$7o=`1|G}~(Qc^5}; z-i3`jfrA3zNmNa$<;1;))1S<`Mx{gu1Pr+l0cWoIN`r*|f|AaL<{`3m>aFDI%u6y6 zf@V6AzF7pGZ6z)-$c60Z`{M&qU4R}3eUzlk1|J?SBt;yI6oVsVq*^Q)QhWj<56kah zzHBti4<N@kA#xig&gCdwN&KJ^B$0!aC`<NO*^^?N>PhsQ!oyh!783LED_6NU)6wf& z5vu^M^9UvnfRG_}SD@jSb}*3pmQYq;Lkc@Om=GpFvav~;KTh@Oi_k5Aeoa(O+y?)X z9t5wD18l^a+Hb7$7LTk8VH6<bueZG6Kp-_y8(@XY?TSl6R2r6`w2aJW%om5$B^}Z$ z9Q8D(aKYYB=~l(dRPA-Fn8uT#LU=sB;K^OPb_v<e_CU5KiopW+0ilw>0u0e3b9E7~ z;Ea@nT3ihH;%QuBWHNSmc=&b1*HA#W;j2MrB7qr#r22r~95j0pBd6Vo>!=>c?23Vb z-u{!zW2H>6+`B^{p$tu#Rt3r4z;+>ngD4<I6>wf)W{n~6c?#F%*UqSU5ho&19$Xd5 zH$!ZhmY-IjF89R-voDurt_KC4LQBEE)zeB}9>cuQ&yuWT42@C1@&VaOQto6{!3f}t zzI_ZB4>^RvTaa0)aE~G8S+ltbOxfJX`%$P<_p`E6Lq$NUCGx+qm5A{G5JZxiF=Lzo zCk!S}e?j#V$d7uJNMB($$BK0Z%LU%x(yqfTprued$>P9;BTcAfsx!~X%<A;@n`A^9 zy41*Ww?~h_ajqh+0;JeNL=sztZ+Q(TEoiZEtZNu*KVQ;ykWy6iQ;tJ!3Yhsz>c_I6 zA|XQ?!M-v|-ueL36)mmZi!0BGLI>P|A<~X`xic}6)gV&D8Kiaw+`{;xK?^uyN>Sq5 zO#b$_=<wR0_V4joz&LQ!Z$Ei<eD(Yjp{F@c0sN-=4vn6Znj7?=P+C?pT0W+NMiwV^ z*U72b!`E@zBc6&Il2i5jatuVKixLIL56zh+lxvbOUqPC6Hd>_V{{ttN>XbUpqEF|` zUw=b6%O$UC{r{hi9Us$Vy{|;_3Hy%p;5Cx=ll81?Mx#L1Maw09T6PT;yZ7h$sfIz+ zzCg3dw83xK2CD)l-@{M1O<?6qlK(@`ji&K1@fU&QfxbWp=My{M6oey8P!D4I42Nap z(o@odz<-2en8;Kuevvj+!d`Dr(<6&tCJ#xqUF0N#p|7Rw4y9As%f3ubd7PVql&>=c zJwAWO$1_m$!k3e0OAbt&f15EA9KtWgXgB!XU)0CMd~5V$emdpAfthm!?Ya@38Jlg} z&EDOz)omKI)lM6<`H(j5IK4aS3!*-pot>jbasfr3xHE)-@%5*o73~Fw$ET;+;J-zX ze~-kL<zqPRiP;Ivc+Y>z&JshpN&SY5M=S9I%N)lxe2OP3;QIQ>_`J6uvmsC}9!-)5 z{e;WaUVMvBs79{T?v-=7=-tKlf78~Cv_Jz^es^l!6@2L&FJ$WD>*5bA-oDwi9Mo46 zxy|C&n>A3a7df5%pWK|uN2RT#TafEx)z{>ewsm>e{}LBw4^S^pC_dPao>`n#v@zcw zMI=9tkkJfNXtB`tkwMu=y~DiBzj1w0;J(nrknl%L$;3nmPBRAb&;R=hW?0gvy)kHB z-_USc`z<#PXEXy)>QteG#)JAUD7p;p5K}I2T|L5Yp%-KOzyAI;_TNS%CLaSmHIuiR zHfQv_|F9X2PEWy&4yVBh1h~f}C3!;O@APf$|NdF`@bY80Z}aUvP!(d+61g=PBh+-y zQUQc+#OKA>s8y&`h(-b}ZQ#N|W@bfpdUDIc?sx@KVhCPP#)Hm`Pv$&ufC}M3V4K!C zdw59aMn5N6I}kzQ!6<{}#c!O03p`gC-4`T$B=H8wrU1B67};lvuM(*@Tqb2v;^Js$ zu+0vaJK#x{A15gfQ2(KqkNL4Y(k62DF~e!x5FConDa3Uz?#TE^;xuG;Z}oNYBOvjj zA;s#q2;KE#SIMNCXp>DT-W5lz>+7S&caKW_xcX$~+Q`E;>34078N1tBvz;bqMV*E} zwPc5n&%XaP^ZGc?%qL#WikrGAOLb=z3RB-W`1p)P^m}EQ3)#5P9>_PwbO_~lwm0;3 zKLi4xeK^}|A@7@o&6NR=lz6ky2tbaw32?;=uuY_M0b~Kj4*aDaTAA*uu$@Q%WrSxQ znlGa6%|+=%q(4Lf2;L7>#S=&jaOwGa%G^w~fEWvMw+FbX46aw;t&na2bv=&tFl-qd zu>d@Op&g<S)Dvst8{iYzp~)<15H-JGBU^+7Ligx#><^;Vr+|V&>=%Vljd+4YxPi}u z1O6jQL5%;oik(LRsuF@W>j-8%LC;!>%plxY)k;HQi1^65naH#c!g3T|-X}vO7WI6X zPF|3++IZ8Z{`=n7ZU?TqJx@xSSJCbYIH#b#P>@xBLv@eWwjH`D?S~tZfBx+Do8#?2 zsXNm2=7N|u&F<}ig2`Ju<6mI5QQIF)Mp63nU2(5g0Id8(t$@q48#lhh%?@!9)z{Zo zBw@sbb(2aD7d(-)q7PGfdHx>?QJBDxh9*fyFghQOUPxqA@^9hV##s(a1swv_>74O} zfW7PjRw3}l0Kbas+5+-o*sKDrfo_MP7D1JOiNKu@KkI=?Eq_bTqXl>S{7(#wz@jU- zZvXAc6_l0brfAJO@iMqH-)<!KJDTl>i@@8i{)m2`6x~D|yw47ig=G8)IuY^KbY9hj zt?0Q{{RF?`AFu4r^wp<Iq_+OsOWCyQ*|}NEfy;cGFHDPfXR)xcJUJ&?BPgQF+A1?S zIW*SLGABHJtVOtGa_<kfe3u3LOX(Mk(Lvh#94@Fobxx4U4bb=sTxZ79w2?Y1J)I~? zh?W(ZDpx^708dJQpVlw*OcE9k&7%jX@=<-P{tqmLqOE_x!rWYXrUmh~=vfe?vG(QW z)l0Z>kZWfdXDS3WH7;;fwgw8xyh9nlp_6w4sx@L>1dVT?iS_eb?pp**Vs*Bl^1URY zzl<9Lp%C&>N0kBy2FfjjfmNZkC5AH!xdAXl2|I+f&3t(zb|(Eh?Ey!VG@bm;KYt7B zZ(R;+y!A?noqnh#SD-E<ULi7Cb8zIldUKa@-J`ODaa)<5Ix^VTFR`!9-SRm{-}L6C zBp0I@sviY*;@TnSlzbUlj2Na&_vq=C-0#q|?izjJ6wfK(`2F6^pu6Q?U8UOt60c~h z%(dK&<F!8A)YIAQbu`0Ru2?_lHoK<FY0<S4-L-TEyWYj!qF5QXMFd-GI`FYQ<<%<r z@Ji&4+Vs)nhHz?u9Z$jo7uC(HFNjd}DugLGw5~mAH~$!ax$C-$7ZmA+K2af8*stHw zkJvGK;DeiTyoL5q#R<NT4tlwktBUeH4Pu&Dv~K1qW@U4m?tasg=G|HsRF-6lPDlv3 zxaO%%4_Dt82|UP)d&1p}wQ2HlE2M{L=1@*OMNy3RdJ2dq5!yorM=~AY;x;hLnpv{r zcR)2`m3C28^*XQ$0-;O~H4}*h6jZg*l5TRXWXv%HUIO(bKH+8Rw+(=DGUa>~2K%SX zka84*0ztiGf4tW!<QZpL52~S2ltdI4s@^!riO{*b0s})Wejf@8oAH;UN5)1O9J!_? z+?iVQBNl8Hdbz7wjuw8ee^y?g%65H*>a2pDOI&bV#*e?+r#B89SwBeAm_1|J*B=mX zvS@j*Pa(=^DbgBkyg6^@zJg;n-Qt4TUJ1XIfT_nj`S`b7$32asI&SLb0{hNF?(v}! z7h!F#B<Fa4-_;sjyZ$jvg*cvP5&=8pE$la?r|I{%_HSVfXZ&5;5s`eL>GC^!V(pzx zn<~tT;9N_+F@(>4Rbk1N7oi_agRLWkY}U?n*!p$s7dI8jqSmWRl20&BaJrKs-dKGU z3ZHvc{l5A4#r7472G-H%@rtBhsLSdZRZUQi)LCt3E;h^9Dmc7fj0d0Ei{fs#_uSyT zsk?_y^hLK|dUjRkE;pa9;a^ot?`P_!0xK@GKHSP2ANuZ2VaChtW*vE_Rb*vXftctF zD?8?qMJfn*EKtUtgAI_pHJ}lB4hz8$%PS@*9w2jC(7}mmTry-@1)D_$N_u1hGQ({V zgUca=t$+WqBQpRYtXgrU5+xr7Q7LWq%EQEH{D{iwW02@ap}%md&CbcqC0QwG>&QL? z&hY|Q7S!FVah4GuBFgjJZZe12lHTq>k*{fA?_PR-o`(@vttq>Y85iZK^qoL(`KGWq zwgo!6Z6QbQtnJ}!eQiHz(NnW=Y0<Vag5Q_UO+hjF7I&|L=EwI%cT+^NgC$B5#c7j; zzJIGN_Lff;JaTVRZ%36zQS#2T@3wlh!Li&_w=M6!aFIP{s4yO_6JRTJLBBZPGj?fo zc-N6;ktw4Nv(z)<-o1K1PviRH<1mp(%!sA1UyNf>>q*H-G47oW>29d5m=_)7<Corh zwj)E5Rx3i|n#SL6IkH^Zues_R40jmT8!HR84$EoA&R^-43?CFZVlnWAt2HFza=iI% z3&qGx>r%Bmhk%d%&kvR_RqWvE8Y?U{+4#fVAh8oyV#BV{zXva$G&;iF`tIZR`V+0D zpohYf`n94$tEX1Lsr($RF{4gIVc{Mj(`INMtwO7XOAEJWFWzh-6I;^7gY4|?kpFV} zdSNmrx@!M};(w!@)~#EI9jT6_*L809SE;7yL_ji*6bdM8(C`d2zN$du#}3mP+`fnk z=V&v>+arR`UE40*$e&dFiapEWW(*($nV44ze}WVcAisbKf}k&h`Z;JMo7hR%$u^zs z8(d<qUUb;;RE?*+m}cO8y(GrUMQGmPsL;;8_h{YBzOS_(uI8Sxf1f(cAmDH`PD&0L zriRO@ON%WT9{jbd6D|GZL&SH9l-6H)>^8l(B<Fqrf8yHb(TS%^NA}*E{dpkzl9~Ot zti{HjcUEb#mxK2IGF??x*39&IZgNZ|Rcq0$ajzz4z)gx&mFKqvn_f}Rn(&h@HD{va z3htP+kChFZG-YWA4_Dqw$rLskQ!cilTRuBC|Fd1P{`MV3wuWzS2EKa2QDQz{)Nk_c z-<U6VCh64Q>4q>=qYDBe37su!RNVcCNM1N1<tW6T4FZ8=E5pQsCq*hIcrMs?Zk>g5 zeLIe}VqAA*x<*68u?obu`^2InMTHcFn-Za1CD?KRN;ZRbl8RR3gS!N;mKgE>qj-b? z#0_y41`M8|y|P=Vv$C=bpt2*i6y7}<uZd=bT`QXp6)t@D>i{z2w<XV<S%V*FfCSk7 z-j@{@h8}GdPt|U{!_k-7oN*`F;(dDJk<!ikx{4=u{_bh&(wP;daLKC2iRcJM@t6u{ zQ7H#lZ7`$b52U)@lFLDXVd<pZI9DBiaI6gF_VS3R$>E@iu`GHHwG)LkQ{lOGeLR%d zR{}hI0nNvqN0xF0^d9!@G&B0;6lB-&;>$TE@2LeCCgh!}mpcSD11fmrF7nTh>^rNC zyDGc>o)l&yYo4?%o#&nyyFOUEW(^;t!N_7L#cG6^;2Lfkn7g)6&^M5#90nR9T*Uc{ zyabCsKTiW3_IqVXQfz?1)?z&HI3A5b4v2$)7#S-;aX^i|1-})Gm>EC;QbLc704vh7 zD^gITq6~Ef`9U%kVb)P^T!?jh@b+edCpGO3f}lV+{1Da*f*}EDB2K{9hzduB67k>$ ziY_85Kpw8T%)R7HGcN6|m!b(aI1*^yQSI>Nj`6FZ=&;6QZh>Zw7n3^rhrB&y2HxKF z7!|vB-NK?bpeMO!Zc*1G>1LApu`RE*Eb4rz<sC73JFzr3EXLXR=TpPE?KIRj<K59} zA2hOa-WzC?ym6O)+X`?j>R_+U(J}M1BOF?)56F`X`*O_V7!-;Kz@Goa|A=-cu@I?` z2nq-+{FIRdVy-^o>**;o(^6963N7W9xAEYefX5AsduyT@fW+A>cmZom{mpfyT+?M8 zsbeDPJ0)PY(9S=5`8q?vOn>?FT9<Fbc4LdwK0U_imV-PR@3Z2bNgbhlHhZL!*S)-H zCpx@qs6WC78e$A1LUHr{<H&ikFekWD^FdP5?t+<N&nu@TZ?$CyMa#9kojKH2Gh}4Z z{UIT3M*y>7qNTCxUrU#>Rl5yZ@<;FAP^vR27CzLpbo}q8pyZ45mr{^!F@JJLaPRoX z^#}4jd{VPG46k^Mj&rXx(H#%Didpd#0*1anY0-R@ZrrtTVRC#>>4Twlb%gLalyv16 z-;}&|K9Ia4jhUl=+4eE@@Misqfhs~<#KpO~1c*ABwp^I3yKWGYu%I+stx?qF=1WPq z*yJy1x5a$0#MB_<;kykf8X3C}(jAm}D!<nP<N7{L7}a>#m@#yIaJO4@7#gj~9oe#U zzZn0__9Rukx&ir<Hj`&^mioKmO*9LJ-{;(I_O`y$V5;hdYs^Ay);{;dreTrC=8~l^ zf-b53hJ<QQ^^{Xj(XS<Va?bOiM6(ZD)|4fYGu;@>x9$DA;gXn#)CMNy*EHg;)U9O{ zzZb4e^}he_Il;9)yFZ?<QL-|f6S3aY)H9cPU5!^;GRL#AKIv($V!5;b>gydZR}b%g z7caH2BO$r(N@3I<%)5)Xtf`l}GQV-8@Y6?0tDEf}ZzEcshbcU5qq56(>=9PkDR){@ zWi>_9PE(CXM=EXehq+f3W<_VjjUIJGB_ukwq~+(eBuZj_N(%se{$t^L`MCvecT;{i z4W4dwE%?<^z5U|$?Qc_BJ#EY_C92)7u`fqoJYCm$`Q>GQUdJt&bb~_!^_g^3d;Z24 zE$C$b?e(<TVCh4>x9f91?Z&-=s;-VS2l7K&row)Tn;kynyYFM-t%DofQMsmF*k;`6 z1=C6|kawuT<sKX;QaC(&k56HrXtcNHV2b#Uqbc4Rk>*1!8D1k!RxfN?dl?vZzc%@` zeEG!H1G%=jM{m?V{QL632EG4k0SZMGRb-c}Rlg;%giV`XYkb(8vecmgaGYJY%$4fk zI=%7bzJ7iVlh47=t11}<1NLg(*|u<aM{-82dR{w|$hr4ceKVma!W(3p_-ub(C!o5a znU^-V2p(Kem+{*S9&cIBotsBQiweJ-Q)g>TYPo1&@G+_~OxV}DqUnm>b!S=Gox6{j zo%d?t76=r#9N%bkA#(1c&8vJFeQuHTY97<QrpHHGKMrlk6uR?=p|#Ph5K93s=u>Qv z*GWDU^Ktq?0|Zx!UJ_Y>&pX2uU)o=ntGHWE0BBTQ6?^GB>4Zc?=$`t09(c7bzuzX{ z)`e5DtUCn4?&L66gQ(xnqfUJ;Ie0v7VGAo8%eIM=t`E*qvodssov?MFwYm7DU-igc zmK_Iq(hiELuWNYnqPy2?`A3ANAm4G0@!}oc|44NIXnel)XhXEqpbS(#bpiWG9;u_P zP5s1qW!$G08)z$(x2?f()$y212Qb&aSQP-WLw-dqJIO685&f~FWBI)qXd+Iul&CpZ zBdr^apB{j5@!=&hfCO!L*T?(eo17S-eCO7TV=BLkhL4_x8h-&mWIF7*_%Ap%{nxEn zfXKh{g&O)wiy&NywLmgJYGyc80#_l)zBu`w5-B2pJ4(#HA6GlSP@NIYKJ*EnZtP+i zqp|K~RL&4l0rGU1G38cfu@1_CKl`w5l&GEBoUowXc=raRKJ-C+4hrko)D<P!<!p?& z1@kTe_#0St%~l2S<JXUM+?(Tf4U+9s$vbnS<!(Rg3Psh>9lFn$c+of8ZCt<pIcO57 z0{(FZl>-yKGz_iC04N6A5BSC?;VRl-WOR9;t|v1OQGEO2kyTe;P>(~`ivbeEZ`tz} zJsAlD#r#sJ)S@LG(QE|f3@esF2!!=LuRp`j{1pj}08ROjt%4fe-n&>6VEAS-#4|9h zp}zjMxwQ5tF4OD36O@+UeQrWLRN(L;GM)nQxH^1XTvpG|&m(yrT!cKpw~PiKDA2l~ zPQX<OsGH>55wo;*2<TaocZFZZU%+*ZNg)GP20Xg?B%%}E5nQMkMN^0(2NdcW@=a_i zae3oAS7pfKh9{HSAypVROpj58eH+1G@gP$G%@HL!(d>;uT#Ki>?ETHag#DUHIa&XN zE6Y#PYP!k|6b7Gw4k7md3{b|9!W9=^G^pzPtEf*{576VAp{^BLv8ccg!$Efe2m%B( zSM>D$)J_x^7n9l4h|bt>>hIzlP%%steF$*TqPKDv1aEbFNn$r%H3i|>La6PEVQK+0 zZeM$u_#m(@t{NCnpTd$&fAfaL34bQZ2!S=kA`%!#PeafC5zmv%XaS{Vgfar5t-=*o zmzI|L8#t%Z+<~rPG%}O8xHu5EQUJCD%0x|92&X><U@X>GA%2x)x`L@e#e{m$1A|gP zL>)A1&m&kAhH#PGM6j?36MBwsNN_*ck(H8EIZ&Gh8N5eZK|VYM(HmtMLx6QKCPg*B z9i0X;CdLD#Rw^^`#^=(sjcFQk_F9Y@>Wk5;_|;jhoLfFw0McrKr2^w6(iEuOf8<(w z;irsar~wHR1OP|{_=_1q4KSV>jK`w|DJm(kz(_9xg@i33nEnM+_Y6oVq?m03ngeD6 zJT^j;9-;)hcA^B17l?7lG%*wy&h_0F)ja7r9$@Hmur7jhswYn%Zvw4Mvi&Pici2c4 zjCI6m`{c<3JPEiY4q)U9()0ELNj6CPSea)#MFV9G1&08UVL)gMFY|58sfJl@?MD=K z6oj{S;i<x9`UDUhC2>Q|n^{cjCFCLc*h-wG>d=~CJrm<#VBi_FS1aSKVHI0u_Bx7; z#w!Rrve!n$2~o>o>)Wp*lmLI@cANR`=oL!fQo!IgV|+}Kz(bOH(Gr-GhyXB85Hs<^ zG=-@i6lcgXpzJ4}N`U2?DRBFpQc<BpTSrLa+6mY+sD6%&q{<Dzdi6C`TZ960=d)kW z0P-Ug)Cc4mTvuzq@Esj@z@uUV!dsD+_6lC6Qv;r1$BY|E3M>f`CA1fuuM`{@fs*G@ zbOP$@!hu$<a|L)DaMB~l55coo^AT<)s`V?Z0X6t^=*d0;B@QZ7tb5{m!xWL%1Z2lh zDt9zBWEKaQNneaqk2CFn&L#H_2R#iq*`7l#<mfNjQXQ?PeSB0%S}C)0rHs>+Pu)YK zk#VieDh->2_9NKc6ZnT+$07K+Ab7(8luTep{HEwdKj<MSK=$iQ6=c`MvJSOhdaZZ5 zXND<s9hba@#X!L~GEewN_9v(+f+SC!gR~0RE}{WnfE5hc_jHFH1zrugt6Of5cS{q~ z57vQ{C;cygsGl%v1ELM~3Roor{yBK|HTYG0<PDoP8K4WnhT;c%3naYf#?F;<hkTEn zL3o!mkC@vAnWa2$LnnqN<1+|Q0nuI{BjuwABXNUJ?~jvo<`yy-8UlHoL9wlo(=C;i zL+^c?7reNx9basK#+@8i5Mn^p)SP9!6MT9%Ha<z6!!dzSv2v>^bS<ZvX%!IqFgrWT zRnvj3M?wYA<b*)=hgpL-WDT>^;N`l8Yyb#PT!AQO;^hLxFM$~d(Hd1p8<BMhmg8jG z+nIp)|9+2$Th)~+hN(cL{Ddu^Xm0>BBZ8d}*(8u0y*x-;XluUU5UPn1FRiKZg__44 z8xsN1<F)fRRjRPjR^oyYeF9J7Pi+1a?e}Uq%a+?Ylpi8(<XX-$z8rX=zU=l@)HIaD z<mhR%cG$u=g+rpXAoH-nVRZ29W0Ej|=uUU2w#e&@6muZs%R$4G-wp>t2of4Q*%M?c zNi{>Ea1Q7>kz5hZy|q=dIhhy7EruN3284;O@o8!*{!k{;MG5spm}?BQx(*2m*pgE> zN8ioR8Q+k+hR`QC?);(%a|v9nwLQ<Ht8r@UEuHsgk~fQ9&a8K{`*o#!skl=1YhtOh zRcVUWP@y(d;ug{&%+y&MgmUjko^~uZg~3}7>cRej5Rh~ryp?-7pJMq*Mk^}f*adI% zg_Ab-y#BE4Zp5aMjB;Fa(2iUIfepZ45_c620ES(=sQ93mC(JprM2HXu_fNBonvIPO z!pCgEmq|gD#&P-eew=bt%7?zr{P{CGWPCpC>ac9GZh;scU=QvDA`S|;vaE)y8aJyO zT;2HL&}Bhs<3JJ=LVFesahMSR7P^kQd<Q#Z&KBY7A}<10G%TOdNlE3fqJ1WB7uhO* z#>Nf^3-3mt2YgULg?fV+CQ(oz4t@ezhcr;qK(-p>QShcnr{(i12vJF}UP!e$w9|O3 zj{CY?XS%9~_oRGhn3WoU40A2j;G)d}+deuKmGQ*9=dKalwXw@)13PiSZOq#CZEv5R zk?gLOhrFw4w-kGdP5`O~D+6490jc{mHCwIf*rY?!RN@e`2|G23;767t3Cbg$^2G<J zEuZ@H>WUHQ0Ajuqs7nB0VVit|(g)!s30#7ZmVmH9<^e1ACU%@kzP$||`|PKAw8SK8 z1MwAxP=x>yCk{MZ)EG2y6kt!3!zNv0pDZLmxd5O82}}I?V9cR#P!*y-UPWdFpu&PP z4}X;oq7aOtfEz@`Rc@{T!4T{5tcYL~+n(I)M9zTYn4VjM-WhNX8FEJA?=av6CVzz3 z{OszIgex72Mq&vDYil4%M%H00HjyoosH(oQi9dhHt_v4;(m`|?dEgIcZMGlF)Eka+ zEXud_@VS*=lQd``cXUs1%_k^aZndoi>Q6ZZv`Nf1CD5NA_JPGq2A~q*)b7TD!2S&e zF^APktBic>my6fE?^sx@Qoa<uxC8Sk2r)$=VF)}Y|CBO05y<2}pmKK`iz7QCJBwIf zzP25_Z)V&6GK}u4vPW@coOY`FM7fuUyR6!$+N9XL{)b<rd~`Y_bb_u4U$iRQ-n7Tu zX75;;a6&L6tDvs^-I2Brk`XsqL(+dLgk}x=QMbO4#5%?%jL?<Uh^k=82KE^2>UuW# zMa7xw1nKo+7a;mZ?Qk0_!E*<<3`mA-iyj&*M6yvxmc#>=m8Dp8H{tpbS}rbGbH3Z9 zMr@7BUz_f^-Td`=n<_3Rq`02`d+N`lg!A};(TZE?>5HJ*B7R7au|%wftz|j;=L%8_ zIZkg|IWIazdXC*<GZntGoFY8lD}DB&nCONBjwG`P<bw46d`f?Musnp$WZD?8acR=y zllOD~M1$#Kvvoo;i|l>?bwM}-0>F@}pn%v=V&qqlLk=VS$)a`PC$a<l3+H9gJc$RP zmWLYo(EVAt6;Ic{e+DQB-TxU<z!`qh*4ggv=y}p@*&q4epIe;VC;IrMY}neN?-^<Y zKyrq?8=q{Odku?67Sj)0oH>|AbSbeF_ClbT#4p<K2niW(%Wk%nkK;ZX>7UNg*u!ag zyCap6NwYGB-g@Ae-+r0{j)#GiB49vvzgPfEqvLHveq2S-s{0B9Z|M=)WV0~+YK`W~ z4_W+53ZsghHCva4Zut0c-c<6mo!TQ!!NR@vobABuU#P|X!owdYB@GCQ-$niq#5-3> zbp0N|5wFIGz`%!?gi7;B;#S6N;s?hCPh<ahVCLO$<D><@T_fQ>Di^Vqr!q<?(vLRY zJgN2a3Nw<~gRn%f9?u{iLwWO`{8thf1O;eDB^oV+O6`XjTLMKXI4Lqp5$lX{4|C{| zxpM%z67*6SPm9E^PspQIFGT<Z0|vH3xP(Ky7L+n7fCq2y3^={UDO3TF0!OJ$?(}x9 z%YG!RtiZ{MB)I}FMyWwEPWr&T5mp%G35hmq&v)=bIvj;u;%Ity4J>TKwM#B5knhn@ zXTh^H;?mPs6;=wJCdrjYNe7+qb~t-`95KrVR0|RH0o-Rp7BLDr0h{q_2rWwD+DS)O zNUZ9&e^TJPCX4`V<tk`#Y;G2BImGje^%7I~lT+RNO7EgF?)G;a9lGTvJ+^9_-w!fR z!2WJ|i)!a=tQ|c+zyFF(f!RV{-xPN?fuo5a7c&Xqs(|Al7Q8r4EXr+=g3wPOtYs6* zdlHNX$Q2{(P9TL0X@H;tCm?De`U_m{kVF$i7r_-YJGj2!y1w!4<p%lCqnPaBiy$9? z8{b&SP2Qtrpbj%5SaBW_>W{BCQLwz^izlm`swIRMdluaej-<~}ih!AP%=(V5z!R^P zM25k1?%^rZd(i#EhYzG;!cs+$uO*V#QPn}~^$+-d<zNxu42V%g4j9uTiBGs1$-~F3 zf!7OFurnGRT*VuaBb8?}fh2z~7#dJRm$bBKL~4x{{#LD(df+F!S52rirb6?EN(|M7 zV?DmBfB<~(dkF{3%TT!OfywgMv>26xr9*>DG~*<WOmYjdZIN*x4w(rHPAf7-3Cc!X zXwI&#Ybk7OY#0_qL<QtFgC(3OGEml^8d$T8|3CnA5Lg9%tRW1uBol@L`-nOkRT3FX z30odZt10q9$h#o(WGN6IUNJF2_iaR6i%{&~>R5+H3CHGqgMK_PP6%quEiCl04Et@L z#{yN^fEJHLA7A;rW<?@_*-QA9jSPIwtEiRjOwJ(t7pJ1ZgG#vcVR^8a8#h3nJDEEN zNnsZxJM<jNXVjnmtE|u;)=)2#UO5WvMGkEq=^ib7k6lF;m*Z{}PW@;9cqjxI@8D$+ zvp<1ym<6L>5D^5c#&;0c6s#Z$;+_J5;U5O?L$U|}_0#j}gyX_SZSq1ci3;9gGQ<%F z*S&>lZj_)!RaM+=c~@}#K1XB<Du%x7w$UxV=;mVqzTl)bSeV7qidJOLSj@=Es!|ih zg$_NE00|vrX66^I0a8LP@!VmOnItBr)6BW`If{>d+lbgryz=i;(Gn{`Hv|+4^9r%s z){*Yf&Tc`$GkWE~UpdR0{ETf=@QK|?SqhmC3`p#l4uDFmznr*#ka}j-N=9%UJz8lY zPS6J`NEr8HLS`L5sxsgL;-$kEL}ubKeX<jeReeMuES_5g$5=IXBFVo;1grGBQT+4e zgZ8@UGpW95FO;ToKnsR!3r@S_4`k*~q{hm<F{b|3^m5sFv1PO4&mV#!ebCxJJ7KQx zIt+EmXLR5QWr1sFqPTkJzXUxrMd6}$!B9q%WV@WX<#Yg6<lOT2uf5y!7P$gs6`;qq zs9N$flC<BD!<OC{BSnipQh_Q`9zPbcFJu#owXME}6$lMN-CisZ5i+R{hXtSqtz0V( z$u{Un0Bb=r?t_yamCvqnaBv2pw`EC=HTWTZljR@V1&{&HuAMu%(<3@Eavw?S0|*86 z0b5)Cn>8BOuCaitmb=^LC-!SP-56}qRy5Q#w>STLW9C1kSI+)h!<xEe>46qKzdy&p z-X0E5-|@^6phINkVoAb@(&-fukA;B*4Hwko%4j*Yw6%LmoYrC1Lp9bNnJ2QQOT@1= zYLl#NMa@X%L5niQuh?c_q2$25CtgmE;ZL_D$7K(MmDNzhV3(`jYQ?L>q27a&m>kGR zM<t5tAIli844v?L8qzbnp!>ddemxrgG359-qIs!*o}BOpP7$jO*+?b%gQ}I+aai{z z`}N(B1Cr@HxL$C?mk$G$z_d^o)R^&{1(+<0A*YFo2NssEkU%r!bM&hTdH<I*K3A^7 zxl(F~d8xFqvD4G|?E4Y+YP^_BMk!v$h1cR1A_CzcQ9veFqE-$+9#3zRt`kPYrM-KQ z9TK7*H^D+XCIGLL_sKm>I4=*-?zE@Mi7CZ$*^Tdg6{QoPQS#{i&JXeeGg|+Wt!B62 ztxDXHcs$HlH}N=k;SeE7EO5(6HfI=cK*xq7hWM?}k&|)&%?9)-ApF)k6KORdIDFyL zF!PcqT_R`s^oeTc)A|M~+^=%DSctlvq{hM$gn0{lj%9^+Y__(x2JTk~>J^qETh}FB z3-;@ws2WL*6*}ry_Umb(ze2~1eZY(b@V&ViNsZDZn-sl0x-Px9gG{Is(DeX(`Ptv^ z0b3xs&vBX}y5rLSHrB?Cw2J96);{|-zBDSh!o;mxA44Bh9=e)CH&zs>Fu)X6`N3%d zXCFZ*(0892_`L%(ZqHk=?@GAQ;OHZB{rrRvjf48cl6i^v>GI`E?@U{FAh`pLN&@4n zI+)dnF#<=fyN?g)g>XpIf&N4(XEWAGjgB9e<(ln5_4y1C3K-$J0;r=Us(Ivb!@&{W zbL;~UFA_5XY!fw>3vM|$D3u)l;@r_gw?!n)==o6i^fx3wwQ#!7dIA|Y0DOw^+8~Rk z!MB0rl#D&ZVM+xw$N;AfTx6lp3cy7Md)mh$7VFsWTZ)ejXKJ8O03`Vn6%4uxMI`WJ zN?00WNh`hxM6T*tH@9Q!fYOE#t+g)cBIgbstuMgr1E=8US%on>6#xotmgbKEazel9 z0=Cy+cN2PEj+(TjB$&{HcESCH;}joYH?S()8G$_jMl104f?CDy=n|}qG}dH71D-l= z^OF_P$z+K@3<ZHU)Uw`4l_o)ZGC}p*YrDF-EUg@H%^O0shjtFOA4%lP!n5I)VOd$) z?nY$u0EtY#zrGJR7x_@4Bo7UrK4|6-HX#9hQKLK&clv8pa~5gH=u60qAj}xiH#PN+ zkB=W09Zz3aPM`LQQb|?M(*uKx-knTi!oVUg2oO-LV%`iXv<@?kqx#2FCd@X<rlT0P z3#czfksSxumO~f9KSBe7=hFof6v%C~K8kEU$PGG!cp2iJ1O7x@U*sn_U0;Rui|AJ^ z<SSl)Y{&fW-FQ@RLIVv!aZbyBwJ>R*!*PMTZs`WRAoH-$RE!BB&5B6YU58O(8lskk z0>87j_biYiGC&wyjZl&<if8;7CA8A875L&b0<ezgzdr%`|LML8mU??HlzlT;t~;J* zH{%1{C4pzK=>uL|ZNkJE5Est`&GwCqj1WU6U@YP=!#@F#i)8Yn&@r@Kgf!g)KO5@7 zLp(g2QTq~{3j&RPB)LM$rp&{52lXWQ`p1CaNMVX$6+@V?P?3`j#Sa6LNO4E&q3b3n z8+03dsB89HN2Wud=?B*f3V8|OyRhD>jVuQqUzMWsVK<U($S>oz+CW324;fqe5)hSg z<6S`1P!^JKVC|Z?l+MuLXLbc`01LYCmPnGFpWpKoBAdfXU_=>=jTD0K2gw`xx4{}H zVzXnJVr+mu@XGL$Hcw!P<g*@7MQwm+I=SE(2YgT>-`YUO#%J|g5j;vI3bf3r$hq$h zR3w`fcm5R!f{9L(xI++#$~8C%@hLzz;<~_Tr~LdB)mh(tueA2_9cr3EWrWQ}a;r&6 zi06Y_)0NO!za4Px%A$%UnRo!Id$GsEMP{5PI8X}E;}Kv`k$?0ioPI#Tfk9VeAArJ9 z%|9o0XIZ?_acrXem}QE(x;j#q?{94FaQe7E%XoT0`n8h)BEEoIF`c}Gc)f&6Y3=JO zU~T;Q>(^=I-s0_C<Z<?kLkR^d&4EI5l&!Kex#0Ei&AXC|h9v1u<i3C3H8K);D-6_E zT^*f*LJgO1#gJRZN-|Ub^8BUj+gG6W=T%7fx4k~B)q3Z1mTA11V>?bq!|#S>W(xSR zma4u;A|x4T%6-xpD8`Eyx?1^M7H&N}?+nK6b%>D#R_9z_O$CgONJh{qA_un&OhMoT zEArn$b)A+j&drfPJJ5qia2f(ndUmDu5gw)m(lBwsod?h4T(4t8a+p!PB5f=tHntd> zCvdn9kqr30L~{&6u{Yb>PuSBCU$a~oJwV`wmq3)TD2mV)07F{$9I`CJ6u`%e6T8W` z#2TL$>EW>Xy&Af&iaag2ws23RSR7TrnhTnmLZg69LntAb(O~WZ7|L!}3;SCbFjZKI z*PrrdmLiZT%INa;2REJs1Q3=1$BxSMdI5fZW^@#qqanU+Pv2OQQFxff!Xe%YAXd0S z7@JiA&cdK07cXAy!Q~CIWSfJ-5~Fqp{z-(JJ%`i=MkdmbL4g)T``uv1;WBK_=+5F& z-hu7FXuC5)$Ldg>Vomyt?|X@k&HyU$$BaVT$mO>{P7$0I&RCNUfBqVSeSN)z*#1t= zvM@njh<Soe{fmnD)8-Z2I}KeLDn*m_yb%2RoB>CgA<7Q1N}HOTw5a!)aht~Z>kGV@ z-EH+@dKv?LbSg*oVmJ!SBO9ksopZUm^S}GA7C=&e7(gjdIMt9mDvX#@VJjnPX5QZN zxi58b&ygb*o3^}2<3h`N@Q{zJta0;!a3U-lN>n6iv(ufr5mhYnlm}yL@4f9rmsyRH z6SJYzTu+cF(76wQ8piE#oAxYK%i(L25w-Xe@t{Y+?`}Y|uu=NZ`H1BA=%Xvc1bva0 zs+?nCKrHd_O1>SLD;=SI$(G;ln~a#<b%VISSOL-zS0U8XhL?wU4>etSYobnSHQZ<k zsPq8Ge?%dVN8j_?VRp25fq0S8JMV(Sj#oFo8&t(EVUJTc^?8pR@yDgk@eY6EywKiS zG*@ExmXUfg`)S(Amr%|wE-sas(;+L7MJvD;)9ucm7ygRn?E$S)lUh+y!ltbd0)rt0 zaK+HdWyqy~GUe%7CzNG2qjUkYWYF`#a$m7=fnRnverqJ4H&9tgscce1{G!G<8vwHk zenO}SiPK*q701vJ|3>vk^^qDk{<hH#NWyVNe0?Nn5iH@qV+9Mbh4|%zr~B*NustmX z>N$9H^1dK`l8o+wE<dDk`EQm{U%ow;|8zbGM*>!o)K4&7=x>QQ8MM{!fK&yP%A^Lu zr9sL=e0q}Jk8Mh*+1AlQr@LAJMz7)b(4Tm~_J!o#^o<+pXQDDIpEx$>EaR4AGFhG> zYE5nC%l4f$yZ<*zPteP8dQx1}2}i;`w8gomqXqe}KRUJK;}9pfH7@iZU6ip`F(5I7 z?IwN%$)f|;a_JVCiHy+xpMWu*g@vs&5gT5i{~4bi*2JF^%ZUK`)W1#Is5+cV*=-^u zER4X-XU;sdZ~`PPh=ZufYgiJTSq|bX$c8_GT;Q)D^R1df>2Qi;*?a?Vg@n6u>``}5 z&rW=%UFBjhALDV5S_kC{DTPVmD9(D&$g9rFZw5hbFyD^br!0~5Es!&!{3UlU*u_e} zXBA~r+OYP(QLkreDi4q1Gaw&eXq2kWpVrM>*$t?H1Sq2{I&tsMe!w*4Z?wo@FeRm5 zs0+(YQLn8e5uj#zF|&Mic&9jTKQV`rRre*!<Q2`g7*nIXlB&g;mglg5%e_p5zZi3V z@ZQUw@-pGIExnAifG`BXts4tF#ia0X`w>403uO#E-9JDfpi`_x%^+9N+S*EdA0<9} zWyvTckRP(aT+z@A;?xJw{TX)$`TqFyzhJ8+W0_D;*?;T2c?0<gU*LZR4*$qvssvkD z3DRmbY-dQ;Ij&FKF;ak}A(V|l=dL9+b(QQrd_fX~jvIm8tz4RKA7cx~@6nk%fC2|F z3OTspIziD#2Be`LdH(8E71ksAW*Bdo3)3OmCGrSVq~Ab+A?>Xh(?cNrJFB3u1vCYY zb2JoCod&_ugD&9d@WK-nv`x^w(J?T%L&Jdvt^5%^;b`;o9eq+2!0_Rm#E^PVl>Ru# zP&sQfzvqe{Eu3jUQY?P-IDiJOoeGHXE-fymM)Cyy5zKf}t6tD1CKwYFoD`6Ii+{)c zzcM~%{TOv|QC??f&mLm5n>`(vtoUfj?(e(;oiQGw5hRraMnDzL52g#J?zw+`uw+)D z%Jlj2R9NO;V?LD*iqK)9><lvM5^7p}udDEFlDSAAy#ej;bED=jN9{t^7s6W3fN4WV z(Gx=&fZ6$ZB7!F}2y`(-LIQnER6{k+33%Ab;YxL`zv4dHB!uB1us)Ha7!Mw}Q6_p& z0KXX*o!}HBQ|w4_`y<=Dd~g7_%-h!AKYx-u5VQld;AcsJKpMXsnMAY=ME?#1eV0!I zsySR`L?etI5g;CJRg|xL&kvjTU}OSTD#nh3)ek^1j#*OWCT&l{52GGK>vaY9ION3J z&~zip(H%xhTmyzEU4(M;v3kf<P?Bql0)Z@c>^Rj_ld!s(Ukl(Q%CHa9v$Bf6e&wsM zKtYEf4>xqTe>#h4%Y(Tq&^rf?`fq7l_RU(hyLRKo+^2i)LrO0f?Sx90oH2)mLS_<+ zuooS&xO0b%=fI-|%`NtI6Krka&cQNVhR27y+1#Vbb>I7JeLca)**HYeYg=Zk9hv!u zv8BgXrRwmu{bMR#*?B&**Y>K)`aIEdXR1yzpnb;oIY9Ir?^Si=<*{OZz|a0A<HeTj zUxKEZxW6G-ISuIIR{c4mRsxm&75v<Wwa8m_^Ykn~G<Ak9$1U$%z9J**im$X!Gv8LE z_0GN+r_noFC-T=`eX(jAk^p9@>dn5ba&}p`Yp5QVAjL7zU^ZFR3I=PRbdra{6O{DQ z&(@E5e^FEf`op}A$MxZOQ|iITV*~!T7Qz!<_yh$m=8M@6g|CqX1W7qt5?8667rFbw z6bpkmAHuE~UzKn?+^XxjlKtoIyWZFR+DOdQnjvAiduLcE5v|LiJpR*vXK+3_a(Rco zeX@V?Ck_-0Ns?GRbd8-9r2d;M?VFUNwWNc2cNb2)cVqGY_dIjFP7-H<zY-2+WvsUQ zGA~>2cBf%62@t~C!rAQa!ootv7e@|EP8me*!2#%ff$>LKhud9rrU#){TRaM+;B`jE zlT-F=E2kuL6YX~!hNfwkGTq7$o)0C4!s4MUB$BD`j=&_-*-ic2_sIu3xIzp04^j`E ztbF9B*y9<=xmY5jh{y9ZXOCmprsUw;<KI7N?R7&n`Wj6x*9?iJ#_lo9Sq5=|7z1`) zL4Y&p!JI7Ysv||cGBYQ(&=mls291bfa5k>b+rxvzyJMTI=EJf@py_;7H5w~hoR+NO z=g*}6J0!h(x6`n&#K-DFF5RPSZPP+3>!f{;;+yu|6c*g?n=4$pnOa{r^X-~$I@Cqp zL>hqVfmc}<@HFCGgFe!XZAaCKNpI|6#lBsI=&(qE8xL`{YfZ`LIF72;P5Q0Xl3E)# z$X`0Uk|K#4H&H2h-?=SARj<>YVY$>B?>{}i1!O!9NB@3ODV%rCe=vF=6?X|MraRs$ zxzzjX*L|=DYdQs%UW+yoz)~XG4|~NZ!BAKB!G1^EBfL6CO7C)4h1Tm=-k!1YTJac< zCbxa0Ud~bRaG2V*x6QtMSc+RlDKSOJZ@kPJxhG`$B?wKMxp7t~67JdBg2P#L3SkGh zgnr@lM)6MCLy#0ljvrsEhqDGGkPl9FJa1ZO@DG3@B;>g#e65+1C(!Ld9YQ2iL}Etf z9!3-W-~_Nbl1zZ^3r9oM`0lp0HYi(2d<|f5G6#?XD2y~G7^ukzdK)>iWI#BFuLwAB zqPxTg0bVsuT;phtC_Gx(A5n4&S^eHh`M=Q*&YH2&%W0bLeDHpNCDEZoxeZ(a=NtYd zlKc(u(-mWM(L}`n!XravfUB-v$t{571`g#<5E@u8-gPF*7ufzG!6UaJnv)kOPEQTo zR{xhsx(5UZGC8zOk~vQJT{wxU5S<JiybRh&JWAj?jJT2vDv@TJgDMnGOSEe}E~5oo zif4hJlF8IK7)hIr`->!V|Gz;zrR&7q$N&&j@c-=EB@OhfE=vfh#UP=Gk_|75ganaK z2jq=HekP<b|IpD9z8<{AnTr>vTuRrEiOwFNGuF<vLMnvxNaQR>`1~QyyJa-xI|S!@ z`*^hK<!(98-gtQK%y`qt!QVY~{@X>tgxWuOyZ3$F+p9Ba%I|=DqJ$|dEL@Gcm{{6z zUlIx)@-9O(+Te4Dz!lU7xgv4f$wi)2S1(0HciY_j3c>L5?L(k#1CJOY*N4VusCnI> zJqRIu@<Qm3ARNPp3TI6CgD$RI7*Ej?|39F~Lih$iNAlqwMO=}<2ZWjEk_#1=<t)-6 zf?7dF5@`vHe){kekd_6{mef-Q%EW4bHi$F>DAI9_)mT*X;P0b+dMS5isTEflo+m8< zXz_B$BqqQf<kml|mVzlFYXMirV_o8-??SgpFb|a7m>t`N3Ke|eVLBrr54%ucEOn5? z;?t?8>r)e43JaD(U>BVIB)Azr2@=OESSg4vS`8rdv;hiIVz`yV+(f)|evAgkA|cWa z5>kOYDh%fRiZL``eNg%72b-2%cnrmcdiL!DsM<;(6*F4){UG8y$g#k5;_uC;$27eZ zy+xzhnKj)_)UWcLy?X7*hYb2nu1ERL_*>^cepQyArZmk>&nA<+gN~o$&GosZ+B6X{ z4VK5lCz*V09*^|3TULwqO()j8w`@Nov<rhM38IV25)dUBZA0oE>?h_y<em3IcSll7 z@VUXdmXYEVfi)C#%dE)az-><+9##uzMjQoXyf9@mlb9TMcy-LhLYY_E+&t&qwElDH zbo1KAHNRxj?MpX()BXHBwMxopy~M5|QHC$$7xTYeh<xEX!*P^Teoe!g(_0_<V$P+} z(2tw|{D-W^EI@RKPY;4*#-==ifga$gG57W)1^w|iu2-v|(t6A!zVAzow-pA0qKfuR z5jb=xzK9<ZSDX1s4tyNU#M|_x#%TKHtv8?(fP@l-5#@S&o)EMxeF#H3oN)nPsk^5~ zNm20>+>^rX1QDI=U)^1Ea{}Xj(XOJ1$~36hfnPk>P>W0+cwIv<?5M+GM%#aiQ1aLw z%(@lT)g{RB24s#)huRtAlkh1a_5C$4;EAIMJ<>;v?*y(2QB*ll2~2e-Vq?(QU)D^u z$92$x0+7yJ$j1bFg8!+h?I<rVw;FBVi*yO(i&H5oD*nV#imWM_tNsB2g%IB*XbCy> zb^|41t$MaFQ6qliNA?j&Vo1E(1B9d_nOFUqrc7Naa)SswhvRkioB(xZMh33&7_50f z3FOtGK2{qN?d$D@!SZlcGsgL}WOt&6CM_t|udL7q=thY~5wb}LFIJPAqQ4{d!`ZE3 z$J1@@?_dAmk9w;epWpBAXHGg<KX)3f{ypGcphNRu|N5w4FzXZJJ+dHaUtYzT&!wBM zHQ$0HWsrk7)<{A+JNtPJHjU?a7W}%GFx>vTFsAg%atR0vhe949eV@@8xV((6xZU() z0Clx-@_Rxou;;Z+^j@7n{k`l+xu_bbF0l*ZUI0wUN->KbMC0Jam4+UyxN!=uaejXO zYxBFuUCfcsKtXYOHdOQuN!;7C`wU7$w6?ha(5^FEc9_+TN@>IGClu0z-z`E@asqB) z)zQJ5O{v_}2|8&4h}#no$du?(*3x_R#^DFL+K?m0SWm=yhg*s~dkE5$(B$cl&P^Rp zPDuFj<^V5m+81wj9fxY1){Biz>kaybhrRJyzGP1OoY9lty5{sOm-}8(YO)7<786~x zHlBPOA8!<rT2o!U)-QPO0}ns{wsq^);YHkNNZJnmk_-w&mQwQ;Gtb!N+37g`?ez4X zd8zvul(jT<w`>b4Nu~;Jx`-$TKmf=@crK9!++z#{phPLH;m;47AoH`Zu%M9mzKRMj z%((%}aicbd8d-VhK~@_W80<c7!k($p**dupHX?*9Eug}FPoJK{sRoC=03zeS#su4@ zc<l~*Qqm*uU=J~h#8OgB%mYlDLt9BiOsF|CF=dq?^jTTfAtgC53&7Q#R<xjW@uI_^ z@xu%+cXz)HhBO8TBhQ}Ry5?8FcDk@HTe^CcvQl%t*Q$``Qd1crMAdYot6kFuk5@`Z zClW7Tx)g7C*U5W)$kXbBff&PlHvydo5Kqpa*!_y?9}?xjKH*DYPy72|oa`ScLH3J( zL0<DGkd+4IAMjx8h6l|s@B^DNFeoT?88Z)jl9P=?jwjcB`xZl{Cm7z>E^yS=uvJIi z6VCo-$O5p4695Cir-=iFq<5m}W*Tay*}R#JnfU>@3~$hgkhr>STdgcrTUW<<ln2=1 zXqK-TB*Rd3-GCDoO&Sq7pisf7sTH@-SXb8t90c66wCR_!vMV@ha39Tm&_1}H$>mu( z+xiFl?Y~VkZX|naax`E$3VUw9{dEPEzrQBS4lZ{7Qi=OnJqTR+ck4S|3?J_9?Uf1J zdFt<zPblFK1(XfbH9$o2N&o}CNJ&AAmuIC#)w{&Zm;B%{%9sDnhy&oAMGjP1Rh0o0 zg&5<YbeIGg;)ALH8^oLjLc-zgA5PbSOkm&1d#5ZcGAdKjI*C#Rqu%a=2LB3CA$rzQ z94Uwcz1%GJ^YCGQd`)N<k+V$$e1Px{0GUamOoY+Ro8{nHUY=P5>2}S)!2H)|YK-^& z`zi7j;61;vunJTR2>d(9#&-7RC1kNAme^v39t1OMp+&;&C66L(d7)$3GonW=SWHb( zQ4eY`c6N3m+C^A6KCn!*L3%9CH*QiQqDVonOh_(VurLG2qmAxpJ@(+w5%*ojIITXP z+ll3Oawf~<hwZ7}?9>a~%AKp4vu#t?CvCuQFNg57E&So)4jR0~oA-{N<cXja$8hzJ zkOB=2nUGn}NlAynm%qeCNpiBld~Dsb$Ls0Sot~baP`U;s<_iX?zphUMok)iC5$!wr z6+GKme#Cz|gG54ST!K~xo`mxvCwE}r5_wvHE-oeA!O9~uEHHeA*cz}43{a;7(axxJ z0F8K^+#JWv+ABd%fhW2G+<-r)8Spq>Xe?l;Q-77q(N&S;A^vr~GB0r6;xNxd)GL{Q zM%YT&en|QPxZK###LESa4iH{T!0HQu%23<;r$EKGzh`{I<p5mE5&|IN!v}<L6)+Sz z^ne`&&$Sy>K0A#y0uApcJQPgQ!Ou|E=S$~mS|1Bs<byKx$45`fdhkCUw{X_=8Mkgf z+ShKc+7lIBXY!1}{=T>7me=+TQQyX^%eq|CP5irdnw#A>O1sj&aA?)$SJ9SkYWlAq z@oEfl>wdH3jQRTUqLu3(se8e}p3+^RkKbDAg(i!b*Zw-`Qy>|o6C7iS14REzMa5GN zvzYy|T-4i9Wr&4-%xuE1Eu`F?eC#2==3esKr#-vL=~JMRu54g(NUk(n-i|Wx`&iL= z-JM%4x8d)sQD=U=vI}oJnTUL8cey{*JRMd!=AGQ<I6V`WtU{~plK<>3pJBCJ>oymI zBk3NM3)Xw7j9lDv98R5NAQvIuy1uy>I#T2aZWC%G|GA~JVXvs^P_IRo;pOPPq6PGp z5v3XCgH)5D>SKl3Io}>1ZO(t@r6a(3{FdlPt-kb5s(;8Q!QbR{UY|l1B-2BUAekW{ zhEoMP5$jL9aedahj?bk1w^!b!uzws9H8qhRlkZk*Y*@@(<4xfj|KU!4*j%rff^PiV zwZ{+6d#*b}ejc7t0%$r&G9UbQzsnN*fZW`pEVG6T__7Z#Xv@caUw$w8Ayk7E+o2%T z@tNS@Ax8#5Ux*VaK2f<`6Uz^}3Mw<`ws>O;85J%6cC^UUxh?Q}BTk|iN%95=7z~Sl z?LVXED=(9WQ2tV{|Ao@RKU@>dXXS=m(sIRV3}11ucQ0S(UjK6YqVDO_I+bepgR_-d z?=AM(h23AY7P)NYiepca<)43l0zTgfVe2>FU*Fuk{9}t=GjnNLzCv%+yhnKW4^(4i zW!p1JO(`b8E#IP)WxMGXweIr%9$o;UAa&;gJ}l!t!$7vU8RbT73*yzIpdEjSYrMO= z+urZaAy!s@<UB{sG$l{<c6Wb83M0D2IrWo+*la(6iWLLaT(ZSRkh$21Bk3|wUpvz% z0H0v5cqh3we2zc0X5~feLuNWw`qpJ5!^jyKnb0A(`%*q@_UX7(x#uS&UfdQT^-JBD zy2#`h-fRu#m@-D>PP4Ggwp_AVMjFp7*HaH5^X@pY9P4d+y1N;m3c&@o%p6G#d>f;c zio!ncB!P6gl>+leJQ&GACUF2uP|jMX94i&`0UDTl9hqHkdY|dbJAFDe>yk(FO|m4@ zQri|X?k*VSCwBZyem_IRjAJvvCOn=z*)~2tdbipmFYhFJxjFs$M9e4ctolhyG=BK% zhlD$|3p%8B;ucz;rjo4eu}84**~wcBg4L!=F?T2JJ9MJ5*bI}gM(x{$t%u9fC!!xO z-B=U}3h?*;L|7qcCx9q?E-gK$K@$=jd=^Ir!kscIkNro@i^J*z03SpkFd%HV8YqB) zp@G4*Yp;Q0ki;rfR}9eXp;HUi*#d<s3PZ*dw<$=j00%*`HBs!KDbzDCkb<zj`TgBA zt18q!2Eh+@)SDqc69_bbPEPBkVxk1Ob0@>eUKq-KP{zzC?!Fk~O_vdxA38UJl!ApP zlPD<Y2e(0~jrD>V(A?v;Kvsx43k?GKM!Y(C#9}xwxC$(WpU?Km4!oyb@L>F>!^OJT zsX3Y~dm7<@3eaAGHF%CQ5#a;Afasw(dx*t@x(Y|SvSI&qTU+7w_I6UZgA*qJ0*rt% zQ+cocAI9E0n(MxgA6L@Q&@Lnu3K<cJLX;f}5tThdl9k<335krzimdDr*$pE*dxgpl z$tLUjc-3`(zkmJC`JHp0b6xjU_;|lx<N187Cx~G%8nVP88bDB8gnSOYfZq|%E3gpa zbUm;~Pzl6-nS87BX>Cw+1e$GP;6h)6dis*_L+u+kxKJg1YHtrll|Ko%#Wytcnwg^< zU4sC4(h^)h`0u1T0Xc`Xw6zqrwzkOOIw#c9(eV;L5&|;nGn$(FRaI4ie*cXN$4hyL z%Mjo=8Hp>=Y!A~cnG6f6`n4WPFfwtRq!izQd=8#lInaj4deOm4sLxSXSLYqQ;~Gd) z<ZyoO^yBxzI7ZYsaQrA6aTz2pXax0KzwVEd^p1hYK8Z6)n>4Hc@8YV6e-o*e^8HjG z0n-VihS_{b6y6PQCy7kHgT4{A7jbRv2x}L>cDNUjN+2&p=F~$E<PFRl4Be-H1uzBr zqcUuQz39-v$d`Tn8UjbJ<ye<+(3xsHal9^VI3<W;1;Ru^m%xVz{iHrFK;94cRulgb zFc31M6XZh!&O6|fkWXWkNsw96aEl^iX{Z%KJd$$>8pf?gQ9+>;YBCsupj^jj9SEvd z5&Ix&9dZ@JsPw+9EVk7FL!-{y+MWRB5Cxt*n(k9o)p^K&?gE*^A&Xkg2jyd6E_&CL zLr0E0LIsX@gUl6@f2w0(;P&XzI#eJi<>Ut77YhXHB{w|;b|1qUbaj8wceJCc0<2?? z(UTO|TjFA3chRXxuZo6J2_;0dDN;koxHtT&5Dk6n>yyAI^wHC{u<(YI94co7hLS{i zAZrIhGc3AAkkVI<?z-Z^h8-s@f2_g&<V;Vpplk4j8Vg@VBz{l=8G$-Qp{q_82&O^$ zgCZHnw{z#t^;>GXpD`mjABuiNSboA{R?oS`0@a2Apq$L>P1FJc*@q~d&<4f5d2?%d z(LQKE6}&I@<t>EHg1{wVix_wZKOs(Kx*(D5h5K~b;>fx(4yrv7kn-pGi`GT0s{WJS zyFw@Sw|Nd$?#t%?>b-L1->q}Lq^#sZ-qT->YzPUwi&hnRcdyS#qFlswer){|$_{Ap ziV-~~At3=!2Vta8QJxiw)jmP<9!u~f1m3_CVI(3(Xgol4RjVSv0%N@5!LpN<|I-45 zB4>||Z_bW(7#o&MW5??v2vuSt2Sn?u-h(_ME@Cn=49q(3EbgnrP-|6!zuHK~m*F6x zfeI5wwz$UWqGWtEgd&y@*g+461U*-%J9SY9N`!?2FaYpM;QQ6Oz$Kunp^&Z=LeF?X zNr^qH3&^RCg~f_$57^&I^iw9Cx%?DR(h8#(#4tsWc7IA85-ue?#Dbfcn7}WVir`v& z6ghN#tMF}gad1-*tqCNoX^yiu*wzIA0YMvx%5m1q+yeS<5dPo(8OF9ku?l$!NfSgT z0fZk5Ei#N71`tSki2@M~G_S$f*PI>5^EJtOiQg1w>o?%e^@6~Bl;~jzBWY1U4_F&s z1oT1#jdj3lRI_jAuE|gI5TgJV3WJM+EFPRpw;{X-CUpe;8JZI-NN3PxDBQex3>7sQ zXh%eZ095O6LZS~e!6ZzWRV1FqMn)cjXq~tmaRO7^go+=R(U$iBmtkxl0LAZD%EigK z8DJxxcM1K-9?8EKexW!|8jZ2empg*T&4;@4GFqL!QL=vi-<c(%6drSPV$j8QVd+EP z#@M#}1!nVh8;5~gQIV18uglPi3&o&|24;jFo#G!?AchZ%lNeO(n=eYGY69EiF1)%| zwQd=l$=2=LKjP#fBmeNrNpM9w@n2Ab<syieT`l>l{=($M1OPt24Qmk9fTa8U?BpU= z;HW>a;y^-ztCa>Zj$@2t;wtbqT%RPi6Q*V?&3|e!hXfwB;h0VMTfRd@*}zmbvjx7y zqrnr61$-|t<BFVmd{l_^BQ6L==cr*%kX$0%Y4D@=;3|2Sn|lin56w7GBxo+z0bL#< z?0nGh#?_ZnfB{6HLE$YcTv>JuO3~unct%}4r!}jv@`a?7R0AUWgR<!M?u~f;`W)sf z;lKu{=Y`9Z1aP7$A8Jmbho|W{mJ-l^GRSj?Rs;?d6c~|kgQGWDjf7w{$4?SFzJA>Y z5)gbZF@%7FMw52tPgr(ZnvY;N0BQgX<G|RqFZ2}k{s+*!n*9gRe5a(IKjrD?ndz-$ zCn>r6L-wZ)IlF4&bevh$eswc`qbyxoICzkmWs_3z<pK)_Lyf(30_SSado=KfMV>7b zzuw1QC$7v9z579}gwxecOL?@<|L{?b3JJYiOkab4P=tuuJoC8wXnbmlL%EHbnp$GN z+Qo}Lx5O?%t^>9mSKC=!xJ0mki2%`C6gaNs`r&ScUd$cBG!PM(y*K1#js$hb9znbe z)UZ1p8OM}d#qNBBzXjfS98+njsjOMo@N1(Sz$A4=XfBSOI1z(R5d3jb(+g~(!ts#E zTlWga?nqnCsv`v9zhr#s0@Ze}-8jeylnz;kBp78Oo1b5xjfRI1Z`EIYt;X-u0i`d) z+_Kq*oiTUL*)KHI3k4)hB><G}ll=vSez1Ur0-#MG-~c|4>6LDxKx{ayk$s^43GRfw zMC7?p#e=**s`3b=3}8@#M8d{`L<#-X{v}>lP>=Z3UX&3W6!C+4xc1gSx1!hImdNSM zLDxq+7N$OwnVyk1u9pZrDwAEJ?X-q+z`FnZh)vx#Ew)g04bsbn`?T9w(;A#|zY(Cy z$8*f7Xl$L35O9-Q7~X6`tAooH_n@!(VH0QqKyYBVbu5GyZUK6`2XNJ**t`R<QTD?- z=xb%g^6Xf+xfN07kumMCZTLNfxrP?+jo3UDrUii2;fg1vA}}M&PtiylfY(0+&*b~B zUtA1%fD0URY7vYu)Qz-nm@ihMguMZloOWFffvk@0H^vk{Mcx43GtPs8G*4WYkTiaP z1~;145C;Rq)1iQR@DSBvvWAe8XO>QpiklB5<7I4sD}l3IdT%hv0vBR=O-+DdlH0?F zzNqw3JT%f^D@k3u777grL3RN#0)3}}EGQP`&za?PC18PM;srbgP&bq{HwRz|?xMY{ ztfKN7kAxha_|Yao)*t5F0A^3Y;`}!*44V{qy?E`_+qd7{W?DY){<pAOr<&n=f`Oi9 zJHx^CT0Y0^>>%K0TJwEJW|_r~BTs)Z9pnmdUQR#$<cQGHqS(*I=H^ngnX;HdkX!>( zl7WZ{C7}&bU=dRlMBA5tP9Qb%0(64dxUkJUg1|q`vPUvKBf}nnp^iZtCOhrAiQXU1 z1jMBrIdI?(95%YR?KFSliaAd7&1S7zuU+fZtp(Ih*am2gFbJ0ne`v$>2&_L8EfENE zLT?5+FHAD(6^g3CJVHp;z_W*%>OL$`J9FI6%2X&>SmLd^p%g|?E%u~4x}^+Um61_V z1o)#MA*?6>dGlN=7Pn|*9EQpf@$%uYNUff~`xX}6h{#AKV?iSmbqGX|q6bD6oX8Pq z)eTT9doo<f3Mq)DZTLazE$o%u9GBn2b$lCS2YLlEn3ExNkv0~qtQ6tZz>;_!XUyUm z0SleSW`Eu0fprPL8x77%a0-O5g!ce^;=9&A)z#G~KqYa?U>L^`fWJ>ru8Bj}f$NSa zuTWgQ4l%{U_%G=1B0xbR*FMdpIyYCG9D!Ew@Bf^}akJgEjH1Idt>9DH_lFEkro)@Z zFvk(UJ1kICc$I)_lP;Wjtu1H_A$@d^dGsrw<S@Ivvm4uwy-h;y2(63@KLf84rB!#% z5;{;4O$UwlGdw_|po32ZsYchQG=Cx=_=tdjn(P{_5SS$ic}yM*CjP)B0C;;NZY0Qf zD3JEX47TC$y00%iF<SzGZ?M=9LJo`FbvR$yAU>NJEoTI37_eI3keiR@2MoE-YIz9X zzM~6e2_XE#Ei0>NS~Xyn1Zo7-xn01H)g4YXK!FEPwF31^MN=mEbYFRdn8Cu?TPCHM zCsCB(dp-*YC<Xs;8RjFLi?|mnLP*35>bJY7qn|Neu0-feTfPo}JwpAUqDIB^C_J1I z9WK{+5FZcEE7bp)<{hes2f`q|M@ntTDZ8gc1&Q1^=r;{ca3O<%g`a;Hh^WIrDC6VV z0Dmqib_aqnovjhmUe!B6oo+yV@ApNHZp-^qfyd%ZCsTg7SpBM&y0lCyRQ=F-*K8i! z90m8Sz7kJ<vAK$noIH<-i5w@3Ih&F7^p2H*Q(vF3^4;8ZpW7*&OY3CyjUv&#C#@T= zSV(W(!%)*RcWP?LVE#h~<`wG~&iB*fLo8nMs)c_5*$>)S#CZ9mQE!>JxxM}T{8moC zzo-hM#xp~m4J!p8tw>wu;UV`b<Z<++RI^;A=oOdaJh;#Z>acc#!9Du_Ei|6R6<4*| zRc8y!A_t1*z1lN}Rr(o3hX3Jqb2qKFlO_bhgdD_)Vz{(c^=-f>&wxOnTtb06Ew%ZL z*z&EKT9!ClknH5Mj?1t3U~JB}fq_Txy=7(?t0SAq-+m_Ix5?JKz-Rt~Wm`oYH@e#D zXLmE!Cx=l!bXrk=>k6iu^o2lS>f-8JGR9>9n_@;DMAZ_#FC64hPJ%P=cseJI6S@q| zB}AT1V|gXi;d}m}S>zjk&#)VEI&h6MY0$7u(J-ccKo{L6Q9f_p?3Hr;E3)xxu^8g7 zWG81PL|qU%6gOkJgdr998GTV4-*<{|{s@)(?-M?#eYjG$bT#KDU7%LL=Y8F-6YWOM zT8xexddngKCkGs-4vz+C;W{&mm}u}&ZC5NB4}RXB_ZRsiyZE<^hqs5$_u8k{Jn`NI z)n{y%a(w3Ay;w<Ho{ZhXYt_D!XDhIsi=eI%u)CXK{67+l{P+io8sb+u=v%mAEgSk1 zzovSaelKrKjyftNNr1+|e{9#kzeqmh*Bk~8t3>%cg*9%%rA9r!YI$Ah&Ong_%)p>f zgoDEqc!RIM|MZ9U+sI->F|Yb_5~L4Q!e@hqt+1k?Ef`B$8mkWj*o+o7I)$Js(DZi2 z3*&p><aSE$*$l`-0R(DV%76G@&gYh8{55Qtl9vI$n-=g9%rPwsE<+xNk9)q?YtekJ z@n;$@bq$YG&3n3q^3l`n_kV|#6dz6pHa#M|^YLWvn3(z{>nG&U^Fx~MLt&Sftv%W9 zbMN-%s%w1Du*|L!^iPO$Zk~n4)&&!2J|g?4@P#E&<>P|TI_HRLI0&F4W^~{b$q$^t z{EhV}iSf>v3d`LTm56;ovmh6U>=Y<;g>Vs0NyWLykj3>jSvOi_)--oTy=Xm$ae$J) ztB+Cqnf`UoR^jOjIu3FOTO6~R@W$H@Ms0Kc=B=e(7t#2HmS+>Pg1Ql!lkqI4_iSjT z`aVjoCp3QUPj5?wk0wW;DS%<<f@j15KOZ!>-02hNr1GV5h2F*~<s1qt68chPXJl=2 zJ?-z=Akj^Jkoa93@Fu$2tX5{t+;VX$5~I(nU#d!_9F+W%d#26B4g*VBaKBOz^asG| zE3`Ebd;kpSgR%>H105hiz%=AIj^fyajQ1wM7Dyr?hd6{uRjUsO2+VpJXhPzP`}HOQ zMet{Y;X-B4o}LW6D+AIUSmxxR)ut{e1lonvk`myHKwb2}^biULmBJ(xLZEMn)S>IP zA{CLZz_Ws8i6q^l)zmREI^z6WQBLj?AOrOE7trFs!{YVwB|V%sXs)GWgi~gkK!wM{ z^of$k0ELCy&jOHYxGkQ!NNHvo`%JZi({BYoNc#Ta;p0da;^Ui83nn69v=xx`5HuBc z1|+{!Xlt?b@F_{KE{=l?d~WgS(vlL8Gt~HZw|n<ilei8Dc@cD35IC-hcz$%uJ!t%& zS6nG_5k^7aFQ|HeiHQ<-1QEsIo8yLLIWkbast+g}9BgthfkDS+P~pdhw6KkLaJWL! zQ;V`yT1G}pODi0=#*<KOH|{4dqz_`t?^aT(pVR9QTf52bW9f|>uUaT+Qgogy-NZY0 zuOTGJgOxpud+yI#6n!aqEwYqLyQkt+bMgD%9$lW_V#8fNmX@wXWCq#{aJvR*yNO*K zkVhJ@HWbjMsLeLe&~T@5fVu+?=>}UB(F0t4cfny)-as?Yb~B6_=rGU%y>?w$CQcz# z(mUS_K#_|VNwH^7;Gz51CCaO+^r0{V_e|md@NYq)t}bk@WwIY&KY-Q;-klmCkU0cR zo(A|syh&soV8M~}It=jf;NTV%q{a1!f)J*`2uSa6Gw-xdA=y4~{E(S6<d&yE%_fK- z0ob<m;g2Q&u+kk4eCfJ%>yYOH!5Ks@L1>eKjZuK`dWCWsKHk-EDIDsUgB;^|P*6EU z*Fmtf5H$cA*D>?_?q;L|VPe`QLY=|eM#9R#X>L!!y3xV386s#wiwa5bN37S+RaI+9 ztpC?S=PM3}r>3TGhaTYJxtPyGt+h#_jtYA)`gn^oXQkRa93l@Du5p@4PVEXk^!F6l z%DCt3p67qgxX?Y>ppfilnCLvtn7eb=MHQ8EsPa(NqedngZag?#T&w2iKj1uv9|5B7 zx2@?oLV@_L`qp7lM3xZfS*RFRN_pW6eTd@$p)^rb4jLMCg@uK{5#(&h{Rd_x2w)xj z)S5pRke_FOkSI7C=<Xmxe?P!-JpGH8I0XkrV9X(_1Z^!Vd%x@XOUlZ6_^TMc;S(Gz z3t1un%0NTUU2CGBq@#Goiy{w^0=tLg%b~2@Eu@BD0m2;$05(2>fq}ouphQQFK&h>x z^8r$L3PPhp^M|-J;t{04Z-zla6j1nVKrsP~@pZb^Si<0-aS*LB9UUFfsug(6b`U+v zv141YM6nVPPn^kyNDj8~Ty%Lv<PEf8RUBO9Kq6VtM?mOf37p$@tc$a6ydJ#;AYLe_ z&|7eK9dP6B5!SSZI!1hGu(>HxD@Eaa8skHqNl8M9ZC&$feY~Qw{t3FcYf^h~Nq<Q) zD&lrfZSTM>w+cNVWJTrs1bBHJ^m1P!FiCpFg<n9)q{0)!s!K~H`)z<WqKY{}Wept! zc7O=bWaPT$PZyvb9vhPeY``{cL!d#^=JV8V#9WS@myvM?w1mHoLo8;e%F){eCjkVa zITY`2MM%hs>Z}q(4s3C2(SaOVx^nFr0|I}c8O6GUK;|yW2AsaKm$Gos;F){?W{x(n z4#8{4&7<ag=^QxjLJy_^#Ht=p8ml7IJiUFpiq>f0+fct*MY<%SwZLb9*s2_V3le(? z%_B}uXj};CwD@35`Y(K@xy|Xgr)U;~?%1m{tG6TvsMnhuk4$+HP59UK5|6z-zW3HG zEv7Yll*+%{(_YtLIMQ4DXSpU^2qS$z)!+I+1C7YCmtd0S=3n{Vyc-RT42+EAR<t0n z2mB8^&FYOCHzLvR%FjPoJKc`q`bI|20ab*AT=g;MmQ_%wIW_i6T}ewyQt}64w}E2f zD*A}H3A_f3B-HvjF*-V$hExRHGep;lN}ojAgFF{$y}ieCa`?&cQDoXca@3W(8K674 zWZ>3F#WABBe(HPA6QErHY9pDtrk!Q~t`C<p@+m>PBuAxEtBmM5H_<(t;FGuZB$Wl| zx?g+(YU{2y)xEk>>k_Hlbp}ok+}Yc<Z$D(Uh+-0YS(j8^7}2N;_5)Rga3lJ`JQg(Z z@*!aDB5cASac07&+>?{J0Ney@SbXrm)_*@i7#yag4cUwkdFkHQnQyP$--@&PwLEoK z6R-th;fFt#=uPpw0@>?a@8a%;C6H)*iTVbGyEcwSf{;QfSeR|Z$|*lH+xBB3WoU4a z=r@1?M(ee;wZROYP;5&KcbMZLt%ajt$F5xxC~+{=??2_stHeal^mJZyro?`WOVLAM z^>>ehhJ@*+cvwwe_c3CXm0^25s*uJ;c6S9rhCfg;{+da(Bh<z4{g<&4rP}RSedM-> z_5&f0x)|nxe5u`d8#p<k<6=f0ftznj;Gg@Ukw54wfwZ3lzD-bifQC@Fw`Q6=_p=(E zo`Ls;7Zb^FmjPVtST`DuzzPJhke9p{d>&-6C1Aqfy@qq?KCII?n4s<;)y(zl>_8XR zvhqFO{2u%otP${K$N;w^y*89Kx=@FfJf(S$V?uV<SC#+Ycmt1W#6Mf(1DL;$!<>tw zl*H9S)q@7Wt!QO=A+#(Ha4flX@lzXswIMnc1c2|YZw0bfuF#{aH|bdqh}ZVlR~dAp z?xU=q&*I9J!a+(9AS|pc3_=kR5qJJSz>E0X%3BNf9!i}FngCvhx(aRRZn!eH*D`?o zgw2lA`l^D$Q}h+&^PvI%-Q3_QDgonEy@Dta#h~uxO<7p@OUVE=L^^*Au1o^w0RE`x z<+$BVvIw9J$6@9)+iABlB)oA&Rz^mz`+EZvITWEn8fI68)%vFk3gylnAp!{Ti$xZv z<#lRj<VR*Yic>0^E6qf->oOR3>jt#bLtbZzln}_STM>bQ208|cF5=<D8h#ZUTZa8> zaLcbCNx<L4%*+$T9vClufLz!MU??jQhVbOCgpf#eaii#7zb=bqFM4?eCE53$p8Fu# zhz*DI<EAZJFywq_A8-hG4=M<mQ&)$AWdFK#j}D2Jfs`SI9rT#xz$D@jZGj4nV@4A{ z@H`@MiS_{7%))F82m{2o0U}awf#iS(L~5ZsJ{a(*QV4-iARoi1T9fQAt6>Agf=dI! z2d(HmD67br!cU)i3cT#F)`(UUz!EMsD2q0eG<mED4~|--ZJ}BOZzzU$4ly**4iyIh zd5FO=l74$!H%!#^Dd1#q{yqT#HAo_XH5<;avsl$oS`rF4_#m6pRebNGJEwMag<aK@ z30g0D5!!^ctjbEI+!E*^pujf5b3z?cSZw97F|pt}n&ap$>ILQ9khU-Vna@+`L^adT zeGr9J@uyEwpL<`DS{I+&!e8`<`G12}9|GC=4JY8rNSGY)WqBl?&KeP!4}W^GreHp< zLiB8GTk1+?7u)fGR40FK3L%@fFNROgh?WOt{lhMXT5o?TTk2Zx{bSa@$ZDa~e0$IK z_zUr?+ifz_20Oi8oj4oanYe5kyQHYMa&6i1#n^XqK8v=+f)<~E;D;ukvPudylN|Ur z$!`i0v7ocD{^{f+dK|b<kbt@7a_yJ>{V(ilR|FkI4?hiy5#AVzccqxr{`S~0KJ6c# z8wQ&jb`|jyL<8Nts3{USwz=qOzGqstlT2;WV{u9~o;j@-jj?Q1Arnl?QWI9p{5RU~ zsZR#h$6Sj|dBgZ4@{|2O*GD1+93^MRebB$&gxeJAwKx?vSA!2YP=k;w1t{%Lge&uk z(jOuv8w;^hm`lgm=P?#Kd#390!V`=jlPl)~^&>4S8wo@dhCfJ{1Go>Rs)k_gr5e{A zI<S)B28<GK9{0&E$XMRFED0ffmekun#OB3qemyJl%Bg-VqSg5{Y>$H0`x|p7gYp@{ z8lQHVd-2Tl;K<k9jYUzeN@+?ZDHCnCeHR}nODS%%;eH|<Ya5jqxmePvhh=su``79- zXXVaPC+-477dW>tUfk_HFy-Dbvf=Ae$yx>~F;?|H_Uo@j<aPW;nI*kKr<%B?6Qf$T zGC5y6pm=&GqYT1)THjeRp;#gFn$hKxAVlZ~&S^*NEZ)Q;Hg~gduzDjA-vOURp4UNu z1~>?L5VbILmKb*llMeEc+`0%_+SATrfcyhwTI|H;!0A971`t$Zn#d)gGE8#C4RrO| z%F<DDB>O-ji$Zc7&K8+62*wprphKZdZS%8{IQ5X`RNjbC7#FB;;24NPJ&0HVv0%#p z)HC=nz*Z?pRzjik%980E`a@`hHlwuVZe%2d0nTm6904p#MoD^+NF@q{X9U#9$;f<1 zKNX1>781q{_!Tv9jd)){yao0r*Yt<-=PNZ&*iWWlZim~ig=>FEPJMk9=jU!0$8z7r zXlFFW>n5@jwjl2t@)DA7q^HNkqA%>aoL?32gDQs^20j8;LdV3q^S1J(OA<J?fs>KY zEfhv_I7`SRW<bTOCSp~*X^%_(G{QrHjdksNY;~}`;<zlZKOwJSVcFybX*Ui*QgEA^ zZtOiGn1llk501df#g5B#RNb3;9$8MtX|#lVd#S^lGCkH11SLmNZUo)?#PSTdg<m~% z*c?+ES{gw4Qhi}S!+~aG^O^Jdo4P#mMP)mJ>G=m{8HVSb%Nz<#epjzcsm;6}uhRWz zbolqxF?Sa$=Xk~%DyM=EHXdD_uM5?eiwvyCIv76Fd`#BNm0T=kUa$2In{8ygqqwTv z&C1-o&Dj^5?a89aM=9vCkhHRnH~}6px@be?118|YAEzVehSQvO<=r#X*N3yXnp82g z^w9e=;K)Lmy$1Xxt}5tBKOmDWw+4b4bUi!h=qg|gA@OSH(pardp}RW^6B<d^0hMrs zjm;hZXn>`3d47PA;^}^2*%GVassCvKpi_bZZ#AYql4xkWaWaw?dWyTi0#KzWx!lJK z;KhH7e+o22+sH`mO`8$60htx2B=}c+gd&w>QzPyNbs))Z178He{o?aq^w2^F6(>M< zjFW)`mLY>^bMLxC{(gS?KVdLF34sE5c!GQo8O$S;0L1VQrVA=nQH1%10ER;abR}*H zkcT!<dotb=Sy5SLZHK0%a2ySG39nG%T4Kj%#SKa*et`TZ!Nca{<baL5iLrG;)`Ra+ zH^i%@gz-Iv<)0ANBq#>se2GsR?VCrzK#Kf%v&hV-fw23lriPnj^7B@Tir<~vzZ_cR z*gQBMMQb2HjVT#bLfM}_8Eun1JTG`FSu5wDRp-)+ll#hq>N;PqOB&Apo>$myX}xkW z$jVSYOkP=@GB;~P@@Ip}NKr;?o(#=?QHS#x8r3FmOkiJIJZx2+Y1x4PbLQpR7<~gn z%gIuCm(2a2yqonN=k8$%4MZWkqIyI7_Sm+}<nD+2lQ=DddOzNOu#UH{;P&!8t!u6Z zcjnJ{PM;s_sCzQqHC$d%#hxyD{d!|@=6$vphVv#JX`G!4myfmCPZZTE>5p6q4D|`S zyI^L=A@5jU5_i-pwwes3;A`TGh>uw{u)2ma)um*tJ}tA+)oxH|&rMCC<)0`0vX(#p zW({$<75OraDrw4d$h`RxRm}}%;m<3dikmj7nIw0L3%T}Lh+xu2&V_Dc6H^)Ih9ors zmn#J?-ZV{Go#<K`PPXn!qR*yXSz433*QP`|mZekP;Ty00?peJbudQbOj6VN0<TB;# zvd|}Ro<57^FmH^CxywtD+sfB~agNlCY8xsnZ|jldf5BtV_bU6oMC|(TL?F@`C~cse zC%JG~_jATW9H`6--@MnrNKPbkp&;D_Wk*f8s5h`bP}}GQJdm<i6JERWjYRU0ji939 z=cio{#V|xvHvyE0iv`uo0+2(+atE6R%|Z#x$%w8w#Lh0O5%UgB7u&^N)O3Kdi029= zSsIkgJ9q5@b)3fMZ-?g6bH8w|ia91t`|{mCub@C8xq!LS^5{OujCu<62|PFn{emV7 z9`#zFGItk))Nts{>o1Ur?ATLS%GN&{-BVLtjkx%c7I|R_$9BEKS=6V9*?0i)=&IkJ zXheTc4Jjk-Cp9bUFh~s?^S)tWT*?m-N(KSOcR(FVTSo`$77v_*{+YB7&qUpvaRW8N z^p0-<YPSZSB*<QRfs!C#PlR9`_%5rD!7+}Bl{g?jU_t{}9GRZiuU=go!893Qa&_g2 zEcB|?2m6~>ok$K_xKwlg=#zUtitPDTd#{_YiyYaaJe-{}pfo3OV&qS_^@#0n^R;&L zRdq{JGM<Xe$C;kftKGNUH(dONC4ssL2ufM)7E!DAl})Ic8{^a|d$Q(Nbsb+eGN{RO zI^k+M%@xqk!j+TBC>X{v8MyTp=YxUTupO?VVa^@1$1}^aPrJMj{WLS5F>u+A@ngTu zmGwnCYAQzp&$&r`AIkC^@Al@N4w$vi$*C_&Xh~dFMMrKl`6;5#Oufke-HVgH`<c`9 zCbt+W+Gn3uj!qI?b~)Xx?p)^MEM>oS<&NjnCf!4NzdOHG&MrxgPld;-Bx&xN+~2R% z*3I`Ns=$6hK=c$fb^Rm%d!5Z6#~$b%|MKvC&s}C==|e}4dpvlzW@di$lkH<pD2P~9 zY67OOw0+C)$!z3<j7hesKmF<0u+4^hzYWJN(?U+B2VTs2-a5coy!^c7Tr3)JCzk&S zqt|b{?3e+JIvwIRq8<Wl4QD+?1fWdx6twt-kfM2DH0CyFX*6D+YieTfM{p|A0HxBv z@B>BBC6ySGS}J`!g<KvOuLd;tEDq*3JDvqhQ>|TF|LW{I5<^B3T^HxlGAH$92yu%d z3h*JjO$THvcO!LOatrPiV&In^($5fZDp$$WfL0J`<VGz5IVn!YT=MBP0BaDW^8_~s zKIT~<KPd5J<T#%D`sxSFu7>Pi57SUUIHDBvt2v5gzJ&yPOc<#ENq$Lm4UC#8S98Qs z_G7=2f*drM<)0}N@V!Gww?et{=Vw=<uI>)?!iqgnGfab`RcT*_>D6jW49{ti?%Zxh z&DhxFOO~F?kL9BSDPNNOshrzq4cgxgC0;8GC=y>5f1Cg2yvth&tJAlaM+6_1J`5>v zewg5+HYmX7nw@<~PHhM6eimh^`uogWVL8PS-zn#98!u2e%v37=+DE%zM*Qlsqjam5 z4zsAj+6c$){UurRt;>2L!a<DB=*RSH%ZHb5C(oXHo6lbE{aihR!*%0)x7wltY6^_o z)lqzZ<3_Qj(Ju?DRqt;8Ih5x-yOaIYe4Sph=DP}<q&c^~v#%;!eL3y6+fJ3?_KeCa z{soR5fHaFXyg&BRqHRa&OMAI~p9#f^Q(jk0hDB3*DB{`TS=o-Jjeb#0mel;7&F>K% z(Se;7-yJ7XxsrXJJ<q5p>>a1pg^60X0q)kU4{CII59R47zRf;jq#dG-3s5sy;1N~m zrY^2)y7*?y_P3T>#UHDl@XJ$a+L=o<Dyj)r-<$}vaneeW+xozD7sqt|WV<JyN7&N4 z-C<RU^Ku5Ze|GAJLO~SN5cBvcm_(25+qQ|pokPf@OJ<tTs&YQxk)-YT9fuu>cM=~E zb((tyREh$zn@m)p6iBVV5OX3wH#!ZjGasAPL_dc!8ptN`)RCSXtUZy+k%Y318$Ebu zKP9}}157v#+W}MtQH%ihFa)}xbPC5@1rBWwGb1yz5u+x7lpE@ZArnLGY@i9IG{}## z2J+S1B2<tUawduxt3Y$yM-Pjm2YpH<M~)M}*3}PO5-c*ZvWJcyb!9`%lt*0<78rP> zPuUb!J7}bd+WT{N=aeZyzbC&=-e=<s=Jz{sXsfKz{7OJtjj8ZDJH}qIMaoE~NcX3Y zmuKgj;>x+@`s>oJNTCzVaS;@)?SA1dDl4~YG^Rbxb-@J{>q*POHPdtzYwHDYWcnp+ zdLNlhb%Sdo6f^xJ<yKCn$Ne_Oa<9ISrX*1%^!BJos_nOv6mN%{OS{VVQ&ejN7nhY< z%`Y0TQ8?dhIY{wm^N`f1!<$#_J*~TSI3NYWmb+04Pj#0%wS&^RLkd~n_FSs=4E~<= zRi64-K-LIl-j>xDiY_`E=G2O#9LMt5j#f%kb>(6|&r$QGa_<S(Z51v-)oGL%z*t^_ zPv*LRysxj)tdZX|Ah$-|R3O7-wEtrfD=YiN{UsWTsr{|%GHMJ}5-xhha0X8-wpj(K zUoZFGzbCh=uIcVY+bB)JSZi~x%DF<#zys@V&VMwSnKS>mGALrQ<DUAVJAc8}v1&CQ zy`*nvFW&{IL-{32iUx-!^-{L|q~TBqjGdJn0`5<quxEv-iC#s~n*Ld+5<rQ5sHNq6 zjtR*i1@s7hxF1>)vY>!0Zr`=*Qa`Ouipxp%*8B)GFvQD+YMKH!vqAgNuV0$_BucZa zHrWE|#q;1u?k+~hq;S*V1MC5a5$n3LjA9b2ma^A^?^$p#dnjbbiiU=V?^r9SBc4Z2 z&Ikwn<zD9&h&fmG{HP2>e84m7Q9zW1xhx6~tLb>l>^bVPXLdNb&?}ET@~|7rzVJ$y zD(2n!QqjRRWy$*tuzqv|rjKZ3R@CNAvkq<BqQ_wGH}$x6(ACOQelP#Uifc^`nXyw} zJ6-$sA)Ed>uNeQGH%$rkG0vtGHDMLu&bC8lE@u)<Yq?msC^?PK>QT1!<wupRybm@{ zu(Y%~eDt_nMRt}+_2Kx?wHnp0)m76{-exfK`0$*2C_#~bsf}ut>~q`0yrQP(+XIAK zzIQZsJ6^C*sfZ`yP{xE6?bl7xRl%7?p)C%dE2C^X+sCFxR8rJK4OXs5-Y=bZJ!#U` z{&-|$--+aQ>6U;_<7-9oK9%>invyfjw!Y;v=NkU^#=rIdeDBWWICR~lL}cZFVYL>! zkOV6;6Vus0J-5`+7{TvSrYpJwTbu_HB8*TTSFe6HR2Q`lHO>5w@VhukQ1cWE+&|#Q z(mcW6m5CHMialIc4?tuKT>AO5XYYZak(?u-S0wt6K(iRygM_bID7F5BsN5=!O5b(` zu}>0cz&jKz9e$ab2d5e7e9;y_4@^Q7fgqFKACVtO1-J*V0|295kWD&hv9N+3!bBLE zl2QxdGEuOjSLLmwg+1c1axLm8A}T`h<Uiry|N4Ndf`&$LqX1ZUlKKb+R0$$S;Jn09 zhr4dyDci(GXELAxCT|%<MI+pwXzA~QwLsb)CN}h6N}2IO5&=e*e1o8d1XCk}iMXh7 zbso(qCpp@Xr4x@K<a{uBT!y~{ALyh|Keyc*(8qv^4IE~O0u}c<%1Nk!9^&bwLD>z; zDcbf%N7iNtlV$)mU{#~SJq*+Y8d?Z-KjV}Iq@V+)%&o$8$$l<m<dk)A!8*z|N7iR| z?k5=BdQE!kWoID|yAfV#=V12gt#2;n?Zy!mgLPs(+~nnc{BXMMyhcEtqtEi8&~Fi2 zcls|crG1HUrVnC?q$)JItk^&NBkSFmjzRhS`;Avc9D`6PB`>X<f9D%;SA6;WVG6<d zA1gD&(QOiB$F%gYu{mCOZYfFpyMC$jy4k*#UBAvO^Nx3X$flhQ6=qXZy1ZCvfqr}` zse<cvxBU{$$q8zMmAs~oMhku2;#ZUF1TzPY-7Q<aKF9XCkxA`I`(EZF-XZ#3zV@5; z^Ter4YfSwr%|6jBtCe8-ChCgfo_V!WowoyY`Frf6;^H1ljNPyvY~LntE-y*LF47WP zVOlF?(*89g+#sk_E&7Gg#jS<Am!)PKs_SL@U(}Xf(-lzs?)Cq47tykzO1lyNa0DGa za8Wj`%-=&$OKt<yiA5!8U8?pPyeQ;u*Q_6`Pb?_FEmV)Z23RGfk*R0W7`M&t*Vlu` zkCSYr`lKrY=spm9VgUGrlzp%xLqqiC%NJG^O(J&xHy8w>hgCa;6rqg-2mGzKx0vYc zka2*p54h4EK{qX9pMyU64m_g}5TU>$W^`b%cNfq7p%NVg73%KoJwRfSp@LJow*o96 z72yPs#>(!xtgKoS&Od=<X9}qGN+Fg3{tpS?Is|HxL}Z-aC_5$3o~4AONNF0B*FJ=s zVAgv$T)`O-kl`<g7<WUe1a+1oTteVEOFBD4!Q$6pssISE2l!YdK@K4tyyjmT7e!}& zolcHfbp19r9J5{4P5PDY1?hvPlIfD-%^&<m_?mqVI@;>-rR&o(Kj69LwTZTz*VCDo z)>JE9#qgL?n{tup9~;BubCLl+mz+*JGzt$`)y#*?3eGk<&#IqShs=S3%(?;pO_KO= zwWLE0SYKa%8}n<~{huISQ6S9uGxnKGA^;B$h#khKTeN!cRDtc2xH1zHldBLGFfcGg zdrSb87epZgOTQt~E-*{x2|%0UH@=>P2oUqrs%AnapR}5eH~z`o<L+L*m+ogn-KFiu zIYVZR>T9hT*T3)0c|N$jq8DNCMq?>_O^leObA{j1z>=VG<AI;2nw2$UnPZ<pPut@z ztC4A5lbO~)YzIdvRVU`^^fo=A&rhdky<@<#QJ1Uq=D4>9Top%U629#kJ~=-u!+zPn z%qX2ES~l=Ujh3BpbBy5Gvc>1dE>pj^fj*(z@G1~=R;1x*uv7bpvh8B({W*Ht9sD~R zxC45euc&7B&NdWT{e01{-5u*vEi{)M@H70+*C$18V&k4SdS-Wwnff_C?^zl9u_Aqh zVYXJN-qlzXLr=;Ie@ibK%{`y9J9NsW)u*ReqdzesC7sQ92L%_y)faipJ%TnrKD!I# z9<kCG$Sx=p-??+3Rx-`D?@)BYK;aG^I(I&qH+@g3RWGr=nY_Asd^d@)1Y2Bxv`1bT z(%25ye&<BcR7mxg2}{UkJ&Cr7Yu1R1HPzK0<S;L+gwO}SgNg?A+<62pL11&V=OHJQ z?Kn~~x9>hI5F{dc`{k7rV3eSYx)0$2Rzw688fF~<HW!GDC<C$|f?gwO6dM<l_QG*! z>Y4<NOL-%p1lfaRR33KuBuH)mp`UR1@N0ai#@u5?k$J+V;q(n%+4rc5%NKS+%*#1N z68^z$Bckap*k4%58bN+S!^4(wzfi_3fLlueUxA#Y7^E2>xs+N|w8(BZJ+v>dX%oRZ zv^h<K_vp56J#+c;URXFu?gzqT@GTjTgiuQmFt|6AY@s5;hauyaK}S7;$%Wi%@UpD1 znKb3vm|{SqJAOVHj|>?&HuK+LBt%t1=|NNi@HX^;FoK8_RaJB`uBlh>W*viN60SE1 zNa%1!-5wQm_4M^+G(CKgqiV1-ilIZi@Zfl<OHkeMz@|4><Y<eX!@32WvTyIYcqHm1 zqjT73_sZu@-9cw0hr{V+_~W!M-F<S2eRXP;EA8yBuAcIqBZ(_Fgr|=RM2Pu6Ty*%| zvX7zT%jaj3^1Qyr!-bX7R|TvrC8cJY+;ggEIa0I=Y9ezy-YpH7AF`E5HU4!7|9IB6 zLnz~_u{7(R+H1_5oL+j5yn9qK*fzi6(;v@g!2m;@f{zuoG>oU6qMWmBEH>(&uR3No zaW(mB;XQ|p;LQnV(+?+C1hI>9FFWwWdWRZT=5ns7qW*khZOW;_+HXA8H-A2!9niXY zba{7)*AqRhGKG1{@-Lr{RTi$CpPLlzRt)O=&K*(^;n_4hT191%=^fH6n4O%mb-t}O zFRS5|+x_|`t82y&nCrWICR8sOx4o&pazwrHIzQIoD-;nh?6W;r@5BHFXmnf_$EBhd z@nQzyK!Gs)c3)NynGFRb7cWf*uNQtD*&d4yoYDI8o5(^XY3N7`3LYP^u|>B1ahF;o zECq8H&aFlq4+(~8e;cg#2)%nS(0?LALcQ}53noK&79kbjI9_|&IboV04natSDPg>O z_wF5;XAON|l9|=7uYyF2fRmEAgdt^f8Y<keeLEo-al)5D5)P*I`qU4iP9i~sU>)J6 zdxD>3QrvHocHgs0n9P8Ikc{A7$aFZ^VbzJB13&8l%&vkm%@T0)zH3Dg&Hxx8za@Y+ zTp(J4D-Zxca<>EfC)N*WL{4dX$iAM1v7%Vet(j(R+o7p3@O_8uB{CKd^sj&IUW}qw zjvn~%AsEABsP(J1%om!2o~;#G*n|oQmMdnvXT^tWkrCMcy}`Qqk!1d)UT6L`x#C}p z8#?!S2GudCR+~R^GxuJ-ZQxrR!|C%4k?uAFG^&r7);&GeYh@Ab(dR*OGT;BCl0DSZ z%3Zeit0}nq(-P527p3!_rJG~!yFV?<5ziNo9Wv;Yk1HQi7;_#f%ywO2!hB{m&2`CI z_mnrrx-LKKbfg%oJ+8rIzR30<_^m1MJ%#WZXRY?_6^Z$@q9bkhUZowh8~Yk9=aIZ< zb5!r%4u)$CVZzmWKh>2sKfKiK`UrnsE>q4I=gV{aXAyXXV1Atw^r{9<t?NY#U*6hP z(RaJsn^8C>)>M%zEbnz%?Gt|tl@iojeA`DK*wEVMQ(UyeWO%S*$uXF-k^NG9@e0=x zH`A*59--?k3I07j2?7a0$8;mpQ>vO1DB6YE6X$jK-wwB|jc_|FxBZ5p=M_Wsa^KKL zpWf|vynkXduL!HdmMQI#%(RsJz|zgr24ys-_yU6bc)rhsXRq3OykuR<LCrV;3!{&Z zH&BcS<$6#DtXTI|nS@&HzdzzVeUKqP)jv|h<aT@BuE$25ZwI|j%QnPb=#!b)IK!|a z;>=^YJJ+6*^QzE=uNQKg3rwS*oQ_VNaa|eA#O)10%M-*oPJBX1BA)~D@bkcvkcMQ0 zm_0HM3`cSUstvF)XMqH3<&FO5BmK|#4j13*w;I)u`o(L!Wd|zU-=HW;vL)!;-(oM; zqf2#J7!?5cCV{|Y-1hx|c8QA`av0)w6&GI(eSZn0dq8H9w{;fVmDnB0af&XJcn{Fp z?%28W99V4rnk8@`PAf~ZAc_dk4c7#5jFa#Ua^7laq=Wt=sT&|K2s4PAK@Wr88!_Ke zAyi1m+&nb8QUhl(b}I2;BSe5G`;ez4EiZ2X=?v&sGJlYmpe80F5$Q?v8?gKk;wK=W z*;KCuHv!Zudm*s^dqeIEDCrUD66v{XNmth#KaZsN<CjLi?5UCD(l?z49v8Y*;(Nm( zg+Y~Kh~<YLehn2BnfHKM79S98h|uQX?aF{H4cdl(`9Pj=40?U?(7^4?!#jF0p^5m1 zVR7cgjRKm~@3#9rF=)9o68l!0BXs!3wP3*^x#r}n>+jguoC<cm&M9VJODD*$JMjHj z^P1%E**h*4cRyD1Rk5`h89ZL>L9M!y*_zn>PUNMnx&HSMi4bp*+E43f;`P<sZ-jLk zcTZRQt_j+)>$LO4&ig;s%5Kb3yBM#M_%;9D*zTl5yr+GS2Buu7c+I>yly1Cauf-;> z;~ISa9G85}7sTg*q=uY!cp}=f@><-VI@89&qwG#CTZ|Tmlue@9lKaHutZC6erHoep z?QRE&smn!r!pjXy^DGm4!i42Y;th6?S@RZwE_7u)>^j90&-$t+UzWLL>^a7!C+?BO z8K+!y;;hJQGwaKS9=nk)pY1GiThk4h0=cjDkDYSW(>Hq{zu9!H=iW0(xo`cP6+#94 z+jbZ|-x~P4K5g>WN4wIKnVBm4S{<JqlJNL#lA$=S>nV`*GpnE6rB`zHW2>cnl~NcE zxzeKcIRDpT8*7$x(bF&0rY67Y!*W(7@HgB2<U0Pne-|A4dJr_8JY}@Vlz<x^lF*lg zPedRY#yiP_tR@McSkn-OT!hkSpE>=$eMHt0?i|3QY6$x#X@s#)qM^e{S<(&-l7{1$ zBnN}uB&;OMHZ>S3G5z)<0O)z}er=g1JHQ|ki$CbbGL%)9(|h}p+%dW2K~~lY4Gj&f zUf<y0H_Uh2v#O1kt|e*kBh#xDBIxIb4VZ_*Pur6*2B7=8<YpJJOLdT)j2_+p6%!fS zg^vb~@inLxat9%8i?u)-$8|9*_)sl;{QQ|!D$Nvwv2X}b@@m+Ts{uOQz{AZHWF#r} zKhLpapX;VO@F{WO8KNteJq-ib`0Q-$+l685N8}ja1A86`R0xY2-ZkXbtwS#W9V4qs z+%sgAVtAuyNNA{4fy5Pk{a(jYs+#ra#{)~rG{%bi*YdT%5y@88%(4x+=mxC3Xff@5 zU~j)sBXj6X&P2fJqSFl0J|0tH{s|YJ%2?g%@_l{%?Hi5_nu`^@$0-e2lUmZFEFx+x zS`>`(y5ik-xPBFSX^;c8*xAB;RnNHB72R5qWbyp{a%{GOcWudLO$X2O*MxKHo1%tY z+XeKkqe4#_MV6*Fl({;ZsCQ`auUAc}PG2;bt{fVFnQ1Kke7C&jmE^Pxrc*SJOgIv> z)tCL#?0sJO9L(5VQ<1)CJrcji*N9=Twcyj07QrJtw>#4we6B2gI#`)i{ej}0-Rx^O zkFUe_O(+ZClF)&`7rJx_y=xeP=NDdvN*e?j>JGKs-hvw)P>wIp7Oj|grX(uGYYHvO zDqKU7l$Ogp)mx2FgHF`#NLqTrdoW}|K&!x^?QpZEAK)KM%0VQ3K<NaUT7&tkmoQ1i zY!<_^NqKVf)-4B<s#O>aQw1M4Je&v60E1j-HMm8WSEZ^;0bN_g7qK?vCaC#d7tEOf zKi!z3rKNYxP>DbL7m+>ypW1@2KM$pOT1LjFy1I*peObQ|83-V4FS_!opB7BrhntZS zwN$@uqyyfsVz6i+=&x5%%yK>T57b|tjYUaEmqwbu3GF>&v%&e?j&>{3gZ*zuI^Twl zIx6Zaf6`QMN%wMEPcp%mJ$7?3#C3m`$#rm9vt?{!Ps@_%_-J<ba_s2wBZc62Xv3W* zWU~5zi`||Xp(U~cY)5qb1-9f7LcFnDnqd#Mm0?~ojBuo<tqIN8V&K!3J0y|w=lZ^& z|7ij0LeJx*4YFJ)X*E!*#jVDR`jcar><&CtlIsl_4y3)FX!A*UICN6othTu8T%soD z>^N@S8U^|_Q-8U4>Q7(qUiA1F`tAc+$Rxzv#2ksIR1CWvLQMPx>{spDu_MYek83&4 z>E&feh8&C*l9gj?$ZpUzGfUh?kB<_g5XyeHyzEBT<7mB&>wJG5(bep_*G2hI?||#v zMnoM8&Wt68{VX`bUwYxWt3n?muFg*w|A{whoar(#J6rbo^J8cz2{eIMCjq?xnVH$` zG)ZQ~)zoD{o^wD*DEP4avdQMt|K`gfZi52XmW=u=WCj=>6K3uVf1Na#;?mdG;2%c5 z$7664x&{Vh2*6;=Shx-i?5?-=dE<7AguV}@Zfa`kg#c98f4r8(u3|m}upb#YIb~mU z=(+omdtwM_8e|V!F}4r~1-#H}zoAQ1%WA2wuednUHT^a_HTCoMT35y1XG7gBzkw=P zSbM_kiYkD_@S(0Rh7OeEiGl{fH16>LeNX;_=bjd{Hf3D3m;0J$uax5Rd~uw+xJdNa zi<c8_ily&NR`wSuU@<M7YKpSjSQB0?Etz@KL~&y294mKPc?t}lIKiZlW&wE73PVcZ z8cxo<w-K}9As|5wLjp-iaA&RP%6XuZsE-izKnANI$}bvQ4N?0OD7$+`L&x(zBf|s> zfxwfvF-icy^BLAunK9U4J^-Bl#94}krGq+zz>vg<0<#87T{A*n;lry1%9OMzs|d2D z7F!<`kq6gJ8vt_={UEqUNT4CAzlRN#PL!!-nLurcX(OfZ&l3_WgV;r=u*iHMY<wj{ z#?3S|We~~$Ni|qO&{FZ|&x3E%92r{*QA<H8bW-m0aTu4C()DF9z>!rm|0JmR@_L5} zo3c6lIwX0I-Jcpuk)#4ctVCkbpVd*FfHWO#JNYuAYt_|_EPb#?{IOY>$@bL;ixc#h zk2sGdk6u<wk<q>oSNdl|!xD?Mcz#5py?VZLK8ATdHrpk@_c8w}72egCct?w&pXwBl zb3O(34BIJ59f3}g2$E46l4219f`I*<#q=Q(4~+<M5Vq1#J!<A~>NCGkQdTCHCLSb7 zj{&*Qc7Kcp@=>f)0(YSFBNHL`OI6advPy9p;i@ABT_U<dk}uvS8O^Ylj*b+u(Nrx* z!ks?CK%!pgB#6rw6433UqM`_1!5YVAB5NFlBpEMGf_UKIw@r6*h4NGf;sWBB!CzKk z--Stp+7Lohw6?aw>{pHTtq6lQutgLT_u;bL4d@e(Zoa*>VAzy~R_`5wMsO}dZ+91C zTVd}YU}#$z+$fh3O$)XD>*(n5HluJ$T$1~UF?B!5{Xj?xyamWzAtR88=L3eFJ-fLz zJz-u&<`2O&U{b%f-E{sc)<b$#*3qsIcC>lIDVKz-F3j9Jt#fCqMJ1~8M~V&VjC4Nc zL9VrfZ6BkBIoqvo54V+tX@x0lOWMg{=D73m$#qhr-*kd^%O{?yls|HA)vezjRi9js z{%$g+dZ1=jV@h&=^pJAg1ar<D_d9#0fN%aDu<r-DO7`gpBy0Ytsh-R<9rlf_WfB$+ z#q6-?E3fBnvm8ELU+C<J#EUr0(Mw+h)PCA!@p8~UvB%l3qNDwf_Au?XuGf`czbfBJ zvUaz*khW%*`0{fu42#Hrr6;xJnCXPES8~{)F?NefZW>J-cc)G}?`QXy^}TK%f-4__ za>R6t9MZt5q)rB6nPtAEG*8iJ-<yCR1J^qEn_Db;AGEyR=Bu&;7fsiNuU?mZe|76E zX3meC+V<RlmFnPw+q?W_S;V#;@_x44RD9<J;DR9{E-xC&XpSCtXm3n?{+<l1vG~^S zbs66~#!b%A?0W0b%2PIDOU<8-tUpN21<MGIyXQR{@b?&>=F7RU##y+So3me5)vdF8 z$uF{=e1jj;`0B&flNwJM1S#&I*qL``fAIMd?~MQLmVb9^RH^>qAJsBym_cz&IpfyE zK{6?4j%K$@Pgym6nyi|lNoYV3L)V_tcRWmFgE`^RKExK4J<#Qwd71orh<>LRuqeK@ zJVL9HH58HNw9uu@yEdL&u@i>6NyyKpASq4B5nETpAT?%Lk3T3rYSyu@sx~h>V|L}p zOq<=IhqwAT{ipvuuQk1O#lg%}e+PFI=WR##*&{jiDL<d;lhfgzu1;m9coKr&yAI>s zBaw8x{c$$)H8DhAuRnP1uD`YDwl8LPo3Zx+(i#jcvr#Ai57I<5ycz*5pX>MDI~{b{ z!_#w(HWvLYDz`cB-X*37TO$v2<Sj^v3#1s}kU>$Uf8+giFY=*QRJ-0#&om|8;oI<V z>!I!o@haONTs^?q{P!cCyIWsq&%m{4xps15_tgGlAzhS@<SrDO3d<=U92I)7bspl( zfHepA{BO|mPSzbU3(u`ZFKR@)3rw86L{E8bXbawoXy#iM7C(AsVayC5&n~ne&sG2H zV;2>krhY5~FPmw4K^Z0a{IuBG@7^0%dQf_A-*oU?7;!se5=D;vWXu|-J<<+J<dul` z(gWQcmk8(jn!n9M%<`-Q&0dA^AClYm`W(d-R62UalA<v|O%XGWKU={WhyKuyTZ=gE z<j&l>`@a`E|4@FjV`4&!etB?GNUn7t&Rc%N#hpLQt;bGL?@+b^iS`*#=sZqz7FO}c zr<vfGLK8zc4&X^F2XfA7B$D-sKN@KDp~O2sFEpfXIJIdLwLgWsow#BOYeB)0n={!_ z``WVa^Ns4BmRT}VEeM|-IrL-Trk8nt)s={GjSA_Ydl&j=3fAqcUv0I+85YkHC=K~5 zka}dCAHX|S=n3^JerzlT8(~`7)%}AxWB~Esm07!9W80bE6%w*)wF;e%crA`<QL2>J z8?V3L@vS_{H6W1P=<KH$H@=E4`_~D7e^iUV;Le;!4qm|*-lb}W38$DBT|J|?N76b; z0`tw%H~FT-#tN1t``L>cpz&g%`E7T`A2dN&3wtNBp(sG~ajRW;yz=?~9)07Q-V2Kp z)=!M1wRJk8&J+utyuOa6A=6m5Q~id}LF#As_hc!l9Nnzwn7!Jvuq9Gpf>l0T-2X_y zq#jk$`fqbSl$pj2Vr2ZlxDR#bLLSC194dOw^Ka=DOAVmIEcF@;G>}wGxy!U3&lNL+ zBi~EshO>%o{lRqjn%LwE80>Jn?M5*nfxMUQavqYai<=KObqQ>CsHe~mO2J(D0vICv zy?z|(zEBvY0SW=**(D>4+@<^Q5y0QzWZniH33LFZD4s~PIR(DV!Kmds*ei_ceSo$v z6~aAu$a?#CwmJM}4w@V<!?Ce5b=JA=Pzm$amG7T=vc~SK=jpzxZ_3D#bFA>s_WOqO zY{aS#^^@6~4xS_0{%g)X_I}3PJn;fHl<$3gB`83;WpqcqMVGaaG`0wp9)Te5pi_As z9Q*`wnCowYarvTbDEDUP0>mHJbe#kMzyUK=<4s1qEzkAP-~o`DY3^r2%HK~n$LDTk zDaEptswyeLuI=r_dh@k>9~<AiUi12ZZ*e>Q(Hq9aAMIUQ8&i$qYMC|OSwEviz&_P- zj`6jE!>?Yptd_nd|JeJ&UHL@t>&V~XwtMfvxW=M$bp$-ca3N7#h&;DJHh@EhG+`}_ zFI`(kottVAjgQX&QSSkPr=L~DXtiYYZ6dy~t&YmBAAPQRl0TVg)wUm0*t?d?-pAE* zttG?e!kHUzl56#^O^ms1$<&-&H5kNH&VNvZi^|aRE#4l1i?Au&5I2hoeGLqyKFdoB zddP_b<AUsvet321IxtN4CHT*a81#i&kqj+Ix?Hm{kD-}ah(T@#(tfqk9;BU5QtcTW zd<^;m{*+>54dHe(a7MfmyaZ)nK2RLOU|oi~gGS%ljzcYZ9|iI(-*<K{b$0I6Souna z40%9fM0)&W54Qqv3O^f?-e_<C_a}33?8OK%NM7g`;GFvg6ahnLo<TQ%%xUf>F!zXy zL|#JDuyMaZNOV?O`f>U%^j(;+<_E)}q~1%w*{_u1AHzEN;lo{$2nf3mk``@#E>0di zao_-jzpTW$bH%5;o|98llXJ-Bkf|EiYxPdW!7m^0S~C4HU}KWu$6av!qqS@Qn#7N} z4)QqF8G}|g>5gJmli_t}V(XDp1>jy5{ir?SSRh@A!pJ6!t|X{9^sx%K%7{7vO+zm} z9tj`;WrPZ;3a^*-(wjzl2GNrzUy#=W^pk{#pn)N&`w(2{jJA8m6u|bb`!@vUQ~-`$ zLMK3$AqM6XK`lQ}t4D*U#mr+eQ=`wpQ}cvOBJvI4`XGs0Z#9xK^(o2x37|bjWK;v} zx~SMKXGYYKLA^=enhE+?vb*sGmdHkj^{5=;2|r%QwxUqUl4u>0$%31lm)Oth>x~;? zHlxpr0_{pPz9=L6TCw9_qg@~=@u>iJDUh4TSM%xPN6;IAcFoTGGzy7i0<hCoM>`9n z{@2`T%PVR3$WDpCUA(3IHK({!2(^kXC*lAI8o-%Mo(}-ecgUu#-=1jNk_@jg@z$e9 zAXzflxxkS@>qYsYA90A%6LtQx6S+9xSl(os4PzD;%6G&b9(P&14a`Xvh%iit7{R)S zl&81^F;wy#ig`RwO6}{{|0t}jas{~z^ns2u4Z1e}@X*ju{tD=qqf`@sM^eDFL8Yju z2w|L;hsQ?nBJa`D07N-zKui_*PY7-<##6-cMpPt(4nnWADm6R1MS36h92uZ~3y&A@ z6amy)v&`u+623x70G*8&#yht9Kul2h+_3^$epJ@w&(Gmjy8U53<Ko`EbSCFdfB6_q zxu9^eT%SxSdhl6*T1E5(`O+Jn|HXP?p?i2RadADxi>e1Uh;O&{J=Pm$1El$?P@=J# zqqqA6i7}ZvOfEk`RfK4R-K6?9H||MG27i$BXe906!7AfGf@^JF^0-BAUS5<^eEL#5 zdKtNFIeGa$SSE=2Rd)M`4VqT|ikY8H9@y+)h>$2LpHkdea#IoWyt@_w7CSbz#|$ zXKtbZR6<-mel2K*NGk>CeN`&p92gO#VFME#6ut!CDsryp(WCd8973V>zH;Pe+bcIp z291-cwk6_T(<NHcnWz-z#%jX7K1L~90h5qD_F(P~wTcHnP(ZI1EDc0cX_zF<SOigi z)5^2;LQ;>R`bxUFCd-KvyYFma(Cq3v$ivg4GY1X#G4pmln4N=8+w&5F8IK<0FLs<g z`)MD8DcUKFyE@V_-PY8J^Nq}$spKx|oy&*AonI5PBXy820$Kqo-9v~2M1U+u2QeSw zHRFIfgIEQ*;i<6zBhb@e>$buslB)A*{e}%6$c&7Xvp48*z`=k13b(*MVfG9W>Vpqh zTZJ;?&VKbWy{wi^x`29fjIr-}_4@Tf$SHa^se#>z9l2_yaXWudvR&Y8*4$@Yh^m>) zc?DI#p#=h(Dgt;OJQ@aVYW*WYDYpO`c<G6ip0waT)rFr<LBc$d$5vccW`+1n^puwi z9Nw0;>ZZWEuYe;ZV+j*JTII4$p>lyFodp}YtjxhuvI;;J*arYB?;9J*pjJpzv~ois z{vwbMdirRdn*6mvgv!MI3i17RK+%9*dhT~CgM=RdQU$0%GVCyxFSTEBaW6+@sp6#3 zLf7-fy_VFdNP5qPp`|*D(lt$l@7~Lnlpl3D<aU#}@L-ODddlx1j!``fqM+N5ikDP} z5(?6>_4-wh=kZQ%!i^t|Y<p@1PX^y@tp5S(hprOaE_%@ndIGTNn9)+lnt|NrH!ATT zan-@_|GuK)305Gk2q^eVaWsGw!5Em3p2<Hc*TeuX6UhKV5v#`2V2z0#cYx~?Q|?H> zEA<{O&(Kf?SXsWX5-S4{*&wHfCgOZ4F$F|dL16;TNl2+Jf|`iWIPdQW8N|OftSfqi z5I2HV61QcnJL!}#8#CzbL7m3yUpMwp*QooNa&}n1k9k<IMiwuB$4k1IL_};4Een&| z=drDr$UJw^qAy|?asPhCz!t#yEVw~1NQZUDCZd-EfJ@R7am)cK*Ty+Sgai;Rur0i~ zmUjZ5`rC6wkyilZKn+=<DM0kwT~Et4qQygXg5~@XbDlv)!OMFFke%O+H(0_X?H{2R zK#V^jT7w9G0f<nZQB&LVSy}<h-o$e$V5rEJiV6{xt3gjOVUfsfEDCgoO$<^C5|0Z- zAb~v)$#v7(dJXYiLBvoWuYAN{;x4Y5?<glg4p2Zafh^)}cpaqv#Gnpb3B{OB(sMbe zQ>^JaKaj2B_wNr#wag=mQy&mN5Dm&xr}6>==6`r_6Nv=;-V`wD)j<miin#)dZ`EbM zv^b+lhKL{MmEB|jtcfSp5z+vuEzpF1Y_r))VViYn&bB7-@4&b9DCdq$6t6Jj);d5X zlx7GH1NR~c3C22LVqtLu2185%EUS=vPzBdK942dkij@NL1YxeDt^FPzL_g=G%W3xx zo_$DyoQYW^GgBYpMcn;zY{y|WCz1`&bg)c)1a1mWgxD9OLFgIcm%<bopOBFAz)gP7 z&bAD)3&90OviWgvW3+^N&2WijZP(z#FU^M`sK5!Z&tEN}2_qjjVK6xcul$~wX+AlC z)wXs2{-=OrkeC#O<H89xw5l-SD{N$zINFeus^IGYQ(^yvsJEiNFAFN0xTc>1KiA?$ zzXZ#|Ucp=XSq6~5#d%t-`wbHr{@XF$Kd~Dt8C@5}pmjk`tx7<_3+-Ll+!IF~JJ_dw zwLSIe5>8D~zl6RK<<+XVdaGcu`Nzb4c?Wu4*Hj@4H4MarEPYzNP2YfN0*+;t;w%(7 z@h_a>9&$Uqd3C0m)U>w2Qg-#~SE-Aluvv@D_wK;JgO1a7Nw}fy=lDXWUu_$kepkz_ z82eb2dUuv^?*@*Ex0FR8ktQ8tBP%9?3SPs~XU>H^d-0-bCVb1*Or3SM{BKgVe+}+4 zx8rnR@~)p^Q?d75a}hAjY82}LxPAY4g@{y*&n`a=<MJHTkmGvzc{Y=IO->4qR(sy~ zwJX;A@(K#eaX+UCLXC(gAhrmUnKSlv0jDBcg<tq_zS+-_U6-58yj#A-UoF_}?TqP# zm?QW(>Yge`F|I9O2madY$DhjR${Qo@NeCMZuf`2jo-5xpCa~-UW+9G(gLZP{tgO5| znbm@Ol+04iu_SBOUh?4|YvbW!iz2fFGb3B_9dd9`0wmI4q`M1R7M<P^RQ_E&GKou4 zxQ_8Yo$NGm!+pbq622Q>_xj6D*EO1gI|IKBd^76&)bCf=#6*sYYJ}B-^u+e1?az*V zb^N0rRA=?tPVruVy#F{Fj6;m9H;i91x8OJNyR&|Li;3L&WqKs7!VEjnm(vE52{INh zHkDUYP#-;-;630Km6XIIb04c<oCl#HhJGocKSLK#fR#pyt4Bt1*G(zYt|(&VB>Z?N ziQC}G>jih<6<r1{XT%SX#p6?iV3e))gUk%=Lf@Mz<+r29y?AG*0H5yx{CGUUzYRQN zTj3;yT1p8|eO_$oL~F(==95^C|A(#bfakJr-=<R0P+CS387bK#Wi%u+A=zXrtBjCY ziBz_%gtC&ov&)_-${vNNtdPCl<Es09{?Ge=U!Tu&-}i&x&-J^m@ArG2=W!h8adzYh zQ_oLC0fe*me)4z5Ye3C~Ro4K;!4;mgFLE`9V#&L9v7X(HKXU;D&=dNPp-BCuxjkxG z0@a)pP@sn4*tn>kk8&1X-GtF6s2CI6xyjiDJT5|+4qz01o|jO8^l;;ER?>B>H?Z&D z<oV%6>K;OP^)q9I6MG{-C2^vJW|%J=EH6s|s|062TFfL&SmhB_7mkkZ$HWkg)Zu4V zrC;%oCRZ1TPRvkUS4L4$`dkfp6i7p!xJ<^ZOqUVTu^U&aTLz+~gNyGJe*N6K1BOuK z{#bXaw8MNffR~=oee?q~K|tH&k9Jv|r82Ynienq4_a;30t}K-FtESDd65dqx^NrE{ z^HZ=plzwsU0Stjx4%^|TGm05!whxf26_U@sU#--6Jkl+!d*jIqR&rECyR2R+&-aCe z=Eu1nVM9=|N_uNcCNCqS5BEr@5-z8x@xVoB0sTV)^{^CfL0tpIAKp3+`6i)_Qg?%0 zH!$k5)j2xiRKmuBA<08tqk3)}aXv1h$mTI!4+PleGe9B)3h%+vj|STxk7HpdNC7iu zsL=x3(&43%im55PQBV<s$#BL29xXUT2d5@fiXPw~P-K2enrW8PgQ`!4{;$o{5DYx( zmr$;<%7qafv+OFb_aaUc(*j>zIfenxVX+;jPoDhp<qOFfK>Yl^T*NBOu<8jSyUW<| z`Bh_{zg3o;9I-Zw*`;5T2b`vm)3^8#bCwuF=<w?rl!CrUjL>HN1^MF{Nv0GOXrO0! zp*cleBZnFp?do3vQhexWM-fY^R&@IWJXvffdcsjs1ciho1QYQUL}E=V->CU<_}<l^ zge5SJ$w>vY*}$-54|@1z!XXYLxKOMO<Z(n`Gz8T9do*-;^-F3>iW+DP66}5l2M3Z} z^z?+W88qrBO?j<{q#$mCpO0eRcyEyqB01qN44e!d7WXk{ldh@{#SFp`DL2N~vD6Vb z>Rt)OGTp{34$~i3Cbaz|N^XhKcMCi{jAXpl4zO+@fJ#s=bjP^0&%%;|7@+_TfdrW^ z;Yp^iUwz*Iu@~L*_dyE~hO;C{79Zv23Lv_FqP|=POb*&y8OL5wO@x^SR_o&LRt3@y zI1fVs11+YItTv(|MtPAUM(l+JZ086(PzfRF-+=@~eTo*m;6j+04}$Ul?g^>TjkIbu zJ{sfe*ONE^<T48gJO#;tIqozh7^I%cOxuj<0qAB0*;3G=pAMY_p-MoHfpASGmv$jP zMlo)V0sS_ewil<)7wiNR3n}WejC#y{3d27q#c$Y71{+3T=h<5Tf@NmwHR)|DA<icE ztpVrB5~?BqI>ASYppqb-lL<ch4kh3%>w42QT3EvYj)0&Uj^$u;-@qvb4{`u14BBN_ zPP97o0ztr%?L@KY|8dCz0_@)ttJ7x>LvtvF4C`qeEioq(h1pI(Fhjssk=D0jNKZT` z6nZj&6Sx6fy;fkDjy*lroz7!WU3{w%!d%3vGpSbzU2H2PNNCKUe2bhN(uiG<57Bm) zbjVEFWgfs0K#>)*<8(%rMf0c5;~B|{;Z`PY>;?&V;0}|la?hHZMpdt0o!o;$?C*zV z&732;Vj&_T+{Gb>2YIgtwG@1f8EV65W^k>TE_!Lu3>b+_!)rM(;t^**wY)?sCZkUb z<tDOgU`?TIgex?>p^Q2&La_}?6~@1X+bPpeEPR=B`YybVckWmtuQe|#UHX|U^e7x2 z$w>W)=3F?VUxo=58r$ANx9|RhX9O85yn1}AZ-<{)v!->z@%8fX==ZK*s}bkA&)*po zts?zYxrA^^o_jd|KEYFT^!yQCT2)(1#@=%eYF}&!Qsevo)Jy_mVq#|`?JH5%LlqeH zZ4^Lr#M#z*+C2OR#1673gHdz50Mw#Rg{heG@<dI`9}=ICfF8YfnB5m|`?we(Uv@M| zn6dmZ`hBrhO(glf|J4HA$kSzIWwkmegoXmZ350XdR!lsBRP3L|6e1JH`&5BR=g&i2 zYNM;tA+Ui-{q(9qA*Tl|yyN|F&7l6#yGM2#A9*l7ALTK=0zua<XV3@|JNG@T)Ijx0 z42(eU$y1pJ*o)Y)Xg#n2O-zEMrUr0of&FAuRxff7!W0c9hVwYjL3(-^yc-}5P9NJ$ zfpc#oNc4mN|0O(mVw4>0rf|$`hN&f70$yhJ;$cD}ClgYslF&0jf%HNxz9<2^m{8sk z(GF)F-0)z!^%frmV@ID?-$r_CwU>BQOyV=NA}Z3a)PuPmmssrk^OXETKsKDRo5Y-9 zCNGN0Au<B%g0CS?474Aefxz`79FkC36QfB<oEN`^!VKXcL_h%72s<pidF07k=fcl? zQGq1rI|ya81(}+e!9p;~q7z9zjNAy1bOM0H5{CVa2Nd4QP&pxF;TBZ3w}&ErejOT; zz*B}7Rt)+j)C*r4Pyhb?DNOzM1>{){v2Q*@Jq`&YF>9q2`acxR<P;R;aH2<aLd6Bs z<!Xos0BtGaok2PaRL3<)kAa?gxYAf^ypQ9==7G=I&vRrzodvKF`d4U=kjLn}eA`Iz z`f4(?^4o#IW6vl<;eb}Ar<R1go|x(mbvz$%4U`+NH1j!eY*f5|&v~(ikSpWtjYC}z zkd(jM8g6l4aoRv#1%_B2u)y{HQc*ku$#^AFn$)WQUv1)HK5qsXH8b;8T2%`4lpKhb zK!pFaqU_i~45CDNc}7>4!5t=!Uj_$3ocp(wTOr89HA=AN&#^LHh;|F}^&cc>>B=8& z*G%A9d@D5>MRk}+IomRiZ29&5Q=~9yavaOEU0jOSsLYTGGx78Perg2V1vI;I-YBNJ zf!S@oT@o8>kT(jYGrvwZM)Vvyv{t`FjMc$;?egJ`%T(0?XZ)`-jq7R(H7@qE^RH6w z$2TtN69Ikb0`eQ0Z=-&v%*9DiB=|dss~gYakPooxxRPH{SV%u8Y3MBYbSPq11hRh| zhaS7ueF9#PmLYrGeg4i(fu)Oc>p5D5PQYzyRUgsO62QeBpGaz<mkcN*FzR~Y03n#f zD=U*hhGyS3poV;hR{ONilkE>hi_ULg#`@N6kV|Wd{LLcxd7Sz1xs}JvZ4zn8C<st@ zzeLGS5O<{qoK772;-kU78fsDLmAMWQAjI-Y;hozz?E-m_ke}PD2BHux!VY(B$@Udo z!d5^)Bc|m3AtAGKxMPz>4=-(>Ql>KQ^e)-25Y6byZ%5B8ohx#VJ}<iVJlj)<czkBS z%n={*9HTdp;|-jUpQV@j;7PC6nU~n?pQKJ@PD7rXg#X88TCp=>X=iYZiy_sXAayz_ zNqv3m5VL0v2IAX>eM1b1dE`^~kVUfx(QZ||{5L^&kku*ad3E{yUiU9P;(A8iQNn9y zNgAgp%j@$*-_f~kh;c69b(pmdig97in-n>NOQcr4S#9>~%G|oOj6q4`1VMHhrt8GA zdRXcluM<#=siP*SI|<HT&-_2q^8<i$7hU$f?%b>;W>J2PyzL?rP!(TMNdwbY%?}wx zfBpQtb2&nVg`0TIPP?CdbhV}h`|Xc}u$bj3^?Z=paD-y-^~?~$)$mWkuUYp0B>aoV z)(~V?=DMwfIin#3M}!kbRe$$U(5`{rI`iDCuP#@ow>3&=49}l?kQ0~NwIw(#OzzX< z0f7*rfGMykX!)%CXFho4v(Zl_LN*(&-dcBkx#|2~sX(;`7UYEQ%F730Q+45^2%ZX# zc{ia00yPz!J|K=$6K8~t)>_eI=@Ce#P#<sD$HrJ$^i4=_7e*PE*!IqhTyN1B-`L^y zeXon>f}TpY==<@k>arh+cFa=5BGmkeKPoO68Q6B?<(Xp{+GpWj@u9uFId|0puGp!0 z=uW$!x!kMmOT{8{w4cw7^C3FFBv9YO-jRxpQ6g>T=eO$M@CI+$1=%C-uD<f$Z)_xw zkLi?mnA?M>r}#f5#+oTh-kB$0eLSv*S}NBxQf7a&dO_5mVb;z$Ha;FMY{N<dTo?^I z3>e!5qcVuF?LsE*g6bj6?SbJELu{5t(+Q(NP&g{}z-z9?9{&!>H<9;4MMyv_xU^ln zcfUx1Y54hgCBg)R=q88c0;f?W{Cr`cr6un6zY2k%DTXC?cM=5K>|h}t9>&9m&p6Tp z903}K{>-q52x90lQgB_T9wa50;46c`byPWFJJeENahA6nwE<j4@S_MagjNY68^Qy( z<>(%C{ebp8s*!UP0TuPk11O0Ahu@QlXMzp_hjsC9tu;d9K$yni>=MGNIXCpFK_gY# zEO&meibnKwp`cMy3p;aPhr)-fBrFx&LkERJqEf8PH*e(ap!KL#ptz#?0p}RcjZa4j zXbcKBVgdk|C1!MKz~(9wUT)|TGHr<?M>n-s)^iXOs2YsG%i@sFfXE9@!0Yt}xp0XD zAH-++;V@EdVoWi-E}29`v|_cOb4Y^Isrc&eJ-}&9@NDp5SlXL=ftKRdZ@~D8yIx)w zee->gQZ+U000#s7obZjpk_FCHY+Xd;_kfo0>C1t`hQrjEK)F%qLRmxY4zF!mA<IUv zgoL*%Drh(m%F>K%CkWdo2ZaN{8E0X3EOb{jK#aF;-yVmQ2l;_M3|>(eFT%A;S3MCG z6_3l3E#_>5jozX<x3%_ETf~#pIZKrGis7I6jv9JZpF-B-T|}Hkq&-D@nkQ?R&;%JW z6j3`*+(y8g#BdcP+$3t4I)X9HUvRO=Pj|W_ZzKjrqjm!$Lb!^-F5?#zl$ex6c?LZT z!7jy(pjZAR8S@kINbyLV=Q>SbP0V|1Kmxo+YOn?79{n?O*b}eD2qABK>$Ugq-_tPg zbDZcmEy7e2h{lMP0+{TQ@@lxQo?cwwMRE~`s0&O+`PF5{_??)EK-82N6$*VdFjGT3 zj(UOGCsvregY233J>(AYWW6TQE|Rbs{P6iR8M@9kK<o_s4<oZzwmil%Gpvtf+>4D` zWH{Lo#P=~YI4x=}uZ4-gO%|xb)UOzQ|1_y4Y{rf2Lz#LkI`KiR6~U(v1e1pv6y~U+ zQ5mB~0mn3m1Q*`e4}lVr66X^N?YOC3Xe*$FX1w#z2oegU?(EV61iy+}bpc9Q$YR5@ ziotWxcDKO~7KDIFkrndVE5}swbOEn4&c5mF>Ovyz>*cit>!@sf?HQDAg!L{7;n$8Q z73<tvU~|#i3UYYlx*=4$Sa=vx6I0m^_aNL&R8=*=^-sdM89Rh*EnK=86HBm*d5CGd z^rr{`Er``id8zOJu*cC5BswN;W9&<AylUp2bJZ#FmSs&uJSIo<oyGR!-Ax4Usom@* zDuk`3fvKq!GMP;hLz-l-t0I6hRFC2fi0-SL;HCV=yQ7!|ehJ#d?38Bnf>~bFUqt<) z4p`wmmN<qqUDmkyltX>m=RKj)BWR^%uXfq@-z<brpC$~%;4n%gqG()&*v1c&BtD=C zjF3-&e--!%EpZ*JdYj;8prg@WS@|R>3_{;(3|S-E%)y#p!6Z1SOs)q%$8N(>MhG&I z6)Qr}fbqTvXq@1{ES?iBSOCOd7f;}Cp*#t&FWNOwQg5Ein)<o#5+ap4UDh7pJz73% ziUz&^CS?OhjFAu{)dhs1`Kq=IM6JB6Z1!Cpd|pCIij1%JHW0w<(A!OT$b!JD@B9!1 z0So?wtzjCZ0inECPI%JtS~gy(<fAa^^Rk(f@TH3!GHfD^>BR^<dwVuv;c(>9JoXb> zgkKoyZm3LTLT-IU#mcS}w-q@$y6z8m8^}LD^>ngynQ%k<fMS=JX97?Tky(ZY>mHOA zJh=(yV#IIQ2$;5~(L?q7K1<=R5B0a+6Jiu5mfFlyx3V_9B+0VocwBORU&_G->%((# zn}&8&X}x(DS@*6|>kX;9?6Rl1*g@YU$2t+4osxGpcZAE#e_Hwe^SQ3HqqQo)4$T%W zEvjUt9W1*RBbh%-xF%}*E0jVz0Sx{L&RzCwX)(ZBK;VffsK74JaJK_cKHI5Nw4jzr z05#R3ddEP-y4z)dL2!yI(WGxXjJ|Z(l!Ihii5?xBnDEs|eSg~paTs1K!hO-uFd<f= z)IXj&1|_b;^pLj_VZd@w^iDAPd9Gc%b_Q;yC|q9PN^o$N+H&w>E8^(rEVMh0Ok6oI z0z0q<4c<7qz77vpqj0+3k--Qbbj3ViNGa8&kn>=6I4QPIJa8FkGH#*y5KXKKum%E2 zK7DCFLn>cB=dE$qdz)Um|KyKn-;(}3<jl&J`M^MF$LU@3yDzB;WyC#n)#HrU8EnZl za~wX%vAsb?|3q}*ospw%EV?0QwoZk}&|L|s)F4Fz{RJ#9pbu2r1Gx5ne^Lq9vku%m z#$^}|)dry5gzDhIM##N`?@kxl{y2x*Xxd*|(sB&0Fpk7&(<`it8BUVWU=aFXf`x$& z7ZJ{8sCGBT94F?#wz0G)O9lJYjQK`TsuGQ1;_E9x;BCy9lOe=yfRI8S{Nj&}%p+jP z!Hj|3^~JP_yyYkimV{Rl`fH|8;o6M0oj}B62V8;#a3gQT<}I5yQz85l=^)ylar&Uq zkZ_=d&y5GrA1qw#U$o{}5ZWR_OqraVtfT+~s7sM2-=S8+;JLfS*;N;-D&I!3wmpXm zs`$m)IO|YnAwzM-KB=I`N1Ja$2pJvp@-Qvp?3SGeN&FGU4p7+YH$f8v;Eo4lpwSW_ z2?i86fDyX%J32c_X-=dQ5;)Ml1ZxGU{WaVzf-8f0Z|H<7vL=(}L#PyLz*7-wVniEa zIJL~VA7%BJ3jxdofFP`|)8*n&$yMM$4(8P^wEZJ9Moo;a<!d+=0p@gqmdN9&?U{$( zfC%@{3gZ(K9^u502E-*2$XiY!=|jeXElQ8oMFdY=_&0=AV)!A*paW1KK!zK9)eOMt z2kLeUwA0B&i4bj}a;(R382`yA{enS_VM(B=e+zLFmaqJ9yvhB#@$L_xrR(qg(9>`p zGgyG%6$EZaCp8U@A?Ml8sz`r^o;@QVaDYS-KlUG=76QL`NKD?u#kA5%bYiBVmHp+@ zd;i^38hCZbxxL(5xXj~UU8RkhBQucfF#Aa77@VN+qCSYCa&e;Q*NGn@!6rq!tQI4$ zC<Yt1#WGVbUjE+K|FQMKZf!Fo&1$eVFlMv{bsf5si&;YF1q-8tyn9X#k;me`5poUK zf)YF+BoJYfyrUS0N9gkY`LOn!7!!fmCjHu5M1jgIT`^Pt0t7HyF*Y2Q#=)H05y+2s z@82IXo)Yu-B}B@Y+t_D%Zpa8F#a)15H=#x#z(u!pF>LzKE$;uk`PP8PeKj`~&J95> z^vz!;zPz#V2@phCZshHMZ?)E55E1~^ko!U|p}2u*dOKPjh^IjOeBgc-s7$PR?3Ax# zV;pq?0Ve+nSBal9M8Yz{tSyBRk{+m}WI(~ioM1#vP;AjZ;j)r5&ITcDz{0bsu;4Yg zNt_l-oPkpHxVMzVF6mwODiaj-ABR(4NIg6!_32pdMITzlSe331uXSTvJi2Zu->81A zu5aai!dPNcc$s)h&k$V~vt+>h%+xP`n~BlL(a2_(i6H#L=ir-9wl7BBD%3qa-ENk8 z8uvT%eD#Je8qGWM{j~Zc-gxxxIPxX2bBv;=>Zz<MJmzb}%0#~%dm3xQ#bw<+vZmmB z9GNu*+3^kB*M>+6=O(7Be@5}&p|UYvTUrs{D#z#F&%-b-c3puky$#m=C85!wO{0R@ zF0Hpos$EZC6+(#;kpCn!RJFW6gtlF)Z}*MNxhQA~2)!Jd8OlOdVbV(vs)OjvK}w8) zqG*77h{sA(Pt2NwOp-7}z*7JM#|jv{&Q(c?RS^!0kkAlqfpt1Xm~sRJ64_^<fZ0{h zAhpO6V8$#IOw8E?`j6dmANhb1=5`_3Uuwz3AUayiFGHK$26tra*9;9M5FG3n>*=Oo zSs^*caDgGc6g`SjUu@oqIIegc4^7J$YG=tw>3+28HsO|9vx|YD*=i<ay1O_&Qp9>7 z)w`*QPU!m2pYNOd*BiZ$ux+K>Hga}h<0bmjRU$AZeBBlnbW!0a6YOv3=qny%zk9+U z7WMS8YH9w6<3KHWg}`xyF)KEBDFa#Dx7CsDRaKW$HSQ2%<G|OiXYo<{9af<y8Hn{0 zW8&qdLSE{7P`DhM8TI#Gs0uFKqN%vABexxdh90_!ku$N?nW2y51$0VigxQZ9OkhW_ z1IL}h)_Jh|h$QqlNVQc6)E%jGSXfvFgdC8&5Kb_thFUHhnM{dx3#*!b&_G)ITuA6d zmmr(l-4(5q2O6Er7JCX@ywXyOGWAETDfV*ur|I*3Yp^PCnGwXG*tavv3ddB_J-`7u z*-up2cgewbwDG`d>9x)fBde7T5T(B~dAGT*_%5zzQ_HWoG8I^ltSsx$HB431EUt}C zk}k}S6uim5-R>oa3F>{c$Yx~n>NKTKv@xqp4>xT)Q|i1}8_d23>p{8=-oa5gBf9df znjv!)hVd$nJOZ;s+M4)=C{!RvBtxV(LV?^Y8iH;yYl#1VNE0^?Vvm=|RQ{l(5*IFf z9T};?yAZ-cK%T@58)TKFAaIuJ^{0xB0@{BpJsn+W{<Y=pi<phFYie@MXGO1f6f?Sr zO*Z}R6el51_20k1wdq=$t$5^^%z&Y4;FhnH+n2iv>_YWc-??~gA8n2<c>Cqh{Uqg5 z&TkaKiF%(Uu^R4v7=1x9wr(RHKPF38zkbqavw&;?ZAfgC#8ib)>viq29s4L$;_$oM z7j3s5;L?(_=G<7gqp~iIc_OVzE!%d7U447J^E(O$!I^=#6B{K&9$u5^-~C3o{bix@ z4o_FXCq?;_YX;8u=C+VtF1c;D+4ta?rx7YgRSOQ5Y@UB<)BK(!QrJ;Y`XCd7<n-*T z^y|ZIk<(<yKD?3KW4y}!_!XO?C-3q7sr;2cm42}#I14|hzo5NUnBiP&#Kg(Yr*U?x zZPLC*b?LI(yAQ67aue%Lwjs^-dgs%A)ihb9+oPUm3UkI}1QLIKI;tKtVc~Ssbp3_l z$oxp2_>Z>2XD_s{-41R$74q8bm0T6?l{B63`p?Ien^KR7F4-sA7I@6h3@kpr{@SY9 zSJZjnBk{+-e~3GBT5vMPEZLH{0PTu7PirIkhOfBa(y?qTou&8=@dNxkDNP*s!{XYu zU$43YA3Zp(*qgNZd5`F#!Dj|f-sjKkhuNYN=Uv%~0y(0x)r9sE=M{kgpaR(r6%B=F zv0aP&<6L6+02-Mf2qF^mT8!vK*-Fe=f#Fr>WlpI1!60P;;UU`2QQ*)3I!8L`xRgP? zuZk#Bz(f+7Al*`DqRWanbu2A+GTZtsP1P`jqdZ}|6nPk4l!W~k(MYzkxJToti1WfO zY;_Xk&xWRY!y_XXp@N0e(l^MlNDS+Py@{*B@ZY(K-Uev=2-_V(<O;9@V$nggM}nLo zA<EC%K$dNUPe=SMTYGz4#%WwNF{P8hA&`Gza3$eL3|Eixjg9W48;OQGWWE2)cO*~~ zG+_QtH$u621~Y0<H<2DTsD6U&!g4wUWhI-)N!9dAfX_+4gG<E~Vd6X+fx}~4B8eyF zAHq9(Lkx@o#(D~ZaMfj3I3O4EgR2dKfKX)uGz6a5T-Qh1p5wsX<Q}zAoTW&9jyXLv zS<5#-*JlVv-B6QN^v_k@0^0{W<xlN8wBJ(gfVG&1{|w_;NAdkCHx8yE`P)4|3o>W@ zQc3o5D7h;c?V;j>I5cPZji`yjbNRg05b-=-mpRHepKeMknbbbmg^kge;e2hL^v><? z7ov(CPued<>a9k^n>}=<uA?OFFZ;D1&l^T^ljA$}MN0a?3SO2S=`MnC2ej|FZ8&4x z&`3T%_|Plob$bLZg1qdYJm1Z)jbzTOaz>NyOIG;T*#M;me&Vrj6B%A?;_I0i6-ui4 zT{m*lbzxJ!Rd2PRm&G|H(`8GNv9a-^DtUXs=iiy&X<pLXFSaMpH6X)ml=HS#cT@9Y zW2J&S<&&>3WFO$M>~H{$Qs>XO5#<d`WFI`&w4WY~{Bb2oou#Q=1-d8_L1FXlqe>e6 z3H?Gmua>U?Hz7HhGr6mzWXT^1YiMDcvc`J8uIn?yl{a3yDGQuNINin<A2+9a4Rprt zF*^SAoALJ(70X&hXDz0OvfFoD=zH}zU*Xp;={Fk~f9RBteA#_5Jx#iGF2q=HZ-$-X z;Z|~ltqSuOM2kdcr~295ENy)`pdWZ_h8sP+=uV%86s!sa*dm-)h<J<~yQjn{6!LLY z{f}T81R!w$#3<O25CFa(QZ>tkz5|rfM|wzN*bw6LPj-*BQ&sFpPN@!GA!783CojQb ziMvIion;24i(;Hq0RQtE0AvOD>#bXg3T$wT0C2ja1VG20%cUq0X)IB2zYgDSjkP9H zB6X|iIQb%T5P*&M!-pha&~fOQ*+ivjMcUELCX%BCLBRzh63}&0kb>{91J)thnTd7^ zqUA#|<J#GZ!pbeDNHA1~FgF6#ykqp<{rl=!hU6qLi+4>@GkhNzj^(YKWmvwk2t+r0 zLIrak?s0t18M#exMXS_$aU|q=cY%DweA%748|4+{JWZzcW8X;kvg>$QJj>&GQ?n~g ztFU}YW%T-ZXT<d~Ie{Dvvky6>II=gqZ8u3^OlmVRWr)bTQNGA`fV$I-Iscp$u|lqH zPJ7EAoAl!S_gAL0xuz12=T4{<+3qN7(v%shkV?}jt>4>UXV?_t<E<+vCpBTwc@{1g zS%#`F6H+6lE8-a5?7Q<=a7UN_5!IcMGN%=DBzB8&%rHKEt-=cl^Gd?<ZfraA&g|V8 zjnU;*6xp_Gyv19bW=8UQIW#jOL+Athln-p?h_j3>9BPOTO}VO`)Sn~P%C;`$>@0FR zf2_R3P8s=lN0AK|$t-QV(^n6Y_H-SS>iqRB?wX=PVhqc|`Br_#Js)l%BzdM^l-IxT z#kzdL^DCd;G1R3wvo|U}aCuqdi5BMnc=uy4aq5srNUmGjyHB5f>-G=EZt>f7XM8gW zRL}=cJ4eQP`Lg!tp3dDQb<1Vy)|4FW%nmGzp`3=g4)(r|p^SGX<yUU6KUb4`9NeZL z!Vy`r)xG+V4~>{B_55USjZ>}K+(gggep_)7C)=0JAH+yJubTBQE3`IAtlh8dlC~6m z^ZLxgmGOn#{?;7r_Z%03)nqS5K7URmxR|~Qy$*o4ILj9=E4!ZU-LuC)$^nb+F)_Ow zYadN0(BuFj+5#YuP~w=H9wnpLdkxMJgw`KAAi`$}<>^J_D1^ENJq8d7dJw!K&US#c z*I^O?aLojo64)zK2j1|WCHx7X%d`g0h19tcQK7|_6M#J7?t)nbUxA}RRHRnsTEa+* zSu0}1t^mXjqonc~h6LY3fUXYYn-YjW>N2-Au8c@wv|}Xd31`mI`^VX74L2$<sO{1G z){FVT7mSo%gQ3Hmr*|0#VN?z?Zz`%C-x`$Bv=ArlnP;K?hWmxA7PK`#x^hQlbv4)N zYCs+QNMo|9%xzF$sJHI;=J0K~%x3gb`6}NjPw}f(hXn<F3wMlW-R#})Qt7qW$iG^E zScBR{F1s}KkYD3Avt6GTZO>4Wpgb}D_5JMjRdZ@&L_^<S#0IBcY0L0&WVNYw=A(Vw zxPR}_AyaCtzVRcfwEPw%?oOi#a?eltU{@UKadUr~``Ko6vM-%Z*!tVGzLWy2wh+x1 z`CEH-NqzA=L(<sT^j+W;oAxqu&z3e0Z8NFbeJ27wY-o#h+2$HG$EV7rYqVNFTSsMf zUvft!^4*FopVS>yk&kXIEnL5C-gg&&?4Y>wz2*6djhD-$OAb^YzgZ!8ep}`4L9ZY- z-9yu-hYdb_KfAJIbMAP`#Q?vpJ#T!FDH2o3Cx4ZO8S_^OdYzyzvb$!uuBcVw{4{n; zQ1$Euu}J~`&Qq;V5`G_=rqLFYHT@XcvV`9MH@6pQNh)e~#7aJ=C2{66ph7w*5UF`1 zKF77@oUbQmQKDb8@hF34(oc=2R>pDzO0`DrZgQ26imwZ2btuO=Y$H3z?2{MWvWfqP zqQ12S@?@FUb($>N%x%1ewPye)V9&sIV5^&(8yXo+!yZf{9+L}wFAI`}K8y*CI%mq& zv^hD+`#sP+L06ASw@n$P=#RnBg+282@^G*NZym%~&>Am$;P8tJZ4iAy3%&<IL!Q^n z=*I7#pBwz;egb&{lzJF5233{;wDaZgIz=6@Q<jmAjAqMxAPFG%QGHw$j#OgdBMZTx zA+b*8tt15hgUW}Pb_mRu5IqBGd{N|H95goFLE~C#KXORex}<AJqhkLK^}L(H3Wvn? z<`lBSS6j_FvelH5G+2toW|^(lin)3Or}da$v{OyU?n}NRH?2|iS|OAxa@(aqlKLPa zq5a_}+&!2zU+Q0~+AjXgq(1oNpb#F|g`(jc)AmS|I=2SCOJ8CTZ_;&I*B=S-u*?zL zlc4lUP49JEO8IOu`H8zD3-9%OCnF7)i>RG>It!1b)s@AVe#jBS^BHcoaHE1rNEAJJ zYOeK2Q<ImGnDf^uoh7Gr_0+6DteJ|X8<r|b!&e2=3m}-%dL+8OE%5QX1Ioc_PElCY zx2^imd!^BHHycI`EFqWZ$}K(jG*Q<<ty@DmL+TxW^V^7s8Tqjg=iwom((gi4?k`y$ zDdp?Y7vFJYPhw?anm&Xit-QNi^R9osOyLiTQ|LB=3)?xg4uMjPLx)RU_2D81p0AV( zCYpK})7=U{%#C9sbGSBdrW~Y`fx*(|XZJze0|GHKBSUJ|aj16c_g7&kE3ZGg09>*) zvP1Sqrl`ZOw;w1@@OT4%3Vq3&lnd$-oKX|HoiJEL55G5r3{|}VM<=5B7O|!8M^4%O z^nkVzu;!%}>q|r0iZQ=hIX@sLi@&-$l*&@GeU5d-_2h*|`vh%2$|dpctIxRIPTR)0 z{`{?Yp7lcd5uzTNDQq&=$q&!$_pORuozc_;eoQyPv)(<!lSwDcoZ~4QaKJX&IRG~j zK{xOwWqu|r63W+wS6{p~+b6b|!sj)|@bdhiN7`^v{&<4^Qwq;G*G$Ssf$fZoo0z@H z%S^(<pRsAeZ^0$uQl#X<>dl=DCmW-?si@4>Kke9icxh5igf4o-amBuY3mf>;EA7Do zqZEr&<-Lh^PHh4GJYTgY`$`{AUeO(XA5jxU@1$0CLv$jwS6Qq7TbbqqmSSz`=GIE) z@fqg$VdunE-lz1Mx3g|FN#d+|_q^Nwq|NkplS@&0-v(4tR&AQLmGwX(xO=yBvhCD? zVYEXKYD9!Y!u%5vWe>)hM+!@8vY=m$fOA3k8LJ(@H&OGFf;{!s<H#^+AR&SV*h=g> z%(oz+sy^s+bs!59QsAKgnv7P*X>J6)9rnEMfm2kYZwspuPtHWjk|hcTu?5OkSOxHg zUckX{eh{)0lm%=wnx+6Y3GY?tKf_=lLUK9J{G6;W!AC}kCg0`y_WVj!E=2;3ytnV8 zVKxR`ts8Ir>jtiyr2%^x$uB4){c`#6*C<2t-~8`x4R-CwGL)Bk-%nW-ZN5ID5X~0l z5RrQ;(QJ~lr^v?dgq!)dqN)Dfw}qa3E=zUQ(?5}IWQ;R`c-_u1u`?q&3N?H^^CPYm z%?}>!cRrEn?dx6Ln@Yp;k%!+@BbvQbIj~qKM?$f}<*4jcSzb-!YQe-4$W9Wj-tWQ@ z9R6Y~w)P@o@`_pcaYfe}=aWxw4=j8gG*`>M8?%v|PjyvU=rT#oIr2%xj$Up|Yq>O0 zOg>-U+ijoy){RjT1B{<o-Q)<n%qhkhW#y7cUH$4Y=tVt=bEBgsn#=LQxv2u<MYpd_ zq<HA<x!3%F`JgJrXt!B$dq(<3r=6Ea22Xj;2-`0?XDv~sSB<*)O@(}SORoxUbDMlG zI?+>lzHentE=({APsNvv{>Ybg<C&VD;%}^fP`HhYO2xn4XEy_M&e6}~!2@t0k{H+n zCpM)sL*jaohc5A+JlXMKytSHDFM-d^ZJl{zZ$;}=7U(2mSQefpVgCtZL$UQ08vrRg zj6eu<7TB<nV5(brZ!bN5gu-L{pgH}?n{WM#i|`^)yPMvEF9#Hbp<cDW?ei0`Y^bp4 zh*+|9tHTeQwZyb2^fuwN;ow?P1<}m^`8s-YVS7yiwIN|dXW4rS={%V`2zZ2oPvA2N z(h%2!q4Cej_pt6l8Uhs25v`11AW@(adj8V2B{QgkD{#PUpS+K`@Q~s16^}sbH`X8h z>jU~8_0TCaH9ISiQ>>nENsk|3<ty3mTx|Yk-*_?6k~}rdmqF}Ok>_~$dV_ObN`Lti zcE0a}pASfdDZa_PXh32;;?l)ryCl>q{AT{JtgCi;j(6Rbn+z*Ic15(=CaQWhzqvKO z8K?MwR$7kU@SyqCI+HuwOV)BUmue=+4PKI2_Ee|*aP28|d?tE&j@!umTwsG(^s4x6 zA7>hP7<q20FMK+>X#G7D?o58IY?t=DxkVRk_tV1k#nnuWc#makQ6Z9$Cf({EdlL7s z(s$(gMZ{|*X?=R|Bgjqe*6QrDPYMgY+$KDV3NP1k>5iV6)%aX@E#jAUd&di1>)B}) z--9CMLI$kaj=jcdx+QnR3ush&Uk85VID-2sw|s{duY2P*DOR!kB*t3tfR=0#O|`A; z2^$-}ie_)=<lr{d7cyk#Hmr^JmOj5q?<{c<N>qp(o@w@+UQnE-3EPuirLAe7ssB-C zZlbH^6>04DY&%w&>#JMlRxSWP;!pkY9tfkik55&m{}Hli=|0-DP7{q!y$jKmQSiqq z)`W8YD0Ij~O~umuk?DE>r$&m)!dEYN5E9cKAUc7a6AM%duxo)KY!!5(Bn00B6$UY~ zYRi@_kkv~#I6yXvnv##%6-jY5&Z#7b%)r>A8a?0+^dm^z&2@UDS>c(W1!4CBoRpY< z2+t#o&4vhf8&;s(x`h>i6cefifUbm_G7&|p?PVWVrUj$COF#!QfD;z=@OFV}T6g$8 z5+Y3KI-tMo$tVTS1M3K}DkG)x{{Cn-1*$@FFxsu$qh`c#dPq440Y5B)_@8)(ngV_C ztR(!{P@7%@giVZ7MMlK~ef=NdI7(+?A{7b>WLrKUzkW_It`{h5ajBn8ZeMh%I`uwM zq{3tOo}l6=Jj%fY*XyP<!%DpmP`TY&?{g7cEe)41W6P;NaV6(+mWz{Xmp9jv&yd;& zo*SX52Bu%h{NM3!)N!NiveMT-H&I+r;QH+pD#-e~qHdW8u6K6+tjm+{7IqsJ5gT;* zMKfv8k&lHFTSoIdAw-2;!1bp?7r&|O_u0$)n{*mqcGlHskM%S(_`8qt<p^~PS|0g* z=V+5zE_cr6O-HBn$i>F{kL>f#z2kg(LbKx9(t4Z54gz4+cC!D92tdTRi~FK1gK$YG zS;NAr#G*x`cP<1^maQFHry*Tgn$glX(D(N9t3edvV)f-_exuaTICfqC#Pnq5MP=pw z7{!f?FZ-n|wMx#mUnS2rMeas^+Uc~VmVCt9*Zn!9d<l(*k@@NVUf$rI(wP;A(%14q zA8)Yi^;H$^<jZjmyWdBhYN%a{r53nm8^}m`<5Sw%OiC)&_}XXveHo>98)}|J@jo)Q zi1^_ND1X{lM(&jzni(MQMZ-dROfB70EZWYF4gyOfj1JKG$FBHt6O<p7b#>(K3~q9! z-u}l`Inj|W2b{k)h>a4pE@7q*VfzPo6rSfb$Ku^iC%7Ff{vmN!Ianoxp=TG~W<sBY zF&vX%KhatYGMpS@6#`>~J_Vr2?(@QE3y1gRSsa0N(c<DTN4^HK{JK~>(l@A*_W`AX zhxDRy0h@2;+)Fp_)V!9LXnZ4_(@-ix2qcfcMGPZ=O_Qujc<@OyYnO!}lv6@j8w<h? zXrC9suYjDx=c+(Mrrj2Q`v@coL2eO^z#2YTsChZ>bj++ZKkH2?`ytCfwrxVoAWVMG z+xjkw{>9Q@fP9T1;kP)3q}s)_X+-W|K^EoT9$6oLA9H8?XGdpu0G_<?xV2LDr5zhW z7TIVH_B&+>`93(v!zWb1D_j1?8>zWkmL5;^&hKZXP1rbiHT|1;7;y5L{7C2z++MqW ziYWPCk01Xb{*ocSKBS!0YsYYSBm3nyBz9tHKI^=0oBa;0{GRtn-O-TGQGp`w;5OmF zcg!1+77%rRqt2l-d1IYQTJ}-Sjz_g|iWle%-eK=b!_S;g+faPdt`9olBxr8aHLw(L z{Bhd}xva87t3`u+4PMin-;fNy#UD(wGou1#GHmfIQP@~L%ZO2R!h`E}p5@k|VV`G7 zuhXR)SXWM(oZ6Rsm07lpBREOT^M{RV%$XM(>VE$0%ctvIx_v~UR0Iw)4`e<cn`*Ff z9jbfArrsI+$ts}pp04XmtW%lgUe2r34ICkcS+x=`UOxD~>S$HE>i9@+EMF3Xgfkq^ zqu$Yvs$0orb%6E5P!mRjzlBy1MhwTOr>kkI%6t{abh9hdJfPjWv*kidEu52UUpWkO zcCSc|`iP$Pm8;%;(z!!WyVCK}hZ{_TPm&aiA8?+M4lT_4Sv@gc#IBvjKhyqRR%oPT z^^+3ElfmypJtwszC#zz&IqWs@mJU#y?8%&5wV`8)lnD}ert&Di)4JZvfvMHjs^9en zySppb!{Q;)wyoQ4>|=<zRvaH<aq6C<<oEWhC-)d9MGYRH7L|}_;mfCZu%K5T>d{ZB z%#Cj_s|2g<LAD!Uo2>LSVz2$NCQ9Yf%3SBOEFEBv_5j8wI*T&6dt1NFD(TDP6*|vY zPteVDet}tRx~0|h+|Sd7+evm)mOs6=XSbsQ7~S2wcDX-$_AJnBT7x|k#Sv4wd{Bt| zcY!wtPIL*Vau21dB+1AhyOQRcnMoeA>Dj~D<+lKQP#+TvzE)7XjJLp_{Cj@MbwXZC z%RJCdCd2z-ad&ZXv0ACKFfg7Y%*+y`0qsop&tY;1)AY{=T7{nTh2cL2v?jd{v78b+ zQY|<&`P&iCIGV0KZ5Mnb3|;i^HTBycd+zi*(P34NdVP!A1>ItodtM6VvtI*kxxU)` zdKjzP9(UbImXD62qwuKz4-ZW2fY&vJN3ozx3IfT);>`!Rv`0eI+v2jb`{s0_wJY!6 z+~AzZ^D5;)!L(Z0qS?>6Kd2UEXl=i*?y{(S_qRqR349nwqrdmqamS6T$Gvpl*cYB` ze|+)4ucPm&f|z3aI&HR=F?gC4Y><!;ahMnlKau^bE^*_pv9{2S?W^8w)8fMK52}wP zDQX4XeHiz7PFZ%}CSbE9C;<VN?c45noz?))rOy!sGe|Qk&@qvxT{t^OEACJV6(71i z-{cp-Z&n!nVr|P;a>{Y#(#U&3+S2u>LR#sLPVWy}otbZze{Gc4crN-~Z0DRRx65Wf zRx`olWU>-N^vIwj5k`q&6Gx9qkp_e&R7T)VJd88@I$!Z?y^gK!6w4?4Z3Vz&fYH$P z7Bdu4O!fDENfpkSn+g2+xf_0t)-kkc5Ju<wb~&>Dx#pJCe$^C%z7$6ft*u|-Z99~g zKNu^?@G;*00FEJ&Ul+;(Qq*!b6p7q!R2P2ceT0?jce`FAU$}CStHqMvcax*rZY~d7 zu=*@|mYkl~53dTu$Zvh)5T2?UTlOK|R@O^{K(;)3x5b)zGueJ`ySpU+ldfLGNv<KE z)1bqo0-S{i-P}6q+xI6<Pc2U!c>nxV4eI3DQU6e4%TEO5g_-cS({W8d`L(Rq7`5mU z$nz;!4y$~fg|%HmCxprji%&;*zwHy?7cA=bBW>e`-&~*N2lL#oW>&ZVYR)abxIpF1 z=`rcLCRk^-o094K&_tb2ve5QXuo_2LStC9MNp8G({P%tfF-x<fHY08H#&Y3jeYJ#@ zoX4UaY`^MNo`)0@whLcBtR3DtPRX=j&MmrSR!#{CRn*6g7qcx|Z{ytDkb(4b$)zQF zVZq^iL2HeI4S=+r<KmUzVMdvDJ6301$etSOQzaL`Hm|LhnRjt?{7G?)iD~jF@|MbE z#}VfBwVpiS2A!qZq(^^@ReG+vW!zfdOv^8Cx-ajW-wBC}7ueVtrVihu6kBmGNOt>{ z%)qK|VSsVvMrCyMD?>ATgwpyB14r7<ApYc=X6VOKStld`G9NP)fErUCIW(Zv7fxT3 zm%Y9s7LjL3@pj#1e$Pm=mXwkhk{J-JF9;xL%6!M$9`zaM4-&6^yl1{-cI33LMNM*| zYPn--e>u#RZ37;PR3tlz+bx1?JgldG9})iOuE1lH=8tocR1)LYOYSpW-%CYx?!CxP z3XeF=jb+qlfSn^0l_T!~$Unu+MCA^k0^=L~EOu(1cjU9-OIYEj!N2OS9IXf_^UXMX zjS-Iud(zivzwA{G4RUuUI{Q<$<0Nk%l2>%DkGLW`obju%8J`x|IU&0B4B~FBWe~l) z!WQTH*Hy_6eSC}tyL|Q@>pD_*drj8u>wP5#>X6Bv!i{fHy{hNyiwcak#WjzVB=yD! zk_hGr-kCu>k%%HX9C6mtp#L72U#z>-qvCAuTz~&vFgcsu`KWjkOWTl3j&(N$#ew6I zhS~#RhRv^sulj(U4LfaiDA6~1V69|%{Z0Q^TPH$0AIFGYt=WPwr=ahsYWfVc86?N9 zriK}IgC1Tr(;wIWb)llUM08mszcJpVo<vPulSTS9DS^dV6|?O@(Qj4b(vUw`TE*!{ z|M`YW?8UMP!-gpN1H?gCYXec=Zffd>NYWuMfQ#huqtoeqb(ImDse|Y1a#*|^_U<@w zBUFi7qAw<j{!XUTvSpTWgG}pHdC=w}?VP&b=qBr~clF<2rra)0M@PrvINn*eLuY7e zkj*ptxJt5z(f$TQBZ~bef(<)+L>47BMMd9H>WddCOINGO^Xc9ycA(M6OlZMk!YW0( zFl4c;85{g%AqnoHMAVVPhu{8kLux;Tiv02r!}aM6z;f)*z4yv+aw>lV%$c$rISL7O zTEMW2Sob}P_2}j6A9C*MEq*CIzg}2k@GkL$l+*U_XX8yYN-l^HF2jeEm0R6<Y6dU* z6#xP-9yWFclX<>+`!Fc|t6bU@C}{}-nXb(o>Q-FD5Cb88`}y;ATtwVMR<2#xb|ZB` ziK!J}M2Nh8ZnYqqY;9X>u>$gqy<F-C-Q#CMGr6zrem8P&QLlw^tUGs7H@{i;usGS{ z{hPidPPz{CJ$EAHGzHniN=Z^i#}3%>FmKcyQXD$cqy6k-PQvN_C*h41550!!pKnWS zxkOFJwo>109G@10_{g*_DJ>w)o{KmiV4HX?;NDPEbSQK=zm})_7hN378>0Z@a|e@` z?2j;<z8CzxD{<R>r|pcar#x>}RMWJX<k!8&@w#?P6TSYDmsaj6bk7_1r@36=Qr{S~ zowZV;hU80M7k&O6%k?*YV}hO6yI!+H@Nf;92DRx?DjZ<g&|41M_x13b&t-Cw5Xr_B zjLj-X@AdKHO-v`Gld2F@;!Z<Jv$8O1dkoM2g7LOTd%4PE6z_-q9&GpIEn5>2`n+rb z0xx_2x*S-HI?JHvNQ-~8<<mupo{BeJMp~B%=0i)|nk7;D5~@}Y8%GmXSMh);<(ha? zhCbzL?&;)!;A5H;beeQ>d=Y`oj9iDtHMva|&4DD^k^YHl=b)2?>KbBA2;&mmrKlvQ zymbv8*Say4$m{HKx_vPG$&s5285V^n`-RUYdq3ezkeu3I*m0pHU_3EIU+>^Wwvp-| z1I}EBwBN>_l$Cq*MCQOq(YIjADT#z1eWl3i+=WV0ABwCVq|rN@I<Rr?lXlb2gd<@l z9<C=EiiJk@LI?_l0h;@ap;W(7z`FUypZtk<hSQWte>$MS1$B$-V?V4P!_!7Ac;ah~ zz<H2%BKcyc){prcqw$HPDA15OI5w7Y=HcDKd$2<L^A#7s$}iRbHzoO#Xc5Qg<(M<1 zc9ilp|9m?@wYoQ#=F)AJpw;8LF&mnCB4!%=)+9u|@1(z9oUUq+qvi>K!pqan>Gs$= zNNFCAaJYWGjnI~GYgS0y=~w9avN&UQ)e36>KR>!&KjYL1;J*p4uy^lB!R!I?+UIX8 zW$-7hdRTk>X)o((5H|cAH8nLehgV2Jbq;mk>bG7TFajO5R3h9Dc8S1`ZV0`)aJQVy zmh@rVk$+N(pFasxc#P~J#;kDvct`|g;(o~(<>$S;xIR(WHc#K>HJqzS$k4Xa@KJ-@ zqG*$S1j8$A>^*ye)>qV+4oO@KuTrvl6nEOYP5_jmyt)6*q~ovt+^*u?*zj+zq{|_W zgmUQ!ruaGRdbjX(kEvD(&$n$?V^m7oQnZSXb<P30NibQ~xw0BVyCYdpzbE6?O6~|3 zq)fDhdTUlfH4=Ly<>DfgXPHNpl&tV(Gb_U%8&ie9ZMzb4yYtkOG4V&;`IZ~G4eGa_ z5|lQ5-X3qHs_|*cA&J^TEjs|Z4W)Pq6|F}qFUD)eJKo$@4kQ#f#%hyaITr26Rm5LO z{wRL`iBr{UvfAY$p`OUXCXB=HzPb{}J5FbJZT0mm`zc?@Eq+h7L~9t`7*tB3=<S~S z@?q_~biJ+lUks!6-4O?YNx5|2=pRg>w#Ru25-K&8V)y@ZPpf;1=saPJAZ$HMLe%^x z#BW>xIxEtZ{}$lx>HG;pD5tcbGHC|Q{^k1%16#|FG&tfMa^~MwR)&4srkb&oFL3>o zZ*beFP%mXPxpI<v;$|Na+FKBR;2>@K+@Lz8yR@yTOR%D=jW~XIemA_hlA^@el&Mag z@etqms#NhHYNHbs{l2D%_+*+6i^JPoqZOn_0zI?5=IHeicylIOqBcCBUo@EAyyCj@ zAf+?=W3K%H64$jg6R-=_TjIsO1T~8>{$ASQIeW-ge&;hc5nqgM8SHG_hD0-G4oL{! zdFn=AI8E967!VleJE1W9JvVm#xu2PGLE&T0n}{d>?vr*-uqT>rRz|Ip&rExh_O<1; zojEt~6ICh4ngI{;j!lH3PxANjy>MldmY3Q#fY874z4LlvOrxIT$_Vk^h8UH^tNckH z`Q>YZTJE%SXv;c=PB(^~uE?-}PR6|ZjSo1^T8rpsFB$fd#UPVKzz~rZ#ocBSS$SvU z$2d0Ol9G~56XK>jbs7qmFtOz=$dNaG=__RaEZg=4nl@=Fm=kKhGYO$&5O}@qOzeK2 zSE`v}%eN=u39!GwWlMh&AN!<Il=@@WYW`Sul6|kQT)XMV*fE^rqGw)uNYuN>(I$Jy zugY`q*Vsqz5Qf~L<N)1F<B=QZJCV}N^fp>0wZ}zqS_iCuEU+;F=!h-~MeaU>zjXjM zxE*p~9467r8k*^XPr^=?XA~Q)UVUAlBEXza<(M;n?c2+z2zq|Bxn>#mBRYk1KW<39 zChho^8Jl_W$o|b7GP67haeb^O(5=aFB+>Wzz^AEJ)T*0_jEsgy&*9O}&SFcxXYCfQ zUq_o++nh(%3I7i4TTeTo^H!w4hTM#ma_jUA@TNqgo?2}Hu=0)gQ(}iMh`rS8xAcso zJ+sO>8$S1WUgVMU@1DT|ptf7)4;d~j=;3GXAjTmFX`I(}YX(GEXAw<DVn$~sc4H-g z^G?7B0-OnmUxEz)WaVC*`0wga{pn^xM&__^_}*KeZZ+O{A*~yQmmv>=5UGILIe?F? zxtM#m{iJ1>o=Bd)8(UtKN}R(-e$lanI34%1v731_mJe%uvOK%CVUb-klLL8UWop!M z#L0Dy_Ao2S*4N4ys@C@A&LDF`I0x@7({;uTLgq&nw75a)GY>h}+T`apn*^)Wk^ZX% zh&F$oIn~OT!$qdMftz{b9<-J!j@^H`iT>nD&+6uA1?uDpJlUDgwdv8CtYem8jG4yK z{f9t1X#ZwvMqTee<i^t8r$9W{Vq?l^kuO8JE)*V8r<Qv(4eL|ReD8~r8w$F~(U?^_ zKjru_B?iSxoKF1G!n!U=F5M@zUT#H;=8d-0aE{%fM-}>dv<e)^+(*7yJ@HWv`2;6K zftARXJEX_}DjA&Gvn>V%Z(H`(0UJ@zG$pGNJ6fNf?8=h;EqOU*{a2H%vx_r3v|qq{ z;;yc0oDAiXuXybfC|(=BHWRDmx!tXqYAN!UVV1*`c?w!6H^ub-Dh;-GTywa!xNXR- zFx^BW*IUXO4Hi&8!;PK-##A72l`i-3{rz|WieDq&6kfb5Ws;`*rfEsND5?MRgB~)A zeg40tyajr@quIp7!1C)XWM<TQD6_M@k340d7TV6pg|^EC^D18HdYvL?pZm)@m16vl zEFPR8y}7hN12KhvTu(tOY%Ycu3`f(nOQQ^hS!N|KUbI<jB(ol_KU*=pgAm1nf@z$3 zUNMxZtX5<`zD+YW{wjD;LO${Qj_rWWB%|jnILYf>x;kFPo?)GZtgE87YmJKsEJLZy zkgXbK3g7vD6yZubPe)=c9Em#mDkv(Q2y7*w{Z){S0U6_S3jVdn>!%B!TzxS5Rih+w z18D3d73VV%`MrOECA$^gh;XXihxF9aT4MxJ%Ua)@JX?SD*fwV7YO!_mql7Y|CUYFM zgW?-Cy^u!jbIY9LwE@TVBtPQBL{Q|dTWe};d;<-Eo_$3|)+JLKBBbA*?C?Q?f8{eB z!vTfSJE_+@UNY`UlqaDo)GYiY7q;5+Jv=ScyF2l7ZbLhYsVc#XEzBJWxzAd1%4AfI z>jAKfE?W<Oq;j0PFm<A*@bR9Dmx^yr-{VlvzMOgE!tgRz40xsU1qi0CB^H!o3%@ps z-L{IMyZf;A-=rr%i1lJxB$(|lt{pB}>PE53ewP3=1Rfd}i&uYUd~*M7!<q#8gI)Fy zn`>Mxsn585cy*Tu0HaG5rfqpNFsR^oR`$gFYUe|TU;K&$hcn!`@)UV-oR2D9`9vSl z7GTINz4RRi`lz#(m&#++-K)%xc=t8jWM817Yqu<~Zrj{mC_tJ~8~GvE`g5QPTCEhD zQg6H-RLYP#3sK{*=&#LC?PoNbG~eyr(vfMj1si+{qTeKyejlc)$8n}^ofcWG24^Up ztCv5#xOEXn(e8Z99>cnx{)LQ2FRijWs}H(&9u)R}-*mdQRB;QfBR`W_UJ7yDHGYR$ z?bR5xE19_1LH&S6Mu$#p+KWlje?UTszF>U@W;wxM7470A{++yhkA*k}4CXhFb!2Uf zo)RPE>*<MZ4@~?nd5+)sl=LozCHv-4|7geT;Y=_cJ(@!C{SRHG=X8@@Pk8z<NOqKP zOHv+e!csnZxi(;8wTOj1wB=5X>!^I<q30*9e;o~~xuiGPB_Eus=IQ=z%`?QN>iMIO z9kDzJt5sHox)B2ep|&n$zxjgr(fWh^!i{0P8;=}O%d`{jUA7x+^yY(jPDEvStgC3- zo&@=~9UauV47&Gr9;|knrjE$DG40pwqL#qCt(`08X6CsS!FeI$OV6a!dW6Z<h82ET z$J_0GTpWi{1cdbxQb~*np{PCm{^*)f*YU@-vd{8{cKN-0aba8unY}@??~$Nen|3nf zeSXF@InN+>daXQFk1{W9Rz>x)<D|Aoq5H*KAz^!I;;*fM*NPE%lrJ@#*R61ZOsX|+ zGTZKWJE2T$8`F|U78_W+pMIY_?#}rQa)!%E@zi=#O4^0r`eER>Y39j?^!VyQrvQ*! z7&Xd%Ifev+AC-n&`!J1vvSz0`2o(cwFil^@usc?6Zh!bW!NF*+zkOKsA4r$T?HwzC z5fauR7<Kc`!3%h7QVWLQFgIXAnMt_^ImHtq$6a9~s;!J$6*?yy;vd#_%Sq?jM^l*v z9zzeGd=~7uQvQ|{D<;4m<i6@Z*td1JBNwU-nyB1wz5i8+^ld=n05LL<0A{Esx3c`F zMCsdrK`d`!L<Q%_>q__SJ^snlk*ZsfqL-iOiUU)Lm$Pwj$end;xc)>y`suobZ^Q{r zK{5g$!Pn3}X}Poq9v;_>jN;vkzXhuWUAf+slXQ<jf$=p>tFEqoeZ&8}xz&dXHp-KZ z7P_zVA3Ia|K2X0(c3Fb?IRzw!pFgYIb!a&Hx4xR*d_nSo&A`v7Pgc|n>G-Ttr}hEf zz+ymKApvh3wt_a7Sp^(^AP1T+34=^BNn8*!8Nhb`VT=68X~rliGDQhydTUfUt*x!x z%C7>=tSE_B!Ky04uoD8`zqmHa-*yh*x0tgA`z^v$nmC&4HP8I}<J|%F#3WzTH?(Hs zCtm)inyWnxd4Tp=hsV^%H~sy<_-9h$emp1KHiay6E35RFKP3w)zrwi6h`2|7et!SC zISjG~O-^ugUw*eh`EO~1UzVc%=N=I67`Pd*VA8<INT?xNsw42I*t!4tOMw&ky(Bba zwlg=N)%%~!4xqRmZV^TK|8@0GU>LW>=Rc(@-u9n5cG}lrVThD1eO&plgw6n;;dSE| zXOJDn;!<UR3kPv@cXuB@O8iADDZ?u#+0rHy@r(E3)j7$|O6brKHxn(odp8jf!ulDa zg2sRHpspl4{P%-l(GaVi_zK{^OkgoXEFN0Lp6!W!_*cll^m8X)`CE>Ee!<b{4?8hU zb&!6V<nosa*7dM!vT|?tEo6{`KlB{`yW)D||CHG}f%Cs68l%dpe>Eq&82tR4w*9To zhi=;m;ShJn<e=aK#5+3D{MW4r2p#(Fzoq%MPnf9N4w5oXEi<XO+|{9Na&l3EyXMW? zrBXltS<&(AnRB-<t6s(htA6RcY6^24@-jmopCFEPE8)M_ng91XDVXy#obv26w0c@S zBD}pbHIfM|?qRXXpJdbVLa~><Ld&Ufb-crkRDyP4nvPQZa|`x4>Ld$XHv4mH9A|Ny z+_JM%IlT5aJug1jY?hnQc7u2U(<IxU9jo(V-Qs_~$MyGnnI@X4Nx6UE|Jvo_f2fz~ z8!UMm&qUpP{$5M18Eo|94;PdDy`mksB9pllfq$+h{Q<Eoi1*To1$t88uwJ+!;F4Wb z$3xxoZD$_p2OJVM#AKF{YKqF^A6__G03?&YyUS5}U-BoziL9o{s)8|)U-Zw~+nz0j zjIR40nnx>#KzBl17;%)Ohd)r?f6!d!=&m1^$!AIA^Evy(a4GsAbeD{EhFD-)x?Cc{ z1y@Y;iOp_qNM2lZnPNir2bB2HpEpn+exknkwlr%4W1w%3>&<-+l@IGM!uSdB>%Of4 z{FsqS7oa#b+3Sd}B%^@hmS%4pks1;EuKZakSZefxfcz?_|7$P&y9)pQ2J?=8>H^}q zN^g|c`RCjM>HX2X^Y6X+`<s0{GASkWpZ@dK694O^f1b$SUv2sCbK?ncAT)YYRJzIi z=QR1>*T8%9pVK2yhoEGJhZD}!ZYyb=&9oc6YS%3HcRAq#18i$rTle4wke$=<2D*+m z<zJGczk#ZL>EH10{+B8o#3K*6FkKHHFnRA>TbX}A9b#$5-Tj|Kp6UAkz*7I$p)>7^ z)%f}3T`N-JLourWQkSx77W<T}7R|fUC`lzj5MbmmcpR%0wL>1_Zvi#|4V=#%b8Go7 zjXovj-*>)i&K^QT2N{^J6!pU4)RR$MUarzjN=vbq`YeH$LEuCW>YtEK!8-v*m@(|% z*6XHV3E&P5d0B{yZn4AeBS&Hl*YP`qV1^hn2ICsfX&5EKJa9huxfyybOkuwJI-eLX z__qUcfT2O<)n!t=kpD9ywp$iI0U5=0WRW#M>%dP&aUe!-#xFDL{d=I*YGu}<$o%vl zP44|Em@-rCi}7Y#0XV1`=6!f}go(@c_jg681-be7V&QeMmqX1T3Wr~qdD5C^p$XH; z8vA~ic}YKZXm=qR7|Ct>Ixuh!J?lFf;M{ElWew4l4oSn{$jH3pdja%AVR%!V<1%_c zZlRGK?TSywa-r+F`Q<IC_7JW%`p@B@#C+;l1o;tGrG$HYYYi)y#UxPNwmj4P{llw` zedmjlbdqU7_P1&^16(%DYm9uE6{613>3lP&FLGX@^8Qwk;>UJP*YCisgoel(dN4E@ znyHDyu9C25#4rIEYa~D`!KI=AHLQBJvAm)>C=To@-BCwXv}|FJF;iV39A4lUxx;nw z+X1j2=h3C2>$I}IFz9!nbpL?^7Xg>S(5~4<6d&KP*8eUV5ygZu^I%z%W!xl#SLiOC zTpw5K+niKTjh)!^=R}nbut{tt+ER&%lR0VgH9ftvbm@X{kg%C;QK?vZxMerdKUn>l zPPz0NnZ{?cruwSnK=WnvaV)$#N9X`6VkYh<P3g&Fc1@Cc{nJYmQFXrmhqd<r$GYwR z$1loAGLj-Qqar0r(y%GAM>G&JqB28d?^$N0vdK&;qO3wniAoZM%*e>z{9Yg3_x(K2 zcl?k4ar}PQ@f>$gx?JP)InVd`9<N~voG8}ad4OVp<dl56>nHh&|C0-iaUjTj$dGC4 zcVgZEejnU+iN(98Cc6<;2&Qo{eX{^J{<lWnfeRlS{<K;X7e>*3WB=y-v&7(tsfkHu zpUyq+@098D#rSRcnFHUm@Pk9Wzdly{A`v*-jBYCZ0!#i&QkRdV^Tb~0$XxSFFVFLG znLw~jZN|M>V0M5`^U<4lKRGyiThem;Gyxn18;wsXn}4wFsW51rz2NUCHRqps8Ub=a z<WV=L@t!wTgo79a!Z+mCLOAFA`D$#D+pv^?Fx@%)Gob;1h5*$0qH-UA!l3!g@d620 z=cwFWKSA%agvH(T^mI@Coh<+|g47eue-h4f1P%uB)oI{S5c(xG1#lN5oTy0<)9WfR zgS4d#bSI+oR^YmF0jjMZ-D(3izz~-Ssi9R<<_iFCvycEs4ex>pXWE?%q-Nl9oCjv6 zrM1<>%1RjvhOjsU>`rOg27d>0;uPF$9j;#iafwilc9`ff`Y8mTc#u+!O-%_~tE{sn zq69EUYr9x&RMZlg3dBJ`t}?Z@4#n&rlP#iG1p2D-0};kZc^vLUi<x0XWHnGJJZd{n zI22yl)oeFBUU)wL>0nbxJ(aj-rv5e(xS$`$Q%~j^6m9<U?7`$*src92N1`dC)E;+5 zdEXSFa-W<UtOfmV;Bc-mSiealwO0tHne*iiB`qg3G0zv)DTlX;CAi&<R#Z1x{5=LN zLtuO2%=Sc6$#TvqF|?BR$4Ge09^|}o#(re-Dl&stIrq1G$v<0_bJfBL5;lV^LbUqT zUJusn6&JtXUzy<et1Wy7IFAq8q$4Pfy6dl88)CR-KF6}`E<SN)yyvYPnvKNtsDC`Y zp#H+!z9}WH*E7SdwHW=(7N7Ff;n3@Rb;(8S*Sjfe@t-Yuyy0i|cXhV=KFJ)G`CZbf z!?i;2x4~2924hrRa>36;O@X!9?O@x1OX^xSR$XkaM`PL(+;{4ykvN_?HBzY>hJtGT z$Zh5_jG_{T3K1dfpd9(kalk}>28JykAPWJhEkYZLxY57?67<erU-EZ+^#G@v&>@Er z#a*n6PbmuP@yt_zP(|5;F4+ZkkK<6wSV^tRkCzm6al7Aw%1Ou<5(>9aw}4Dj1au0? z0pOK^1?equ8&05G```pn4F?aXYv~kvJh$iHR&v?QY)Z?{0H{{Ner4tB^NHg4k5Y7< ziEY`cQPk=6d)$JfE-N4Zz$5GUn9ceXcYJQ-?q4m=9y(F4;|i(eOxfPjzKRku9ZLJ? z<mtKpyWMvly$|D8D-0)G>)l5j-d}w`b2s|^Qw!*^5N69Dc6Sa9g&{&;b817-CJ3@D zP%%T2L*?M$K!{vGE%HXzui@cvoDA^a(ac`NKN5Bcuyon>xITn=S7Vu*b7of7ww*hv zAq_2i`t(*zjm`%|Y|hy2|Gfj}2Ktlqcr{`?38n#dq`Z?;QIkkTA15zQT~+nzH-HYx z%j@i%7n}ScuHrZKRy;}j*LzFj&*XWR&ZNc`@^2Nlp*3S3FueFHF0V?;bHz8Ph5FWK zF$&Tw-BeB5hv)*=4^}SI(_S7M+)0%k*Ovpzq8Yrmc}ET3d*)O<9c~;X#ZG_SkdtH| z7@QDtn8Mjcd}q#1-hI2Y=W8aLITmfcADCU@ep#Gk7d5UYY{+v>`=FvK`?|c4)70zY zeFrH&7w{gC6}|Y``M8ve#8qRb&Wl5;L31fBL)Y8s4UQTJwLQ^!B=t1m`#I}&x{7;q z8~W<sk&&96nzh>U42tNkM{yay7@W&IS+<o_ne5<JEk9DTFgc@<mqbH4SiHc!zi|J0 zmF;~y4j*P)pV1KL9ZB|*-cPA7d19zL__EVw{R1;wg!GSAG!A<%4@sQdeR@OafzA4C z<%;%YyYqI7Tz;*dsZ|)#(_*x-W8aSV+>TS^jzg`H^sLNxfYwfdI`G5CkA1=01uc>~ zS%VsdB!ojxe0E=6ltAGQqI@V8+VQ%j#l=Pxm@0P;Y}lpuASR}^y1EnVTuV+6DkqGP zVg|_jcpaQhl9Q6WAQ0N$_6m|EthaC9o}Kz)#P5&~8TBe?s6q|*6%1SVg5^xGp$P>K zm7jzbV30fypuMJ@mELpHb<vAL*QRI?HwkMTgxbi_#=4gqLu){5BM0=+8&F6kS5Y=r zR%39Z6TdG5?i&;q<_D`^Qe|x|6;5S<8d<=><JQ&Nxk(o4onYo+Va3D*1_q{LTf??D z>gx(J7(%KPX9<;+l~vT1Mj(lKFn|NX$fzmq<<KaQ?D`MSkwJ@;-(h-gVIdWIPFhD< zLc6dsQ=)v2HU|Y|HhR#`GoG2gC?hOoQ|d9d^=;oCxsJ<AjTs4IuU7Vw&0Ow?ZaV7p z`(SHp+i{pEKARbc$jueHd-#f4cInN;$%ni^7bfT24^5?7c4G}b%ziy=hShdBVZ~^O ze)O=<(9p%%rOq9?l6HzDatn1uH77d5ifiHPErgRSDmb+qUg&D%khZAneOvmqhg0g> zqJrJ2!b|*gJ2<0SJEYVT$Vu%L4X>T=RDTNA7j;|bIx`xsW^h=kj^of_o+$%C57k*_ z(v91<gL$bUUViw>D46Y^D7SM!Z6ndbY+rIy+C6ek<1LBfxs&|jk6B@>m9Ksd$4{tM zVFsQSm5JTslU$J8Gja3P(?^%h4wf7m`;lVO3w<YOz!G<+@xzA~IJ+P_NCR=8CpI01 zR&)=62P$=)qQP)fYatWDd@v^>w1u+l(R;Y<N03WkznSiOSwT?|Hdos^DAwEC+dp*Z zSA#;%u|YR$YwL^crKcX5^y8*~fG);uT~R7%3F4ZvOL?5lzJ{Y!E5~Q>=g%ag3XjH* zABp*S)8}5J*<P$y!mJm00gu?x%HKcy**+3Z-%32|UvrF~g1}!|Mn+xv!<{<`=pPmg zLugIZcu){^@$={Xhbvm41w>fZDs7}CaUQK3zH~1kVY%j#z`JEIBYrP0xxuJJHhUN= zB=(S@<~LJBEc=JGTNxSMBRA*8rz+(1k-Tm;81am~vpHBgx|eQ5GN@S2uI1|fjg*D7 zHyp*+iLVp~E6(@Ljr8}1>S-lfH)jh>@O(}V<#qB`;1v^N9vmDbevvkuw(;AetK|zS z<u}8WV|U3s4VfHtFu$V8zpng5g#kI~9Ubk9p~@TAW+nz}cIo7wDr*$c)Yhx2sSXV~ zyy^OL&+iH!@9|~vb=M|HUJbF6^OW^{wPCNvpt@E^;iCe1h2fBcPMs`6?8COxJ*S8T z{eI-Vk>fp1zN51<-|Eko=NLRa;B<fZ)#LjO8VdQ!$*rC%BTL`5R47iR)lra>4T|N4 z;$u*fzLmMpgm#^Id{UKqExDw=&JYe0hE1FN&^#pU1(0#IJa-m`eB*Us_dy|{g-TK- z4n2~z=$6)6$%KRL)z$JD5!lnvJn@IrI5H=4#4Ca?2%OKYm5KRIvi1tk>i{*^UArmn zs3Wwwn>c%5*X7;t4yS3Fq-UvMrjHLfCeMu!EWSPWy}Su03>oRlU{j>vmrtK|H8E&I zHsQ7?g}={A-zZKoWQ$!`XizYF0WT15#JY1}KFu{_an`~EhmncN7b;s!VDLtrJ#)2H z>(&yOvuw)B%AyN7XP!Q2>Q5=IMNsHDXA&M1MAJJy_txdsBK7;rEA<oUsX1$Q+@!O$ zAMnB+G>Pf3^t(h`tFHY@=F}-xCyt>;XN~2Fb)Wa%P&?cEG%7PXl<DPxfNsZc7h0$< z-LTzH=25jobJ5h?yw5_<;q9-ldk>x4B-E<kcjkWFo10U#`iGoZPwqbJzkPRnQmpE( zC!<ZA^+NF}q2Zw_-?2Bs*yfo;_w6gNRn2_IaN5kwjQZY??U4g?+I_tk#`-qqrGz#Y z85NyQW+^E<H10;Gr}29C+q!pxjtj9d%i-3NqZ3wKldf|gXS3aFXddz3vw42ucg+*~ z>FAW%_g~(>S9>r#(q0ydQjd_`hic=}Ou4ok@Z=xhMH|L>bSt3+3qux&Q_yE-_tCw; zjq@ze5%ySA>HPvKu@M)fUs}rC#52?rBcL*onUixuwR0sIKpoG-$3C87Td3D+Xli;x z{u6aov<;y!l+`eoM>8)bd3T3Ue*N4^*xX{$t?OG}%-w~&MrC6o1A6ATjWS@nLQxM^ zJhyC?u=WTyEOap+K7AU9d>ui`7ufhWpAqeaQ8R0d?F?~KG3-~XO?@C~L{<TgDmye@ zS*ZmZzkK6@&_$reP9evo(q|Zj(JneyCuU=t7kmCR?S5$fX6utjMKST}yVOtQ#F4ya z21dz9l!}xtiE(=dzQ^@zj`N;9=uEcV{8i%73<b9JKTm$BDe$em5q-}W%BsaKjsg!o zc1NFX*-}VvV^|@hFuY=Ta{1ZVmZIEt@(+6>&$pyJdh|%8?ZMenpPn=-(cZ-IzA3ic zQ>Bzk0h7^#?$cxPpRaDNKkYYBkx(mqCN4R^IQ-hLgNF{AG(@oQC8$;7y&7v$-&!yQ zaP8Y)ceJ5R+@>l!&rxe+y1p$l=_J|bY`tBM3md}h`!7AS8{VExo%4D~-ToU-gw2J% zK8wJN`Kh{^l5!7wirXg4JAbw^zg<p!DgLHx7^O5uHNMzaRp6TTx#7+<W$T_0z1#3k z=;_V-;N3JGIV(M)A+=#cxGcvPgjNv2UV{C@z{<J~uog=ztG+W|FdxAtnw!pyM^zUS z6Z0GCHWUChK|c~VF)}+kCI(nuziZKo<MXz!&D#5Wdd@?%lbWOf89JqFsMB_3&5ZY+ zfFE4S>rpaqX)h$$CMcu`^&>P9q4FAOBRURoiCYlA!-1Z@nS1BY{_*}zPM&DKD4LsZ zht6!8>+L0^D_T<_gPhUFRONVGCMS0@NjY=!byPk0UEeW03ohB*v=d*{F?e1O9<b6e zS}M7WjGD<U&YMi?qr`5p?qzx$_daTF{rvQXHZszs)<R0sK_{omI{zHoA43#R9$Ia? zD#9jpjr{W2=ibqJ>er@kg@lCI+B!*JR1wnB{?z!yOq=EQR%ZQ*dvb?`5-Zs6oV@hv z_1H?i-Q!D32Zp|F_Al@@P1e<1pLg-Jaa+K!?eK`8(nd+Qd0Jn$4Kh`)Hl(}uy1!5D zIrjSfdqw7>1Lr?Qx*t0~$obWCEnA#Tb!Frl<9>Zx@|TL1YtU99&Wbx<Y|33&c9QU} zFIwKQv^CoV-g$bb$MvYPhu@kHRK~x~t@oDJ_&I&%tG<kZ+)?ooD^r1iACb0;n;xvd z2fH?fSRo@j<SWd3O(`nUcC|&urLL<vB(>%3azjGIO}8^RvI)0A*n`OcLS|;@*WKN1 z2fq}8b0|Gx^p)dj(cf5rl`D>pgs~J@=O;Fbqi~XiLJoW$XpuZDKdzmcoUDfTl?bxO zv#=QH_`HX|;>Iq4WaNV_rC0YsLU}EWxJi{5J%~Ur{tdeU!1A|5Oex{?#`i|3KtBww zl%3FFoQ2hV2wyuS1S^rIVqy<1cL*#WmxMD3m$|vQ{$ukE5INbQv@C2?UfMFU58a`6 ziBeoxP*srRfh=*?wO`PcqQ?jYiqkg*uZM(0{VMkVZUku&xZC+*)^t1|Go;2O%bz-A ze04FIhOuwa+3qoN_bA7T0h2XP<j+vo&5Z8KXd^LMr?W6w6E%+NnFBS|tV|-Ft=Akc zec6!LR-|~vcMv&OO?t<k?yizh+c8IPISVS1bj7wM299zz*c<yle|k8~nytGhGP>Ah zgy)qyTjFSA!=j_(71`=OYWA<rYojgPU5{&b_x0KS8VHC@-ZjQOKo);`f6dg}FN_?V z;pM*9lJcUT&v&#`7W|(0EyuNmtJu*QAt)rMb+iBE6UVUlgIeYlZQ_11k5ewHN6&{3 z5@pw<^`L3h_=e*rPROZN#m^2SvAH_L*VOdc_R#YFR_8~7YGxd3Bj#A2xjwUOdcqjN zab$a;@Z)FZQEA2$g*29J?UaEt95VQNmY2T@bxedG-V|8DB^4>~<w4p8;lvRZaWi(s zg>Xfieo5Pb5g)`;#sUfQ5ake-qaxzL=T6r?(HPFxuAmE;rGN4A_WB>)ty|O{sJYC~ zNVOD*Z4AF79(Vdi=ZWqUI(PSr5n~22+(We4sy=^a&Hz<)k70!e$HL6$F$jo4d3o|% z|FJV{IKXPbcY@C=6(E6F5uFh3^aeE#Nyf>{ZfO0k!9hi=4j3M+XMc2S2N;Rah4PM# zWu~NMA?fPs=IR%aXJ==#9Ci}&`o_E+I?O>xaH<*_=%Jce1$$b%!6pH}fB<FH+!ZlD z*=>Y=CRSzT*RQP5U3h^;LHcr1N^WjgW1|K#&nh5N3HT=%w@}w4GcfqBPG=pfhlNN8 zaH3|GR#(rj{BaV(-CmHrCivOty}qcfUW<XzL5Qb8eSuIlhF#qT=&1Q08o4=SgEF3I zF~H?D1oJdwjJcxx&Ks&A>JfE~cd!@_kKhxm3g~Dsz7$vJghtxIhYKV@-3M!VnRwrw zk)Y?0(*Le0r@e+T@Fn9R-c38M->ZJ`&|q)YC7&B+kJq35yiY2wIW9Bz4699hsZ9Un zdy;27`%+jl4H{?{oQ7*ZEFIJQF3q*<z5G*rwxXnctGM{FmKVeN2OYNYJXPe`wQGHN z{Qm06#tW9MT27(^Vkeq=+;S7jk2W^9Q|>C-Q+qe5q@~nq-;-Szs<JQblIQC1=Q!hh zu4+6ysI`3k9!`VbIMcq}?Dk$3T^OgItgU2R!<Wc$kHgNTgYn>Vi7f{;=Kx@^J2xY9 z+y2v`?Z3wd=-hT}JD_-}Qh4Gfv7`ydzxbzC5q2_CFOyD=G^#P{#>y6YzI-yqa?pg7 z{Zm(FHp4debTLiF6%~;y?VC#7@A&Ah*L%ZZl+p!h;p>=8@EknIa&wQtOWg3}>}(oj znS@p-dgIaYzpOx*-@A7&ArOORWO8~s1=3606FE`!GsfuWq02`G*6Z?+?Pc^uAd19^ zu3RmgtZ&4|QlR;WgYfuCzow5LqnZeLyaN|oypY5gV@&|#Ch*9Gf{LmeA+V*;dJ6&@ zB@RP`&`x|3ttq1a4G92@$&CRH1JSkyT9%ElS4Gvo<!#~Q^z<ua=G!&~(}ZPZXA7c2 zrUD5{=xi|qw9*xk(Ao9YG3OeVZACA$3K5cV>(<kmrXFvW``+IRg>v>eH2r~(@kg%` z9BgAVGk)gilxMX;nb(+t<!NQtaWi-x@g`?<*A8_)D}Q=>=RjlH*}FE=d8``R$GtVX z4STz;i5rE;SVfSxexB*6S2N-p_dG1pTPx6V)$#s2<-0oL_hub-G%?#B-1H=9*5#nI z>h0)#p7g{jL(}M;V2Xr<1hGA#jtE_a6Uxdyz}&(4H5dwQF**82(Q|-0pnr69bQ8NZ zsKGwR`Cbx<KE8_Q(`hs*c*W$x@-&aVl`qBNb<Bu9P&i?TW7dYu$EIwzrVKE0+cwc$ zHHy`up@nIa<{PHM-NYierNf&tnhi+}Q6({M=k&KwGl4NQ2SnhcAz=&2EPqswspx0_ zgbYtfXza6Rhfs}DNl8g9!c^=yd<tM(1Ij0*p5B+ycpdD*uBvL(j+*N>mJ6^;ZD70p zwqLmFOi#TRgJy6c4l5`^QbBr)sBdw_$Kcz?e#Aw?-22YBG)$5~Iem!*)Cg}8OH0ej z=H|_WI2Lq-T&frtV>R!ZqBzN1E8);ZyJyLXwOr2qwmxsl#yqf{4?;o_iO}!gKjhG8 zuFJi9svS_YE?5N+CZoUm>f!O)C~dVYi3s6#OP?2i$ZXC#Q_anwnT5XmT4aBeJfjpR zl-402o#PJ5Z0W79eI;?cLYR(%e|4OT|3s}J4!ge|_N%25F`C2RpKkS|t^WVa7{=8V zQ%PKh)rY3QKh41Z@pp+t?61e0)fl$=ekodtXaViqB+Agi`oF&I|M;sZ_T1QrxV(hM z0JQQ*aAk9GbL&PO(~*7@WoRH)xp3HBc^*dmZ_U^}0g<(TeK4~&EGQ93J~id&(c?}i zLYVW$-Me9s@P=X@6PgRZ;jfr+Df@JxAgYtAc0JW;=yU%4RW(`TA|t(_=~;>HFG>xK zlPBY@iNhWMLQ<x`J74_#R<aL%89kC}Cr@q`6%|c-@W3#09v%GBgF1tDJ9qxM3Yx6Y zRuDt6{i3EuK`@Jwv~}xN@a(ygGBN^Ss;pdBV<G|z4&w_K)~Twh5-W;G(Og}hrM*X; zwvB<>78=<qxAu~WSvOZhnt^azM<e$K@*b_)XVBkWOHPJrYz>L%tg+H;-Li!WlUYKF z5cj?^Q#18Oe7vA;KaQl|lapYek~um$Hlky5SJFLxOMLnI?P#B^KI--&YeSa+Sk-L; z0!n=g*sR<V60GPv#>7mGd}l-W3q+3+!O|FW5MYWxA>iGy<CxU~Ms$Ri7GP<LzLXm} zdwcySCmnBdUO+c`Eol}S$!+f;M1m#E!_QBF$vPYpUSS_JVt#qv%#4imK2hpAPT)?s zGY}fa*pz{3X?)cFvXDm#+H$8{;-6(JSVc=qyB8zGcX!0;ks?}R<chI9!oi1Z-CdcP zna4k)kI6zHHeqO<b+r^PNo;Veh)NVjS{~i6Ho!Yj*|qA*z()!+bAlow;^Lp~hWsls z%0PdAe-*ZUqN2aMuS>nGt5dcr&W5wocJ+iX$nwKwg8~ALGLUh#h0F1qho6C$g2?q| z7BQ<$@DpgTcQEjm-RAYp&|`9Gwdq8x08LhLXwO44;T&uXk+D*M%X-e*dOZZI3AJem z6T~#Z^g<Davp;_Q+EsWR1aiWn2U6A#b>=!>_@)Qk%FfBDLS}-P89TY(4SXUv7x?0N zeQo7v&wN7$j7uNaO1S<Npz#99;2%(@A<T%O^^@M`18A);=B-fC^hU*IcyT^sHE0k! zQ;-TWbq_HuB|;#PkfR_BMyT9&<On-Du*GnJ^ZLeVDQsBkg&xjYa_=4~et!P6`}e!y zucfshCL$6k5r=jmoD?CXu10#z(YKC-MDESgOEB!BBxU``BqinLg<~;Q!lVj`7>R^d z#OZ7Ua&HodDJ1Ss2U5CT5wnGwp@Fu*mfOXC-rRx~VNY`QQ7i03ADG<R%bR<iCNFom z?wbV8hKZM(Um_BQ;A$vs=*ZPdr-5HMB-nzX&P0U!w>O1rAv#E$_plGW2!FATC0J4^ z4LW$_csb!7qRUJ)IUsF&9Ze3xI0ZxdxsiRmylY5FX*#y?vbws=BnEbN+QZKmNBgmY zCVW1L*B~rtidbbEZBdM!wKw4W{(ObyPSLQxjm;L8FI`79ICZm=YPdHsUQbv$r%TC1 zF5hulRW4QZ${yZYiZ_MBzCCU&%;hC#4HhtLI)@P>1C2jXv&^;QTp;o7@`%5E`v=A_ zn3BdeJtTawi4cVYV_`@{Z2$g7pyyGJnSM-DI(T)Q4`+n#xusIS7PRU#Dm>hkuA+1f zpy#544ZV`6s)7Oq5@fBm#KX>X&<u(c5PmCpcO{~ni`eWTn%YaQw6a&HI*&xy9KJ3f z>1%B9EouyxnDeMR>sp>DlXI3r&L+b{1GDxw^$F8^$gPK?32%yQqY=vR^UFxb$w=7F z<Ro~P7Fd4Zfk+q$cR!}-4iwpmr-LlJd9WCR(V&nJSq%+_nAlh#fOO9pKzUUs?=&+M zZG6y1f<!-A58XBc<O)C|Cg<gCfI|UcH4GYS6yFcL3K6Q%bCk{2GdxvGOric2otvLs zu@*Z0gc=eKa!BcZbar~-Y$pQaL5DtCNL%_789l^tq(=r5@zZ0lEQCtnTF?s5qUu0< z-2cv<Gs$lL5dKQSjw3RbXU?N^mo5#H|FW{QJcnKOv!g-^ttT86-uKlLO`uHzS5+^3 zauV^cxQ8(L4#3tzP<;p)9rUhEFnKO*nXI=PBVRrFcv3_!PyYM>BET^u1TdYUAO?1L zK@DBq8f126ecJ2k>5tjk?!DYz8jL+eI5onbrW#2j&Cz~1`)vmpkhVboY^PNc*T!IZ zqG5}epP_Qw2gyJR>I@VZUgt*$UzNMK;gI=!3F!f>-!S~avu6(-p4R7|VG}`2O8$g; zGIRrQifze$fK82p-479*Ss!koWF`f((F<?RY}hXaVSQ0E0QwNDmwP1H;5vYm)lfr_ zv4u!nRv*X?X#8;zXsexH{e52m4`Tl(y8PEHqqB?Rud`V`J?uE234=1}pYtb3nRR=! zGUY#^{=fe2|D2)JabU$zdMk@zY(|2-o$20k-oIZm^IPTxOTWl&pPtrfnen4TG#&4_ zH%6}gJO9wR%)+wu?LGa&Zif<3Y(>44`1|?<1@%;N{*xU2-+j`5B{i|4vdIjx$v3=b z-~X={=s&)sGLhY`J~Rh-HU_i!ZS*3F!qrELRzPs!pCXa?dy4Qm8=D4h2F8C~Pnz_M zWJbk^^@4W8^WB5|B<VF1=KQHgofD)rWx|@nvnMj<$bna<l@+M|``&48!xg#tx;ozA z1QRj=p$-;Zo>rzSJ<d8BWQuNL{i<?ioO*v}kb>zcFU5b1Rh>WLYRjeh;||LWd-?yK z<E1xsm3&Id%v`IWpdfTxJe+LX{HxV(KTk6g#VNDjQE-I+sGy^Jh4CjEgG8*VOzx7h z&m+8t53fg8XQ)tdl4EZH0JW3MVc}LJlFC-+rSm?!g~=>F7VoMyDL8+a`1V(Or%t}D zGPIWO;9R}AMi;Cq&ygxKF1Z=wX^R1>RUPUHWTa(}aaxb6j2CHkdKX)Z^s*&!Z6ix1 zhw|bA%bM%9L>^xodAwTsn#Eb>n4qAayI9#idm*|jbzMBsB6)Gqh?Z2@W+S?Zd1c{n z9DY815_xw@e%zNc!>AH|8|8~#PbjC=Vyz`|$ko-O&{<_@`Sq*B%Cbog_tadsE3x*+ zU*|t<^{fbd_+{&dr>j?9lkxSh!fE5tC-h8KAt5wFc?5n~!DQ^)1r41#4zG-)-yNfz zS6A{g)2d6FgUT6Kmv_kPCa)J-Z=TSbS@b`AN+jp*VoElJx!>BYK2KV|OmjMVtLZ_Y z)x4O4=jiJ?lYFrh8$V+4r0_)HlX{hXCNA|)q1$)bm{cpCk;!h(ms|9I-k6h3UzR-| zRAIYiTj&>=if1yHzhC{t`cPEmKkux<&I&{8zdq<T!D~!wX~_MA3<H_AjP^d4_CBU= zbhp@ro_Garl3N}l^S(bHxv{pax#bKIJVKOJ<xJVtOARuarH;DJce!+3YTCcwyfY|- zarFi&4B>A6_4+k+ts{3X%E|lRuVw%;gE_C%&>9D?na%1}5ZufY86>llY;{cuQfjZE zH2qXm_UBjSYkdCNBPk|N=Co*|45iGu>#_z!d<%Np4LdnaH91)?=pCyte?UtBhc#J= zAf_pzt@qg{^70x=pG<Zo*XbR;(zqQubi^vudADCjFvYmOnRczF#Q4y#97*D@->lV@ zS;gGqyD76G6eVlC#zOkn-e>%`T8M{7Vs%Lqs{y!1sy&9pI}i_@It<_`lh^($3S`C@ zo8+DE)X*T7`Re08fA{ZDhxhII=V)Zq6yV)NgPfQ(Xf*7fi1A<F_TPUsY4y^shML$t z|8_xF-%-%)mi{i*gPX7^{ydJ;{r~u4I(5k3@juG0!`(#G@6}OLqnlP3u8PQuJpL@& zFXpL0X0{P?(eNb^+a8k6=lprqwu$NbGfy70Q8HPm$aly{DwMCtxzSg7CRM1ENOMc; zE@fM`q-_hhzCgBNtN2dsFINHbcvA2<;Gyrmw0EjMBhGlXM`h(?WKhbP$`v~~`!`fd zYG~@tWeorM8kdMQB6y#?cCCqXHRqE@7yL0?ms3z+b-w&vrm1KrkRsQI7oNUoeEs`Q z;W2Io>52f!Cb6c_se_oY%YF~I!k_r~(IeZH5%=m>)m>krD|iNfsx^sb>6$-qXnAF8 zX?3BF?;-lV7$F+Hu|S&}nua_-^vO5B)A#C0JFjw^ZL+>?m9L-^zq*>}3=X=HJ?EWH zTA}xt75RBAYgE2!vGe;^winUn#=R>iuYI;_PcQWi$WTlkw6eO;_0ylX#$zPHfW5Kg zjK%Ho=Ax6->=XN(E`Hs{6#eM<eS3n$te<hM$uIJ^?eFS{-G_gFim|In__=u6$o~53 z&1DvQPb_0PgAzw(QfKE7_r$$Z2HK9c17648Tu-ahZqGPpF1`2cB?Dt)t6)wJH{wEN z9Xio3<R5Kz?1<+yyA&N~pi}rT&<g9GZ<n2Pn242V9tk>XnPwA_N)^F_0eSdjeSum1 z$*e<YRKI{6^^@#N6i{wIUry!a_H@+votKDZ$2YP|gQrYw^S2e>av2LXG3me6<}e72 z3a#E9*Ax5_Ej*uOS!^)Yb|d=M*SRF)vZL?a3uj(`vhsWTn!H~{-gpN4^`mqvGNxmn z8WTyRh6G7DUUvuiixD?kr$2}45g+FgTao9O-RSa_Ev8gU)Oa!d_(GqX+Vt$-92iJ9 zTn;hfgGR>2`p2%<OO7m;XfMCI@bzHkNeu<Iwq{@sj;krh#aNU!NdJ82Z~JuS#@Wz3 z>-78@bSxW>cd$FJJlmz~VL58N|D$-Vk@)ib`R6@lwT~V@p3{J2l4c3-#$aw7*nhTP ziv97Y%4Vd>qN%n>)Z!CMxiv3Z988|}I1E15agms+EYEajaAGj&nPqHzpu-rgsQMBe z&=Z&L;7e<!e1IiwZDw$466P6n?<0uWLV}dr9^IMW<yD#bSv;*0Pu@Pg=it+K&b(5k zX>X=CWB0Mj#ukQcQi0qqABB|eS>Mc0cB2wrywZKZb$;5<@)q@Vt|LCLtLX1{sckv5 zUD&9UoTQWUKv-aX$EFBlNhxU=88RqXD1I4vaL~1Lr>n%wt4}-B`KLU#9^!o4x8xIF zXl2rw>D`!}>Cafs8)+*f^Wc~Et?qJw6g(1^>;|5m`1vi5X?~?M@LiZn7~440M)e&j zGToFdk1U9M|3ls}y^CKTr#F^07?!(G;PF%U^O_rnxr~5dC@`L*yd`e!_oLgx>p{XE zbE<%It+NA0j+ZG4e_4-+P*5x%f)A5?FFlhr|C&Ibjf}B!90rGZE@~enBFk7y_roIV zH918mEzLgBm|v;qW}r?R%s*2^x3aiI&tt-S?&OzEq|U=lTL7JWHK>sD<7?ZRc`<t? zEl;U=wrMrlwE=6Di<}2r8Qa7Iwk_KpIpTU11?@{`!I4eh%DyM%ri8uARmn1KFS$as za^~p<52>KNdo1s!Eq)HYNsWuA$#hIMCuOs&PhZkAnJYs<;nJ9}lw4x_NN=cSc#Gp8 zn;VmfBx~rqcPig19JpRYTO_AADNK%Z{Oaf~eVKA96qiPPt#E=s)@a`_rF!j%KCs6b z?>4-gGiu;HuzmBIfSKBs#9HOS`rD#g?&w`w?y}Zy=}tI$g-$*3NX`7cU&i>>q{f<0 z6l=;Ty8wE4_@qca^khAHNE&74SDsjYUVL2ZF)_aZGoT3Gh3f=%DRa?=+-c>(5;o#G z?S55nJtN^aQ;Cwshny}3%O{eRX+Ad`mc5sl5ocaLJm2aTR(F7lp6c#ljs|H4_w<v^ zPAyxn#58!Yp46*2U)FV9AlVLkKn~-28y3Nh#32zYS0`B4!cs_9OUGF|)?#|l`4Ho= z>Hg)Eyqx>%vBFpmIPC<sj*NtVKX%5AP3a}C`i$iFg2UIup}7?Jz|erdw`|c>>GNaD z?;A0~!iv1bSFqXnASdhSzRuR~VjTxK+nv`URs6m1LOuIhR~YH;%9MD|`TkCf)rxJW z$;r*tVxX{}<a3&$*yWjt((8>!JZ4!wTc5mnO&qv97V(}Px4U(-&#nV{ETG`F2#Fim zAQxPv`3(cRbQ_*T2%J7$Sbcl;Te~gQOttJO1y*0F`235VugUEBn`zv>l`<cu2a3pi z_Z`_nA3-Ztwk{2mi^&m?Y%)M_Xqb6!$YY}tFQzmuF`~ilnDg^x>eE1-GBRFD?rYj> z)z~FY{j5GMQ$Ed^`ltbzVb9nd^EBv)cRg0u2}z_7ELlCX@>fVZ1zx;KUvu~DArdJn zI%@1!uo2^92jK$`z>!g34tcnyjhJ_)Jy$B8tF0$z61iHnbD}9%S9|a=3cN#(c9;uU zqBxZ`FyO$rFCaOREdESiN3`Iap(rs=`Y!lPtCv78y}9|{(Mws?t+hEmedjHB2S(i4 zgCxBKq||C&8L%Z`Jm&V)p?}?k<>>8EkBO*=H%f2zW}0!&-97d+<bg`t`qYQDg_k-u zbDTZIZQ`)j#Hl(dOF>)z^qH4m^Bj^-iY>Y5YLHxJ4p7(yb{Cjh^R>1VVyYVG*)T>H z_AU%X!%p{m8?0ragiAlN!}D|5&ds3}^TdS$P5}THGwz_CO<Y{pn`lK6-Mbz$aM2g& zzmwMZP^L$8fO{K&wMa@%7hsj8CIu)4>>aYE&M!Blo|9O6rukw`R`DkWr-#0e9*GVe zYI(zFGLF!>XKJph-Sq0+vv(PaMA+|lt5qpl`ue4=`^`3@cMeXJ`^+{<aA|A487>t2 z8}`%R&8IDs<I`-TLqCyGUu$FGA$P~=65oR1Tm^IE?9L06q^qfB9rr70kMUZp<fL-X z4i&zB{|5M8N$0afMRfW(!xzsAs7!R(jx<T+>OUmkwK-S1@f){`N7M5mQ;IyALqoO3 zKP^A6V>@{<hOe1a@a}i#5t<O1*LCmadkXCBCCiGvq`7xSx#gyfT@v3=e(Pjc+l4Ph z{9V)eUG$2`_ld?kIVrHvV>KI%l><7r7P?RC?|SC?>z=rH>M=9xzhPwdseeiBaS1l` z`5Oyxtk`yhTxFR<a!z3LC84A<3R|#K<p6;E`0-<B*N61V`9c*dW1}X?(yL39%!Rv@ z>sJ1O%P`W0Zy+Pp)zyh!dTu{P6BW(5$?CEQ$?|I}IXYf8eZyyt=~4C7r@3izNNu9J za;5N_(^j^wk2W`d@9E{(zh8{+ieZ_HKlG>HviEma+>lg`GaPvQu-1L?w+N2w&;Zq~ zDaO$OERrKs#FqATrF#{1gFjI<mswubU;}2CFYBdjmcMN0mAiX5HV7Ebtn>Yl^BBX6 zTixF3{z0Pe{?3tts8{t3bgEOoVt*dEj2h3A8}he|?i56JT(@l@LRN^@(68{s7n8~$ zGOe;mpnn(;{bO`vK0H!ALGm)Spvpa4Ota;^q4fHk%_RHi9o>QIl_2%ljt>SmaB6F6 zGSa=L3knZbu(H}3Uv!Zs_4-CpF01}VI*gKPGj#8W*e_Bm%^@{-=x}D!E6ND}lM1_u z)D-DURpIkp**baTB$w$)R?3;JJ^|rut2@UeB_*4d<g&1xf^_b}rQncQL6f<~Uj0XQ z>tbT;SKb_9cHYW6A71%-9k21{W*T6egBCl~a!)<lY`Q%>ZpfzoW1-f%?HH1iN<u}i z?X>?zm(}2Z^m5ONlZn|+<?4jP)V`-FNnV3uIaRq21UxEi6|54C4t<kR&}9`^c|}HQ zl_;u09#pVPXzTZdMREBn-#=bqpk8Zf-Ep#yUG-NF-4{^%<sDrd%WLNz8YtLZkKI4w zAv<Z^cRHnw#d-Y5e_Z0^a^Lwb42tzxN~r^<ITfS(!U-HM^a+0S6hs$$&PZnLzP0@} zZ*Ip8k082^@NH#*kox9$RTyqJ{#xnm5ogBk`{BZq4@)B+s0FpKd-E<toEy)5lbG99 zTq5z!^_elg(vjn?UKyKJ?Uu%`$Oj%v9p&b-{AIa@L|pB}K(6`X*~;|i>jka!<WU5q z-A^}ds*d!J2sYFOhNYY8I*D|exo|IT(^~a-ipb~G!-plfk>|FFUksGly~br>dMyc{ z+vF0neYnJwGb7*R974^D;ostMbSy_l{V)*fnKp)8Kko1*C1;VD?lZl6B+d(Ujf~PM zrEEjvh_>VtR1hoM%tJ2)c)VZyX$ruS>Bf$!oy%Ndc(aTO#7=Nzztxv~UpfQ!<lNlF zIend+5R^|YOM_y>@Er%fF<X_0`=aC7GDqgZU$=jJ%aUh5YP;#^{FL;P;?#1Pd3Z9W zO}EKj%r1QB(8$vu(`-xmy>l+|orvURJ<abx$>PxwMJ^{Tud|FCxMnE*v555>A)WlJ z>wd+A&IHHCh#g7gSKTj$f93hSPw5Zl;KxuiD}gxx?qh86tAn4=oedq^mT271p0GZl z)0{`Ez^~vq(me2Z%3@EvH>UW+eWt$08G@z%LaLF=)RS{p)+gMimvonp4ZE5LecKw0 z<URHDR5p<@0EDyKu}x4fi?+<ll>n1m=jx#|82Vlz9(PfnmO?=x*RXW!^P*9ohrSGV zQp8iVdX*s=PIZQ@V6!{bTxT=a(}(sy1WT_@QL;H-l8(*(7`jF1>~n_Z;ns6)^TX6E z66<PBubWPP{EAGLUpIemZC*zv=gEr(5nCz69-7r}UKI6|NI>4Wy&)m&ht!6fL%#y; zq&lHRc}v8OvVG<7lNpD>rRT*}A8(|WR0!?dd9LdH)KBp=;XMh@gK8e%#4{@x_d4+S z^eqZ~Rm+d(jJtf=$Lezqk7J*#3WqpzL!u<GGheU1`qwS!^*_$nm0iN`CH<~<Z%}zo z&EY*fyP1AFT59ciFTT)Bn<<c1nQ51AZ&!XtxQ1Ko7}@Zk+qk)KAnG;E3Qcz)Le%@7 zTc{_>oH(&Qn_1^=PYD2eodePL8*?(KDE!A~oJ*NJNBo}2ss(e=2s*wW{yqzFL_UMB z5l`4vcQ((J=(i4Z{~(E(sl8X36hqrPGJe~u=FHUejQVTSS;YwyIA0e+LTHJ^o6((0 zcQQL>U_x6%L%MMAvr973|H!<!(*Bm{()@Y2OH8j0r?)Rr&iH~eu?9F$bU747h>o6) zO6k0-2=(eugaE-Iy_cIU`)6$W-`dElZ;F@Iyv?(Th7neg(i+|vN1bg$?q~OtU&)xj z`Vx;ic8tG?K8jDsI}G!cb=+J$Z_@tVvCzD2dhuddmiB#72A%T+kYID@2^GxH(cmCp z;(s3hA06y}Z_WJI=JyA26AVTNVG1$p4n`xI^e?NbDlR-vIC7O7@DJUblVl|5o)a@& zz&=bQ3;|zUOD>bVjKM&w#mvnw?eHD)eAEs+6rO_y{<}Mo=#{PgtL2(vlMT|uEeVGV z9q{1Z-oS7g14!jqy}Z-0WB@B{4E@vVnKwW1z!1#`!z$;-ojOeqs=YQvhlSB%IC^cy z3G>!%7<6IssnrY1WvE?0(m4e$b}cQf^L4=ul8l;4tHU^ST<d|mta<f0bla1?H6Q0b zjrC3KqU_yAz?BFX1zeytw6*0h4BhMaQ{#x|@>wm&-e6isg1tSQExZ6+0qO~(&QPGh zm4`>6ZT%AlVYC}I7+YEf!A|T4sDz1^0`-Ly0l^X4y*oK8D;S<6=jvhvQ1jkwl5m~e ziEDVdqdXiY1WCY1VXpH|@17@SUHFSXCdSg*+D!BF^MrmJUd(LJ{QUXKjt(xk75gV7 zaDp=C174WtGz-j)r2(5sPEGZNeR2|v@s6vh_0`u}BxcG362kzD!#^Vo_kBoXWcIu4 z*J>|+f$u#RwZsYlEJ0sHAE^_AMkr*YVtS+x{@47>sIYd&moHle1_lV448$r3Y))}; zaVI8DBri~F2r+Mh7lWZu03AmN!0bs#UK1~!fGxC$$D#|+aJ+yi6%`dx)6oUMlNC&h zv_ZA_{p$cFASCy<?zjA{D*{l_3(N<Br}ma`=sVY#By-Stg#GT_yJ=Cbq(%U_0X<>B z*bL8;0gMHcFn`c|HW#k|GL!=MXbtRBkjdt!heP25`=YLn1{R-vs;_Ygs!jgYW1P>p zMiu^YK6X9Qq`V$}x2kM~-)y28XK9&EC@H-FF>icnq}xy*B;pr9RKRhI2FN6(5`7`U z<;1M$OLH@U*2cJxtmnPibQQf+=S+v^F;p}R8#nquTI&Z~>7vfJV#Wokr7y63<Zai6 zT0{UkFvbi|OBj&ULSG2>6PJ2;c(*_Wd}uc^G4T)I&{I^Tgey~Hp|u{MPp7ro)0CJn zSX%>ZsvJO9{JoI)IOw^21YWf}uMRwU_)r>iNBqM1>6OJf2VnFnahu?TbR%YAZY~*g zW5Tc&prki}am2FsUhN?BV3Vad@lSFMUQeI6f1<AL(+MYFhqkNT)z5r}k0yt|0Q13T zW>Ua{>lZhWM?vzX+z_Q~e*4ayH6$%<Z88!VQ<mSK73$Cf+A9W)kW^4$D2jOD&W>9V zke{Ega}{Jmk&7*R*(I;(3l<!3`Q4eXmQCh`Vg$R2f1lAejPps-+bMY!!)_3@>xEAl zw$9GAnD~!>_XJXvcvkF_wO)X?RAGD^NYB-`=B-6y(9h_<y0qmI9Wtp%H~6&;IRk<R ze*KaY+T6cjq_2NV#{lDd1Oa2<AX)aFBO&mDZxw(g(fiy6GM)oQ)6B~YSEP3Apu%KP z;ZugHS<&14Avp~0D*>oWN=hPmOLNbuD=DqRl6S!%dq>+gVQ*;?zUxqX85{4h2ze+~ z8e3U~0PfavMoKtxcB}+b%&yUjmG?8H<Ac{+3=t%bMX*Oo2&2rEpA=a=BzOdCyO`d~ zX?#ctnm&;Cx;1tts?2U`<4>Jt|KLcOl2S>qJ!J99V?$r}5im+1f~u;6nd}y4?SSK~ zYg*9}7QrV|3UgPHlk1Ki#(32qR=J=d#Ej0w3M$R)op)i@xS9?i4nP8Udv9ztNH>I% zt)Y|_BCso*a|zwG=uq+Z-{`nXRtcnDy6ZD{E6?<=C7;GJbBrDtk)3$?SoqKB<H`Lq zT1vea7}Y>*cAhdlvP}b$18-MPHhCSj;Q3-{%eL{HnTh!~%;!UNlaj1w6xxPq+kz^% zj^4OED|tO2W3#;Pu^&yrslC%%5k+ZW@cM!n=Y#96G=~GK$CYuvh{zjfvHFI*yBEyO z127Fw!5O0U=J~(S0lVTRq!1=?st~U0#m}B8$M*N*1R{Lwb-p4dBIK<B)TGpN#jvlG z1ehxQ=FK11^}(39VLSVYJMjUEzkd!ad~eg-n&6IeQ;kMlAv;M*MPs0LbttGQ!9Uo2 znr}hr*&~h%67d?j6AsBwCEN}fS|y}kH_Be1pSt<3&~e&vZdhU*evvXj9$=Bj#iRou zf}L(Z3<mjS>UHa0z{s_;r)Mqc^}Bb~pdLz=+yiWjK;2TT-+a8Do`6+Bh=Xm1dV)~V z1eJYYgGXjk@<Z%vA{ab*Vjs8_i=G*PBB8x9NC1dPj%&n&O{J_{l7H;qK<c;U4KAGg z)W`zdh5MHSF>w|hf7%7TQk(uh@mYgJgK$=+m`qO&%MGcVeJLHYZ(vmP_sl(uM)5o! znyrGg$0g3E8QhbZ_2YNAFDoO%C?WBcqFjfGU@#HO9LNS@M`A}j1mGs9#)Q~XKx+`l z!sHo(rc3}qe*!!K!$3a-V;Gjx*mRWpXJnXrw}I<G_rC!W*nr0g>0QL@&|{~<ErkKz z5?+`KA|mqbx`oq_12TV6^GZremY*^<%uf#Z-V`=6!FJBn$=c|#EU}W7UsQB}pzr`x z`ttpIIsliCBX$c{ERth`!|ftWd|RSBPl~z;Ql?B6vCsMIQ<blk?70pYBGcpTm#R~y zOh4or8&IaPXY0s>%IvKC`f2R=;uN1z_Il%=jma)xrGnxW2oB-?%imallLa}%J35*P zj-B)t7X7lL`S9{2Lh-8zj!=A0UO<U4`o<xo0GPK_$t5I5Fq|ilMt~IH0gWC8pv#1d zN?ynBzu*J59|-r%0E$8LLJkhD@KIa~M6NNS5hz19`6!q+Zrlr0)#CVDVS-_SyHA`% za&lGV*F{XKsfm}|zkd@1J^;=1f$|BQ>j2s|X!pA|9jq)wr?-c7t;w_k-~5Bkqs2i_ zR4P9uE0rAV7&@~rDdPrt>Z>xBu`HEP^@H@u6AabEMikGBcC#`Fekym|X~{J+ef)?+ zN~7>+NlrQq?Y^FO@<vtd?VMSxCKto!JtpGZ-&(9#Z@<#-_gm*|Lz2EwpSvz)!JpL9 zeV>JTZFht9O%eOE3~pHrV_$dh2u}Td?hIlGiWRo{L#Drf_^PfoLWY|NlRD-*_pT+9 zW#W+@s<AWX-MZr)XoUay3I6+^7|y>FUSzayyhFGAos-9k{y!h@nf{%56VTEBc-#MR z_NYVi9p*s3RP>6VAOSNOf%69(Q!->WT2nA91JA_^0wwv9=D_P9k*IKa4)9$VyTnGI zic4)3<oPzr@NE=f=(xQR_3G+F=Na7BDF_Atl1}W1H2}OJBV{Lu1^I?$wj<vpa7M)4 znqHo6v(YcQ$cn&I9jYgPOJ^7BpDT^cC1?ghQgTX)4*+CU5Pj2<1mlI+e<0q=HzXVd zj9NG2{b8(SP=(O%C|#E+|Ic7HMQgi=2xDAaoR(7I5qvKn04*ONOX-}xuXfKTAb=7A zF2vA`<TBB>K{1TQ8)1P+UIDQKXJ*2pZM<X{1s5olAXCjtxy`ULf1VOMx0vj^bK21I zWIP8W+c{yDnvksuOl&mz54Xp)6eKb7HJ{_#+?YA5Csg1?vA$(PFf-eMO$FpM?-snj zupN^wP^eit6Uy(<|IMM{yMy`p=|c)<?jQ~eHUJn+3)l>JeE^NZQ#BM}H}i9Ta`Foh zA6O6Cat8zi{2UxKF*c4&Em}8L@FqxXn;(NLHU87JUOQJ`gpTvbYw*i2v^-31bpX_i z-~qKcAR$r%w+L~W4u8lUI~EA8o;Gw{z)C!Ul!q|q0Y@wq{L!<|90{zG`6)fM_Y)Hn zrV!~M%;ka1t&817bw2mY0bKm}1DERH@(trta>N1<!~=0kXXo`Eux=ItvFsJVvTtq* zb3ux(2EuX>IWV!X*uf!tO*BfMY4yY6KjnMjoDEI4SnyYI2Ni%}v$M5LlJTYNX-Jf6 zn^<06umMbpfCjfbxj+;X1Yi}$Ahkf>!o!t@gtW8@FOP3EzBJQeA#rHWj(((?#uqQr zN+X<o`1rAl;4;n6f6O~0j(jq!sUKz*$VyK=a}-!ZMW++d0mFOLKw3o?Pm#7BxEKTv zA2?XeSut5dgNg(Pwzq6uOi@`$e}CrcEg2B{Sp)}eaq^Qo1b-NSU<E!-;B4_;H8r(U zBP7>>kNmhg%1HDPej%x%ca?BU$Fkn}reF(l{1aAIf_RTNk2pqM+kqrQqQ;7gck*yM zfQ0at`&xSSajPASLin>NHDFit<jE74`N=Ft$}uG+6IJ3y6CY-l99iu?)F|*mKVf3R zLm=8=Yy+a#heu{75W<`4=vhSxw%5l`pT=fp^dfR}1Oeu408s}>L5+nhP|HckBkLWP zoR89rMqP`Kj|VjQ)NnDj1VEWTKuou)1@3v@=WT2(K@Hw26wIqvA+ZdHz#KV$wcDdY zn@QcW8Q{-itc~6x5<uSI!iN+J==@MtR#uWA--IFvCVMYmy$S$6`Xjzz?-5|(kebRN zU%U;94fK&Xzy*_+lUsvR0Cdx;8{6a_mG*^Z-4%BGGQrI&aFYWc!c5M8i=m;7YEVNk zU-Rk}sJ5t2KcPO@A|548J(i$+Q*7C)ec_j{=iATuy{G~QKBQ8i$jU%5csqeGEkzjO z+p!~uBk|A`LHv6^@U(zfQNql-=<;_OFXW>*wt+@hAHIt^@S*352iO`hW@p-`j(}r= zizwkXEd*k$Rl^e28qCRL+Mn2T_VsO-nWa7c*L}IxXY{~LiNWv2%<>rtcGk!oQ&Sro z^++BCLYDz-1f+JEN4a1aYUH`(fB*jdRy)=Du+K??dJn=t<3<Hd9HWFWIKYSdu8yCd z9_`3i>DI~8A>96uW7^}Q3AK$E^_Q_^LDp&?ppB?0Asll2Dxt4(w$w$WjnTHtC`y0f z2=ST565ZxIw|b5y3V1V=w8nmNUS8?=yjZ_yUC`^P{o}fk+|=8uJ^8>50bypfc+1Tt z@Zay=(R$(Z2U0r|Vmr_zsVj;Y>#lu_bP8oMq0&RB_8=DDhx_d0$oD(Yse<<YEAKT_ zzo({ZDl56)y4t(8hlXzi#pS}sv~^-uP3NE;^bTIofChgkz3Q;%Y;h&<*??$fpyD5w z4Hfnw?(2#`z3c`hahS$+?K&=6T|@d|xydR>aou`+;-$n<R|S4*S?Vz^dq>9}_>U9P zKY+F}z%dXl1&CUx#P1KrhXBtaW_S+BVNizFfTo6yL=C(*Un6V~(lkhW`Juc?eCSP^ z<dMTX%G+TCkD@~jh(+j6h}d<>6OLm*YoepG9hrJR^5@=KF3&WT+oFU?9*Vv<@m5`L z=bjr)J!4+u&%pJ)!TNgiB>SQLMUm&IBSYDP!;%6|n0gph{0coQbeh<&QB$@T!o2Aa zQYxlO5L_ys-`gec1c1C|md?j~`)#W0y#Iu8Y_jT`(A<|%S{tf<Y@_wxp$DtJbd6H( zy1&CcE(9H4!S=s`Kk-|KZy3n<|MMc{dy!pk^lR|?Ys0KQv3xJFHeTy!{OeV=wR`{j zGyGhr{w+r1RN;8N^`FmRe-C>ft)upVZwT5-q#BC`qz7I&8`izr!593`QB)Fg;$}iq zf#=hnmif*OJ}=oI_UP*Pzdl;f*_!kd`OcCvw&(rUYEVa-e%;;-LsmsCt*F)lm_Xdv zq0ZSm2Bsm5?VrU&ifZ*8tvgcU>1&a7<L+5b_a)D_d^@E0mrve3eN^E@Oq}Scm=B-C zlk|f{$AkC$bv3%A(&GRgofd*eVISxyF?$T+iw{l-7lCc9ldG?Jl^w>zK%%FI;q|$a z-gO^(wMi@|=}5WT63vT>`K(yGU;W$69tou0xA8K4tER8}G+N#1&n1(8(En%CTW0DA zGFRe*qlQYB_9n-X7l_lwLJ$Y&1yCVkY7tOwN7-m8)*WXRm@vAjq^wLxCmcL*$zL(S zBrV<g_5J6wJrVMXjaBI)75N?hd1XRgLih&P1YC>vzmh(-*>4M1Txu$>V65`I?w&Nh zX4H}dpA9*J&ce8$AA?cU6UPlB+XtfJ_Y^QB#;xAQAmM1f=IUDR;KFYwn7DWE4)3Z7 z?e6O0Q4-r$y+!qewgOq1#m2u+5eBY%+oi3nvgKu|BdyOqzm}kyvqhQv?<I8&pf}vo z*SkS@Iyf^U0MU~@FR<39tZ$Zh438g^I=1>cK{q819_;Mw<o2Ge-j706!oDYH^#x0B zeN2d&)bZIS`ApPIb+$QOB(<#E&GgaGeBs-$>G=p7gRR;bxxs7I|GbRwUGxa~&@qWS zPW)i>b(Px@1oWybr#T^}i^3bN)C%7|mYfwoQd)bW@y};`O|hSUEwS_$>;^uDMy0<c ze`t4!fp}hv&301gl6jZT^CN6uD9JP)Ii5QEY%(h8&-RI2Z+U{+kD){r5>>Z*x@)U8 z?Oqo!+u#y@_qApIC|aCU=RB(utyanpj)>W(tUa;%0ZS1|#MiyFkXBsXgj|)On%430 ztn0pS34XF=1=NK%mVY+D7-9CIWIwzwX?S?J{?q+Ol~1ws$I1=EPQO)lb&U@@N#sD~ zMBWef*>Ub_s?Q)8u9Mx9maLVfL+rku1J@HSyw%YdyRdvN`$FCD9dUdn?T8CjR+^>f z1$ZJ}DSP1p1T}oa?h1PDq(DB#xm@nZwp*3iDUP3?MHqdqNJjIL8^nqzQ9~+1MaPj| zT6k2Ef<{TY+H<+q<z#BJ`C_R`hBpl^b=R+1H_Lv3$ty2%w6ClScs+GKTx^^CgXkMK zDo3QMTx?e!td(2rH#D$TZC?H8ehgf`kibP0+Zd#-0#Z&CdP~Roh>t{HZQUbpCYYI< z_a`#RkEBDd{tY+^{2PfIMKx~Hw3R#c^I!L`=Dc7gacvk&S<<)v^V6~?xT`_mG9Wys zGCsB-<U~<sw>>VcddGOmHUI3m2f>AAyYa`Hnk5T9Zli6!<N+~{L}R6iyVR9>gF{h@ zo{O(w8bN!k&8ET*B?czqL<tTa(S5YjHvXW85?5qQ%%+en!h2r0yg{l1Nd1d1UuN$8 zB6z=OljA5E$ZOz$mKVCelS5x8?md8y_c<IIHE59TJKEpds{jjd;$TOIs0+vA*JeUt z7+a;~sf|&QS;*$ybs$c@h!N1nF9fzaz^sz6nIz5+@F11A7Ar3&l)PNBcIIO+%JSYF z!05JDk$fyxP`~KbpLjg>;ke20#9wPjy`#hj*)+&BN@cE7Ajc)iHwF)M{EY<&-nfXY z1oVtB6jq;}JKF@0yCIokLNo0}M~9Km*v9qi*VAkkntk)2BS~hBimKG|mJBpnBje%< zsONDouA`+rkF@OPuU~5jR~h6x@FibQqUVx45A#fjZj!*OB_ZL6nFrI4kUA!1Wo1DK z&<Dw+$Ku#AqG5#EWby@lu5B=ROJP*rEC1Pe_Ey)sj_Z7h$S?n3rk%GLNd>IYG}*>X zFI-SUX>l8#o^TCK2m2ziw;mbet~cu<Wzw}GqoT$?bmMCJL`2ZV$B&l3ODro}+7bRa zRKB^^{KAE-MNTd*dauGlF-l6xQ*jl}qnso(Ur?Q%ca=m&V^wS2lqrK@4DkkFa-~6G zVO;UA6Nq}7eP<byHIDu+luGDyCm|VQ&)3W6L@jW#d=Y<p>^fzI<XjOgUA*k%aNA8x zCrA&?>$gHyQs87R3uq$n0w>hp(5ysrAFkLMPb@W2Kqm81;B)Jv#K67Sf!Y(7{>YId zXqdcxl!u*3P$Wl38=$<2^ygJol`oR^f{R~8_ZXHQFTXZNw2sPLm!A8!K~HC{_ofjc z7KcPD9jOx`&Vr^sl!Ax`F<692_+gy{D)tNz!|Dpe4dAlq2U?)*%!mP@&xqm}V%q`` zq2tCN!wbp(Be>6#pj_5c;R*e#Bgt5cF0QWdGOZCB#T=#$-FS?^1TYx$0jm>LR4US^ zzn#(10+L1PPiU6p8zth-gB}S7h*Vr$oNR%3dGo8`U_;tsZU9lCmU8#QSS4T;#iF>? z!sQ`BLbL$x#n^sms6usuywZna{XzR4YLtAnA3tt{)-U{GiFg6~2U-|ozxej;Y!-@s zFAS-eSXf@;qQqt;;f_|rQkc-w(aM5CTNNs06zk-y!A)LXBz%oPkaO@3Cl!6l1OW*E z`RXqT<p?T#!QY)S;QFC5Nx_$^(`f`(m9W|-Sa{F~AlR8zRdSb}wDb#lE-#XfT#dTA z`(f^UQbIzzbw@+u>Hi3*Z+4XZwXA}!ca=F)T>Ab@4BI9RU2D+7ZvZL<y)ME%7J&p) zA}TL5FbKjmdS4Jr0o7+8hICLsxZlLg%<PZi%frKir~yzF?X~TcMj!kw80R0Fn_o3F zs0&CTIR(9`3Vg!bhn@FeZ4zBWHu1~n+uAYFdVKB$If9G=I`T2+@067tg&5O5OhI_y z8Gy2k+;(bE7wUpLwKE7F*R_dyaV)8cLTkgaLDesq(4KqYLjf|SKV(KV(H+Q8yH^K_ z5<#^>GO2&+Q~`9xklW~^(uj<VB-Cqhn?NG0M>F72*)pU=im&|IjMLyeh_9OjwEPel zqZ5`#`C?HX!bkT-v0ZnjZq8=w=3K%i8eQ?O(b4PJK`4B9LDX&gC9j~s2vcjQn6OHD z905h{1<Kl{CY^K@=#*hGfn9q2$kp+^=I>beB~hDnf$Rtwi0Hd_Q4U<OY%i6-7Xzoa z03>jt-o=I5XRv^eK|BeWCAzQg)uooI(flM<7&ge7bBL5f-=E(^=`fD5m@TG+gi>m6 z894u+a5AF<6=NzG8-ic?gz2tI?+lcEaZiYyg<>cb6%2-C1(=e3nGUC-=VX9E)xeIN zwO?vM@Kttn+>d^9Dy}RG8=Hk-v@X}XHw$yx%EynNn@Kc2QtYWG+51=4UJ~`MQ}r-T zRf~oM@Oc;1nBk4>i)Dl02~w;%8p@UzE@+myUcs&Q4-emf%K~k{?vW9-oRfK8VN(St zucSd+1O0A|=w?kW$!iBu-Q(?vMUHMoY{W<Kdod6tBU-^|(VAeN*Xd{@P(iD$rPz)a zvM{HL#QFLC!OGh?a~OIuxJIdALOD1^5pbbQ69P^r5e{)0Bnj;`DzwtYgssEs#bSr& z%2MGMB4`mO9;PB_S7{~7QV_E&#BxlwaamI_PzHUML?}SKd@0L%^kjw#E8ZJvPJ_Lb z*<<s1VpBt;gS311ZfxgTnA{V73O7V*Y$0IR^Y$ja7sRTFb{!IA67~uo+%-{Fi?s>d z!t{%%1aYg#&t1N(hW8+dd1%j&aJ=oxuSY9_U~0nKK%?~}6i6}IuEqvLRks0Kh0uHy zGAzA?EPJn2)6Vq&L)m+WbN#pP<FuoJ63S>G$|id>q+}IEMwFS5?2*+_WF>niMM{K_ zow75My_3DNve)-~*86>bKA+>b|M>mh$9>#K%j@-gKE^f9>%7jZnj6b<1a`oiK}=Qs z2)bG?pu2hxBpsog053q;WHB+ZgL+R4ke;VIcLMX+wsU7cK5_4HRc~NigfR{mGLKF) z#qA<8P{5&=mVUy$Xh-yI7Pg-}h(l8~olRr0^39h?D%!a3M$cz=IIgtsWet+d+QSb- zoTbSBrXPssqHGZ+qwGBqd37U?pr9cnAy1Yze1ihq{Ra;)Kv8$<X9-@ExDm4>BVKlR zpD1K#{T2Ws0*jxb0^u;-cS&=i0xh<S5@-$&x^NbjpFbl(zv5Yn*~C{BR;Y2b!au;Q zR9yx_cvtJ=oqJfN*9@-h!?(?I9-*SJzTCZzio-@E%&(x%3*?0R>n5s`z@b9XXiAg- zYifK^?g9c5Gwx`>AZmROCmPWghQjYuj1~_|lt+XS^il&J7U-lQ=30cwe5O7YxcVUn zJ}Hsq!IQo0_W{eG)Lxki=<#jItBbLxFDUOk2)9M($^wAcNH==VhhF3vlmH0zO_bxG z_GdtmsEQCZ#-)iL1dl`(`a3Er0jTt_69P%ERp0GD7If^ZDDKJyED3Qo2(?;_D26Pt zVYcCOXw?8bNP@uf$B)9vTG^jrJsN*;9_Q5fP;RUC&9A?|nq0fMqoqZ-Ohpy3Mp)c@ z`oEEde|!G?eiK~0I7DkO?i-(S>qsKYhY&Is*5Ep@RtOsle6>PRHvL-fh<lPP+a?|X zfns#;0+-_$b&jH`SJ7$Uhv^r!9(tM>i8}+|=sLPYnlbE$wTFiQur+|75_*o25bT0@ zcp1i#2YxK5s;l$Il3PK}20gYV_`~uDArzOUz0y>h$e~%ggHfC|=mOL|_8@jg?=I$0 zSB3~3x2KGOb5KU`K|k-Fq<tKKTgFxWtRHJ{XY-tDpp-(2_RX)hm!shvvEK$zln#ro z2f7O5s;(`kq~zDv*Jrw%fX_uvgQ6=1BO`<fje-sA##-*Bq;SI%mwx_yAL;?FnFsXp zavX{tKMVL@ix!cGMT5G}4Vcp~-vkuVo;^E_j$U^4G-lwwVKNu;ua2!<gW?9hdJk;U zLl}>ooE+GBUdz66(*QPgUVi>w6rC?H;EyXc^dRK`d$)(0Iw-L-#c^RWcKWWRB_FOr z_0$PL^DcR)-r-G3Iy<9aucBZoplbY!qdgC6f+*k=nxJWMQGo>ZfjogQ_pnMUpeCY6 z7(LVqI4j2Ap5JR@-J{McAn;P!lPXf&LHS)ZL_3LQ{nltZC`0`G`7^K#|FRe91)khc z9fFMj+BV@ty`YEk%1;0j7Yd0sesGJ75M_4&j3$Kc)kC1=04pj4Lw4bA3JxGZ@sH4x z0APvs{<rzIMCgqU>WtKz9Upm7#?ZyrpsErHe_){g3s~241dPzzypLj_9-wtlh>};n zEznL3?tb?-sftT7KD!u5gSZ@-Sl<PBcOqv!D49CXm?{We4ipS)5nWkWTffAuD=sTb zw;k6YB1%}uZ%s|{INdOkw7{1M$QnXAb#-<4gN3MM%{Q0XiShsr=+y7um*aa%ph#|P z9M<ayT?rX5YhZqN!MOGTe<7-bm5NUxc4zp>jTDwN01s>hQ4W{YS+xw&3n5(9z6@fo zwev;KKG}N+<3h}=9w5d&bm$&5E`UQ3Q<kAm)&sT$)T1fOX}1R=XT@n_!@+^~?1#t# z{J!4Jo8@rsxL@U#*6PSQWR;YRVDezJ{9r|n^A@_SI%d`@0G9W}X@!frs-cl)-Ytpn z7E#vi%+!W&->!Fjw5a^>1C~V@@<*mLDjyKN1K0)uis$CHgG5vWmJxooQq&M3NK0!g z92NQR-@gem3tV;_z{6WMCvJCy!jsGR(#xX!Wk22LnRd_>R>;lU--Qs@LOKBJmkK|f z@$jbVo6Cu(QRxVka+QQYWQFC<G#U#@xGrQ7L&L+V24DBVl6s<`g}WUyK9mvZn6cYs z=sy?BBkPE4M(u-4OcGOyk#rs;Lmi9AD2R#dsE-f8vhU^82!v%PG^jBHfczx>8MF_M zvaxxfhK#M9nw@<Gk2m2o4yAv{y6GZyuwR%wgz6X~prh^jmsAnxFDwqmsDHe32;#0R z@RSJarpL3oH_&;d)rRsuz*$T+Y39Peqea98=FG*Bq2?4OED{1&U!>tsFpkaqv>uTR zCEQAwo9)}S{Xk9{$ad*IPB~k}E1YWp%#R>Q3A&Ol4EqW+Ju11v5&ohibP;Sa`FS<e zN}zdw-2DcIG81vNLFHaTbtx;$E;Cm*)42W-$QjU6t$oHKN6d>rH0UzEWC^K~KXEYP zO5|(sd|zJ52Bay-kV20t3dsZ!Fyexye)ymd_l^4Xdy8HfBC<qP%gwvE1qT!${0*F{ zTeCz)h|~_5$ii>}@}qB`ApI#jeX8`=Ym~_d!jW>~9e~$>C0JNl2Qr+CD=Iue+~Kg> z=v;&p1{pc8emMm`Sq3i}3bTXop%3@@qG0lZa(>%ZsnTWjKUWt2xA2{SARCvNn+jwA zUl2J1uxbdP#?z}MUWSy4^84#WDO{{XtPRz@FqxU{Q;Fh^@zr1UzEkS*jWhXN=d`vX zSGBSKEalJRmN!Q&zGv7Zwl<!+Pv9tT(_Q`|LIEo~IV`0Vwii8)i;bPS!rJ(7p#z#J z6a`ktckkK5O#9b_Y&#^mUp6WK!I<r_)$rk=q1WS?$BGk(+xQ;@>F;y<9|u~fpBtaO zFmdyojLbARLr^$o*)I|(<?4bA;U3bYO}*<jL*MT0Vh}z5A24#;RO2^aZ*OK!P6dpl z8J6qrzx3}y02pTqBLg1#@1p<-&-Om;p_TAI0Mg&u)oBmEqUvge`;&AFLr0pBmqAPZ z$+SJ#AwW2O#C5kolm~*=Md#kS!V}>buO)D+f)W9QV)dX+#;wLDM2fqNlNFftW&&kT z0n?VJzz^01Sdl^wLK3D`vJhhlx9nzIDOyVT_kpHF?pRp`^tBnC#H^$Us8zUpAcr1= zFUXnz92jx3ewPpGqM`$TMtTP`MG%@}dP8QKt=7E_o0-plcl{5nKeb<H<?=V&89*`L z8Up)&c0B@bYWgmsD3Ye8CLGW6cO+jxN~U$;bXWbq3u5Uxub2el&woWVh-?^W{$@8s z(zuhA4$wYgg#keHhv6GV$i&#S^Mpx&W)qVba6JP;Ly1BuCP9RTg^_{_==1I!Cr+Sg z$47sRK8GENNHjyf1pd%_+<Ov)Bv69NeI^b=vll6S4<Ko%n(7)Dl%UwhhSD>EGZoj? z`XO1yd%Z@iL?j@rXp%V`%4a|(wYG2s)fmTK2|^MRKopVRAg>|;(xDISo|`5{c^s%~ zA3>oN4i=e)49a1R$qtyI63lh264#;vfe^MR0fawbu1-&DDCy{ALApdl@CsosBKmqj z6wT2ywNmTv5d?7Q6v*Rn5OxaNuB`3%U%?K|?g;H8ZiwD_pwJFeEDJ~w6*R&-syErq z3@CuNAmY3va4d)nDFPDjv@t35*={((0vj!YPXliQDhvXF$Iyq*OJ#sC59`5$ZeXEe zc*Gvq-(fre0F_OI08{0nhK2@_3KC3#BRh$}06r9^*Odq$ySk#mmt%7lI=VV>rL{2; z%0A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%0A3~!v)B6WrWi4l^ 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 EAKq=P5dZ)H literal 0 HcmV?d00001 diff --git a/docs/localstack-concepts/service-implementation.png b/docs/localstack-concepts/service-implementation.png new file mode 100644 index 0000000000000000000000000000000000000000..93f8840255e6dd3d6c99f6bf68030b4edb45b6cf GIT binary patch literal 260436 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%0DxZo@ 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 literal 0 HcmV?d00001 diff --git a/doc/localstack-readme-banner.svg b/docs/localstack-readme-banner.svg similarity index 100% rename from doc/localstack-readme-banner.svg rename to docs/localstack-readme-banner.svg diff --git a/docs/multi-account-region-testing/index.md b/docs/multi-account-region-testing/index.md new file mode 100644 index 0000000000000..dd153cbe3b30a --- /dev/null +++ b/docs/multi-account-region-testing/index.md @@ -0,0 +1,66 @@ +# Multi-account and Multi-region Testing + +LocalStack has multi-account and multi-region support. This document contains some tips to make sure that your contributions are compatible with this functionality. + +## Overview + +For cross-account inter-service access, specify a role with which permissions the source service makes a request to the target service to access another service's resource. +This role should be in the source account. +When writing an AWS validated test case, you need to properly configure IAM roles. + +For example: +The test case [`test_apigateway_with_step_function_integration`](https://github.com/localstack/localstack/blob/628b96b44a4fc63d880a4c1238a4f15f5803a3f2/tests/aws/services/apigateway/test_apigateway_basic.py#L999) specifies a [role](https://github.com/localstack/localstack/blob/628b96b44a4fc63d880a4c1238a4f15f5803a3f2/tests/aws/services/apigateway/test_apigateway_basic.py#L1029-L1034) which has permissions to access the target step function account. +```python +role_arn = create_iam_role_with_policy( + RoleName=f"sfn_role-{short_uid()}", + PolicyName=f"sfn-role-policy-{short_uid()}", + RoleDefinition=STEPFUNCTIONS_ASSUME_ROLE_POLICY, + PolicyDefinition=APIGATEWAY_LAMBDA_POLICY, +) +``` + +For cross-account inter-service access, you can create the client using `connect_to.with_assumed_role(...)`. +For example: +```python +connect_to.with_assumed_role( + role_arn="role-arn", + service_principal=ServicePrincial.service_name, + region_name=region_name, +).lambda_ +``` + +When there is no role specified, you should use the source arn conceptually if cross-account is allowed. +This can be seen in a case where `account_id` was added [added](https://github.com/localstack/localstack/blob/ae31f63bb6d8254edc0c85a66e3c36cd0c7dc7b0/localstack/utils/aws/message_forwarding.py#L42) to [send events to the target](https://github.com/localstack/localstack/blob/ae31f63bb6d8254edc0c85a66e3c36cd0c7dc7b0/localstack/utils/aws/message_forwarding.py#L31) service like SQS, SNS, Lambda, etc. + +Always refer to the official AWS documentation and investigate how the the services communicate with each other. +For example, here are the [AWS Firehose docs](https://docs.aws.amazon.com/firehose/latest/dev/controlling-access.html#cross-account-delivery-s3) explaining Firehose and S3 integration. + + +## Test changes in CI with random credentials + +We regularly run the test suite in CircleCI to check the multi-account and multi-region feature compatibility. +There is a [scheduled CircleCI workflow](https://github.com/localstack/localstack/blob/master/.circleci/config.yml) which executes the tests with randomized account ID and region at 01:00 UTC daily. + +If you have permissions, this workflow can be manually triggered on CircleCI as follows: +1. Go to the [LocalStack project on CircleCI](https://app.circleci.com/pipelines/github/localstack/localstack). +1. Select a branch for which you want to trigger the workflow from the filters section. + - For PRs coming from forks, you can select the branch by using the PR number like this: `pull/<pr-number>` +1. Click on the **Trigger Pipeline** button on the right and use the following values: + 1. Set **Parameter type** to `boolean` + 1. Set **Name** to `randomize-aws-credentials` + 1. Set **Value** to `true` +1. Click the **Trigger Pipeline** button to commence the workflow. + +![CircleCI Trigger Pipeline](./randomize-aws-credentials.png) + +## Test changes locally with random credentials + +To test changes locally for multi-account and multi-region compatibility, set the environment config values as follows: + +- `TEST_AWS_ACCOUNT_ID` (Any value except `000000000000`) +- `TEST_AWS_ACCESS_KEY_ID` (Any value except `000000000000`) +- `TEST_AWS_REGION` (Any value except `us-east-1`) + +You may also opt to create a commit (for example: [`da3f8d5`](https://github.com/localstack/localstack/pull/9751/commits/da3f8d5f2328adb7c5c025722994fea4433c08ba)) to test the pipeline for non-default credentials against your changes. +Note that within all tests you must use `account_id`, `secondary_account_id`, `region_name`, `secondary_region_name` fixtures. +Importing and using `localstack.constants.TEST_` values is not advised. diff --git a/docs/multi-account-region-testing/randomize-aws-credentials.png b/docs/multi-account-region-testing/randomize-aws-credentials.png new file mode 100644 index 0000000000000000000000000000000000000000..9f57fc84b945a7a08101ca41af124da28b020fb0 GIT binary patch literal 30971 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 T{PuX7F{7crOD$oe@#%j9&T@c$ literal 0 HcmV?d00001 diff --git a/docs/parity-testing/index.md b/docs/parity-testing/index.md new file mode 100644 index 0000000000000..b89031151ff22 --- /dev/null +++ b/docs/parity-testing/index.md @@ -0,0 +1,248 @@ +# Parity Testing + +Parity tests (also called snapshot tests) are a special form of integration tests that should verify and improve the correctness of LocalStack compared to AWS. + +Initially, the integration test is executed against AWS and collects responses of interest. Those responses are called "snapshots" and will be used later on to compare the results from AWS with the ones from LocalStack. +Those responses aka "snapshots" are stored in a **snapshot.json** file. + +Once the snapshot is recorded, the test can be executed against LocalStack. During this “normal” test execution, the test runs against LocalStack and compares the LocalStack responses with the recorded content. + +In theory, every integration test can be converted to a parity conform snapshot test. + +This guide assumes you are already familiar with writing [integration tests](integration-tests.md) for LocalStack in general. + +## How to write Parity tests + +In a nutshell, the necessary steps include: + +1. Make sure that the test works against AWS. + * Check out our [Integration Test Guide](integration-tests.md#running-integration-tests-against-aws) for tips on how run integration tests against AWS. +2. Add the `snapshot` fixture to your test and identify which responses you want to collect and compare against LocalStack. + * Use `snapshot.match(”identifier”, result)` to mark the result of interest. It will be recorded and stored in a file with the name `<testfile-name>.snapshot.json` + * The **identifier** can be freely selected, but ideally it gives a hint on what is recorded - so typically the name of the function. The **result** is expected to be a `dict`. + * Run the test against AWS: use the parameter `--snapshot-update` (or the environment variable `SNAPSHOT_UPDATE=1`) and set the environment variable as `TEST_TARGET=AWS_CLOUD`. + * Check the recorded result in `<testfile-name>.snapshot.json` and consider [using transformers](#using-transformers) to make the result comparable. +3. Run the test against LocalStack. + * Hint: Ensure that the `AWS_CLOUD` is not set as a test target and that the parameter `--snapshot-update` is removed. + * If you used the environment variable make sure to delete it or reset the value, e.g. `SNAPSHOT_UPDATE=0` + +Here is an example of a parity test: + +```python +def test_invocation(self, lambda_client, snapshot): + # add transformers to make the results comparable + snapshot.add_transformer(snapshot.transform.lambda_api() + + result = lambda_client.invoke( + .... + ) + # records the 'result' using the identifier 'invoke' + snapshot.match("invoke", result) +``` + + +## The Snapshot + +When an integration test is executed against AWS with the `snapshot-update` flag, the response will automatically be updated in the snapshot-file. + +**The file is automatically created if it doesn't exist yet.** The naming pattern is `<filename>.snapshot.json` where `<filename>` is the name of the file where the test is located. +One file can contain several snapshot recordings, e.g. the result from several tests. + +The snapshot file is a json-file, and each json-object on the root-level represents one test. +E.g., imagine the test file name is `test_lambda_api.py` (example is outlined in ['Reference Replacement'](#reference-replacement)), with the class `TestLambda`. + +When running the test `test_basic_invoke` it will create a json-object `test_lambda_api.py::TestLambda::test_basic_invoke`. + +Each recorded snapshot contains: + * `recorded-date` the timestamp when this test was last updated + * `recorded-content` contains all `identifiers` as keys, with the `response` as values, from the tests `snapshot.match(identifier, response)` definitions + +Note that all json-strings of a response will automatically be parsed to json. This makes the comparison, transformation, and exclusion of certain keys easier (string vs json-object). + +**Snapshot files should never be modified manually.** If one or more snapshots need to be updated, simply execute the test against AWS, and [use transformers](#using-transformers) to make the recorded responses comparable. + +## Using Transformers + +In order to make results comparable, some parts response might need to be adapted before storing the record as a snapshot. +For example, AWS responses could contain special IDs, usernames, timestamps, etc. + +Transformers should bring AWS response in a comparable form by replacing any request-specific parameters. Replacements require thoughtful handling so that important information is not lost in translation. + +The `snapshot` fixture uses some basic transformations by default, including: + +- Trimming MetaData (we only keep the `HTTPStatusCode` and `content-type` if set). +- Replacing all UUIDs (that match a regex) with [reference-replacement](#reference-replacement). +- Replacing everything that matches the ISO8601 pattern with “date”. +- Replacing any value with datatype `datetime` with “datetime”. +- Replace all values where the key contains “timestamp” with “timestamp”. +- Regex replacement of the `account-id`. +- Regex replacement of the location. + +## API Transformer + +APIs for one service often require similar transformations. Therefore, we introduced some utilities that collect common transformations grouped by service. + +Ideally, the service-transformation already includes every transformation that is required. +The [TransformerUtility](https://github.com/localstack/localstack/blob/master/localstack/testing/snapshots/transformer_utility.py) already provides some collections of transformers for specific service APIs. + +For example, to add common transformers for lambda, you can use: `snapshot.add_transformer(snapshot.transform.lambda_api()`. + +## Transformer Types + +The Parity testing framework currently includes some basic transformer types: + +- `KeyValueBasedTransformer` replaces a value directly, or by reference; based on key-value evaluation. +- `JsonPathTransformer` replaces the JSON path value directly, or by reference. [jsonpath-ng](https://pypi.org/project/jsonpath-ng/) is used for the JSON path evaluation. +- `RegexTransformer` replaces the regex pattern globally. Please be aware that this will be applied on the json-string. The JSON will be transformed into a string, and the replacement happens globally - use it with care. + +Hint: There are also some simplified transformers in [TransformerUtility](https://github.com/localstack/localstack/blob/master/localstack/testing/snapshots/transformer_utility.py). + +### Examples + +A transformer, that replaces the key `logGroupName` only if the value matches the value `log_group_name`: + +```python +snapshot.add_transformer( + KeyValueBasedTransformer( + lambda k, v: v if k == "logGroupName" and v == log_group_name else None, + replacement="log-group", + ) + ) +``` + +If you only want to check for the key name, a simplified transformer could look like this: + +```python +snapshot.add_transformer(snapshot.transform.key_value("logGroupName")) +``` + +## Reference Replacement + +Parameters can be replaced by reference. In contrast to the “direct” replacement, the value to be replaced will be **registered, and replaced later on as regex pattern**. It has the advantage of keeping information, when the same reference is used in several recordings in one test. + +Consider the following example: + +```python +def test_basic_invoke( + self, lambda_client, create_lambda, snapshot + ): + + # custom transformers + snapshot.add_transformer(snapshot.transform.lambda_api()) + + # predefined names for functions + fn_name = f"ls-fn-{short_uid()}" + fn_name_2 = f"ls-fn-{short_uid()}" + + # create function 1 + response = create_lambda(FunctionName=fn_name, ... ) + snapshot.match("lambda_create_fn", response) + + # create function 2 + response = create_lambda(FunctionName=fn_name_2, ... ) + snapshot.match("lambda_create_fn_2", response) + + # get function 1 + get_fn_result = lambda_client.get_function(FunctionName=fn_name) + snapshot.match("lambda_get_fn", get_fn_result) + + # get function 2 + get_fn_result_2 = lambda_client.get_function(FunctionName=fn_name_2) + snapshot.match("lambda_get_fn_2", get_fn_result_2) +``` + +The information that the function-name of the first recording (`lambda_create_fn`) is the same as in the record for `lambda_get_fn` is important. + +Using reference replacement, this information is preserved in the `snapshot.json`. The reference replacement automatically adds an ascending number, to ensure that different values can be differentiated. + +```json +{ + "test_lambda_api.py::TestLambda::test_basic_invoke": { + "recorded-date": ..., + "recorded-content": { + "lambda_create_fn": { + ... + "FunctionName": "<function-name:1>", + "FunctionArn": "arn:aws:lambda:<region>:111111111111:function:<function-name:1>", + "Runtime": "python3.9", + "Role": "arn:aws:iam::111111111111:role/<resource:1>", + ... + }, + "lambda_create_fn_2": { + ... + "FunctionName": "<function-name:2>", + "FunctionArn": "arn:aws:lambda:<region>:111111111111:function:<function-name:2>", + "Runtime": "python3.9", + "Role": "arn:aws:iam::111111111111:role/<resource:1>", + ... + }, + "lambda_get_fn": { + ... + "Configuration": { + "FunctionName": "<function-name:1>", + "FunctionArn": "arn:aws:lambda:<region>:111111111111:function:<function-name:1>", + "Runtime": "python3.9", + "Role": "arn:aws:iam::111111111111:role/<resource:1>", + ... + }, + "lambda_get_fn_2": { + ... + "Configuration": { + "FunctionName": "<function-name:2>", + "FunctionArn": "arn:aws:lambda:<region>:111111111111:function:<function-name:2>", + "Role": "arn:aws:iam::111111111111:role/<resource:1>", + .... + }, + }, + + } + } +} +``` + +## Tips and Tricks for Transformers + +Getting the transformations right can be a tricky task and we appreciate the time you spend on writing parity snapshot tests for LocalStack! We are aware that it might be challenging to implement transformers that work for AWS and LocalStack responses. + +In general, we are interested in transformers that work for AWS. Therefore, we recommend also running the tests and testing the transformers against AWS itself. + +Meaning, after you have executed the test with the `snapshot-update` flag and recorded the snapshot, you can run the test without the update flag against the `AWS_CLOUD` test target. If the test passes, we can be quite certain that the transformers work in general. Any deviations with LocalStack might be due to missing parity. + +You do not have to fix any deviations right away, even though we would appreciate this very much! It is also possible to exclude the snapshot verification of single test cases, or specific json-pathes of the snapshot. + +### Skipping verification of snapshot test + +Snapshot verification is enabled by default. If for some reason you want to skip any snapshot verification, you can set the parameter `--snapshot-skip-all`. + +If you want to skip verification for or a single test case, you can set the pytest marker `skip_snapshot_verify`. If you set the marker without a parameter, the verification will be skipped entirely for this test case. + +Additionally, you can exclude certain paths from the verification only. +Simply include a list of json-paths. Those paths will then be excluded from the comparison: + +```python +@pytest.mark.skip_snapshot_verify( + paths=["$..LogResult", "$..Payload.context.memory_limit_in_mb"] + ) + def test_something_that_does_not_work_completly_yet(self, lambda_client, snapshot): + snapshot.add_transformer(snapshot.transform.lambda_api()) + result = lambda_client.... + snapshot.match("invoke-result", result) +``` + +> [!NOTE] +> Generally, [transformers](#using-transformers) should be used wherever possible to make responses comparable. +> If specific paths are skipped from the verification, it means LocalStack does not have parity yet. + +### Debugging the Transformers + +Sometimes different transformers might interfere, especially regex transformers and reference transformations can be tricky We added debug logs so that each replacement step should be visible in the output to help locate any unexpected behavior. You can enable the debug logs by setting the env `DEBUG_SNAPSHOT=1`. + +```bash +localstack.testing.snapshots.transformer: Registering regex pattern '000000000000' in snapshot with '111111111111' +localstack.testing.snapshots.transformer: Registering regex pattern 'us-east-1' in snapshot with '<region>'localstack.testing.snapshots.transformer: Replacing JsonPath '$.json_encoded_delivery..Body.Signature' in snapshot with '<signature>' +localstack.testing.snapshots.transformer: Registering reference replacement for value: '1ad533b5-ac54-4354-a273-3ea885f0d59d' -> '<uuid:1>' +localstack.testing.snapshots.transformer: Replacing JsonPath '$.json_encoded_delivery..MD5OfBody' in snapshot with '<md5-hash>' +localstack.testing.snapshots.transformer: Replacing regex '000000000000' with '111111111111' +localstack.testing.snapshots.transformer: Replacing regex 'us-east-1' with '<region>' +localstack.testing.snapshots.transformer: Replacing '1ad533b5-ac54-4354-a273-3ea885f0d59d' in snapshot with '<uuid:1>' +``` diff --git a/docs/terraform-tests/index.md b/docs/terraform-tests/index.md new file mode 100644 index 0000000000000..1a79f76ac51c0 --- /dev/null +++ b/docs/terraform-tests/index.md @@ -0,0 +1,3 @@ +# Terraform test suite + +We regularly run the test suite of the Terraform AWS provider against LocalStack to test the compatibility of LocalStack to Terraform. To achieve that, we have a dedicated [GitHub action](https://github.com/localstack/localstack-terraform-test/blob/main/.github/workflows/main.yml) on [LocalStack](https://github.com/localstack/localstack), which executes the allow listed set of tests of [hashicorp/terraform-provider-aws](https://github.com/hashicorp/terraform-provider-aws/). From e8c6706d7720b22b265134db4a254d0daf9ac0aa Mon Sep 17 00:00:00 2001 From: Dominik Schubert <dominik.schubert91@gmail.com> Date: Wed, 8 May 2024 09:26:55 +0200 Subject: [PATCH 125/169] Fix CFn UpdateStack response on identical templates that include a transformation (#10782) --- .../services/cloudformation/provider.py | 8 +++++ .../cloudformation/api/test_stacks.py | 34 +++++++++++++++---- .../api/test_stacks.snapshot.json | 16 +++++++++ .../api/test_stacks.validation.json | 6 ++++ tests/aws/templates/simple_no_change.yaml | 3 ++ .../simple_no_change_with_transformation.yaml | 4 +++ 6 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 tests/aws/templates/simple_no_change.yaml create mode 100644 tests/aws/templates/simple_no_change_with_transformation.yaml diff --git a/localstack/services/cloudformation/provider.py b/localstack/services/cloudformation/provider.py index ca796a9f2d537..eb03047adbdd8 100644 --- a/localstack/services/cloudformation/provider.py +++ b/localstack/services/cloudformation/provider.py @@ -1,3 +1,4 @@ +import copy import json import logging import re @@ -351,6 +352,7 @@ def update_stack( stack_name=stack_name, ) + raw_new_template = copy.deepcopy(template) try: template = template_preparer.transform_template( context.account_id, @@ -362,6 +364,9 @@ def update_stack( resolved_stack_conditions, resolved_parameters, ) + processed_template = copy.deepcopy( + template + ) # copying it here since it's being mutated somewhere downstream except FailedTransformationException as e: stack.add_stack_event( stack.stack_name, @@ -384,6 +389,9 @@ def update_stack( deployer.update_stack(new_stack) except NoStackUpdates as e: stack.set_stack_status("UPDATE_COMPLETE") + if raw_new_template != processed_template: + # processed templates seem to never return an exception here + return UpdateStackOutput(StackId=stack.stack_id) raise ValidationError(str(e)) except Exception as e: stack.set_stack_status("UPDATE_FAILED") diff --git a/tests/aws/services/cloudformation/api/test_stacks.py b/tests/aws/services/cloudformation/api/test_stacks.py index 00816e27cd5e7..3f39b04b32876 100644 --- a/tests/aws/services/cloudformation/api/test_stacks.py +++ b/tests/aws/services/cloudformation/api/test_stacks.py @@ -221,10 +221,12 @@ def test_list_stack_resources_for_removed_resource(self, deploy_cfn_template, aw statuses = {res["ResourceStatus"] for res in resources} assert statuses == {"UPDATE_COMPLETE"} - @markers.aws.needs_fixing - def test_update_stack_with_same_template_withoutchange(self, deploy_cfn_template, aws_client): + @markers.aws.validated + def test_update_stack_with_same_template_withoutchange( + self, deploy_cfn_template, aws_client, snapshot + ): template = load_file( - os.path.join(os.path.dirname(__file__), "../../../templates/fifo_queue.json") + os.path.join(os.path.dirname(__file__), "../../../templates/simple_no_change.yaml") ) stack = deploy_cfn_template(template=template) @@ -236,9 +238,29 @@ def test_update_stack_with_same_template_withoutchange(self, deploy_cfn_template StackName=stack.stack_name ) - error_message = str(ctx.value) - assert "UpdateStack" in error_message - assert "No updates are to be performed." in error_message + snapshot.match("no_change_exception", ctx.value.response) + + @markers.aws.validated + def test_update_stack_with_same_template_withoutchange_transformation( + self, deploy_cfn_template, aws_client + ): + template = load_file( + os.path.join( + os.path.dirname(__file__), + "../../../templates/simple_no_change_with_transformation.yaml", + ) + ) + stack = deploy_cfn_template(template=template) + + # transformations will always work even if there's no change in the template! + aws_client.cloudformation.update_stack( + StackName=stack.stack_name, + TemplateBody=template, + Capabilities=["CAPABILITY_AUTO_EXPAND"], + ) + aws_client.cloudformation.get_waiter("stack_update_complete").wait( + StackName=stack.stack_name + ) @markers.aws.validated def test_update_stack_actual_update(self, deploy_cfn_template, aws_client): diff --git a/tests/aws/services/cloudformation/api/test_stacks.snapshot.json b/tests/aws/services/cloudformation/api/test_stacks.snapshot.json index 856734c7871cb..ffa38b8d6c237 100644 --- a/tests/aws/services/cloudformation/api/test_stacks.snapshot.json +++ b/tests/aws/services/cloudformation/api/test_stacks.snapshot.json @@ -1165,5 +1165,21 @@ } } } + }, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_update_stack_with_same_template_withoutchange": { + "recorded-date": "07-05-2024, 08:34:18", + "recorded-content": { + "no_change_exception": { + "Error": { + "Code": "ValidationError", + "Message": "No updates are to be performed.", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } } } diff --git a/tests/aws/services/cloudformation/api/test_stacks.validation.json b/tests/aws/services/cloudformation/api/test_stacks.validation.json index 91ab8beaf6f7c..1ac8f9d01443b 100644 --- a/tests/aws/services/cloudformation/api/test_stacks.validation.json +++ b/tests/aws/services/cloudformation/api/test_stacks.validation.json @@ -20,6 +20,12 @@ "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_stack_update_resources": { "last_validated_date": "2022-08-29T22:13:26+00:00" }, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_update_stack_with_same_template_withoutchange": { + "last_validated_date": "2024-05-07T08:35:29+00:00" + }, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_update_stack_with_same_template_withoutchange_transformation": { + "last_validated_date": "2024-05-07T09:26:39+00:00" + }, "tests/aws/services/cloudformation/api/test_stacks.py::test_blocked_stack_deletion": { "last_validated_date": "2023-09-06T09:01:18+00:00" }, diff --git a/tests/aws/templates/simple_no_change.yaml b/tests/aws/templates/simple_no_change.yaml new file mode 100644 index 0000000000000..679d564db69a6 --- /dev/null +++ b/tests/aws/templates/simple_no_change.yaml @@ -0,0 +1,3 @@ +Resources: + MyTopic: + Type: AWS::SNS::Topic diff --git a/tests/aws/templates/simple_no_change_with_transformation.yaml b/tests/aws/templates/simple_no_change_with_transformation.yaml new file mode 100644 index 0000000000000..28450ca27ee7f --- /dev/null +++ b/tests/aws/templates/simple_no_change_with_transformation.yaml @@ -0,0 +1,4 @@ +Transform: AWS::Serverless-2016-10-31 +Resources: + MyTopic: + Type: AWS::SNS::Topic From 5af7689feb54472e595508fa10b649ea36d32fd8 Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Wed, 8 May 2024 09:40:54 +0200 Subject: [PATCH 126/169] fix banner link in READMEs (#10792) --- DOCKER.md | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/DOCKER.md b/DOCKER.md index 71b5f15d9fe64..3f1ab1ff70bfc 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -1,5 +1,5 @@ <p align="center"> - <img src="https://raw.githubusercontent.com/localstack/localstack/master/doc/localstack-readme-banner.svg" alt="LocalStack - A fully functional local cloud stack"> + <img src="https://raw.githubusercontent.com/localstack/localstack/master/docs/localstack-readme-banner.svg" alt="LocalStack - A fully functional local cloud stack"> </p> <p align="center"> diff --git a/README.md b/README.md index e141a7789e1d7..6513cc82a050f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ </p> <p align="center"> - <img src="https://raw.githubusercontent.com/localstack/localstack/master/doc/localstack-readme-banner.svg" alt="LocalStack - A fully functional local cloud stack"> + <img src="https://raw.githubusercontent.com/localstack/localstack/master/docs/localstack-readme-banner.svg" alt="LocalStack - A fully functional local cloud stack"> </p> <p align="center"> From d8af44bbb40ddc12b8a9e5cabd7c3b19e79e9dd0 Mon Sep 17 00:00:00 2001 From: Thomas Rausch <thomas@thrau.at> Date: Wed, 8 May 2024 12:05:39 +0200 Subject: [PATCH 127/169] Update PULL_REQUEST_TEMPLATE.md (#10793) --- .github/PULL_REQUEST_TEMPLATE.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 56b1aebdec095..ee3b8eecf5459 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,25 +1,24 @@ -<!-- Please refer to the contribution guidelines before raising a PR: https://github.com/localstack/localstack/blob/master/CONTRIBUTING.md --> +<!-- Please refer to the contribution guidelines before raising a PR: https://github.com/localstack/localstack/blob/master/docs/CONTRIBUTING.md --> <!-- Why am I raising this PR? Add context such as related issues, PRs, or documentation. --> ## Motivation -<!-- What notable changes does this PR make? --> +<!-- What changes does this PR make? How does LocalStack behave differently now? --> ## Changes - -<!-- The following sections are optional, but can be useful! - +<!-- Optional section: How to test these changes? --> +<!-- ## Testing -Description of how to test the changes +--> +<!-- Optional section: What's left to do before it can be merged? --> +<!-- ## TODO What's left to do: - [ ] ... - [ ] ... - --> - From 89257bf5d7cd154b6448802fe3e63f8824c21364 Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Wed, 8 May 2024 12:54:18 +0200 Subject: [PATCH 128/169] StepFunctions: Execution of Reentrant Distributed Map States (#10763) --- .../distributed_iteration_component.py | 2 + .../scenarios/scenarios_templates.py | 12 + ...p_state_config_distributed_reentrant.json5 | 61 + ..._config_distributed_reentrant_lambda.json5 | 65 + .../map_state_legacy_reentrant.json5 | 57 + .../v2/scenarios/test_base_scenarios.py | 88 + .../test_base_scenarios.snapshot.json | 2073 +++++++++++++++++ .../test_base_scenarios.validation.json | 9 + 8 files changed, 2367 insertions(+) create mode 100644 tests/aws/services/stepfunctions/templates/scenarios/statemachines/map_state_config_distributed_reentrant.json5 create mode 100644 tests/aws/services/stepfunctions/templates/scenarios/statemachines/map_state_config_distributed_reentrant_lambda.json5 create mode 100644 tests/aws/services/stepfunctions/templates/scenarios/statemachines/map_state_legacy_reentrant.json5 diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/distributed_iteration_component.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/distributed_iteration_component.py index c201b90c3b0bf..a070b27af00e8 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/distributed_iteration_component.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_map/iteration/distributed_iteration_component.py @@ -202,6 +202,8 @@ def _eval_body(self, env: Environment) -> None: raise ex finally: env.event_history = execution_event_history + self._eval_input = None + self._workers.clear() # TODO: review workflow of program stops and maprunstops # program_state = env.program_state() diff --git a/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py b/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py index e1084e2d4d4ad..50800f12102e9 100644 --- a/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py +++ b/tests/aws/services/stepfunctions/templates/scenarios/scenarios_templates.py @@ -58,6 +58,15 @@ class ScenariosTemplate(TemplateLoader): MAP_STATE_CONFIG_DISTRIBUTED_ITEM_SELECTOR: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/map_state_config_distributed_item_selector.json5" ) + MAP_STATE_LEGACY_REENTRANT: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/map_state_legacy_reentrant.json5" + ) + MAP_STATE_CONFIG_DISTRIBUTED_REENTRANT: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/map_state_config_distributed_reentrant.json5" + ) + MAP_STATE_CONFIG_DISTRIBUTED_REENTRANT_LAMBDA: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/map_state_config_distributed_reentrant_lambda.json5" + ) MAP_STATE_CONFIG_INLINE_PARAMETERS: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/map_state_config_inline_parameters.json5" ) @@ -110,6 +119,9 @@ class ScenariosTemplate(TemplateLoader): MAP_STATE_CATCH_LEGACY: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/map_state_catch_legacy.json5" ) + MAP_STATE_LEGACY_REENTRANT: Final[str] = os.path.join( + _THIS_FOLDER, "statemachines/map_state_legacy_reentrant.json5" + ) MAP_STATE_RETRY: Final[str] = os.path.join(_THIS_FOLDER, "statemachines/map_state_retry.json5") MAP_STATE_RETRY_LEGACY: Final[str] = os.path.join( _THIS_FOLDER, "statemachines/map_state_retry_legacy.json5" diff --git a/tests/aws/services/stepfunctions/templates/scenarios/statemachines/map_state_config_distributed_reentrant.json5 b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/map_state_config_distributed_reentrant.json5 new file mode 100644 index 0000000000000..2722f9f06ae7c --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/map_state_config_distributed_reentrant.json5 @@ -0,0 +1,61 @@ +{ + "Comment": "MAP_STATE_CONFIG_DISTRIBUTED_REENTRANT", + "StartAt": "StartState", + "States": { + // Populate memory with two fields: number of iterations left, and input values. + "StartState": { + "Type": "Pass", + "Parameters": { + "Iterations": 3, + "Values.$": "States.ArrayRange(0, 3, 1)" + }, + "Next": "BeforeIteration" + }, + // Prepare the iteration by updating the iterations count. + "BeforeIteration": { + "Type": "Pass", + "Parameters": { + "Iterations.$": "States.MathAdd($.Iterations, -1)", + "Values.$": "$.Values" + }, + "Next": "IterationBody" + }, + // Run a distributed map state on the values field. + "IterationBody": { + "Type": "Map", + "MaxConcurrency": 1, + "ItemsPath": "$.Values", + "ItemProcessor": { // Use ItemProcessor over legacy's Iterator. + "ProcessorConfig": { + "Mode": "DISTRIBUTED", + "ExecutionType": "STANDARD" + }, + "StartAt": "ProcessValue", + "States": { + "ProcessValue": { + "Type": "Pass", + "End": true + } + } + }, + "ResultPath": "$.Values", + "Next": "CheckIteration" + }, + // Check the number of iterations is zero and terminate, otherwise run another iteration. + "CheckIteration": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.Iterations", + "NumericEquals": 0, + "Next": "Terminate" + } + ], + "Default": "BeforeIteration" + }, + // Terminate the execution. + "Terminate": { + "Type": "Succeed" + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/templates/scenarios/statemachines/map_state_config_distributed_reentrant_lambda.json5 b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/map_state_config_distributed_reentrant_lambda.json5 new file mode 100644 index 0000000000000..4360254731f60 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/map_state_config_distributed_reentrant_lambda.json5 @@ -0,0 +1,65 @@ +{ + "Comment": "MAP_STATE_CONFIG_DISTRIBUTED_REENTRANT_LAMBDA", + "StartAt": "StartState", + "States": { + // Populate memory with two fields: number of iterations left, and input values. + "StartState": { + "Type": "Pass", + "Parameters": { + "Iterations": 2, + "Values": [ + "HelloWorld" + ] + }, + "Next": "BeforeIteration" + }, + // Prepare the iteration by updating the iterations count. + "BeforeIteration": { + "Type": "Pass", + "Parameters": { + "Iterations.$": "States.MathAdd($.Iterations, -1)", + "Values.$": "$.Values" + }, + "Next": "IterationBody" + }, + // Run a distributed map state on the values field. + "IterationBody": { + "Type": "Map", + "MaxConcurrency": 1, + "ItemsPath": "$.Values", + "ItemProcessor": { // Use ItemProcessor over legacy's Iterator. + "ProcessorConfig": { + "Mode": "DISTRIBUTED", + "ExecutionType": "STANDARD" + }, + "StartAt": "ProcessValue", + "States": { + // Delegate the input to a task state, allowing the resource to be configured on creation. + "ProcessValue": { + "Type": "Task", + "Resource": "_tbd_", + "End": true + } + } + }, + "ResultPath": "$.Values", + "Next": "CheckIteration" + }, + // Check the number of iterations is zero and terminate, otherwise run another iteration. + "CheckIteration": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.Iterations", + "NumericEquals": 0, + "Next": "Terminate" + } + ], + "Default": "BeforeIteration" + }, + // Terminate the execution. + "Terminate": { + "Type": "Succeed" + } + } +} \ No newline at end of file diff --git a/tests/aws/services/stepfunctions/templates/scenarios/statemachines/map_state_legacy_reentrant.json5 b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/map_state_legacy_reentrant.json5 new file mode 100644 index 0000000000000..a0ab87551d309 --- /dev/null +++ b/tests/aws/services/stepfunctions/templates/scenarios/statemachines/map_state_legacy_reentrant.json5 @@ -0,0 +1,57 @@ +{ + "Comment": "MAP_STATE_LEGACY_REENTRANT", + "StartAt": "StartState", + "States": { + // Populate memory with two fields: number of iterations left, and input values. + "StartState": { + "Type": "Pass", + "Parameters": { + "Iterations": 3, + "Values.$": "States.ArrayRange(0, 3, 1)" + }, + "Next": "BeforeIteration" + }, + // Prepare the iteration by updating the iterations count. + "BeforeIteration": { + "Type": "Pass", + "Parameters": { + "Iterations.$": "States.MathAdd($.Iterations, -1)", + "Values.$": "$.Values" + }, + "Next": "IterationBody" + }, + // Run a distributed map state on the values field. + "IterationBody": { + "Type": "Map", + "MaxConcurrency": 1, + "ItemsPath": "$.Values", + "Iterator": { + "StartAt": "ProcessValue", + "States": { + "ProcessValue": { + "Type": "Pass", + "End": true + } + } + }, + "ResultPath": "$.Values", + "Next": "CheckIteration" + }, + // Check the number of iterations is zero and terminate, otherwise run another iteration. + "CheckIteration": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.Iterations", + "NumericEquals": 0, + "Next": "Terminate" + } + ], + "Default": "BeforeIteration" + }, + // Terminate the execution. + "Terminate": { + "Type": "Succeed" + } + } +} diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py index 15d25e4e6d765..f622b8e9b73aa 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py @@ -579,6 +579,94 @@ def test_map_state_config_distributed_item_selector( exec_input, ) + @markers.aws.validated + def test_map_state_legacy_reentrant( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + ): + template = ST.load_sfn_template(ST.MAP_STATE_LEGACY_REENTRANT) + definition = json.dumps(template) + + exec_input = json.dumps({}) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + + @markers.aws.validated + def test_map_state_config_distributed_reentrant( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + ): + # Replace MapRunArns with fixed values to circumvent random ordering issues. + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..mapRunArn", replacement="map_run_arn", replace_reference=False + ) + ) + + template = ST.load_sfn_template(ST.MAP_STATE_CONFIG_DISTRIBUTED_REENTRANT) + definition = json.dumps(template) + + exec_input = json.dumps({}) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + + @markers.aws.validated + def test_map_state_config_distributed_reentrant_lambda( + self, + aws_client, + create_iam_role_for_sfn, + create_state_machine, + create_lambda_function, + sfn_snapshot, + ): + # Replace MapRunArns with fixed values to circumvent random ordering issues. + sfn_snapshot.add_transformer( + JsonpathTransformer( + jsonpath="$..mapRunArn", replacement="map_run_arn", replace_reference=False + ) + ) + + function_name = f"sfn_lambda_{short_uid()}" + create_res = create_lambda_function( + func_name=function_name, + handler_file=SerT.LAMBDA_ID_FUNCTION, + runtime=Runtime.python3_12, + ) + sfn_snapshot.add_transformer(RegexTransformer(function_name, "lambda_function_name")) + function_arn = create_res["CreateFunctionResponse"]["FunctionArn"] + + template = ST.load_sfn_template(ST.MAP_STATE_CONFIG_DISTRIBUTED_REENTRANT_LAMBDA) + definition = json.dumps(template) + definition = definition.replace("_tbd_", function_arn) + + exec_input = json.dumps({}) + create_and_record_execution( + aws_client.stepfunctions, + create_iam_role_for_sfn, + create_state_machine, + sfn_snapshot, + definition, + exec_input, + ) + @markers.aws.validated @markers.snapshot.skip_snapshot_verify( paths=[ diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json index 8da6dde63660f..2fae4002bafc2 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.snapshot.json @@ -16122,5 +16122,2078 @@ } } } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_config_distributed_reentrant": { + "recorded-date": "03-05-2024, 13:45:28", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "StartState" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "StartState", + "output": { + "Iterations": 3, + "Values": [ + 0, + 1, + 2, + 3 + ] + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 4, + "previousEventId": 3, + "stateEnteredEventDetails": { + "input": { + "Iterations": 3, + "Values": [ + 0, + 1, + 2, + 3 + ] + }, + "inputDetails": { + "truncated": false + }, + "name": "BeforeIteration" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 5, + "previousEventId": 4, + "stateExitedEventDetails": { + "name": "BeforeIteration", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 2 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 6, + "previousEventId": 5, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 2 + }, + "inputDetails": { + "truncated": false + }, + "name": "IterationBody" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "id": 7, + "mapStateStartedEventDetails": { + "length": 4 + }, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "MapStateStarted" + }, + { + "id": 8, + "mapRunStartedEventDetails": { + "mapRunArn": "map_run_arn" + }, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "MapRunStarted" + }, + { + "id": 9, + "previousEventId": 8, + "timestamp": "timestamp", + "type": "MapRunSucceeded" + }, + { + "id": 10, + "previousEventId": 9, + "timestamp": "timestamp", + "type": "MapStateSucceeded" + }, + { + "id": 11, + "previousEventId": 9, + "stateExitedEventDetails": { + "name": "IterationBody", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 2 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "MapStateExited" + }, + { + "id": 12, + "previousEventId": 11, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 2 + }, + "inputDetails": { + "truncated": false + }, + "name": "CheckIteration" + }, + "timestamp": "timestamp", + "type": "ChoiceStateEntered" + }, + { + "id": 13, + "previousEventId": 12, + "stateExitedEventDetails": { + "name": "CheckIteration", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 2 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "ChoiceStateExited" + }, + { + "id": 14, + "previousEventId": 13, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 2 + }, + "inputDetails": { + "truncated": false + }, + "name": "BeforeIteration" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 15, + "previousEventId": 14, + "stateExitedEventDetails": { + "name": "BeforeIteration", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 1 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 16, + "previousEventId": 15, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 1 + }, + "inputDetails": { + "truncated": false + }, + "name": "IterationBody" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "id": 17, + "mapStateStartedEventDetails": { + "length": 4 + }, + "previousEventId": 16, + "timestamp": "timestamp", + "type": "MapStateStarted" + }, + { + "id": 18, + "mapRunStartedEventDetails": { + "mapRunArn": "map_run_arn" + }, + "previousEventId": 17, + "timestamp": "timestamp", + "type": "MapRunStarted" + }, + { + "id": 19, + "previousEventId": 18, + "timestamp": "timestamp", + "type": "MapRunSucceeded" + }, + { + "id": 20, + "previousEventId": 19, + "timestamp": "timestamp", + "type": "MapStateSucceeded" + }, + { + "id": 21, + "previousEventId": 19, + "stateExitedEventDetails": { + "name": "IterationBody", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 1 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "MapStateExited" + }, + { + "id": 22, + "previousEventId": 21, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 1 + }, + "inputDetails": { + "truncated": false + }, + "name": "CheckIteration" + }, + "timestamp": "timestamp", + "type": "ChoiceStateEntered" + }, + { + "id": 23, + "previousEventId": 22, + "stateExitedEventDetails": { + "name": "CheckIteration", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 1 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "ChoiceStateExited" + }, + { + "id": 24, + "previousEventId": 23, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 1 + }, + "inputDetails": { + "truncated": false + }, + "name": "BeforeIteration" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 25, + "previousEventId": 24, + "stateExitedEventDetails": { + "name": "BeforeIteration", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 26, + "previousEventId": 25, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "IterationBody" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "id": 27, + "mapStateStartedEventDetails": { + "length": 4 + }, + "previousEventId": 26, + "timestamp": "timestamp", + "type": "MapStateStarted" + }, + { + "id": 28, + "mapRunStartedEventDetails": { + "mapRunArn": "map_run_arn" + }, + "previousEventId": 27, + "timestamp": "timestamp", + "type": "MapRunStarted" + }, + { + "id": 29, + "previousEventId": 28, + "timestamp": "timestamp", + "type": "MapRunSucceeded" + }, + { + "id": 30, + "previousEventId": 29, + "timestamp": "timestamp", + "type": "MapStateSucceeded" + }, + { + "id": 31, + "previousEventId": 29, + "stateExitedEventDetails": { + "name": "IterationBody", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "MapStateExited" + }, + { + "id": 32, + "previousEventId": 31, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "CheckIteration" + }, + "timestamp": "timestamp", + "type": "ChoiceStateEntered" + }, + { + "id": 33, + "previousEventId": 32, + "stateExitedEventDetails": { + "name": "CheckIteration", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "ChoiceStateExited" + }, + { + "id": 34, + "previousEventId": 33, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "Terminate" + }, + "timestamp": "timestamp", + "type": "SucceedStateEntered" + }, + { + "id": 35, + "previousEventId": 34, + "stateExitedEventDetails": { + "name": "Terminate", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "SucceedStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "id": 36, + "previousEventId": 35, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_config_distributed_reentrant_lambda": { + "recorded-date": "03-05-2024, 13:44:53", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "StartState" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "StartState", + "output": { + "Iterations": 2, + "Values": [ + "HelloWorld" + ] + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 4, + "previousEventId": 3, + "stateEnteredEventDetails": { + "input": { + "Iterations": 2, + "Values": [ + "HelloWorld" + ] + }, + "inputDetails": { + "truncated": false + }, + "name": "BeforeIteration" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 5, + "previousEventId": 4, + "stateExitedEventDetails": { + "name": "BeforeIteration", + "output": { + "Values": [ + "HelloWorld" + ], + "Iterations": 1 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 6, + "previousEventId": 5, + "stateEnteredEventDetails": { + "input": { + "Values": [ + "HelloWorld" + ], + "Iterations": 1 + }, + "inputDetails": { + "truncated": false + }, + "name": "IterationBody" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "id": 7, + "mapStateStartedEventDetails": { + "length": 1 + }, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "MapStateStarted" + }, + { + "id": 8, + "mapRunStartedEventDetails": { + "mapRunArn": "map_run_arn" + }, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "MapRunStarted" + }, + { + "id": 9, + "previousEventId": 8, + "timestamp": "timestamp", + "type": "MapRunSucceeded" + }, + { + "id": 10, + "previousEventId": 9, + "timestamp": "timestamp", + "type": "MapStateSucceeded" + }, + { + "id": 11, + "previousEventId": 9, + "stateExitedEventDetails": { + "name": "IterationBody", + "output": { + "Values": [ + "HelloWorld" + ], + "Iterations": 1 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "MapStateExited" + }, + { + "id": 12, + "previousEventId": 11, + "stateEnteredEventDetails": { + "input": { + "Values": [ + "HelloWorld" + ], + "Iterations": 1 + }, + "inputDetails": { + "truncated": false + }, + "name": "CheckIteration" + }, + "timestamp": "timestamp", + "type": "ChoiceStateEntered" + }, + { + "id": 13, + "previousEventId": 12, + "stateExitedEventDetails": { + "name": "CheckIteration", + "output": { + "Values": [ + "HelloWorld" + ], + "Iterations": 1 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "ChoiceStateExited" + }, + { + "id": 14, + "previousEventId": 13, + "stateEnteredEventDetails": { + "input": { + "Values": [ + "HelloWorld" + ], + "Iterations": 1 + }, + "inputDetails": { + "truncated": false + }, + "name": "BeforeIteration" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 15, + "previousEventId": 14, + "stateExitedEventDetails": { + "name": "BeforeIteration", + "output": { + "Values": [ + "HelloWorld" + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 16, + "previousEventId": 15, + "stateEnteredEventDetails": { + "input": { + "Values": [ + "HelloWorld" + ], + "Iterations": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "IterationBody" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "id": 17, + "mapStateStartedEventDetails": { + "length": 1 + }, + "previousEventId": 16, + "timestamp": "timestamp", + "type": "MapStateStarted" + }, + { + "id": 18, + "mapRunStartedEventDetails": { + "mapRunArn": "map_run_arn" + }, + "previousEventId": 17, + "timestamp": "timestamp", + "type": "MapRunStarted" + }, + { + "id": 19, + "previousEventId": 18, + "timestamp": "timestamp", + "type": "MapRunSucceeded" + }, + { + "id": 20, + "previousEventId": 19, + "timestamp": "timestamp", + "type": "MapStateSucceeded" + }, + { + "id": 21, + "previousEventId": 19, + "stateExitedEventDetails": { + "name": "IterationBody", + "output": { + "Values": [ + "HelloWorld" + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "MapStateExited" + }, + { + "id": 22, + "previousEventId": 21, + "stateEnteredEventDetails": { + "input": { + "Values": [ + "HelloWorld" + ], + "Iterations": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "CheckIteration" + }, + "timestamp": "timestamp", + "type": "ChoiceStateEntered" + }, + { + "id": 23, + "previousEventId": 22, + "stateExitedEventDetails": { + "name": "CheckIteration", + "output": { + "Values": [ + "HelloWorld" + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "ChoiceStateExited" + }, + { + "id": 24, + "previousEventId": 23, + "stateEnteredEventDetails": { + "input": { + "Values": [ + "HelloWorld" + ], + "Iterations": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "Terminate" + }, + "timestamp": "timestamp", + "type": "SucceedStateEntered" + }, + { + "id": 25, + "previousEventId": 24, + "stateExitedEventDetails": { + "name": "Terminate", + "output": { + "Values": [ + "HelloWorld" + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "SucceedStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "Values": [ + "HelloWorld" + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "id": 26, + "previousEventId": 25, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_legacy_reentrant": { + "recorded-date": "03-05-2024, 13:41:47", + "recorded-content": { + "get_execution_history": { + "events": [ + { + "executionStartedEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "roleArn": "snf_role_arn" + }, + "id": 1, + "previousEventId": 0, + "timestamp": "timestamp", + "type": "ExecutionStarted" + }, + { + "id": 2, + "previousEventId": 0, + "stateEnteredEventDetails": { + "input": {}, + "inputDetails": { + "truncated": false + }, + "name": "StartState" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 3, + "previousEventId": 2, + "stateExitedEventDetails": { + "name": "StartState", + "output": { + "Iterations": 3, + "Values": [ + 0, + 1, + 2, + 3 + ] + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 4, + "previousEventId": 3, + "stateEnteredEventDetails": { + "input": { + "Iterations": 3, + "Values": [ + 0, + 1, + 2, + 3 + ] + }, + "inputDetails": { + "truncated": false + }, + "name": "BeforeIteration" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 5, + "previousEventId": 4, + "stateExitedEventDetails": { + "name": "BeforeIteration", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 2 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 6, + "previousEventId": 5, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 2 + }, + "inputDetails": { + "truncated": false + }, + "name": "IterationBody" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "id": 7, + "mapStateStartedEventDetails": { + "length": 4 + }, + "previousEventId": 6, + "timestamp": "timestamp", + "type": "MapStateStarted" + }, + { + "id": 8, + "mapIterationStartedEventDetails": { + "index": 0, + "name": "IterationBody" + }, + "previousEventId": 7, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 9, + "previousEventId": 8, + "stateEnteredEventDetails": { + "input": "0", + "inputDetails": { + "truncated": false + }, + "name": "ProcessValue" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 10, + "previousEventId": 9, + "stateExitedEventDetails": { + "name": "ProcessValue", + "output": "0", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 11, + "mapIterationSucceededEventDetails": { + "index": 0, + "name": "IterationBody" + }, + "previousEventId": 10, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 12, + "mapIterationStartedEventDetails": { + "index": 1, + "name": "IterationBody" + }, + "previousEventId": 10, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 13, + "previousEventId": 12, + "stateEnteredEventDetails": { + "input": "1", + "inputDetails": { + "truncated": false + }, + "name": "ProcessValue" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 14, + "previousEventId": 13, + "stateExitedEventDetails": { + "name": "ProcessValue", + "output": "1", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 15, + "mapIterationSucceededEventDetails": { + "index": 1, + "name": "IterationBody" + }, + "previousEventId": 14, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 16, + "mapIterationStartedEventDetails": { + "index": 2, + "name": "IterationBody" + }, + "previousEventId": 14, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 17, + "previousEventId": 16, + "stateEnteredEventDetails": { + "input": "2", + "inputDetails": { + "truncated": false + }, + "name": "ProcessValue" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 18, + "previousEventId": 17, + "stateExitedEventDetails": { + "name": "ProcessValue", + "output": "2", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 19, + "mapIterationSucceededEventDetails": { + "index": 2, + "name": "IterationBody" + }, + "previousEventId": 18, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 20, + "mapIterationStartedEventDetails": { + "index": 3, + "name": "IterationBody" + }, + "previousEventId": 18, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 21, + "previousEventId": 20, + "stateEnteredEventDetails": { + "input": "3", + "inputDetails": { + "truncated": false + }, + "name": "ProcessValue" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 22, + "previousEventId": 21, + "stateExitedEventDetails": { + "name": "ProcessValue", + "output": "3", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 23, + "mapIterationSucceededEventDetails": { + "index": 3, + "name": "IterationBody" + }, + "previousEventId": 22, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 24, + "previousEventId": 23, + "timestamp": "timestamp", + "type": "MapStateSucceeded" + }, + { + "id": 25, + "previousEventId": 23, + "stateExitedEventDetails": { + "name": "IterationBody", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 2 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "MapStateExited" + }, + { + "id": 26, + "previousEventId": 25, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 2 + }, + "inputDetails": { + "truncated": false + }, + "name": "CheckIteration" + }, + "timestamp": "timestamp", + "type": "ChoiceStateEntered" + }, + { + "id": 27, + "previousEventId": 26, + "stateExitedEventDetails": { + "name": "CheckIteration", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 2 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "ChoiceStateExited" + }, + { + "id": 28, + "previousEventId": 27, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 2 + }, + "inputDetails": { + "truncated": false + }, + "name": "BeforeIteration" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 29, + "previousEventId": 28, + "stateExitedEventDetails": { + "name": "BeforeIteration", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 1 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 30, + "previousEventId": 29, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 1 + }, + "inputDetails": { + "truncated": false + }, + "name": "IterationBody" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "id": 31, + "mapStateStartedEventDetails": { + "length": 4 + }, + "previousEventId": 30, + "timestamp": "timestamp", + "type": "MapStateStarted" + }, + { + "id": 32, + "mapIterationStartedEventDetails": { + "index": 0, + "name": "IterationBody" + }, + "previousEventId": 31, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 33, + "previousEventId": 32, + "stateEnteredEventDetails": { + "input": "0", + "inputDetails": { + "truncated": false + }, + "name": "ProcessValue" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 34, + "previousEventId": 33, + "stateExitedEventDetails": { + "name": "ProcessValue", + "output": "0", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 35, + "mapIterationSucceededEventDetails": { + "index": 0, + "name": "IterationBody" + }, + "previousEventId": 34, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 36, + "mapIterationStartedEventDetails": { + "index": 1, + "name": "IterationBody" + }, + "previousEventId": 34, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 37, + "previousEventId": 36, + "stateEnteredEventDetails": { + "input": "1", + "inputDetails": { + "truncated": false + }, + "name": "ProcessValue" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 38, + "previousEventId": 37, + "stateExitedEventDetails": { + "name": "ProcessValue", + "output": "1", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 39, + "mapIterationSucceededEventDetails": { + "index": 1, + "name": "IterationBody" + }, + "previousEventId": 38, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 40, + "mapIterationStartedEventDetails": { + "index": 2, + "name": "IterationBody" + }, + "previousEventId": 38, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 41, + "previousEventId": 40, + "stateEnteredEventDetails": { + "input": "2", + "inputDetails": { + "truncated": false + }, + "name": "ProcessValue" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 42, + "previousEventId": 41, + "stateExitedEventDetails": { + "name": "ProcessValue", + "output": "2", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 43, + "mapIterationSucceededEventDetails": { + "index": 2, + "name": "IterationBody" + }, + "previousEventId": 42, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 44, + "mapIterationStartedEventDetails": { + "index": 3, + "name": "IterationBody" + }, + "previousEventId": 42, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 45, + "previousEventId": 44, + "stateEnteredEventDetails": { + "input": "3", + "inputDetails": { + "truncated": false + }, + "name": "ProcessValue" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 46, + "previousEventId": 45, + "stateExitedEventDetails": { + "name": "ProcessValue", + "output": "3", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 47, + "mapIterationSucceededEventDetails": { + "index": 3, + "name": "IterationBody" + }, + "previousEventId": 46, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 48, + "previousEventId": 47, + "timestamp": "timestamp", + "type": "MapStateSucceeded" + }, + { + "id": 49, + "previousEventId": 47, + "stateExitedEventDetails": { + "name": "IterationBody", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 1 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "MapStateExited" + }, + { + "id": 50, + "previousEventId": 49, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 1 + }, + "inputDetails": { + "truncated": false + }, + "name": "CheckIteration" + }, + "timestamp": "timestamp", + "type": "ChoiceStateEntered" + }, + { + "id": 51, + "previousEventId": 50, + "stateExitedEventDetails": { + "name": "CheckIteration", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 1 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "ChoiceStateExited" + }, + { + "id": 52, + "previousEventId": 51, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 1 + }, + "inputDetails": { + "truncated": false + }, + "name": "BeforeIteration" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 53, + "previousEventId": 52, + "stateExitedEventDetails": { + "name": "BeforeIteration", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 54, + "previousEventId": 53, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "IterationBody" + }, + "timestamp": "timestamp", + "type": "MapStateEntered" + }, + { + "id": 55, + "mapStateStartedEventDetails": { + "length": 4 + }, + "previousEventId": 54, + "timestamp": "timestamp", + "type": "MapStateStarted" + }, + { + "id": 56, + "mapIterationStartedEventDetails": { + "index": 0, + "name": "IterationBody" + }, + "previousEventId": 55, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 57, + "previousEventId": 56, + "stateEnteredEventDetails": { + "input": "0", + "inputDetails": { + "truncated": false + }, + "name": "ProcessValue" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 58, + "previousEventId": 57, + "stateExitedEventDetails": { + "name": "ProcessValue", + "output": "0", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 59, + "mapIterationSucceededEventDetails": { + "index": 0, + "name": "IterationBody" + }, + "previousEventId": 58, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 60, + "mapIterationStartedEventDetails": { + "index": 1, + "name": "IterationBody" + }, + "previousEventId": 58, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 61, + "previousEventId": 60, + "stateEnteredEventDetails": { + "input": "1", + "inputDetails": { + "truncated": false + }, + "name": "ProcessValue" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 62, + "previousEventId": 61, + "stateExitedEventDetails": { + "name": "ProcessValue", + "output": "1", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 63, + "mapIterationSucceededEventDetails": { + "index": 1, + "name": "IterationBody" + }, + "previousEventId": 62, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 64, + "mapIterationStartedEventDetails": { + "index": 2, + "name": "IterationBody" + }, + "previousEventId": 62, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 65, + "previousEventId": 64, + "stateEnteredEventDetails": { + "input": "2", + "inputDetails": { + "truncated": false + }, + "name": "ProcessValue" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 66, + "previousEventId": 65, + "stateExitedEventDetails": { + "name": "ProcessValue", + "output": "2", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 67, + "mapIterationSucceededEventDetails": { + "index": 2, + "name": "IterationBody" + }, + "previousEventId": 66, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 68, + "mapIterationStartedEventDetails": { + "index": 3, + "name": "IterationBody" + }, + "previousEventId": 66, + "timestamp": "timestamp", + "type": "MapIterationStarted" + }, + { + "id": 69, + "previousEventId": 68, + "stateEnteredEventDetails": { + "input": "3", + "inputDetails": { + "truncated": false + }, + "name": "ProcessValue" + }, + "timestamp": "timestamp", + "type": "PassStateEntered" + }, + { + "id": 70, + "previousEventId": 69, + "stateExitedEventDetails": { + "name": "ProcessValue", + "output": "3", + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "PassStateExited" + }, + { + "id": 71, + "mapIterationSucceededEventDetails": { + "index": 3, + "name": "IterationBody" + }, + "previousEventId": 70, + "timestamp": "timestamp", + "type": "MapIterationSucceeded" + }, + { + "id": 72, + "previousEventId": 71, + "timestamp": "timestamp", + "type": "MapStateSucceeded" + }, + { + "id": 73, + "previousEventId": 71, + "stateExitedEventDetails": { + "name": "IterationBody", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "MapStateExited" + }, + { + "id": 74, + "previousEventId": 73, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "CheckIteration" + }, + "timestamp": "timestamp", + "type": "ChoiceStateEntered" + }, + { + "id": 75, + "previousEventId": 74, + "stateExitedEventDetails": { + "name": "CheckIteration", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "ChoiceStateExited" + }, + { + "id": 76, + "previousEventId": 75, + "stateEnteredEventDetails": { + "input": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 0 + }, + "inputDetails": { + "truncated": false + }, + "name": "Terminate" + }, + "timestamp": "timestamp", + "type": "SucceedStateEntered" + }, + { + "id": 77, + "previousEventId": 76, + "stateExitedEventDetails": { + "name": "Terminate", + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "timestamp": "timestamp", + "type": "SucceedStateExited" + }, + { + "executionSucceededEventDetails": { + "output": { + "Values": [ + 0, + 1, + 2, + 3 + ], + "Iterations": 0 + }, + "outputDetails": { + "truncated": false + } + }, + "id": 78, + "previousEventId": 77, + "timestamp": "timestamp", + "type": "ExecutionSucceeded" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json index ca715e51082ed..ce2c326388ac4 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.validation.json @@ -119,6 +119,12 @@ "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_config_distributed_parameters": { "last_validated_date": "2024-02-08T21:44:45+00:00" }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_config_distributed_reentrant": { + "last_validated_date": "2024-05-03T13:45:28+00:00" + }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_config_distributed_reentrant_lambda": { + "last_validated_date": "2024-05-03T13:44:53+00:00" + }, "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_config_inline_item_selector": { "last_validated_date": "2024-02-08T21:47:17+00:00" }, @@ -152,6 +158,9 @@ "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_legacy_config_inline_parameters": { "last_validated_date": "2024-02-08T21:07:39+00:00" }, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_legacy_reentrant": { + "last_validated_date": "2024-05-03T13:41:47+00:00" + }, "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_nested": { "last_validated_date": "2024-03-29T16:26:02+00:00" }, From 1ab2f1c91b6fd660995f43ecda8e6047ce087a81 Mon Sep 17 00:00:00 2001 From: Max <max.hoheiser@gmail.com> Date: Wed, 8 May 2024 13:56:51 +0200 Subject: [PATCH 129/169] Refactor: Events v2: Move existing provider code to v1 folder (#10730) --- localstack/services/events/event_bus.py | 2 +- localstack/services/events/event_ruler.py | 2 +- localstack/services/events/models.py | 103 +- localstack/services/events/models_v2.py | 96 -- localstack/services/events/provider.py | 1073 +++++++++-------- localstack/services/events/provider_v2.py | 698 ----------- localstack/services/events/rule.py | 6 +- localstack/services/events/v1/__init__.py | 0 localstack/services/events/v1/models.py | 11 + localstack/services/events/v1/provider.py | 575 +++++++++ localstack/services/events/{ => v1}/utils.py | 10 +- localstack/services/providers.py | 6 +- tests/aws/services/events/test_events.py | 2 +- .../legacy/test_stepfunctions_legacy.py | 2 +- .../stepfunctions/v2/test_stepfunctions_v2.py | 2 +- 15 files changed, 1293 insertions(+), 1295 deletions(-) delete mode 100644 localstack/services/events/models_v2.py delete mode 100644 localstack/services/events/provider_v2.py create mode 100644 localstack/services/events/v1/__init__.py create mode 100644 localstack/services/events/v1/models.py create mode 100644 localstack/services/events/v1/provider.py rename localstack/services/events/{ => v1}/utils.py (98%) diff --git a/localstack/services/events/event_bus.py b/localstack/services/events/event_bus.py index c014461be20c5..348246c616449 100644 --- a/localstack/services/events/event_bus.py +++ b/localstack/services/events/event_bus.py @@ -1,7 +1,7 @@ from typing import Optional from localstack.aws.api.events import Arn, EventBusName, TagList -from localstack.services.events.models_v2 import EventBus, RuleDict +from localstack.services.events.models import EventBus, RuleDict class EventBusService: diff --git a/localstack/services/events/event_ruler.py b/localstack/services/events/event_ruler.py index 0a7ef73eaf78c..e48712687bbce 100644 --- a/localstack/services/events/event_ruler.py +++ b/localstack/services/events/event_ruler.py @@ -3,8 +3,8 @@ from functools import cache from pathlib import Path +from localstack.services.events.models import InvalidEventPatternException from localstack.services.events.packages import event_ruler_package -from localstack.services.events.utils import InvalidEventPatternException from localstack.utils.objects import singleton_factory THIS_FOLDER = os.path.dirname(os.path.realpath(__file__)) diff --git a/localstack/services/events/models.py b/localstack/services/events/models.py index 4096215c82499..3d42c621c6b41 100644 --- a/localstack/services/events/models.py +++ b/localstack/services/events/models.py @@ -1,11 +1,104 @@ -from typing import Dict +from dataclasses import dataclass, field +from typing import Optional -from localstack.services.stores import AccountRegionBundle, BaseStore, LocalAttribute +from localstack.aws.api.core import ServiceException +from localstack.aws.api.events import ( + Arn, + CreatedBy, + EventBusName, + EventPattern, + ManagedBy, + RoleArn, + RuleDescription, + RuleName, + RuleState, + ScheduleExpression, + TagList, + Target, + TargetId, +) +from localstack.services.stores import ( + AccountRegionBundle, + BaseStore, + LocalAttribute, +) + +TargetDict = dict[TargetId, Target] + + +@dataclass +class Rule: + name: RuleName + region: str + account_id: str + schedule_expression: Optional[ScheduleExpression] = None + event_pattern: Optional[EventPattern] = None + state: Optional[RuleState] = None + description: Optional[RuleDescription] = None + role_arn: Optional[RoleArn] = None + tags: TagList = field(default_factory=list) + event_bus_name: EventBusName = "default" + targets: TargetDict = field(default_factory=dict) + managed_by: Optional[ManagedBy] = None # can only be set by AWS services + created_by: CreatedBy = field(init=False) + arn: Arn = field(init=False) + + def __post_init__(self): + if self.event_bus_name == "default": + self.arn = f"arn:aws:events:{self.region}:{self.account_id}:rule/{self.name}" + else: + self.arn = f"arn:aws:events:{self.region}:{self.account_id}:rule/{self.event_bus_name}/{self.name}" + self.created_by = self.account_id + if self.tags is None: + self.tags = [] + if self.targets is None: + self.targets = {} + if self.state is None: + self.state = RuleState.ENABLED + + +RuleDict = dict[RuleName, Rule] + + +@dataclass +class EventBus: + name: EventBusName + region: str + account_id: str + event_source_name: Optional[str] = None + tags: TagList = field(default_factory=list) + policy: Optional[str] = None + rules: RuleDict = field(default_factory=dict) + arn: Arn = field(init=False) + + def __post_init__(self): + self.arn = f"arn:aws:events:{self.region}:{self.account_id}:event-bus/{self.name}" + if self.rules is None: + self.rules = {} + if self.tags is None: + self.tags = [] + + +EventBusDict = dict[EventBusName, EventBus] class EventsStore(BaseStore): - # maps rule name to job_id - rule_scheduled_jobs: Dict[str, str] = LocalAttribute(default=dict) + # Map of eventbus names to eventbus objects. The name MUST be unique per account and region (works with AccountRegionBundle) + event_buses: EventBusDict = LocalAttribute(default=dict) + + +events_store = AccountRegionBundle("events", EventsStore) + + +class ValidationException(ServiceException): + code: str = "ValidationException" + sender_fault: bool = True + status_code: int = 400 + +class InvalidEventPatternException(Exception): + reason: str -events_stores = AccountRegionBundle("events", EventsStore) + def __init__(self, reason=None, message=None) -> None: + self.reason = reason + self.message = message or f"Event pattern is not valid. Reason: {reason}" diff --git a/localstack/services/events/models_v2.py b/localstack/services/events/models_v2.py deleted file mode 100644 index 89b232fe30291..0000000000000 --- a/localstack/services/events/models_v2.py +++ /dev/null @@ -1,96 +0,0 @@ -from dataclasses import dataclass, field -from typing import Optional - -from localstack.aws.api.core import ServiceException -from localstack.aws.api.events import ( - Arn, - CreatedBy, - EventBusName, - EventPattern, - ManagedBy, - RoleArn, - RuleDescription, - RuleName, - RuleState, - ScheduleExpression, - TagList, - Target, - TargetId, -) -from localstack.services.stores import ( - AccountRegionBundle, - BaseStore, - LocalAttribute, -) - -TargetDict = dict[TargetId, Target] - - -@dataclass -class Rule: - name: RuleName - region: str - account_id: str - schedule_expression: Optional[ScheduleExpression] = None - event_pattern: Optional[EventPattern] = None - state: Optional[RuleState] = None - description: Optional[RuleDescription] = None - role_arn: Optional[RoleArn] = None - tags: TagList = field(default_factory=list) - event_bus_name: EventBusName = "default" - targets: TargetDict = field(default_factory=dict) - managed_by: Optional[ManagedBy] = None # can only be set by AWS services - created_by: CreatedBy = field(init=False) - arn: Arn = field(init=False) - - def __post_init__(self): - if self.event_bus_name == "default": - self.arn = f"arn:aws:events:{self.region}:{self.account_id}:rule/{self.name}" - else: - self.arn = f"arn:aws:events:{self.region}:{self.account_id}:rule/{self.event_bus_name}/{self.name}" - self.created_by = self.account_id - if self.tags is None: - self.tags = [] - if self.targets is None: - self.targets = {} - if self.state is None: - self.state = RuleState.ENABLED - - -RuleDict = dict[RuleName, Rule] - - -@dataclass -class EventBus: - name: EventBusName - region: str - account_id: str - event_source_name: Optional[str] = None - tags: TagList = field(default_factory=list) - policy: Optional[str] = None - rules: RuleDict = field(default_factory=dict) - arn: Arn = field(init=False) - - def __post_init__(self): - self.arn = f"arn:aws:events:{self.region}:{self.account_id}:event-bus/{self.name}" - if self.rules is None: - self.rules = {} - if self.tags is None: - self.tags = [] - - -EventBusDict = dict[EventBusName, EventBus] - - -class EventsStore(BaseStore): - # Map of eventbus names to eventbus objects. The name MUST be unique per account and region (works with AccountRegionBundle) - event_buses: EventBusDict = LocalAttribute(default=dict) - - -events_store = AccountRegionBundle("events", EventsStore) - - -class ValidationException(ServiceException): - code: str = "ValidationException" - sender_fault: bool = True - status_code: int = 400 diff --git a/localstack/services/events/provider.py b/localstack/services/events/provider.py index c0f683d549aa9..7ff7ea0c40a71 100644 --- a/localstack/services/events/provider.py +++ b/localstack/services/events/provider.py @@ -1,305 +1,250 @@ -import datetime +import base64 import json import logging -import os -import re -import time -from typing import Any, Dict, Optional - -from moto.events import events_backends -from moto.events.responses import EventsHandler as MotoEventsHandler -from werkzeug import Request -from werkzeug.exceptions import NotFound - -from localstack import config -from localstack.aws.api import RequestContext -from localstack.aws.api.core import CommonServiceException, ServiceException +from datetime import datetime, timezone +from typing import Optional + +from localstack.aws.api import RequestContext, handler from localstack.aws.api.events import ( + Arn, Boolean, - ConnectionAuthorizationType, - ConnectionDescription, - ConnectionName, - CreateConnectionAuthRequestParameters, - CreateConnectionResponse, + CreateEventBusResponse, + DescribeEventBusResponse, + DescribeRuleResponse, + EndpointId, + EventBusList, + EventBusName, EventBusNameOrArn, EventPattern, EventsApi, + EventSourceName, InvalidEventPatternException, + LimitMax100, + ListEventBusesResponse, + ListRuleNamesByTargetResponse, + ListRulesResponse, + ListTargetsByRuleResponse, + NextToken, + PutEventsRequestEntry, + PutEventsRequestEntryList, + PutEventsResponse, + PutEventsResultEntry, + PutEventsResultEntryList, + PutPartnerEventsRequestEntryList, + PutPartnerEventsResponse, PutRuleResponse, PutTargetsResponse, + RemoveTargetsResponse, + ResourceAlreadyExistsException, + ResourceNotFoundException, RoleArn, RuleDescription, RuleName, + RuleResponseList, RuleState, ScheduleExpression, - String, TagList, + Target, + TargetArn, + TargetId, + TargetIdList, TargetList, TestEventPatternResponse, ) -from localstack.constants import APPLICATION_AMZ_JSON_1_1 -from localstack.http import route -from localstack.services.edge import ROUTER +from localstack.aws.api.events import EventBus as ApiTypeEventBus +from localstack.aws.api.events import Rule as ApiTypeRule +from localstack.services.events.event_bus import EventBusService, EventBusServiceDict from localstack.services.events.event_ruler import matches_rule -from localstack.services.events.models import EventsStore, events_stores -from localstack.services.events.scheduler import JobScheduler -from localstack.services.events.utils import ( +from localstack.services.events.models import ( + EventBus, + EventBusDict, + EventsStore, + Rule, + RuleDict, + TargetDict, + ValidationException, + events_store, +) +from localstack.services.events.models import ( InvalidEventPatternException as InternalInvalidEventPatternException, ) -from localstack.services.events.utils import matches_event -from localstack.services.moto import call_moto +from localstack.services.events.rule import RuleService, RuleServiceDict +from localstack.services.events.target import TargetSender, TargetSenderDict, TargetSenderFactory from localstack.services.plugins import ServiceLifecycleHook -from localstack.utils.aws.arns import event_bus_arn, parse_arn -from localstack.utils.aws.client_types import ServicePrincipal -from localstack.utils.aws.message_forwarding import send_event_to_target -from localstack.utils.collections import pick_attributes -from localstack.utils.common import TMP_FILES, mkdir, save_file, truncate -from localstack.utils.json import extract_jsonpath -from localstack.utils.strings import long_uid, short_uid -from localstack.utils.time import TIMESTAMP_FORMAT_TZ, timestamp +from localstack.utils.strings import long_uid LOG = logging.getLogger(__name__) -# list of events used to run assertions during integration testing (not exposed to the user) -TEST_EVENTS_CACHE = [] -EVENTS_TMP_DIR = "cw_events" -DEFAULT_EVENT_BUS_NAME = "default" -CONNECTION_NAME_PATTERN = re.compile("^[\\.\\-_A-Za-z0-9]+$") +def decode_next_token(token: NextToken) -> int: + """Decode a pagination token from base64 to integer.""" + return int.from_bytes(base64.b64decode(token), "big") -class ValidationException(ServiceException): - code: str = "ValidationException" - sender_fault: bool = True - status_code: int = 400 +def encode_next_token(token: int) -> NextToken: + """Encode a pagination token to base64 from integer.""" + return base64.b64encode(token.to_bytes(128, "big")).decode("utf-8") -class EventsProvider(EventsApi, ServiceLifecycleHook): - def __init__(self): - apply_patches() - def on_after_init(self): - ROUTER.add(self.trigger_scheduled_rule) +def get_filtered_dict(name_prefix: str, input_dict: dict) -> dict: + """Filter dictionary by prefix.""" + return {name: value for name, value in input_dict.items() if name.startswith(name_prefix)} - def on_before_start(self): - JobScheduler.start() - def on_before_stop(self): - JobScheduler.shutdown() +def get_event_time(event: PutEventsRequestEntry) -> str: + event_time = datetime.now(timezone.utc) + if event_timestamp := event.get("Time"): + try: + # use time from event if provided + event_time = event_timestamp.replace(tzinfo=timezone.utc) + except ValueError: + # use current time if event time is invalid + LOG.debug( + "Could not parse the `Time` parameter, falling back to current time for the following Event: '%s'", + event, + ) + formatted_time_string = event_time.strftime("%Y-%m-%dT%H:%M:%SZ") + return formatted_time_string - @route("/_aws/events/rules/<path:rule_arn>/trigger") - def trigger_scheduled_rule(self, request: Request, rule_arn: str): - """Developer endpoint to trigger a scheduled rule.""" - arn_data = parse_arn(rule_arn) - account_id = arn_data["account"] - region = arn_data["region"] - rule_name = arn_data["resource"].split("/", maxsplit=1)[-1] - job_id = events_stores[account_id][region].rule_scheduled_jobs.get(rule_name) - if not job_id: - raise NotFound() - job = JobScheduler().instance().get_job(job_id) - if not job: - raise NotFound() +def validate_event(event: PutEventsRequestEntry) -> None | PutEventsResultEntry: + if not event.get("Source"): + return { + "ErrorCode": "InvalidArgument", + "ErrorMessage": "Parameter Source is not valid. Reason: Source is a required argument.", + } + elif not event.get("DetailType"): + return { + "ErrorCode": "InvalidArgument", + "ErrorMessage": "Parameter DetailType is not valid. Reason: DetailType is a required argument.", + } + elif not event.get("Detail"): + return { + "ErrorCode": "InvalidArgument", + "ErrorMessage": "Parameter Detail is not valid. Reason: Detail is a required argument.", + } - # TODO: once job scheduler is refactored, we can update the deadline of the task instead of running - # it here - job.run() - @staticmethod - def get_store(context: RequestContext) -> EventsStore: - return events_stores[context.account_id][context.region] +def format_event(event: PutEventsRequestEntry, region: str, account_id: str) -> dict: + # See https://docs.aws.amazon.com/AmazonS3/latest/userguide/ev-events.html + formatted_event = { + "version": "0", + "id": str(long_uid()), + "detail-type": event.get("DetailType"), + "source": event.get("Source"), + "account": account_id, + "time": get_event_time(event), + "region": region, + "resources": event.get("Resources", []), + "detail": json.loads(event.get("Detail", "{}")), + } - def test_event_pattern( - self, context: RequestContext, event_pattern: EventPattern, event: String, **kwargs - ) -> TestEventPatternResponse: - """Test event pattern uses EventBridge event pattern matching: - https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html - """ - if config.EVENT_RULE_ENGINE == "java": - try: - result = matches_rule(event, event_pattern) - except InternalInvalidEventPatternException as e: - raise InvalidEventPatternException(e.message) from e - else: - event_pattern_dict = json.loads(event_pattern) - event_dict = json.loads(event) - result = matches_event(event_pattern_dict, event_dict) - - # TODO: unify the different implementations below: - # event_pattern_dict = json.loads(event_pattern) - # event_dict = json.loads(event) - - # EventBridge: - # result = matches_event(event_pattern_dict, event_dict) - - # Lambda EventSourceMapping: - # from localstack.services.lambda_.event_source_listeners.utils import does_match_event - # - # result = does_match_event(event_pattern_dict, event_dict) - - # moto-ext EventBridge: - # from moto.events.models import EventPattern as EventPatternMoto - # - # event_pattern = EventPatternMoto.load(event_pattern) - # result = event_pattern.matches_event(event_dict) - - # SNS: The SNS rule engine seems to differ slightly, for example not allowing the wildcard pattern. - # from localstack.services.sns.publisher import SubscriptionFilter - # subscription_filter = SubscriptionFilter() - # result = subscription_filter._evaluate_nested_filter_policy_on_dict(event_pattern_dict, event_dict) - - # moto-ext SNS: - # from moto.sns.utils import FilterPolicyMatcher - # filter_policy_matcher = FilterPolicyMatcher(event_pattern_dict, "MessageBody") - # result = filter_policy_matcher._body_based_match(event_dict) + return formatted_event - return TestEventPatternResponse(Result=result) - @staticmethod - def get_scheduled_rule_func( - store: EventsStore, - rule_name: RuleName, - event_bus_name_or_arn: Optional[EventBusNameOrArn] = None, - ): - def func(*args, **kwargs): - account_id = store._account_id - region = store._region_name - moto_backend = events_backends[account_id][region] - event_bus_name = get_event_bus_name(event_bus_name_or_arn) - event_bus = moto_backend.event_buses[event_bus_name] - rule = event_bus.rules.get(rule_name) - if not rule: - LOG.info("Unable to find rule `%s` for event bus `%s`", rule_name, event_bus_name) - return - if rule.targets: - LOG.debug( - "Notifying %s targets in response to triggered Events rule %s", - len(rule.targets), - rule_name, - ) - - default_event = { - "version": "0", - "id": long_uid(), - "detail-type": "Scheduled Event", - "source": "aws.events", - "account": account_id, - "time": timestamp(format=TIMESTAMP_FORMAT_TZ), - "region": region, - "resources": [rule.arn], - "detail": {}, - } - - for target in rule.targets: - arn = target.get("Arn") - - if input_ := target.get("Input"): - event = json.loads(input_) - else: - event = default_event - if target.get("InputPath"): - event = filter_event_with_target_input_path(target, event) - if input_transformer := target.get("InputTransformer"): - event = process_event_with_input_transformer(input_transformer, event) - - attr = pick_attributes(target, ["$.SqsParameters", "$.KinesisParameters"]) +class EventsProvider(EventsApi, ServiceLifecycleHook): + # api methods are grouped by resource type and sorted in hierarchical order + # each group is sorted alphabetically + def __init__(self): + self._event_bus_services_store: EventBusServiceDict = {} + self._rule_services_store: RuleServiceDict = {} + self._target_sender_store: TargetSenderDict = {} - try: - send_event_to_target( - arn, - event, - target_attributes=attr, - role=target.get("RoleArn"), - target=target, - source_arn=rule.arn, - source_service=ServicePrincipal.events, - ) - except Exception as e: - LOG.info( - "Unable to send event notification %s to target %s: %s", - truncate(event), - target, - e, - ) - - return func - - @staticmethod - def convert_schedule_to_cron(schedule): - """Convert Events schedule like "cron(0 20 * * ? *)" or "rate(5 minutes)" """ - cron_regex = r"\s*cron\s*\(([^\)]*)\)\s*" - if re.match(cron_regex, schedule): - cron = re.sub(cron_regex, r"\1", schedule) - return cron - rate_regex = r"\s*rate\s*\(([^\)]*)\)\s*" - if re.match(rate_regex, schedule): - rate = re.sub(rate_regex, r"\1", schedule) - value, unit = re.split(r"\s+", rate.strip()) - - value = int(value) - if value < 1: - raise ValueError("Rate value must be larger than 0") - # see https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rate-expressions.html - if value == 1 and unit.endswith("s"): - raise ValueError("If the value is equal to 1, then the unit must be singular") - if value > 1 and not unit.endswith("s"): - raise ValueError("If the value is greater than 1, the unit must be plural") - - if "minute" in unit: - return "*/%s * * * *" % value - if "hour" in unit: - return "0 */%s * * *" % value - if "day" in unit: - return "0 0 */%s * *" % value - raise ValueError("Unable to parse events schedule expression: %s" % schedule) - return schedule - - @staticmethod - def put_rule_job_scheduler( - store: EventsStore, - name: Optional[RuleName], - state: Optional[RuleState], - schedule_expression: Optional[ScheduleExpression], - event_bus_name_or_arn: Optional[EventBusNameOrArn] = None, - ): - if not schedule_expression: - return + ########## + # EventBus + ########## - try: - cron = EventsProvider.convert_schedule_to_cron(schedule_expression) - except ValueError as e: - LOG.error("Error parsing schedule expression: %s", e) - raise ValidationException("Parameter ScheduleExpression is not valid.") from e + @handler("CreateEventBus") + def create_event_bus( + self, + context: RequestContext, + name: EventBusName, + event_source_name: EventSourceName = None, + tags: TagList = None, + **kwargs, + ) -> CreateEventBusResponse: + region = context.region + account_id = context.account_id + store = self.get_store(context) + if name in store.event_buses.keys(): + raise ResourceAlreadyExistsException(f"Event bus {name} already exists.") + event_bus_service = self.create_event_bus_service( + name, region, account_id, event_source_name, tags + ) + store.event_buses[event_bus_service.event_bus.name] = event_bus_service.event_bus - job_func = EventsProvider.get_scheduled_rule_func( - store, name, event_bus_name_or_arn=event_bus_name_or_arn + response = CreateEventBusResponse( + EventBusArn=event_bus_service.arn, ) - LOG.debug("Adding new scheduled Events rule with cron schedule %s", cron) + return response - enabled = state != "DISABLED" - job_id = JobScheduler.instance().add_job(job_func, cron, enabled) - rule_scheduled_jobs = store.rule_scheduled_jobs - rule_scheduled_jobs[name] = job_id + @handler("DeleteEventBus") + def delete_event_bus(self, context: RequestContext, name: EventBusName, **kwargs) -> None: + if name == "default": + raise ValidationException("Cannot delete event bus default.") + store = self.get_store(context) + try: + if event_bus := self.get_event_bus(name, store): + del self._event_bus_services_store[event_bus.arn] + if rules := event_bus.rules: + self._delete_rule_services(rules) + del store.event_buses[name] + except ResourceNotFoundException as error: + return error + + @handler("DescribeEventBus") + def describe_event_bus( + self, context: RequestContext, name: EventBusNameOrArn = None, **kwargs + ) -> DescribeEventBusResponse: + name = self._extract_event_bus_name(name) + store = self.get_store(context) + event_bus = self.get_event_bus(name, store) - def put_rule( + response = self._event_bus_dict_to_api_type_event_bus(event_bus) + return response + + @handler("ListEventBuses") + def list_event_buses( + self, + context: RequestContext, + name_prefix: EventBusName = None, + next_token: NextToken = None, + limit: LimitMax100 = None, + **kwargs, + ) -> ListEventBusesResponse: + store = self.get_store(context) + event_buses = ( + get_filtered_dict(name_prefix, store.event_buses) if name_prefix else store.event_buses + ) + limited_event_buses, next_token = self._get_limited_dict_and_next_token( + event_buses, next_token, limit + ) + + response = ListEventBusesResponse( + EventBuses=self._event_bust_dict_to_api_type_list(limited_event_buses) + ) + if next_token is not None: + response["NextToken"] = next_token + return response + + ####### + # Rules + ####### + @handler("EnableRule") + def enable_rule( self, context: RequestContext, name: RuleName, - schedule_expression: ScheduleExpression = None, - event_pattern: EventPattern = None, - state: RuleState = None, - description: RuleDescription = None, - role_arn: RoleArn = None, - tags: TagList = None, event_bus_name: EventBusNameOrArn = None, **kwargs, - ) -> PutRuleResponse: + ) -> None: store = self.get_store(context) - self.put_rule_job_scheduler( - store, name, state, schedule_expression, event_bus_name_or_arn=event_bus_name - ) - return call_moto(context) + event_bus_name = self._extract_event_bus_name(event_bus_name) + event_bus = self.get_event_bus(event_bus_name, store) + rule = self.get_rule(name, event_bus) + rule.state = RuleState.ENABLED + @handler("DeleteRule") def delete_rule( self, context: RequestContext, @@ -308,13 +253,35 @@ def delete_rule( force: Boolean = None, **kwargs, ) -> None: - rule_scheduled_jobs = self.get_store(context).rule_scheduled_jobs - job_id = rule_scheduled_jobs.get(name) - if job_id: - LOG.debug("Removing scheduled Events: {} | job_id: {}".format(name, job_id)) - JobScheduler.instance().cancel_job(job_id=job_id) - call_moto(context) + store = self.get_store(context) + event_bus_name = self._extract_event_bus_name(event_bus_name) + event_bus = self.get_event_bus(event_bus_name, store) + try: + rule = self.get_rule(name, event_bus) + if rule.targets and not force: + raise ValidationException("Rule can't be deleted since it has targets.") + self._delete_rule_services(rule) + del event_bus.rules[name] + except ResourceNotFoundException as error: + return error + + @handler("DescribeRule") + def describe_rule( + self, + context: RequestContext, + name: RuleName, + event_bus_name: EventBusNameOrArn = None, + **kwargs, + ) -> DescribeRuleResponse: + store = self.get_store(context) + event_bus_name = self._extract_event_bus_name(event_bus_name) + event_bus = self.get_event_bus(event_bus_name, store) + rule = self.get_rule(name, event_bus) + response = self._rule_dict_to_api_type_rule(rule) + return response + + @handler("DisableRule") def disable_rule( self, context: RequestContext, @@ -322,45 +289,126 @@ def disable_rule( event_bus_name: EventBusNameOrArn = None, **kwargs, ) -> None: - rule_scheduled_jobs = self.get_store(context).rule_scheduled_jobs - job_id = rule_scheduled_jobs.get(name) - if job_id: - LOG.debug("Disabling Rule: {} | job_id: {}".format(name, job_id)) - JobScheduler.instance().disable_job(job_id=job_id) - call_moto(context) - - def create_connection( + store = self.get_store(context) + event_bus_name = self._extract_event_bus_name(event_bus_name) + event_bus = self.get_event_bus(event_bus_name, store) + rule = self.get_rule(name, event_bus) + rule.state = RuleState.DISABLED + + @handler("ListRules") + def list_rules( + self, + context: RequestContext, + name_prefix: RuleName = None, + event_bus_name: EventBusNameOrArn = None, + next_token: NextToken = None, + limit: LimitMax100 = None, + **kwargs, + ) -> ListRulesResponse: + store = self.get_store(context) + event_bus_name = self._extract_event_bus_name(event_bus_name) + event_bus = self.get_event_bus(event_bus_name, store) + rules = get_filtered_dict(name_prefix, event_bus.rules) if name_prefix else event_bus.rules + limited_rules, next_token = self._get_limited_dict_and_next_token(rules, next_token, limit) + + response = ListRulesResponse(Rules=list(self._rule_dict_to_api_type_list(limited_rules))) + if next_token is not None: + response["NextToken"] = next_token + return response + + @handler("ListRuleNamesByTarget") + def list_rule_names_by_target( + self, + context: RequestContext, + target_arn: TargetArn, + event_bus_name: EventBusNameOrArn = None, + next_token: NextToken = None, + limit: LimitMax100 = None, + **kwargs, + ) -> ListRuleNamesByTargetResponse: + raise NotImplementedError + + @handler("PutRule") + def put_rule( self, context: RequestContext, - name: ConnectionName, - authorization_type: ConnectionAuthorizationType, - auth_parameters: CreateConnectionAuthRequestParameters, - description: ConnectionDescription = None, + name: RuleName, + schedule_expression: ScheduleExpression = None, + event_pattern: EventPattern = None, + state: RuleState = None, + description: RuleDescription = None, + role_arn: RoleArn = None, + tags: TagList = None, + event_bus_name: EventBusNameOrArn = None, **kwargs, - ) -> CreateConnectionResponse: - errors = [] + ) -> PutRuleResponse: + region = context.region + account_id = context.account_id + event_bus_name = self._extract_event_bus_name(event_bus_name) + store = self.get_store(context) + event_bus = self.get_event_bus(event_bus_name, store) + existing_rule = event_bus.rules.get(name) + targets = existing_rule.targets if existing_rule else None + rule_service = self.create_rule_service( + name, + region, + account_id, + schedule_expression, + event_pattern, + state, + description, + role_arn, + tags, + event_bus_name, + targets, + ) + event_bus.rules[name] = rule_service.rule + response = PutRuleResponse(RuleArn=rule_service.arn) + return response - if not CONNECTION_NAME_PATTERN.match(name): - error = f"{name} at 'name' failed to satisfy: Member must satisfy regular expression pattern: [\\.\\-_A-Za-z0-9]+" - errors.append(error) + @handler("TestEventPattern") + def test_event_pattern( + self, context: RequestContext, event_pattern: EventPattern, event: str, **kwargs + ) -> TestEventPatternResponse: + """Test event pattern uses EventBridge event pattern matching: + https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html + """ + try: + result = matches_rule(event, event_pattern) + except InternalInvalidEventPatternException as e: + raise InvalidEventPatternException(e.message) from e - if len(name) > 64: - error = f"{name} at 'name' failed to satisfy: Member must have length less than or equal to 64" - errors.append(error) + return TestEventPatternResponse(Result=result) - if authorization_type not in ["BASIC", "API_KEY", "OAUTH_CLIENT_CREDENTIALS"]: - error = f"{authorization_type} at 'authorizationType' failed to satisfy: Member must satisfy enum value set: [BASIC, OAUTH_CLIENT_CREDENTIALS, API_KEY]" - errors.append(error) + ######### + # Targets + ######### - if len(errors) > 0: - error_description = "; ".join(errors) - error_plural = "errors" if len(errors) > 1 else "error" - errors_amount = len(errors) - message = f"{errors_amount} validation {error_plural} detected: {error_description}" - raise CommonServiceException(message=message, code="ValidationException") + @handler("ListTargetsByRule") + def list_targets_by_rule( + self, + context: RequestContext, + rule: RuleName, + event_bus_name: EventBusNameOrArn = None, + next_token: NextToken = None, + limit: LimitMax100 = None, + **kwargs, + ) -> ListTargetsByRuleResponse: + store = self.get_store(context) + event_bus_name = self._extract_event_bus_name(event_bus_name) + event_bus = self.get_event_bus(event_bus_name, store) + rule = self.get_rule(rule, event_bus) + targets = rule.targets + limited_targets, next_token = self._get_limited_dict_and_next_token( + targets, next_token, limit + ) - return call_moto(context) + response = ListTargetsByRuleResponse(Targets=list(limited_targets.values())) + if next_token is not None: + response["NextToken"] = next_token + return response + @handler("PutTargets") def put_targets( self, context: RequestContext, @@ -369,207 +417,282 @@ def put_targets( event_bus_name: EventBusNameOrArn = None, **kwargs, ) -> PutTargetsResponse: - validation_errors = [] - - id_regex = re.compile(r"^[\.\-_A-Za-z0-9]+$") - for index, target in enumerate(targets): - id = target.get("Id") - if not id_regex.match(id): - validation_errors.append( - f"Value '{id}' at 'targets.{index + 1}.member.id' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\.\\-_A-Za-z0-9]+" - ) - - if len(id) > 64: - validation_errors.append( - f"Value '{id}' at 'targets.{index + 1}.member.id' failed to satisfy constraint: Member must have length less than or equal to 64" - ) - - if validation_errors: - errors_message = "; ".join(validation_errors) - message = f"{len(validation_errors)} validation {'errors' if len(validation_errors) > 1 else 'error'} detected: {errors_message}" - raise CommonServiceException(message=message, code="ValidationException") - - return call_moto(context) - + region = context.region + account_id = context.account_id + rule_service = self.get_rule_service(context, rule, event_bus_name) + failed_entries = rule_service.add_targets(targets) + rule_arn = rule_service.arn + for target in targets: # TODO only add successful targets + self.create_target_sender(target, region, account_id, rule_arn) + + response = PutTargetsResponse( + FailedEntryCount=len(failed_entries), FailedEntries=failed_entries + ) + return response -def _get_events_tmp_dir(): - return os.path.join(config.dirs.tmp, EVENTS_TMP_DIR) + @handler("RemoveTargets") + def remove_targets( + self, + context: RequestContext, + rule: RuleName, + ids: TargetIdList, + event_bus_name: EventBusNameOrArn = None, + force: Boolean = None, + **kwargs, + ) -> RemoveTargetsResponse: + rule_service = self.get_rule_service(context, rule, event_bus_name) + failed_entries = rule_service.remove_targets(ids) + self._delete_target_sender(ids, rule_service.rule) + response = RemoveTargetsResponse( + FailedEntryCount=len(failed_entries), FailedEntries=failed_entries + ) + return response -def _create_and_register_temp_dir(): - tmp_dir = _get_events_tmp_dir() - if not os.path.exists(tmp_dir): - mkdir(tmp_dir) - TMP_FILES.append(tmp_dir) - return tmp_dir + ######## + # Events + ######## + @handler("PutEvents") + def put_events( + self, + context: RequestContext, + entries: PutEventsRequestEntryList, + endpoint_id: EndpointId = None, + **kwargs, + ) -> PutEventsResponse: + entries, failed_entry_count = self._process_entries(context, entries) -def _dump_events_to_files(events_with_added_uuid): - try: - _create_and_register_temp_dir() - current_time_millis = int(round(time.time() * 1000)) - for event in events_with_added_uuid: - target = os.path.join( - _get_events_tmp_dir(), - "%s_%s" % (current_time_millis, event["uuid"]), - ) - save_file(target, json.dumps(event["event"])) - except Exception as e: - LOG.info("Unable to dump events to tmp dir %s: %s", _get_events_tmp_dir(), e) - - -def filter_event_based_on_event_format( - self, rule_name: str, event_bus_name: str, event: dict[str, Any] -): - rule_information = self.events_backend.describe_rule( - rule_name, event_bus_arn(event_bus_name, self.current_account, self.region) - ) - - if not rule_information: - LOG.info('Unable to find rule "%s" in backend: %s', rule_name, rule_information) - return False - if rule_information.event_pattern._pattern: - event_pattern = rule_information.event_pattern._pattern - if config.EVENT_RULE_ENGINE == "java": - event_str = json.dumps(event) - event_pattern_str = json.dumps(event_pattern) - match_result = matches_rule(event_str, event_pattern_str) - else: - match_result = matches_event(event_pattern, event) - if not match_result: - return False - return True - - -def filter_event_with_target_input_path(target: Dict, event: Dict) -> Dict: - input_path = target.get("InputPath") - if input_path: - event = extract_jsonpath(event, input_path) - return event - - -def process_event_with_input_transformer(input_transformer: Dict, event: Dict) -> Dict: - """ - Process the event with the input transformer of the target event, - by replacing the message with the populated InputTemplate. - docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-input.html - """ - try: - input_paths = input_transformer["InputPathsMap"] - input_template = input_transformer["InputTemplate"] - except KeyError as e: - LOG.error("%s key does not exist in input_transformer.", e) - raise e - for key, path in input_paths.items(): - value = extract_jsonpath(event, path) - if not value: - value = "" - input_template = input_template.replace(f"<{key}>", value) - templated_event = re.sub('"', "", input_template) - return templated_event - - -def process_events(event: Dict, targets: list[Dict]): - for target in targets: - arn = target["Arn"] - changed_event = filter_event_with_target_input_path(target, event) - if input_transformer := target.get("InputTransformer"): - changed_event = process_event_with_input_transformer(input_transformer, changed_event) - if target.get("Input"): - changed_event = json.loads(target.get("Input")) - try: - send_event_to_target( - arn, - changed_event, - pick_attributes(target, ["$.SqsParameters", "$.KinesisParameters"]), - role=target.get("RoleArn"), - target=target, - source_service=ServicePrincipal.events, - source_arn=target.get("RuleArn"), + response = PutEventsResponse( + Entries=entries, + FailedEntryCount=failed_entry_count, + ) + return response + + @handler("PutPartnerEvents") + def put_partner_events( + self, context: RequestContext, entries: PutPartnerEventsRequestEntryList, **kwargs + ) -> PutPartnerEventsResponse: + raise NotImplementedError + + ######### + # Methods + ######### + + def get_store(self, context: RequestContext) -> EventsStore: + """Returns the events store for the account and region. + On first call, creates the default event bus for the account region.""" + region = context.region + account_id = context.account_id + store = events_store[account_id][region] + # create default event bus for account region on first call + default_event_bus_name = "default" + if default_event_bus_name not in store.event_buses.keys(): + event_bus_service = self.create_event_bus_service( + default_event_bus_name, region, account_id, None, None ) - except Exception as e: - LOG.info(f"Unable to send event notification {truncate(event)} to target {target}: {e}") - - -def get_event_bus_name(event_bus_name_or_arn: Optional[EventBusNameOrArn] = None) -> str: - event_bus_name_or_arn = event_bus_name_or_arn or DEFAULT_EVENT_BUS_NAME - return event_bus_name_or_arn.split("/")[-1] - - -# specific logic for put_events which forwards matching events to target listeners -def events_handler_put_events(self): - entries = self._get_param("Entries") - - # keep track of events for local integration testing - if config.is_local_test_mode(): - TEST_EVENTS_CACHE.extend(entries) - - events = [{"event": event, "uuid": str(long_uid())} for event in entries] - - _dump_events_to_files(events) + store.event_buses[event_bus_service.event_bus.name] = event_bus_service.event_bus + return store + + def get_event_bus(self, name: EventBusName, store: EventsStore) -> EventBus: + if event_bus := store.event_buses.get(name): + return event_bus + raise ResourceNotFoundException(f"Event bus {name} does not exist.") + + def get_rule(self, name: RuleName, event_bus: EventBus) -> Rule: + if rule := event_bus.rules.get(name): + return rule + raise ResourceNotFoundException(f"Rule {name} does not exist on EventBus {event_bus.name}.") + + def get_target(self, target_id: TargetId, rule: Rule) -> Target: + if target := rule.targets.get(target_id): + return target + raise ResourceNotFoundException(f"Target {target_id} does not exist on Rule {rule.name}.") + + def get_rule_service( + self, context: RequestContext, rule_name: RuleName, event_bus_name: EventBusName + ) -> RuleService: + store = self.get_store(context) + event_bus_name = self._extract_event_bus_name(event_bus_name) + event_bus = self.get_event_bus(event_bus_name, store) + rule = self.get_rule(rule_name, event_bus) + return self._rule_services_store[rule.arn] - for event_envelope in events: - event = event_envelope["event"] - event_bus_name = get_event_bus_name(event.get("EventBusName")) - event_bus = self.events_backend.event_buses.get(event_bus_name) - if not event_bus: - continue + def create_event_bus_service( + self, + name: EventBusName, + region: str, + account_id: str, + event_source_name: Optional[EventSourceName], + tags: Optional[TagList], + ) -> EventBusService: + event_bus_service = EventBusService( + name, + region, + account_id, + event_source_name, + tags, + ) + self._event_bus_services_store[event_bus_service.arn] = event_bus_service + return event_bus_service - matching_rules = [ - r - for r in event_bus.rules.values() - if r.event_bus_name == event_bus_name and not r.scheduled_expression + def create_rule_service( + self, + name: RuleName, + region: str, + account_id: str, + schedule_expression: Optional[ScheduleExpression], + event_pattern: Optional[EventPattern], + state: Optional[RuleState], + description: Optional[RuleDescription], + role_arn: Optional[RoleArn], + tags: Optional[TagList], + event_bus_name: Optional[EventBusName], + targets: Optional[TargetDict], + ) -> RuleService: + rule_service = RuleService( + name, + region, + account_id, + schedule_expression, + event_pattern, + state, + description, + role_arn, + tags, + event_bus_name, + targets, + ) + self._rule_services_store[rule_service.arn] = rule_service + return rule_service + + def create_target_sender( + self, target: Target, region: str, account_id: str, rule_arn: Arn + ) -> TargetSender: + target_sender = TargetSenderFactory( + target, region, account_id, rule_arn + ).get_target_sender() + self._target_sender_store[target_sender.arn] = target_sender + return target_sender + + def _get_limited_dict_and_next_token( + self, input_dict: dict, next_token: NextToken | None, limit: LimitMax100 | None + ) -> tuple[dict, NextToken]: + """Return a slice of the given dictionary starting from next_token with length of limit + and new last index encoded as a next_token for pagination.""" + input_dict_len = len(input_dict) + start_index = decode_next_token(next_token) if next_token is not None else 0 + end_index = start_index + limit if limit is not None else input_dict_len + limited_dict = dict(list(input_dict.items())[start_index:end_index]) + + next_token = ( + encode_next_token(end_index) + # return a next_token (encoded integer of next starting index) if not all items are returned + if end_index < input_dict_len + else None + ) + return limited_dict, next_token + + def _extract_event_bus_name( + self, event_bus_name_or_arn: EventBusNameOrArn | None + ) -> EventBusName: + """Return the event bus name. Input can be either an event bus name or ARN.""" + if not event_bus_name_or_arn: + return "default" + return event_bus_name_or_arn.split("/")[-1] + + def _event_bust_dict_to_api_type_list(self, event_buses: EventBusDict) -> EventBusList: + """Return a converted dict of EventBus model objects as a list of event buses in API type EventBus format.""" + event_bus_list = [ + self._event_bus_dict_to_api_type_event_bus(event_bus) + for event_bus in event_buses.values() ] - if not matching_rules: - continue + return event_bus_list - event_time = datetime.datetime.utcnow() - if event_timestamp := event.get("Time"): - try: - # if provided, use the time from event - event_time = datetime.datetime.utcfromtimestamp(event_timestamp) - except ValueError: - # if we can't parse it, pass and keep using `utcnow` - LOG.debug( - "Could not parse the `Time` parameter, falling back to `utcnow` for the following Event: '%s'", - event, - ) - - # See https://docs.aws.amazon.com/AmazonS3/latest/userguide/ev-events.html - formatted_event = { - "version": "0", - "id": event_envelope["uuid"], - "detail-type": event.get("DetailType"), - "source": event.get("Source"), - "account": self.current_account, - "time": event_time.strftime("%Y-%m-%dT%H:%M:%SZ"), - "region": self.region, - "resources": event.get("Resources", []), - "detail": json.loads(event.get("Detail", "{}")), + def _event_bus_dict_to_api_type_event_bus(self, event_bus: EventBus) -> ApiTypeEventBus: + event_bus_api_type = { + "Name": event_bus.name, + "Arn": event_bus.arn, } + if event_bus.policy: + event_bus_api_type["Policy"] = event_bus.policy - targets = [] - for rule in matching_rules: - if filter_event_based_on_event_format(self, rule.name, event_bus_name, formatted_event): - rule_targets = self.events_backend.list_targets_by_rule( - rule.name, event_bus_arn(event_bus_name, self.current_account, self.region) - ).get("Targets", []) - - targets.extend([{"RuleArn": rule.arn} | target for target in rule_targets]) - # process event - process_events(formatted_event, targets) - - content = { - "FailedEntryCount": 0, # TODO: dynamically set proper value when refactoring - "Entries": [{"EventId": event["uuid"]} for event in events], - } - - self.response_headers.update( - {"Content-Type": APPLICATION_AMZ_JSON_1_1, "x-amzn-RequestId": short_uid()} - ) - - return json.dumps(content), self.response_headers + return event_bus_api_type + def _delete_rule_services(self, rules: RuleDict | Rule) -> None: + """ + Delete all rule services associated to the input from the store. + Accepts a single Rule object or a dict of Rule objects as input. + """ + if isinstance(rules, Rule): + rules = {rules.name: rules} + for rule in rules.values(): + del self._rule_services_store[rule.arn] + + def _rule_dict_to_api_type_list(self, rules: RuleDict) -> RuleResponseList: + """Return a converted dict of Rule model objects as a list of rules in API type Rule format.""" + rule_list = [self._rule_dict_to_api_type_rule(rule) for rule in rules.values()] + return rule_list + + def _rule_dict_to_api_type_rule(self, rule: Rule) -> ApiTypeRule: + rule = { + "Name": rule.name, + "Arn": rule.arn, + "EventPattern": rule.event_pattern, + "State": rule.state, + "Description": rule.description, + "ScheduleExpression": rule.schedule_expression, + "RoleArn": rule.role_arn, + "ManagedBy": rule.managed_by, + "EventBusName": rule.event_bus_name, + "CreatedBy": rule.created_by, + } + return {key: value for key, value in rule.items() if value is not None} -def apply_patches(): - MotoEventsHandler.put_events = events_handler_put_events + def _delete_target_sender(self, ids: TargetIdList, rule) -> None: + for target_id in ids: + if target := rule.targets.get(target_id): + target_arn = target["Arn"] + try: + del self._target_sender_store[target_arn] + except KeyError: + LOG.error(f"Error deleting target service {target_arn}.") + + def _process_entries( + self, context: RequestContext, entries: PutEventsRequestEntryList + ) -> tuple[PutEventsResultEntryList, int]: + processed_entries = [] + failed_entry_count = 0 + for event in entries: + event_bus_name = event.get("EventBusName", "default") + if event_failed_validation := validate_event(event): + processed_entries.append(event_failed_validation) + failed_entry_count += 1 + continue + event = format_event(event, context.region, context.account_id) + store = self.get_store(context) + try: + event_bus = self.get_event_bus(event_bus_name, store) + except ResourceNotFoundException: + # ignore events for non-existing event buses but add processed event + processed_entries.append({"EventId": event["id"]}) + continue + matching_rules = [rule for rule in event_bus.rules.values()] + for rule in matching_rules: + event_pattern = rule.event_pattern + event_str = json.dumps(event) + if matches_rule(event_str, event_pattern): + for target in rule.targets.values(): + target_sender = self._target_sender_store[target["Arn"]] + try: + target_sender.send_event(event) + processed_entries.append({"EventId": event["id"]}) + except Exception as error: + processed_entries.append( + { + "ErrorCode": "InternalException", + "ErrorMessage": str(error), + } + ) + failed_entry_count += 1 + return processed_entries, failed_entry_count diff --git a/localstack/services/events/provider_v2.py b/localstack/services/events/provider_v2.py deleted file mode 100644 index 2c8544b09ee0e..0000000000000 --- a/localstack/services/events/provider_v2.py +++ /dev/null @@ -1,698 +0,0 @@ -import base64 -import json -import logging -from datetime import datetime, timezone -from typing import Optional - -from localstack.aws.api import RequestContext, handler -from localstack.aws.api.events import ( - Arn, - Boolean, - CreateEventBusResponse, - DescribeEventBusResponse, - DescribeRuleResponse, - EndpointId, - EventBusList, - EventBusName, - EventBusNameOrArn, - EventPattern, - EventsApi, - EventSourceName, - InvalidEventPatternException, - LimitMax100, - ListEventBusesResponse, - ListRuleNamesByTargetResponse, - ListRulesResponse, - ListTargetsByRuleResponse, - NextToken, - PutEventsRequestEntry, - PutEventsRequestEntryList, - PutEventsResponse, - PutEventsResultEntry, - PutEventsResultEntryList, - PutPartnerEventsRequestEntryList, - PutPartnerEventsResponse, - PutRuleResponse, - PutTargetsResponse, - RemoveTargetsResponse, - ResourceAlreadyExistsException, - ResourceNotFoundException, - RoleArn, - RuleDescription, - RuleName, - RuleResponseList, - RuleState, - ScheduleExpression, - TagList, - Target, - TargetArn, - TargetId, - TargetIdList, - TargetList, - TestEventPatternResponse, -) -from localstack.aws.api.events import EventBus as ApiTypeEventBus -from localstack.aws.api.events import Rule as ApiTypeRule -from localstack.services.events.event_bus import EventBusService, EventBusServiceDict -from localstack.services.events.event_ruler import matches_rule -from localstack.services.events.models_v2 import ( - EventBus, - EventBusDict, - EventsStore, - Rule, - RuleDict, - TargetDict, - ValidationException, - events_store, -) -from localstack.services.events.rule import RuleService, RuleServiceDict -from localstack.services.events.target import TargetSender, TargetSenderDict, TargetSenderFactory -from localstack.services.events.utils import ( - InvalidEventPatternException as InternalInvalidEventPatternException, -) -from localstack.services.plugins import ServiceLifecycleHook -from localstack.utils.strings import long_uid - -LOG = logging.getLogger(__name__) - - -def decode_next_token(token: NextToken) -> int: - """Decode a pagination token from base64 to integer.""" - return int.from_bytes(base64.b64decode(token), "big") - - -def encode_next_token(token: int) -> NextToken: - """Encode a pagination token to base64 from integer.""" - return base64.b64encode(token.to_bytes(128, "big")).decode("utf-8") - - -def get_filtered_dict(name_prefix: str, input_dict: dict) -> dict: - """Filter dictionary by prefix.""" - return {name: value for name, value in input_dict.items() if name.startswith(name_prefix)} - - -def get_event_time(event: PutEventsRequestEntry) -> str: - event_time = datetime.now(timezone.utc) - if event_timestamp := event.get("Time"): - try: - # use time from event if provided - event_time = event_timestamp.replace(tzinfo=timezone.utc) - except ValueError: - # use current time if event time is invalid - LOG.debug( - "Could not parse the `Time` parameter, falling back to current time for the following Event: '%s'", - event, - ) - formatted_time_string = event_time.strftime("%Y-%m-%dT%H:%M:%SZ") - return formatted_time_string - - -def validate_event(event: PutEventsRequestEntry) -> None | PutEventsResultEntry: - if not event.get("Source"): - return { - "ErrorCode": "InvalidArgument", - "ErrorMessage": "Parameter Source is not valid. Reason: Source is a required argument.", - } - elif not event.get("DetailType"): - return { - "ErrorCode": "InvalidArgument", - "ErrorMessage": "Parameter DetailType is not valid. Reason: DetailType is a required argument.", - } - elif not event.get("Detail"): - return { - "ErrorCode": "InvalidArgument", - "ErrorMessage": "Parameter Detail is not valid. Reason: Detail is a required argument.", - } - - -def format_event(event: PutEventsRequestEntry, region: str, account_id: str) -> dict: - # See https://docs.aws.amazon.com/AmazonS3/latest/userguide/ev-events.html - formatted_event = { - "version": "0", - "id": str(long_uid()), - "detail-type": event.get("DetailType"), - "source": event.get("Source"), - "account": account_id, - "time": get_event_time(event), - "region": region, - "resources": event.get("Resources", []), - "detail": json.loads(event.get("Detail", "{}")), - } - - return formatted_event - - -class EventsProvider(EventsApi, ServiceLifecycleHook): - # api methods are grouped by resource type and sorted in hierarchical order - # each group is sorted alphabetically - def __init__(self): - self._event_bus_services_store: EventBusServiceDict = {} - self._rule_services_store: RuleServiceDict = {} - self._target_sender_store: TargetSenderDict = {} - - ########## - # EventBus - ########## - - @handler("CreateEventBus") - def create_event_bus( - self, - context: RequestContext, - name: EventBusName, - event_source_name: EventSourceName = None, - tags: TagList = None, - **kwargs, - ) -> CreateEventBusResponse: - region = context.region - account_id = context.account_id - store = self.get_store(context) - if name in store.event_buses.keys(): - raise ResourceAlreadyExistsException(f"Event bus {name} already exists.") - event_bus_service = self.create_event_bus_service( - name, region, account_id, event_source_name, tags - ) - store.event_buses[event_bus_service.event_bus.name] = event_bus_service.event_bus - - response = CreateEventBusResponse( - EventBusArn=event_bus_service.arn, - ) - return response - - @handler("DeleteEventBus") - def delete_event_bus(self, context: RequestContext, name: EventBusName, **kwargs) -> None: - if name == "default": - raise ValidationException("Cannot delete event bus default.") - store = self.get_store(context) - try: - if event_bus := self.get_event_bus(name, store): - del self._event_bus_services_store[event_bus.arn] - if rules := event_bus.rules: - self._delete_rule_services(rules) - del store.event_buses[name] - except ResourceNotFoundException as error: - return error - - @handler("DescribeEventBus") - def describe_event_bus( - self, context: RequestContext, name: EventBusNameOrArn = None, **kwargs - ) -> DescribeEventBusResponse: - name = self._extract_event_bus_name(name) - store = self.get_store(context) - event_bus = self.get_event_bus(name, store) - - response = self._event_bus_dict_to_api_type_event_bus(event_bus) - return response - - @handler("ListEventBuses") - def list_event_buses( - self, - context: RequestContext, - name_prefix: EventBusName = None, - next_token: NextToken = None, - limit: LimitMax100 = None, - **kwargs, - ) -> ListEventBusesResponse: - store = self.get_store(context) - event_buses = ( - get_filtered_dict(name_prefix, store.event_buses) if name_prefix else store.event_buses - ) - limited_event_buses, next_token = self._get_limited_dict_and_next_token( - event_buses, next_token, limit - ) - - response = ListEventBusesResponse( - EventBuses=self._event_bust_dict_to_api_type_list(limited_event_buses) - ) - if next_token is not None: - response["NextToken"] = next_token - return response - - ####### - # Rules - ####### - @handler("EnableRule") - def enable_rule( - self, - context: RequestContext, - name: RuleName, - event_bus_name: EventBusNameOrArn = None, - **kwargs, - ) -> None: - store = self.get_store(context) - event_bus_name = self._extract_event_bus_name(event_bus_name) - event_bus = self.get_event_bus(event_bus_name, store) - rule = self.get_rule(name, event_bus) - rule.state = RuleState.ENABLED - - @handler("DeleteRule") - def delete_rule( - self, - context: RequestContext, - name: RuleName, - event_bus_name: EventBusNameOrArn = None, - force: Boolean = None, - **kwargs, - ) -> None: - store = self.get_store(context) - event_bus_name = self._extract_event_bus_name(event_bus_name) - event_bus = self.get_event_bus(event_bus_name, store) - try: - rule = self.get_rule(name, event_bus) - if rule.targets and not force: - raise ValidationException("Rule can't be deleted since it has targets.") - self._delete_rule_services(rule) - del event_bus.rules[name] - except ResourceNotFoundException as error: - return error - - @handler("DescribeRule") - def describe_rule( - self, - context: RequestContext, - name: RuleName, - event_bus_name: EventBusNameOrArn = None, - **kwargs, - ) -> DescribeRuleResponse: - store = self.get_store(context) - event_bus_name = self._extract_event_bus_name(event_bus_name) - event_bus = self.get_event_bus(event_bus_name, store) - rule = self.get_rule(name, event_bus) - - response = self._rule_dict_to_api_type_rule(rule) - return response - - @handler("DisableRule") - def disable_rule( - self, - context: RequestContext, - name: RuleName, - event_bus_name: EventBusNameOrArn = None, - **kwargs, - ) -> None: - store = self.get_store(context) - event_bus_name = self._extract_event_bus_name(event_bus_name) - event_bus = self.get_event_bus(event_bus_name, store) - rule = self.get_rule(name, event_bus) - rule.state = RuleState.DISABLED - - @handler("ListRules") - def list_rules( - self, - context: RequestContext, - name_prefix: RuleName = None, - event_bus_name: EventBusNameOrArn = None, - next_token: NextToken = None, - limit: LimitMax100 = None, - **kwargs, - ) -> ListRulesResponse: - store = self.get_store(context) - event_bus_name = self._extract_event_bus_name(event_bus_name) - event_bus = self.get_event_bus(event_bus_name, store) - rules = get_filtered_dict(name_prefix, event_bus.rules) if name_prefix else event_bus.rules - limited_rules, next_token = self._get_limited_dict_and_next_token(rules, next_token, limit) - - response = ListRulesResponse(Rules=list(self._rule_dict_to_api_type_list(limited_rules))) - if next_token is not None: - response["NextToken"] = next_token - return response - - @handler("ListRuleNamesByTarget") - def list_rule_names_by_target( - self, - context: RequestContext, - target_arn: TargetArn, - event_bus_name: EventBusNameOrArn = None, - next_token: NextToken = None, - limit: LimitMax100 = None, - **kwargs, - ) -> ListRuleNamesByTargetResponse: - raise NotImplementedError - - @handler("PutRule") - def put_rule( - self, - context: RequestContext, - name: RuleName, - schedule_expression: ScheduleExpression = None, - event_pattern: EventPattern = None, - state: RuleState = None, - description: RuleDescription = None, - role_arn: RoleArn = None, - tags: TagList = None, - event_bus_name: EventBusNameOrArn = None, - **kwargs, - ) -> PutRuleResponse: - region = context.region - account_id = context.account_id - event_bus_name = self._extract_event_bus_name(event_bus_name) - store = self.get_store(context) - event_bus = self.get_event_bus(event_bus_name, store) - existing_rule = event_bus.rules.get(name) - targets = existing_rule.targets if existing_rule else None - rule_service = self.create_rule_service( - name, - region, - account_id, - schedule_expression, - event_pattern, - state, - description, - role_arn, - tags, - event_bus_name, - targets, - ) - event_bus.rules[name] = rule_service.rule - response = PutRuleResponse(RuleArn=rule_service.arn) - return response - - @handler("TestEventPattern") - def test_event_pattern( - self, context: RequestContext, event_pattern: EventPattern, event: str, **kwargs - ) -> TestEventPatternResponse: - """Test event pattern uses EventBridge event pattern matching: - https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html - """ - try: - result = matches_rule(event, event_pattern) - except InternalInvalidEventPatternException as e: - raise InvalidEventPatternException(e.message) from e - - return TestEventPatternResponse(Result=result) - - ######### - # Targets - ######### - - @handler("ListTargetsByRule") - def list_targets_by_rule( - self, - context: RequestContext, - rule: RuleName, - event_bus_name: EventBusNameOrArn = None, - next_token: NextToken = None, - limit: LimitMax100 = None, - **kwargs, - ) -> ListTargetsByRuleResponse: - store = self.get_store(context) - event_bus_name = self._extract_event_bus_name(event_bus_name) - event_bus = self.get_event_bus(event_bus_name, store) - rule = self.get_rule(rule, event_bus) - targets = rule.targets - limited_targets, next_token = self._get_limited_dict_and_next_token( - targets, next_token, limit - ) - - response = ListTargetsByRuleResponse(Targets=list(limited_targets.values())) - if next_token is not None: - response["NextToken"] = next_token - return response - - @handler("PutTargets") - def put_targets( - self, - context: RequestContext, - rule: RuleName, - targets: TargetList, - event_bus_name: EventBusNameOrArn = None, - **kwargs, - ) -> PutTargetsResponse: - region = context.region - account_id = context.account_id - rule_service = self.get_rule_service(context, rule, event_bus_name) - failed_entries = rule_service.add_targets(targets) - rule_arn = rule_service.arn - for target in targets: # TODO only add successful targets - self.create_target_sender(target, region, account_id, rule_arn) - - response = PutTargetsResponse( - FailedEntryCount=len(failed_entries), FailedEntries=failed_entries - ) - return response - - @handler("RemoveTargets") - def remove_targets( - self, - context: RequestContext, - rule: RuleName, - ids: TargetIdList, - event_bus_name: EventBusNameOrArn = None, - force: Boolean = None, - **kwargs, - ) -> RemoveTargetsResponse: - rule_service = self.get_rule_service(context, rule, event_bus_name) - failed_entries = rule_service.remove_targets(ids) - self._delete_target_sender(ids, rule_service.rule) - - response = RemoveTargetsResponse( - FailedEntryCount=len(failed_entries), FailedEntries=failed_entries - ) - return response - - ######## - # Events - ######## - - @handler("PutEvents") - def put_events( - self, - context: RequestContext, - entries: PutEventsRequestEntryList, - endpoint_id: EndpointId = None, - **kwargs, - ) -> PutEventsResponse: - entries, failed_entry_count = self._process_entries(context, entries) - - response = PutEventsResponse( - Entries=entries, - FailedEntryCount=failed_entry_count, - ) - return response - - @handler("PutPartnerEvents") - def put_partner_events( - self, context: RequestContext, entries: PutPartnerEventsRequestEntryList, **kwargs - ) -> PutPartnerEventsResponse: - raise NotImplementedError - - ######### - # Methods - ######### - - def get_store(self, context: RequestContext) -> EventsStore: - """Returns the events store for the account and region. - On first call, creates the default event bus for the account region.""" - region = context.region - account_id = context.account_id - store = events_store[account_id][region] - # create default event bus for account region on first call - default_event_bus_name = "default" - if default_event_bus_name not in store.event_buses.keys(): - event_bus_service = self.create_event_bus_service( - default_event_bus_name, region, account_id, None, None - ) - store.event_buses[event_bus_service.event_bus.name] = event_bus_service.event_bus - return store - - def get_event_bus(self, name: EventBusName, store: EventsStore) -> EventBus: - if event_bus := store.event_buses.get(name): - return event_bus - raise ResourceNotFoundException(f"Event bus {name} does not exist.") - - def get_rule(self, name: RuleName, event_bus: EventBus) -> Rule: - if rule := event_bus.rules.get(name): - return rule - raise ResourceNotFoundException(f"Rule {name} does not exist on EventBus {event_bus.name}.") - - def get_target(self, target_id: TargetId, rule: Rule) -> Target: - if target := rule.targets.get(target_id): - return target - raise ResourceNotFoundException(f"Target {target_id} does not exist on Rule {rule.name}.") - - def get_rule_service( - self, context: RequestContext, rule_name: RuleName, event_bus_name: EventBusName - ) -> RuleService: - store = self.get_store(context) - event_bus_name = self._extract_event_bus_name(event_bus_name) - event_bus = self.get_event_bus(event_bus_name, store) - rule = self.get_rule(rule_name, event_bus) - return self._rule_services_store[rule.arn] - - def create_event_bus_service( - self, - name: EventBusName, - region: str, - account_id: str, - event_source_name: Optional[EventSourceName], - tags: Optional[TagList], - ) -> EventBusService: - event_bus_service = EventBusService( - name, - region, - account_id, - event_source_name, - tags, - ) - self._event_bus_services_store[event_bus_service.arn] = event_bus_service - return event_bus_service - - def create_rule_service( - self, - name: RuleName, - region: str, - account_id: str, - schedule_expression: Optional[ScheduleExpression], - event_pattern: Optional[EventPattern], - state: Optional[RuleState], - description: Optional[RuleDescription], - role_arn: Optional[RoleArn], - tags: Optional[TagList], - event_bus_name: Optional[EventBusName], - targets: Optional[TargetDict], - ) -> RuleService: - rule_service = RuleService( - name, - region, - account_id, - schedule_expression, - event_pattern, - state, - description, - role_arn, - tags, - event_bus_name, - targets, - ) - self._rule_services_store[rule_service.arn] = rule_service - return rule_service - - def create_target_sender( - self, target: Target, region: str, account_id: str, rule_arn: Arn - ) -> TargetSender: - target_sender = TargetSenderFactory( - target, region, account_id, rule_arn - ).get_target_sender() - self._target_sender_store[target_sender.arn] = target_sender - return target_sender - - def _get_limited_dict_and_next_token( - self, input_dict: dict, next_token: NextToken | None, limit: LimitMax100 | None - ) -> tuple[dict, NextToken]: - """Return a slice of the given dictionary starting from next_token with length of limit - and new last index encoded as a next_token for pagination.""" - input_dict_len = len(input_dict) - start_index = decode_next_token(next_token) if next_token is not None else 0 - end_index = start_index + limit if limit is not None else input_dict_len - limited_dict = dict(list(input_dict.items())[start_index:end_index]) - - next_token = ( - encode_next_token(end_index) - # return a next_token (encoded integer of next starting index) if not all items are returned - if end_index < input_dict_len - else None - ) - return limited_dict, next_token - - def _extract_event_bus_name( - self, event_bus_name_or_arn: EventBusNameOrArn | None - ) -> EventBusName: - """Return the event bus name. Input can be either an event bus name or ARN.""" - if not event_bus_name_or_arn: - return "default" - return event_bus_name_or_arn.split("/")[-1] - - def _event_bust_dict_to_api_type_list(self, event_buses: EventBusDict) -> EventBusList: - """Return a converted dict of EventBus model objects as a list of event buses in API type EventBus format.""" - event_bus_list = [ - self._event_bus_dict_to_api_type_event_bus(event_bus) - for event_bus in event_buses.values() - ] - return event_bus_list - - def _event_bus_dict_to_api_type_event_bus(self, event_bus: EventBus) -> ApiTypeEventBus: - event_bus_api_type = { - "Name": event_bus.name, - "Arn": event_bus.arn, - } - if event_bus.policy: - event_bus_api_type["Policy"] = event_bus.policy - - return event_bus_api_type - - def _delete_rule_services(self, rules: RuleDict | Rule) -> None: - """ - Delete all rule services associated to the input from the store. - Accepts a single Rule object or a dict of Rule objects as input. - """ - if isinstance(rules, Rule): - rules = {rules.name: rules} - for rule in rules.values(): - del self._rule_services_store[rule.arn] - - def _rule_dict_to_api_type_list(self, rules: RuleDict) -> RuleResponseList: - """Return a converted dict of Rule model objects as a list of rules in API type Rule format.""" - rule_list = [self._rule_dict_to_api_type_rule(rule) for rule in rules.values()] - return rule_list - - def _rule_dict_to_api_type_rule(self, rule: Rule) -> ApiTypeRule: - rule = { - "Name": rule.name, - "Arn": rule.arn, - "EventPattern": rule.event_pattern, - "State": rule.state, - "Description": rule.description, - "ScheduleExpression": rule.schedule_expression, - "RoleArn": rule.role_arn, - "ManagedBy": rule.managed_by, - "EventBusName": rule.event_bus_name, - "CreatedBy": rule.created_by, - } - return {key: value for key, value in rule.items() if value is not None} - - def _delete_target_sender(self, ids: TargetIdList, rule) -> None: - for target_id in ids: - if target := rule.targets.get(target_id): - target_arn = target["Arn"] - try: - del self._target_sender_store[target_arn] - except KeyError: - LOG.error(f"Error deleting target service {target_arn}.") - - def _process_entries( - self, context: RequestContext, entries: PutEventsRequestEntryList - ) -> tuple[PutEventsResultEntryList, int]: - processed_entries = [] - failed_entry_count = 0 - for event in entries: - event_bus_name = event.get("EventBusName", "default") - if event_failed_validation := validate_event(event): - processed_entries.append(event_failed_validation) - failed_entry_count += 1 - continue - event = format_event(event, context.region, context.account_id) - store = self.get_store(context) - try: - event_bus = self.get_event_bus(event_bus_name, store) - except ResourceNotFoundException: - # ignore events for non-existing event buses but add processed event - processed_entries.append({"EventId": event["id"]}) - continue - matching_rules = [rule for rule in event_bus.rules.values()] - for rule in matching_rules: - event_pattern = rule.event_pattern - event_str = json.dumps(event) - if matches_rule(event_str, event_pattern): - for target in rule.targets.values(): - target_sender = self._target_sender_store[target["Arn"]] - try: - target_sender.send_event(event) - processed_entries.append({"EventId": event["id"]}) - except Exception as error: - processed_entries.append( - { - "ErrorCode": "InternalException", - "ErrorMessage": str(error), - } - ) - failed_entry_count += 1 - return processed_entries, failed_entry_count diff --git a/localstack/services/events/rule.py b/localstack/services/events/rule.py index 61011ccec6286..4298f9c4622e8 100644 --- a/localstack/services/events/rule.py +++ b/localstack/services/events/rule.py @@ -19,11 +19,7 @@ TargetIdList, TargetList, ) -from localstack.services.events.models_v2 import ( - Rule, - TargetDict, - ValidationException, -) +from localstack.services.events.models import Rule, TargetDict, ValidationException TARGET_ID_REGEX = re.compile(r"^[\.\-_A-Za-z0-9]+$") TARGET_ARN_REGEX = re.compile(r"arn:[\d\w:\-/]*") diff --git a/localstack/services/events/v1/__init__.py b/localstack/services/events/v1/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/localstack/services/events/v1/models.py b/localstack/services/events/v1/models.py new file mode 100644 index 0000000000000..4096215c82499 --- /dev/null +++ b/localstack/services/events/v1/models.py @@ -0,0 +1,11 @@ +from typing import Dict + +from localstack.services.stores import AccountRegionBundle, BaseStore, LocalAttribute + + +class EventsStore(BaseStore): + # maps rule name to job_id + rule_scheduled_jobs: Dict[str, str] = LocalAttribute(default=dict) + + +events_stores = AccountRegionBundle("events", EventsStore) diff --git a/localstack/services/events/v1/provider.py b/localstack/services/events/v1/provider.py new file mode 100644 index 0000000000000..cfaae5743c51e --- /dev/null +++ b/localstack/services/events/v1/provider.py @@ -0,0 +1,575 @@ +import datetime +import json +import logging +import os +import re +import time +from typing import Any, Dict, Optional + +from moto.events import events_backends +from moto.events.responses import EventsHandler as MotoEventsHandler +from werkzeug import Request +from werkzeug.exceptions import NotFound + +from localstack import config +from localstack.aws.api import RequestContext +from localstack.aws.api.core import CommonServiceException, ServiceException +from localstack.aws.api.events import ( + Boolean, + ConnectionAuthorizationType, + ConnectionDescription, + ConnectionName, + CreateConnectionAuthRequestParameters, + CreateConnectionResponse, + EventBusNameOrArn, + EventPattern, + EventsApi, + InvalidEventPatternException, + PutRuleResponse, + PutTargetsResponse, + RoleArn, + RuleDescription, + RuleName, + RuleState, + ScheduleExpression, + String, + TagList, + TargetList, + TestEventPatternResponse, +) +from localstack.constants import APPLICATION_AMZ_JSON_1_1 +from localstack.http import route +from localstack.services.edge import ROUTER +from localstack.services.events.event_ruler import matches_rule +from localstack.services.events.models import ( + InvalidEventPatternException as InternalInvalidEventPatternException, +) +from localstack.services.events.scheduler import JobScheduler +from localstack.services.events.v1.models import EventsStore, events_stores +from localstack.services.events.v1.utils import matches_event +from localstack.services.moto import call_moto +from localstack.services.plugins import ServiceLifecycleHook +from localstack.utils.aws.arns import event_bus_arn, parse_arn +from localstack.utils.aws.client_types import ServicePrincipal +from localstack.utils.aws.message_forwarding import send_event_to_target +from localstack.utils.collections import pick_attributes +from localstack.utils.common import TMP_FILES, mkdir, save_file, truncate +from localstack.utils.json import extract_jsonpath +from localstack.utils.strings import long_uid, short_uid +from localstack.utils.time import TIMESTAMP_FORMAT_TZ, timestamp + +LOG = logging.getLogger(__name__) + +# list of events used to run assertions during integration testing (not exposed to the user) +TEST_EVENTS_CACHE = [] +EVENTS_TMP_DIR = "cw_events" +DEFAULT_EVENT_BUS_NAME = "default" +CONNECTION_NAME_PATTERN = re.compile("^[\\.\\-_A-Za-z0-9]+$") + + +class ValidationException(ServiceException): + code: str = "ValidationException" + sender_fault: bool = True + status_code: int = 400 + + +class EventsProvider(EventsApi, ServiceLifecycleHook): + def __init__(self): + apply_patches() + + def on_after_init(self): + ROUTER.add(self.trigger_scheduled_rule) + + def on_before_start(self): + JobScheduler.start() + + def on_before_stop(self): + JobScheduler.shutdown() + + @route("/_aws/events/rules/<path:rule_arn>/trigger") + def trigger_scheduled_rule(self, request: Request, rule_arn: str): + """Developer endpoint to trigger a scheduled rule.""" + arn_data = parse_arn(rule_arn) + account_id = arn_data["account"] + region = arn_data["region"] + rule_name = arn_data["resource"].split("/", maxsplit=1)[-1] + + job_id = events_stores[account_id][region].rule_scheduled_jobs.get(rule_name) + if not job_id: + raise NotFound() + job = JobScheduler().instance().get_job(job_id) + if not job: + raise NotFound() + + # TODO: once job scheduler is refactored, we can update the deadline of the task instead of running + # it here + job.run() + + @staticmethod + def get_store(context: RequestContext) -> EventsStore: + return events_stores[context.account_id][context.region] + + def test_event_pattern( + self, context: RequestContext, event_pattern: EventPattern, event: String, **kwargs + ) -> TestEventPatternResponse: + """Test event pattern uses EventBridge event pattern matching: + https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html + """ + if config.EVENT_RULE_ENGINE == "java": + try: + result = matches_rule(event, event_pattern) + except InternalInvalidEventPatternException as e: + raise InvalidEventPatternException(e.message) from e + else: + event_pattern_dict = json.loads(event_pattern) + event_dict = json.loads(event) + result = matches_event(event_pattern_dict, event_dict) + + # TODO: unify the different implementations below: + # event_pattern_dict = json.loads(event_pattern) + # event_dict = json.loads(event) + + # EventBridge: + # result = matches_event(event_pattern_dict, event_dict) + + # Lambda EventSourceMapping: + # from localstack.services.lambda_.event_source_listeners.utils import does_match_event + # + # result = does_match_event(event_pattern_dict, event_dict) + + # moto-ext EventBridge: + # from moto.events.models import EventPattern as EventPatternMoto + # + # event_pattern = EventPatternMoto.load(event_pattern) + # result = event_pattern.matches_event(event_dict) + + # SNS: The SNS rule engine seems to differ slightly, for example not allowing the wildcard pattern. + # from localstack.services.sns.publisher import SubscriptionFilter + # subscription_filter = SubscriptionFilter() + # result = subscription_filter._evaluate_nested_filter_policy_on_dict(event_pattern_dict, event_dict) + + # moto-ext SNS: + # from moto.sns.utils import FilterPolicyMatcher + # filter_policy_matcher = FilterPolicyMatcher(event_pattern_dict, "MessageBody") + # result = filter_policy_matcher._body_based_match(event_dict) + + return TestEventPatternResponse(Result=result) + + @staticmethod + def get_scheduled_rule_func( + store: EventsStore, + rule_name: RuleName, + event_bus_name_or_arn: Optional[EventBusNameOrArn] = None, + ): + def func(*args, **kwargs): + account_id = store._account_id + region = store._region_name + moto_backend = events_backends[account_id][region] + event_bus_name = get_event_bus_name(event_bus_name_or_arn) + event_bus = moto_backend.event_buses[event_bus_name] + rule = event_bus.rules.get(rule_name) + if not rule: + LOG.info("Unable to find rule `%s` for event bus `%s`", rule_name, event_bus_name) + return + if rule.targets: + LOG.debug( + "Notifying %s targets in response to triggered Events rule %s", + len(rule.targets), + rule_name, + ) + + default_event = { + "version": "0", + "id": long_uid(), + "detail-type": "Scheduled Event", + "source": "aws.events", + "account": account_id, + "time": timestamp(format=TIMESTAMP_FORMAT_TZ), + "region": region, + "resources": [rule.arn], + "detail": {}, + } + + for target in rule.targets: + arn = target.get("Arn") + + if input_ := target.get("Input"): + event = json.loads(input_) + else: + event = default_event + if target.get("InputPath"): + event = filter_event_with_target_input_path(target, event) + if input_transformer := target.get("InputTransformer"): + event = process_event_with_input_transformer(input_transformer, event) + + attr = pick_attributes(target, ["$.SqsParameters", "$.KinesisParameters"]) + + try: + send_event_to_target( + arn, + event, + target_attributes=attr, + role=target.get("RoleArn"), + target=target, + source_arn=rule.arn, + source_service=ServicePrincipal.events, + ) + except Exception as e: + LOG.info( + "Unable to send event notification %s to target %s: %s", + truncate(event), + target, + e, + ) + + return func + + @staticmethod + def convert_schedule_to_cron(schedule): + """Convert Events schedule like "cron(0 20 * * ? *)" or "rate(5 minutes)" """ + cron_regex = r"\s*cron\s*\(([^\)]*)\)\s*" + if re.match(cron_regex, schedule): + cron = re.sub(cron_regex, r"\1", schedule) + return cron + rate_regex = r"\s*rate\s*\(([^\)]*)\)\s*" + if re.match(rate_regex, schedule): + rate = re.sub(rate_regex, r"\1", schedule) + value, unit = re.split(r"\s+", rate.strip()) + + value = int(value) + if value < 1: + raise ValueError("Rate value must be larger than 0") + # see https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rate-expressions.html + if value == 1 and unit.endswith("s"): + raise ValueError("If the value is equal to 1, then the unit must be singular") + if value > 1 and not unit.endswith("s"): + raise ValueError("If the value is greater than 1, the unit must be plural") + + if "minute" in unit: + return "*/%s * * * *" % value + if "hour" in unit: + return "0 */%s * * *" % value + if "day" in unit: + return "0 0 */%s * *" % value + raise ValueError("Unable to parse events schedule expression: %s" % schedule) + return schedule + + @staticmethod + def put_rule_job_scheduler( + store: EventsStore, + name: Optional[RuleName], + state: Optional[RuleState], + schedule_expression: Optional[ScheduleExpression], + event_bus_name_or_arn: Optional[EventBusNameOrArn] = None, + ): + if not schedule_expression: + return + + try: + cron = EventsProvider.convert_schedule_to_cron(schedule_expression) + except ValueError as e: + LOG.error("Error parsing schedule expression: %s", e) + raise ValidationException("Parameter ScheduleExpression is not valid.") from e + + job_func = EventsProvider.get_scheduled_rule_func( + store, name, event_bus_name_or_arn=event_bus_name_or_arn + ) + LOG.debug("Adding new scheduled Events rule with cron schedule %s", cron) + + enabled = state != "DISABLED" + job_id = JobScheduler.instance().add_job(job_func, cron, enabled) + rule_scheduled_jobs = store.rule_scheduled_jobs + rule_scheduled_jobs[name] = job_id + + def put_rule( + self, + context: RequestContext, + name: RuleName, + schedule_expression: ScheduleExpression = None, + event_pattern: EventPattern = None, + state: RuleState = None, + description: RuleDescription = None, + role_arn: RoleArn = None, + tags: TagList = None, + event_bus_name: EventBusNameOrArn = None, + **kwargs, + ) -> PutRuleResponse: + store = self.get_store(context) + self.put_rule_job_scheduler( + store, name, state, schedule_expression, event_bus_name_or_arn=event_bus_name + ) + return call_moto(context) + + def delete_rule( + self, + context: RequestContext, + name: RuleName, + event_bus_name: EventBusNameOrArn = None, + force: Boolean = None, + **kwargs, + ) -> None: + rule_scheduled_jobs = self.get_store(context).rule_scheduled_jobs + job_id = rule_scheduled_jobs.get(name) + if job_id: + LOG.debug("Removing scheduled Events: {} | job_id: {}".format(name, job_id)) + JobScheduler.instance().cancel_job(job_id=job_id) + call_moto(context) + + def disable_rule( + self, + context: RequestContext, + name: RuleName, + event_bus_name: EventBusNameOrArn = None, + **kwargs, + ) -> None: + rule_scheduled_jobs = self.get_store(context).rule_scheduled_jobs + job_id = rule_scheduled_jobs.get(name) + if job_id: + LOG.debug("Disabling Rule: {} | job_id: {}".format(name, job_id)) + JobScheduler.instance().disable_job(job_id=job_id) + call_moto(context) + + def create_connection( + self, + context: RequestContext, + name: ConnectionName, + authorization_type: ConnectionAuthorizationType, + auth_parameters: CreateConnectionAuthRequestParameters, + description: ConnectionDescription = None, + **kwargs, + ) -> CreateConnectionResponse: + errors = [] + + if not CONNECTION_NAME_PATTERN.match(name): + error = f"{name} at 'name' failed to satisfy: Member must satisfy regular expression pattern: [\\.\\-_A-Za-z0-9]+" + errors.append(error) + + if len(name) > 64: + error = f"{name} at 'name' failed to satisfy: Member must have length less than or equal to 64" + errors.append(error) + + if authorization_type not in ["BASIC", "API_KEY", "OAUTH_CLIENT_CREDENTIALS"]: + error = f"{authorization_type} at 'authorizationType' failed to satisfy: Member must satisfy enum value set: [BASIC, OAUTH_CLIENT_CREDENTIALS, API_KEY]" + errors.append(error) + + if len(errors) > 0: + error_description = "; ".join(errors) + error_plural = "errors" if len(errors) > 1 else "error" + errors_amount = len(errors) + message = f"{errors_amount} validation {error_plural} detected: {error_description}" + raise CommonServiceException(message=message, code="ValidationException") + + return call_moto(context) + + def put_targets( + self, + context: RequestContext, + rule: RuleName, + targets: TargetList, + event_bus_name: EventBusNameOrArn = None, + **kwargs, + ) -> PutTargetsResponse: + validation_errors = [] + + id_regex = re.compile(r"^[\.\-_A-Za-z0-9]+$") + for index, target in enumerate(targets): + id = target.get("Id") + if not id_regex.match(id): + validation_errors.append( + f"Value '{id}' at 'targets.{index + 1}.member.id' failed to satisfy constraint: Member must satisfy regular expression pattern: [\\.\\-_A-Za-z0-9]+" + ) + + if len(id) > 64: + validation_errors.append( + f"Value '{id}' at 'targets.{index + 1}.member.id' failed to satisfy constraint: Member must have length less than or equal to 64" + ) + + if validation_errors: + errors_message = "; ".join(validation_errors) + message = f"{len(validation_errors)} validation {'errors' if len(validation_errors) > 1 else 'error'} detected: {errors_message}" + raise CommonServiceException(message=message, code="ValidationException") + + return call_moto(context) + + +def _get_events_tmp_dir(): + return os.path.join(config.dirs.tmp, EVENTS_TMP_DIR) + + +def _create_and_register_temp_dir(): + tmp_dir = _get_events_tmp_dir() + if not os.path.exists(tmp_dir): + mkdir(tmp_dir) + TMP_FILES.append(tmp_dir) + return tmp_dir + + +def _dump_events_to_files(events_with_added_uuid): + try: + _create_and_register_temp_dir() + current_time_millis = int(round(time.time() * 1000)) + for event in events_with_added_uuid: + target = os.path.join( + _get_events_tmp_dir(), + "%s_%s" % (current_time_millis, event["uuid"]), + ) + save_file(target, json.dumps(event["event"])) + except Exception as e: + LOG.info("Unable to dump events to tmp dir %s: %s", _get_events_tmp_dir(), e) + + +def filter_event_based_on_event_format( + self, rule_name: str, event_bus_name: str, event: dict[str, Any] +): + rule_information = self.events_backend.describe_rule( + rule_name, event_bus_arn(event_bus_name, self.current_account, self.region) + ) + + if not rule_information: + LOG.info('Unable to find rule "%s" in backend: %s', rule_name, rule_information) + return False + if rule_information.event_pattern._pattern: + event_pattern = rule_information.event_pattern._pattern + if config.EVENT_RULE_ENGINE == "java": + event_str = json.dumps(event) + event_pattern_str = json.dumps(event_pattern) + match_result = matches_rule(event_str, event_pattern_str) + else: + match_result = matches_event(event_pattern, event) + if not match_result: + return False + return True + + +def filter_event_with_target_input_path(target: Dict, event: Dict) -> Dict: + input_path = target.get("InputPath") + if input_path: + event = extract_jsonpath(event, input_path) + return event + + +def process_event_with_input_transformer(input_transformer: Dict, event: Dict) -> Dict: + """ + Process the event with the input transformer of the target event, + by replacing the message with the populated InputTemplate. + docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-input.html + """ + try: + input_paths = input_transformer["InputPathsMap"] + input_template = input_transformer["InputTemplate"] + except KeyError as e: + LOG.error("%s key does not exist in input_transformer.", e) + raise e + for key, path in input_paths.items(): + value = extract_jsonpath(event, path) + if not value: + value = "" + input_template = input_template.replace(f"<{key}>", value) + templated_event = re.sub('"', "", input_template) + return templated_event + + +def process_events(event: Dict, targets: list[Dict]): + for target in targets: + arn = target["Arn"] + changed_event = filter_event_with_target_input_path(target, event) + if input_transformer := target.get("InputTransformer"): + changed_event = process_event_with_input_transformer(input_transformer, changed_event) + if target.get("Input"): + changed_event = json.loads(target.get("Input")) + try: + send_event_to_target( + arn, + changed_event, + pick_attributes(target, ["$.SqsParameters", "$.KinesisParameters"]), + role=target.get("RoleArn"), + target=target, + source_service=ServicePrincipal.events, + source_arn=target.get("RuleArn"), + ) + except Exception as e: + LOG.info(f"Unable to send event notification {truncate(event)} to target {target}: {e}") + + +def get_event_bus_name(event_bus_name_or_arn: Optional[EventBusNameOrArn] = None) -> str: + event_bus_name_or_arn = event_bus_name_or_arn or DEFAULT_EVENT_BUS_NAME + return event_bus_name_or_arn.split("/")[-1] + + +# specific logic for put_events which forwards matching events to target listeners +def events_handler_put_events(self): + entries = self._get_param("Entries") + + # keep track of events for local integration testing + if config.is_local_test_mode(): + TEST_EVENTS_CACHE.extend(entries) + + events = [{"event": event, "uuid": str(long_uid())} for event in entries] + + _dump_events_to_files(events) + + for event_envelope in events: + event = event_envelope["event"] + event_bus_name = get_event_bus_name(event.get("EventBusName")) + event_bus = self.events_backend.event_buses.get(event_bus_name) + if not event_bus: + continue + + matching_rules = [ + r + for r in event_bus.rules.values() + if r.event_bus_name == event_bus_name and not r.scheduled_expression + ] + if not matching_rules: + continue + + event_time = datetime.datetime.utcnow() + if event_timestamp := event.get("Time"): + try: + # if provided, use the time from event + event_time = datetime.datetime.utcfromtimestamp(event_timestamp) + except ValueError: + # if we can't parse it, pass and keep using `utcnow` + LOG.debug( + "Could not parse the `Time` parameter, falling back to `utcnow` for the following Event: '%s'", + event, + ) + + # See https://docs.aws.amazon.com/AmazonS3/latest/userguide/ev-events.html + formatted_event = { + "version": "0", + "id": event_envelope["uuid"], + "detail-type": event.get("DetailType"), + "source": event.get("Source"), + "account": self.current_account, + "time": event_time.strftime("%Y-%m-%dT%H:%M:%SZ"), + "region": self.region, + "resources": event.get("Resources", []), + "detail": json.loads(event.get("Detail", "{}")), + } + + targets = [] + for rule in matching_rules: + if filter_event_based_on_event_format(self, rule.name, event_bus_name, formatted_event): + rule_targets = self.events_backend.list_targets_by_rule( + rule.name, event_bus_arn(event_bus_name, self.current_account, self.region) + ).get("Targets", []) + + targets.extend([{"RuleArn": rule.arn} | target for target in rule_targets]) + # process event + process_events(formatted_event, targets) + + content = { + "FailedEntryCount": 0, # TODO: dynamically set proper value when refactoring + "Entries": [{"EventId": event["uuid"]} for event in events], + } + + self.response_headers.update( + {"Content-Type": APPLICATION_AMZ_JSON_1_1, "x-amzn-RequestId": short_uid()} + ) + + return json.dumps(content), self.response_headers + + +def apply_patches(): + MotoEventsHandler.put_events = events_handler_put_events diff --git a/localstack/services/events/utils.py b/localstack/services/events/v1/utils.py similarity index 98% rename from localstack/services/events/utils.py rename to localstack/services/events/v1/utils.py index a93fdcc07b895..9fdd1550d93c5 100644 --- a/localstack/services/events/utils.py +++ b/localstack/services/events/v1/utils.py @@ -4,19 +4,13 @@ import re from typing import Any +from localstack.services.events.models import InvalidEventPatternException + CONTENT_BASE_FILTER_KEYWORDS = ["prefix", "anything-but", "numeric", "cidr", "exists"] LOG = logging.getLogger(__name__) -class InvalidEventPatternException(Exception): - reason: str - - def __init__(self, reason=None, message=None) -> None: - self.reason = reason - self.message = message or f"Event pattern is not valid. Reason: {reason}" - - def matches_event(event_pattern: dict[str, any], event: dict[str, Any]) -> bool: """Decides whether an event pattern matches an event or not. Returns True if the `event_pattern` matches the given `event` and False otherwise. diff --git a/localstack/services/providers.py b/localstack/services/providers.py index c892cf2d5c7f1..2610420627822 100644 --- a/localstack/services/providers.py +++ b/localstack/services/providers.py @@ -338,7 +338,7 @@ def ssm(): @aws_provider(api="events", name="default") def events(): - from localstack.services.events.provider import EventsProvider + from localstack.services.events.v1.provider import EventsProvider from localstack.services.moto import MotoFallbackDispatcher provider = EventsProvider() @@ -347,7 +347,7 @@ def events(): @aws_provider(api="events", name="v1") def events_v1(): - from localstack.services.events.provider import EventsProvider + from localstack.services.events.v1.provider import EventsProvider from localstack.services.moto import MotoFallbackDispatcher provider = EventsProvider() @@ -356,7 +356,7 @@ def events_v1(): @aws_provider(api="events", name="v2") def events_v2(): - from localstack.services.events.provider_v2 import EventsProvider + from localstack.services.events.provider import EventsProvider provider = EventsProvider() return Service.for_provider(provider) diff --git a/tests/aws/services/events/test_events.py b/tests/aws/services/events/test_events.py index 6b3c8ee9ec244..5f3efddada7ab 100644 --- a/tests/aws/services/events/test_events.py +++ b/tests/aws/services/events/test_events.py @@ -14,7 +14,7 @@ from werkzeug import Request, Response from localstack import config -from localstack.services.events.provider import _get_events_tmp_dir +from localstack.services.events.v1.provider import _get_events_tmp_dir from localstack.testing.aws.eventbus_utils import allow_event_rule_to_sqs_queue from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers diff --git a/tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py b/tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py index c289a4ebdbf1c..8f2718011f438 100644 --- a/tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py +++ b/tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py @@ -4,7 +4,7 @@ import pytest -from localstack.services.events.provider import TEST_EVENTS_CACHE +from localstack.services.events.v1.provider import TEST_EVENTS_CACHE from localstack.services.stepfunctions.stepfunctions_utils import await_sfn_execution_result from localstack.testing.pytest import markers from localstack.utils import testutil diff --git a/tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py b/tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py index e1e264682df17..ec623c9298997 100644 --- a/tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py +++ b/tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py @@ -8,7 +8,7 @@ SECONDARY_TEST_AWS_ACCESS_KEY_ID, SECONDARY_TEST_AWS_SECRET_ACCESS_KEY, ) -from localstack.services.events.provider import TEST_EVENTS_CACHE +from localstack.services.events.v1.provider import TEST_EVENTS_CACHE from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers from localstack.utils import testutil From c0cc26f7eba53d2e709be55b0e926979de0903d4 Mon Sep 17 00:00:00 2001 From: Dominik Schubert <dominik.schubert91@gmail.com> Date: Wed, 8 May 2024 15:04:47 +0200 Subject: [PATCH 130/169] Separate CircleCI test selection env setup from tinybird setup (#10790) --- .circleci/config.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 113e17260ca3e..cde3d127c506f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,7 +23,7 @@ executors: image: << pipeline.parameters.ubuntu-amd64-machine-image >> commands: - prepare-pytest-tinybird: + prepare-testselection: steps: - unless: condition: << pipeline.parameters.skip_test_selection >> @@ -34,6 +34,9 @@ commands: if [[ -n "$CI_PULL_REQUEST" ]] ; then echo "export TESTSELECTION_PYTEST_ARGS='--path-filter=target/testselection/test-selection.txt '" >> $BASH_ENV fi + + prepare-pytest-tinybird: + steps: - run: name: Setup Environment Variables command: | @@ -204,6 +207,7 @@ jobs: steps: - attach_workspace: at: /tmp/workspace + - prepare-testselection - prepare-pytest-tinybird - prepare-account-region-randomization - run: @@ -232,6 +236,7 @@ jobs: steps: - attach_workspace: at: /tmp/workspace + - prepare-testselection - prepare-pytest-tinybird - prepare-account-region-randomization - run: @@ -260,6 +265,7 @@ jobs: steps: - attach_workspace: at: /tmp/workspace + - prepare-testselection - prepare-pytest-tinybird - prepare-account-region-randomization - run: @@ -288,6 +294,7 @@ jobs: steps: - attach_workspace: at: /tmp/workspace + - prepare-testselection - prepare-pytest-tinybird - prepare-account-region-randomization - run: @@ -384,6 +391,7 @@ jobs: key: common-functions-<< parameters.platform >>-{{ checksum "/tmp/common-functions-checksums" }} paths: - "tests/aws/services/lambda_/functions/common" + - prepare-testselection - prepare-pytest-tinybird - prepare-account-region-randomization - run: From 84f4a31a12299a391b5b14fd538c46751ecfddff Mon Sep 17 00:00:00 2001 From: Gabriel Vasile <gabriel.vasile@email.com> Date: Wed, 8 May 2024 22:05:45 +0900 Subject: [PATCH 131/169] fix missing href inside README.md (#10671) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6513cc82a050f..ac76cc8ebcae7 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ <p align="center"> <a href="#overview">Overview</a> • - <a href="#installing">Install</a> • + <a href="#install">Install</a> • <a href="#quickstart">Quickstart</a> • <a href="#running">Run</a> • <a href="#usage">Usage</a> • @@ -48,7 +48,7 @@ LocalStack supports a growing number of AWS services, like AWS Lambda, S3, Dynam LocalStack also provides additional features to make your life as a cloud developer easier! Check out LocalStack's [User Guides](https://docs.localstack.cloud/user-guide/) for more information. -## Installation +## Install The quickest way get started with LocalStack is by using the LocalStack CLI. It enables you to start and manage the LocalStack Docker container directly through your command line. Ensure that your machine has a functional [`docker` environment](https://docs.docker.com/get-docker/) installed before proceeding. From eeab6fb40d60e792af44708f84d61be4f46037e3 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Wed, 8 May 2024 16:47:52 +0200 Subject: [PATCH 132/169] fix SNS cross-account listing call of subscriptions (#10788) --- localstack/services/sns/provider.py | 24 ++++++++++++++++++++++++ tests/aws/services/sns/test_sns.py | 25 +++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/localstack/services/sns/provider.py b/localstack/services/sns/provider.py index c5ff62fa7b6a9..eca0a21275647 100644 --- a/localstack/services/sns/provider.py +++ b/localstack/services/sns/provider.py @@ -21,6 +21,8 @@ GetTopicAttributesResponse, InvalidParameterException, InvalidParameterValueException, + ListSubscriptionsByTopicResponse, + ListSubscriptionsResponse, ListTagsForResourceResponse, MapStringToString, MessageAttributeMap, @@ -34,6 +36,7 @@ String, SubscribeInput, SubscribeResponse, + Subscription, SubscriptionAttributesMap, TagKeyList, TagList, @@ -46,6 +49,7 @@ authenticateOnUnsubscribe, boolean, messageStructure, + nextToken, subscriptionARN, topicARN, topicName, @@ -69,6 +73,7 @@ extract_region_from_arn, parse_arn, ) +from localstack.utils.collections import select_from_typed_dict from localstack.utils.strings import short_uid # set up logger @@ -452,6 +457,25 @@ def get_subscription_attributes( attributes = {k: v for k, v in sub.items() if k not in removed_attrs} return GetSubscriptionAttributesResponse(Attributes=attributes) + def list_subscriptions( + self, context: RequestContext, next_token: nextToken = None, **kwargs + ) -> ListSubscriptionsResponse: + store = self.get_store(context.account_id, context.region) + subscriptions = [ + select_from_typed_dict(Subscription, sub) for sub in list(store.subscriptions.values()) + ] + return ListSubscriptionsResponse(Subscriptions=subscriptions) + + def list_subscriptions_by_topic( + self, context: RequestContext, topic_arn: topicARN, next_token: nextToken = None, **kwargs + ) -> ListSubscriptionsByTopicResponse: + self._get_topic(topic_arn, context) + parsed_topic_arn = parse_and_validate_topic_arn(topic_arn) + store = self.get_store(parsed_topic_arn["account"], parsed_topic_arn["region"]) + sns_subscriptions = store.get_topic_subscriptions(topic_arn) + subscriptions = [select_from_typed_dict(Subscription, sub) for sub in sns_subscriptions] + return ListSubscriptionsByTopicResponse(Subscriptions=subscriptions) + def publish( self, context: RequestContext, diff --git a/tests/aws/services/sns/test_sns.py b/tests/aws/services/sns/test_sns.py index 59a217a0bd9bd..0393a2b01f4ff 100644 --- a/tests/aws/services/sns/test_sns.py +++ b/tests/aws/services/sns/test_sns.py @@ -4765,6 +4765,17 @@ def test_cross_account_publish_to_sqs( region_name, ) + # create a second queue with the secondary AccountId + queue_name_2 = "sample_queue_two" + queue_3 = sqs_secondary_client.create_queue(QueueName=queue_name_2) + queue_3_url = queue_3["QueueUrl"] + # test that we get the right queue URL at the same time, even if we use the primary client + queue_3_arn = sqs_queue_arn( + queue_3_url, + secondary_account_id, + region_name, + ) + # test that we can subscribe with the primary client to a queue from the same account sns_primary_client.subscribe( TopicArn=topic_1_arn, @@ -4779,8 +4790,17 @@ def test_cross_account_publish_to_sqs( Endpoint=queue_2_arn, ) - # now, we have 2 subscriptions in topic_1, one to the queue_1 located in the same account, and to queue_2 - # located in the secondary account + # test that we can subscribe with the secondary client (not owning the topic) to a queue of the secondary client + sns_secondary_client.subscribe( + TopicArn=topic_1_arn, + Protocol="sqs", + Endpoint=queue_3_arn, + ) + + # now, we have 3 subscriptions in topic_1, one to the queue_1 located in the same account, and 2 to queue_2 and + # queue_3 located in the secondary account + subscriptions = sns_primary_client.list_subscriptions_by_topic(TopicArn=topic_1_arn) + assert len(subscriptions["Subscriptions"]) == 3 sns_primary_client.publish(TopicArn=topic_1_arn, Message="TestMessageOwner") @@ -4788,6 +4808,7 @@ def get_messages_from_queues(message_content: str): for client, queue_url in ( (sqs_primary_client, queue_1_url), (sqs_secondary_client, queue_2_url), + (sqs_secondary_client, queue_3_url), ): response = client.receive_message( QueueUrl=queue_url, From ff8f716a028778f2171108165e804578ce8f813b Mon Sep 17 00:00:00 2001 From: Bernhard Matyas <90144234+baermat@users.noreply.github.com> Date: Wed, 8 May 2024 18:57:04 +0200 Subject: [PATCH 133/169] enable sqs test selection (#10784) --- localstack/testing/testselection/opt_in.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/localstack/testing/testselection/opt_in.py b/localstack/testing/testselection/opt_in.py index ec9ef6e4d1aac..70a671d7391ae 100644 --- a/localstack/testing/testselection/opt_in.py +++ b/localstack/testing/testselection/opt_in.py @@ -39,6 +39,9 @@ # SSM "localstack/services/ssm/**", "tests/aws/services/ssm/**", + # SQS + "localstack/services/sqs/**", + "tests/aws/services/sqs/**", ] From 7917b4af501720cdff328b38298f6bd71245b307 Mon Sep 17 00:00:00 2001 From: Giovanni Grano <me@giograno.com> Date: Wed, 8 May 2024 13:00:19 -0400 Subject: [PATCH 134/169] Remove fix to relative persistence path for Kinesis (#10774) --- localstack/services/kinesis/kinesis_mock_server.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/localstack/services/kinesis/kinesis_mock_server.py b/localstack/services/kinesis/kinesis_mock_server.py index 8e1a80c65c18d..af23e3940ef24 100644 --- a/localstack/services/kinesis/kinesis_mock_server.py +++ b/localstack/services/kinesis/kinesis_mock_server.py @@ -86,9 +86,7 @@ def _create_shell_command(self) -> Tuple[List, Dict]: if self._data_dir and config.KINESIS_PERSISTENCE: env_vars["SHOULD_PERSIST_DATA"] = "true" - # FIXME use relative path to current working directory until - # https://github.com/etspaceman/kinesis-mock/issues/554 is resolved - env_vars["PERSIST_PATH"] = os.path.relpath(self._data_dir) + env_vars["PERSIST_PATH"] = self._data_dir env_vars["PERSIST_FILE_NAME"] = self._data_filename env_vars["PERSIST_INTERVAL"] = config.KINESIS_MOCK_PERSIST_INTERVAL From fa4ab5a4aae89da029e5edefd84dd93605bed7dd Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Mon, 13 May 2024 10:56:22 +0200 Subject: [PATCH 135/169] fix asf update action by updating ruff fix command (#10805) --- .github/workflows/asf-updates.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/asf-updates.yml b/.github/workflows/asf-updates.yml index fd72300378ca4..abc1622120710 100644 --- a/.github/workflows/asf-updates.yml +++ b/.github/workflows/asf-updates.yml @@ -48,7 +48,7 @@ jobs: run: | source .venv/bin/activate # explicitly perform an unsafe fix to remove unused imports in the generated ASF APIs - ruff check --select F401 --unsafe-fixes --fix . --config "lint.ignore-init-module-imports = false" + ruff check --select F401 --unsafe-fixes --fix . --config "lint.preview = true" make format-modified - name: Check for changes From cbec2f42c1230a7edc8a4d6652064cd09aad5e23 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Mon, 13 May 2024 12:31:14 +0200 Subject: [PATCH 136/169] Update ASF APIs, update SQS signatures (#10806) Co-authored-by: LocalStack Bot <localstack-bot@users.noreply.github.com> Co-authored-by: Alexander Rashed <alexander.rashed@localstack.cloud> --- localstack/aws/api/ec2/__init__.py | 15 ++++++++++++++- localstack/aws/api/sqs/__init__.py | 4 ++++ localstack/services/sqs/provider.py | 3 +++ pyproject.toml | 8 ++++---- requirements-base-runtime.txt | 4 ++-- requirements-dev.txt | 6 +++--- requirements-runtime.txt | 6 +++--- requirements-test.txt | 6 +++--- requirements-typehint.txt | 8 ++++---- 9 files changed, 40 insertions(+), 20 deletions(-) diff --git a/localstack/aws/api/ec2/__init__.py b/localstack/aws/api/ec2/__init__.py index d45500bd143ff..ef2191f301855 100644 --- a/localstack/aws/api/ec2/__init__.py +++ b/localstack/aws/api/ec2/__init__.py @@ -1,7 +1,14 @@ from datetime import datetime from typing import List, Optional, TypedDict -from localstack.aws.api import RequestContext, ServiceRequest, handler +from localstack.aws.api import ( + RequestContext, + ServiceRequest, + handler, +) +from localstack.aws.api import ( + ServiceException as ServiceException, +) AddressMaxResults = int AllocationId = str @@ -2552,6 +2559,11 @@ class PermissionGroup(str): all = "all" +class PhcSupport(str): + unsupported = "unsupported" + supported = "supported" + + class PlacementGroupState(str): pending = "pending" available = "available" @@ -11534,6 +11546,7 @@ class InstanceTypeInfo(TypedDict, total=False): NitroTpmInfo: Optional[NitroTpmInfo] MediaAcceleratorInfo: Optional[MediaAcceleratorInfo] NeuronInfo: Optional[NeuronInfo] + PhcSupport: Optional[PhcSupport] InstanceTypeInfoList = List[InstanceTypeInfo] diff --git a/localstack/aws/api/sqs/__init__.py b/localstack/aws/api/sqs/__init__.py index fb1ad5e814d22..2096e49fa948b 100644 --- a/localstack/aws/api/sqs/__init__.py +++ b/localstack/aws/api/sqs/__init__.py @@ -14,6 +14,7 @@ class MessageSystemAttributeName(str): + All = "All" SenderId = "SenderId" SentTimestamp = "SentTimestamp" ApproximateReceiveCount = "ApproximateReceiveCount" @@ -457,6 +458,7 @@ class MessageSystemAttributeValue(TypedDict, total=False): MessageSystemAttributeNameForSends, MessageSystemAttributeValue ] MessageList = List[Message] +MessageSystemAttributeList = List[MessageSystemAttributeName] class PurgeQueueRequest(ServiceRequest): @@ -466,6 +468,7 @@ class PurgeQueueRequest(ServiceRequest): class ReceiveMessageRequest(ServiceRequest): QueueUrl: String AttributeNames: Optional[AttributeNameList] + MessageSystemAttributeNames: Optional[MessageSystemAttributeList] MessageAttributeNames: Optional[MessageAttributeNameList] MaxNumberOfMessages: Optional[NullableInteger] VisibilityTimeout: Optional[NullableInteger] @@ -705,6 +708,7 @@ def receive_message( context: RequestContext, queue_url: String, attribute_names: AttributeNameList = None, + message_system_attribute_names: MessageSystemAttributeList = None, message_attribute_names: MessageAttributeNameList = None, max_number_of_messages: NullableInteger = None, visibility_timeout: NullableInteger = None, diff --git a/localstack/services/sqs/provider.py b/localstack/services/sqs/provider.py index c9521c9ee1541..881c8a2fa7291 100644 --- a/localstack/services/sqs/provider.py +++ b/localstack/services/sqs/provider.py @@ -45,6 +45,7 @@ MessageAttributeNameList, MessageBodyAttributeMap, MessageBodySystemAttributeMap, + MessageSystemAttributeList, MessageSystemAttributeName, NullableInteger, PurgeQueueInProgress, @@ -1183,6 +1184,7 @@ def receive_message( context: RequestContext, queue_url: String, attribute_names: AttributeNameList = None, + message_system_attribute_names: MessageSystemAttributeList = None, message_attribute_names: MessageAttributeNameList = None, max_number_of_messages: NullableInteger = None, visibility_timeout: NullableInteger = None, @@ -1190,6 +1192,7 @@ def receive_message( receive_request_attempt_id: String = None, **kwargs, ) -> ReceiveMessageResult: + # TODO add support for message_system_attribute_names queue = self._resolve_queue(context, queue_url=queue_url) if wait_time_seconds is None: diff --git a/pyproject.toml b/pyproject.toml index 66506fad12b9d..17f5d8cef1405 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,9 +48,9 @@ Issues = "https://github.com/localstack/localstack/issues" # minimal required to actually run localstack on the host for services natively implemented in python base-runtime = [ # pinned / updated by ASF update action - "boto3==1.34.98", + "boto3==1.34.103", # pinned / updated by ASF update action - "botocore==1.34.98", + "botocore==1.34.103", "awscrt>=0.13.14", "cbor2>=5.2.0", "dnspython>=1.16.0", @@ -76,7 +76,7 @@ base-runtime = [ runtime = [ "localstack-core[base-runtime]", # pinned / updated by ASF update action - "awscli==1.32.98", + "awscli==1.32.103", "airspeed-ext>=0.6.3", "amazon_kclpy>=2.0.6,!=2.1.0,!=2.1.4", # antlr4-python3-runtime: exact pin because antlr4 runtime is tightly coupled to the generated parser code @@ -133,7 +133,7 @@ typehint = [ # typehint is an optional extension of the dev dependencies "localstack-core[dev]", # pinned / updated by ASF update action - "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.98", + "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.103", ] [tool.setuptools] diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index c1326e2c65ba9..c1fd05e40c5be 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -14,9 +14,9 @@ blinker==1.8.2 # via # flask # quart -boto3==1.34.98 +boto3==1.34.103 # via localstack-core (pyproject.toml) -botocore==1.34.98 +botocore==1.34.103 # via # boto3 # localstack-core (pyproject.toml) diff --git a/requirements-dev.txt b/requirements-dev.txt index fa1ec8208ad07..3c4c96dbf6259 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.88.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.98 +awscli==1.32.103 # via localstack-core awscrt==0.20.9 # via localstack-core @@ -55,12 +55,12 @@ blinker==1.8.2 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.98 +boto3==1.34.103 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.98 +botocore==1.34.103 # via # aws-xray-sdk # awscli diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 1859af4951eae..791f4f0bdf59d 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -33,7 +33,7 @@ aws-sam-translator==1.88.0 # localstack-core (pyproject.toml) aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.98 +awscli==1.32.103 # via localstack-core (pyproject.toml) awscrt==0.20.9 # via localstack-core @@ -43,12 +43,12 @@ blinker==1.8.2 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.98 +boto3==1.34.103 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.98 +botocore==1.34.103 # via # aws-xray-sdk # awscli diff --git a/requirements-test.txt b/requirements-test.txt index 64250ce1a7494..24d406ae51286 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.88.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.98 +awscli==1.32.103 # via localstack-core awscrt==0.20.9 # via localstack-core @@ -55,12 +55,12 @@ blinker==1.8.2 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.98 +boto3==1.34.103 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.98 +botocore==1.34.103 # via # aws-xray-sdk # awscli diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 70a136ba336c1..8f30010393827 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.88.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.98 +awscli==1.32.103 # via localstack-core awscrt==0.20.9 # via localstack-core @@ -55,14 +55,14 @@ blinker==1.8.2 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.98 +boto3==1.34.103 # via # aws-sam-translator # localstack-core # moto-ext -boto3-stubs==1.34.98 +boto3-stubs==1.34.103 # via localstack-core (pyproject.toml) -botocore==1.34.98 +botocore==1.34.103 # via # aws-xray-sdk # awscli From cdb940eebf9537aac9e26b221a2ff3cb04f81d6b Mon Sep 17 00:00:00 2001 From: Max <max.hoheiser@gmail.com> Date: Mon, 13 May 2024 13:59:59 +0200 Subject: [PATCH 137/169] Feature: Eventbridge v2: Add input path (#10733) --- localstack/services/events/models.py | 19 +- localstack/services/events/provider.py | 5 +- localstack/services/events/target.py | 19 +- tests/aws/services/events/conftest.py | 54 +++-- .../aws/services/events/test_events_inputs.py | 192 +++++++++--------- .../events/test_events_inputs.snapshot.json | 126 ++++++++++++ .../events/test_events_inputs.validation.json | 18 ++ .../events/test_events_integrations.py | 3 +- 8 files changed, 316 insertions(+), 120 deletions(-) diff --git a/localstack/services/events/models.py b/localstack/services/events/models.py index 3d42c621c6b41..d64c74bbaa7fd 100644 --- a/localstack/services/events/models.py +++ b/localstack/services/events/models.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from typing import Optional +from typing import Optional, TypeAlias, TypedDict from localstack.aws.api.core import ServiceException from localstack.aws.api.events import ( @@ -7,6 +7,8 @@ CreatedBy, EventBusName, EventPattern, + EventResourceList, + EventSourceName, ManagedBy, RoleArn, RuleDescription, @@ -102,3 +104,18 @@ class InvalidEventPatternException(Exception): def __init__(self, reason=None, message=None) -> None: self.reason = reason self.message = message or f"Event pattern is not valid. Reason: {reason}" + + +class FormattedEvent(TypedDict): + version: str + id: str + detail_type: Optional[str] # key "detail-type" is automatically interpreted as detail_type + source: Optional[EventSourceName] + account: str + time: str + region: str + resources: Optional[EventResourceList] + detail: dict[str, str | dict] + + +TransformedEvent: TypeAlias = FormattedEvent | dict | str diff --git a/localstack/services/events/provider.py b/localstack/services/events/provider.py index 7ff7ea0c40a71..80d9c8e7bb8ec 100644 --- a/localstack/services/events/provider.py +++ b/localstack/services/events/provider.py @@ -59,6 +59,7 @@ EventBus, EventBusDict, EventsStore, + FormattedEvent, Rule, RuleDict, TargetDict, @@ -125,7 +126,7 @@ def validate_event(event: PutEventsRequestEntry) -> None | PutEventsResultEntry: } -def format_event(event: PutEventsRequestEntry, region: str, account_id: str) -> dict: +def format_event(event: PutEventsRequestEntry, region: str, account_id: str) -> FormattedEvent: # See https://docs.aws.amazon.com/AmazonS3/latest/userguide/ev-events.html formatted_event = { "version": "0", @@ -685,7 +686,7 @@ def _process_entries( for target in rule.targets.values(): target_sender = self._target_sender_store[target["Arn"]] try: - target_sender.send_event(event) + target_sender.process_event(event) processed_entries.append({"EventId": event["id"]}) except Exception as error: processed_entries.append( diff --git a/localstack/services/events/target.py b/localstack/services/events/target.py index 6a22f4e08d059..0937bd9680ac6 100644 --- a/localstack/services/events/target.py +++ b/localstack/services/events/target.py @@ -7,10 +7,11 @@ from localstack.aws.api.events import ( Arn, - PutEventsRequestEntry, Target, + TargetInputPath, ) from localstack.aws.connect import connect_to +from localstack.services.events.models import FormattedEvent, TransformedEvent from localstack.utils import collections from localstack.utils.aws.arns import ( extract_service_from_arn, @@ -18,12 +19,20 @@ sqs_queue_url_for_arn, ) from localstack.utils.aws.client_types import ServicePrincipal +from localstack.utils.json import extract_jsonpath from localstack.utils.strings import to_bytes from localstack.utils.time import now_utc LOG = logging.getLogger(__name__) +def transform_event_with_target_input_path( + input_path: TargetInputPath, event: FormattedEvent +) -> TransformedEvent: + formatted_event = extract_jsonpath(event, input_path) + return formatted_event + + class TargetSender(ABC): def __init__( self, @@ -54,9 +63,15 @@ def client(self): return self._client @abstractmethod - def send_event(self, event: PutEventsRequestEntry): + def send_event(self, event: FormattedEvent | TransformedEvent): pass + def process_event(self, event: FormattedEvent): + """Processes the event and send it to the target.""" + if input_path := self.target.get("InputPath"): + event = transform_event_with_target_input_path(input_path, event) + self.send_event(event) + def _validate_input(self, target: Target): """Provide a default implementation that does nothing if no specific validation is needed.""" # TODO add For Lambda and Amazon SNS resources, EventBridge relies on resource-based policies. diff --git a/tests/aws/services/events/conftest.py b/tests/aws/services/events/conftest.py index fbb6df5269434..ec0ea6bcba1db 100644 --- a/tests/aws/services/events/conftest.py +++ b/tests/aws/services/events/conftest.py @@ -194,23 +194,12 @@ def _delete_log_group(): @pytest.fixture -def put_events_with_filter_to_sqs(aws_client, sqs_get_queue_arn, clean_up): +def create_sqs_events_target(aws_client, sqs_get_queue_arn): queue_urls = [] - event_bus_names = [] - rule_names = [] - target_ids = [] - - def _put_events_with_filter_to_sqs( - pattern: dict, - entries_asserts: list[Tuple[list[dict], bool]], - input_path: str = None, - input_transformer: dict[dict, str] = None, - ): - queue_name = f"queue-{short_uid()}" - rule_name = f"rule-{short_uid()}" - target_id = f"target-{short_uid()}" - bus_name = f"bus-{short_uid()}" + def _create_sqs_events_target(queue_name: str | None = None) -> tuple[str, str]: + if not queue_name: + queue_name = f"tests-queue-{short_uid()}" sqs_client = aws_client.sqs queue_url = sqs_client.create_queue(QueueName=queue_name)["QueueUrl"] queue_urls.append(queue_url) @@ -231,6 +220,34 @@ def _put_events_with_filter_to_sqs( sqs_client.set_queue_attributes( QueueUrl=queue_url, Attributes={"Policy": json.dumps(policy)} ) + return queue_url, queue_arn + + yield _create_sqs_events_target + + for queue_url in queue_urls: + try: + aws_client.sqs.delete_queue(QueueUrl=queue_url) + except Exception as e: + LOG.debug("error cleaning up queue %s: %s", queue_url, e) + + +@pytest.fixture +def put_events_with_filter_to_sqs(aws_client, create_sqs_events_target, clean_up): + event_bus_names = [] + rule_names = [] + target_ids = [] + + def _put_events_with_filter_to_sqs( + pattern: dict, + entries_asserts: list[Tuple[list[dict], bool]], + input_path: str = None, + input_transformer: dict[dict, str] = None, + ): + rule_name = f"test-rule-{short_uid()}" + target_id = f"test-target-{short_uid()}" + bus_name = f"test-bus-{short_uid()}" + + queue_url, queue_arn = create_sqs_events_target() events_client = aws_client.events events_client.create_event_bus(Name=bus_name) @@ -262,7 +279,7 @@ def _put_events_with_filter_to_sqs( entry["EventBusName"] = bus_name message = _put_entries_assert_results_sqs( events_client, - sqs_client, + aws_client.sqs, queue_url, entries=entries, should_match=entry_asserts[1], @@ -274,14 +291,11 @@ def _put_events_with_filter_to_sqs( yield _put_events_with_filter_to_sqs - for queue_url, event_bus_name, rule_name, target_id in zip( - queue_urls, event_bus_names, rule_names, target_ids - ): + for event_bus_name, rule_name, target_id in zip(event_bus_names, rule_names, target_ids): clean_up( bus_name=event_bus_name, rule_name=rule_name, target_ids=target_id, - queue_url=queue_url, ) diff --git a/tests/aws/services/events/test_events_inputs.py b/tests/aws/services/events/test_events_inputs.py index 5ff7962ce7c8e..6619ec4c76b51 100644 --- a/tests/aws/services/events/test_events_inputs.py +++ b/tests/aws/services/events/test_events_inputs.py @@ -4,103 +4,127 @@ import pytest -from localstack.constants import TEST_AWS_ACCOUNT_ID, TEST_AWS_REGION_NAME from localstack.testing.pytest import markers -from localstack.utils.aws import arns from localstack.utils.strings import short_uid from tests.aws.services.events.conftest import sqs_collect_messages from tests.aws.services.events.helper_functions import is_v2_provider from tests.aws.services.events.test_events import EVENT_DETAIL, TEST_EVENT_PATTERN +EVENT_DETAIL_DUPLICATED_KEY = { + "command": "update-account", + "payload": {"acc_id": "0a787ecb-4015", "payload": {"message": "baz", "id": "123"}}, +} -class TestEventsInputPath: - @markers.aws.unknown - @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") - def test_put_events_with_input_path(self, aws_client, clean_up): - queue_name = f"queue-{short_uid()}" - rule_name = f"rule-{short_uid()}" - target_id = f"target-{short_uid()}" - bus_name = f"bus-{short_uid()}" - - queue_url = aws_client.sqs.create_queue(QueueName=queue_name)["QueueUrl"] - queue_arn = arns.sqs_queue_arn(queue_name, TEST_AWS_ACCOUNT_ID, TEST_AWS_REGION_NAME) - aws_client.events.create_event_bus(Name=bus_name) - aws_client.events.put_rule( - Name=rule_name, - EventBusName=bus_name, - EventPattern=json.dumps(TEST_EVENT_PATTERN), - ) - aws_client.events.put_targets( - Rule=rule_name, - EventBusName=bus_name, - Targets=[{"Id": target_id, "Arn": queue_arn, "InputPath": "$.detail"}], +class TestEventsInputPath: + @markers.aws.validated + def test_put_events_with_input_path(self, put_events_with_filter_to_sqs, snapshot): + entries1 = [ + { + "Source": TEST_EVENT_PATTERN["source"][0], + "DetailType": TEST_EVENT_PATTERN["detail-type"][0], + "Detail": json.dumps(EVENT_DETAIL), + } + ] + entries_asserts = [(entries1, True)] + messages = put_events_with_filter_to_sqs( + pattern=TEST_EVENT_PATTERN, + entries_asserts=entries_asserts, + input_path="$.detail", ) - aws_client.events.put_events( - Entries=[ - { - "EventBusName": bus_name, - "Source": TEST_EVENT_PATTERN["source"][0], - "DetailType": TEST_EVENT_PATTERN["detail-type"][0], - "Detail": json.dumps(EVENT_DETAIL), - } + snapshot.add_transformers_list( + [ + snapshot.transform.key_value("MD5OfBody"), + snapshot.transform.key_value("ReceiptHandle"), ] ) + snapshot.match("message", messages) - messages = sqs_collect_messages(aws_client, queue_url, min_events=1, retries=3) - assert json.loads(messages[0].get("Body")) == EVENT_DETAIL + @markers.aws.validated + @pytest.mark.parametrize("event_detail", [EVENT_DETAIL, EVENT_DETAIL_DUPLICATED_KEY]) + def test_put_events_with_input_path_nested( + self, event_detail, put_events_with_filter_to_sqs, snapshot + ): + entries1 = [ + { + "Source": TEST_EVENT_PATTERN["source"][0], + "DetailType": TEST_EVENT_PATTERN["detail-type"][0], + "Detail": json.dumps(event_detail), + } + ] + entries_asserts = [(entries1, True)] + messages = put_events_with_filter_to_sqs( + pattern=TEST_EVENT_PATTERN, + entries_asserts=entries_asserts, + input_path="$.detail.payload", + ) - aws_client.events.put_events( - Entries=[ - { - "EventBusName": bus_name, - "Source": "dummySource", - "DetailType": TEST_EVENT_PATTERN["detail-type"][0], - "Detail": json.dumps(EVENT_DETAIL), - } + snapshot.add_transformers_list( + [ + snapshot.transform.key_value("MD5OfBody"), + snapshot.transform.key_value("ReceiptHandle"), ] ) + snapshot.match("message", messages) - messages = sqs_collect_messages(aws_client, queue_url, min_events=0, retries=1, wait_time=3) - assert messages == [] - - # clean up - clean_up(bus_name=bus_name, rule_name=rule_name, target_ids=target_id, queue_url=queue_url) - - @markers.aws.unknown - @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") - def test_put_events_with_input_path_multiple(self, aws_client, clean_up): - queue_name = "queue-{}".format(short_uid()) - queue_name_1 = "queue-{}".format(short_uid()) - rule_name = "rule-{}".format(short_uid()) - target_id = "target-{}".format(short_uid()) - target_id_1 = "target-{}".format(short_uid()) - bus_name = "bus-{}".format(short_uid()) + @markers.aws.validated + def test_put_events_with_input_path_max_level_depth( + self, put_events_with_filter_to_sqs, snapshot + ): + entries1 = [ + { + "Source": TEST_EVENT_PATTERN["source"][0], + "DetailType": TEST_EVENT_PATTERN["detail-type"][0], + "Detail": json.dumps(EVENT_DETAIL), + } + ] + entries_asserts = [(entries1, True)] + messages = put_events_with_filter_to_sqs( + pattern=TEST_EVENT_PATTERN, + entries_asserts=entries_asserts, + input_path="$.detail.payload.sf_id", + ) - queue_url = aws_client.sqs.create_queue(QueueName=queue_name)["QueueUrl"] - queue_arn = arns.sqs_queue_arn(queue_name, TEST_AWS_ACCOUNT_ID, TEST_AWS_REGION_NAME) + snapshot.add_transformers_list( + [ + snapshot.transform.key_value("MD5OfBody"), + snapshot.transform.key_value("ReceiptHandle"), + ] + ) + snapshot.match("message", messages) - queue_url_1 = aws_client.sqs.create_queue(QueueName=queue_name_1)["QueueUrl"] - queue_arn_1 = arns.sqs_queue_arn(queue_name_1, TEST_AWS_ACCOUNT_ID, TEST_AWS_REGION_NAME) + @markers.aws.validated + def test_put_events_with_input_path_multiple_targets( + self, + aws_client, + create_sqs_events_target, + events_create_event_bus, + events_put_rule, + snapshot, + ): + # prepare target queues + queue_url_1, queue_arn_1 = create_sqs_events_target() + queue_url_2, queue_arn_2 = create_sqs_events_target() - aws_client.events.create_event_bus(Name=bus_name) + bus_name = f"test-bus-{short_uid()}" + events_create_event_bus(Name=bus_name) - aws_client.events.put_rule( + rule_name = f"test-rule-{short_uid()}" + events_put_rule( Name=rule_name, EventBusName=bus_name, EventPattern=json.dumps(TEST_EVENT_PATTERN), ) + target_id_1 = f"target-{short_uid()}" + target_id_2 = f"target-{short_uid()}" aws_client.events.put_targets( Rule=rule_name, EventBusName=bus_name, Targets=[ - {"Id": target_id, "Arn": queue_arn, "InputPath": "$.detail"}, - { - "Id": target_id_1, - "Arn": queue_arn_1, - }, + {"Id": target_id_1, "Arn": queue_arn_1, "InputPath": "$.detail"}, + {"Id": target_id_2, "Arn": queue_arn_2}, ], ) @@ -115,35 +139,17 @@ def test_put_events_with_input_path_multiple(self, aws_client, clean_up): ] ) - messages = sqs_collect_messages(aws_client, queue_url, min_events=1, retries=3) - assert len(messages) == 1 - assert json.loads(messages[0].get("Body")) == EVENT_DETAIL + messages_queue_1 = sqs_collect_messages(aws_client, queue_url_1, min_events=1, retries=3) + messages_queue_2 = sqs_collect_messages(aws_client, queue_url_2, min_events=1, retries=3) - messages = sqs_collect_messages(aws_client, queue_url_1, min_events=1, retries=3) - assert len(messages) == 1 - assert json.loads(messages[0].get("Body")).get("detail") == EVENT_DETAIL - - aws_client.events.put_events( - Entries=[ - { - "EventBusName": bus_name, - "Source": "dummySource", - "DetailType": TEST_EVENT_PATTERN["detail-type"][0], - "Detail": json.dumps(EVENT_DETAIL), - } + snapshot.add_transformers_list( + [ + snapshot.transform.key_value("MD5OfBody"), + snapshot.transform.key_value("ReceiptHandle"), ] ) - - messages = sqs_collect_messages(aws_client, queue_url, min_events=0, retries=1, wait_time=3) - assert messages == [] - - # clean up - clean_up( - bus_name=bus_name, - rule_name=rule_name, - target_ids=[target_id, target_id_1], - queue_url=queue_url, - ) + snapshot.match("message-queue-1", messages_queue_1) + snapshot.match("message-queue-2", messages_queue_2) class TestEventsInputTransformers: diff --git a/tests/aws/services/events/test_events_inputs.snapshot.json b/tests/aws/services/events/test_events_inputs.snapshot.json index 3c89c16dc138d..c282663105c32 100644 --- a/tests/aws/services/events/test_events_inputs.snapshot.json +++ b/tests/aws/services/events/test_events_inputs.snapshot.json @@ -19,5 +19,131 @@ } ] } + }, + "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path": { + "recorded-date": "08-05-2024, 13:54:10", + "recorded-content": { + "message": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": { + "command": "update-account", + "payload": { + "acc_id": "0a787ecb-4015", + "sf_id": "baz" + } + } + } + ] + } + }, + "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_nested": { + "recorded-date": "06-05-2024, 15:11:52", + "recorded-content": { + "message": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": { + "acc_id": "0a787ecb-4015", + "sf_id": "baz" + } + } + ] + } + }, + "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_max_level_depth": { + "recorded-date": "06-05-2024, 15:11:54", + "recorded-content": { + "message": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": "\"baz\"" + } + ] + } + }, + "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_multiple_targets": { + "recorded-date": "06-05-2024, 15:22:58", + "recorded-content": { + "message-queue-1": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": { + "command": "update-account", + "payload": { + "acc_id": "0a787ecb-4015", + "sf_id": "baz" + } + } + } + ], + "message-queue-2": [ + { + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>", + "MD5OfBody": "<m-d5-of-body:2>", + "Body": { + "version": "0", + "id": "<uuid:3>", + "detail-type": "core.update-account-command", + "source": "core.update-account-command", + "account": "111111111111", + "time": "date", + "region": "<region>", + "resources": [], + "detail": { + "command": "update-account", + "payload": { + "acc_id": "0a787ecb-4015", + "sf_id": "baz" + } + } + } + } + ] + } + }, + "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_nested[event_detail0]": { + "recorded-date": "08-05-2024, 13:54:42", + "recorded-content": { + "message": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": { + "acc_id": "0a787ecb-4015", + "sf_id": "baz" + } + } + ] + } + }, + "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_nested[event_detail1]": { + "recorded-date": "08-05-2024, 13:54:44", + "recorded-content": { + "message": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": { + "acc_id": "0a787ecb-4015", + "payload": { + "message": "baz", + "id": "123" + } + } + } + ] + } } } diff --git a/tests/aws/services/events/test_events_inputs.validation.json b/tests/aws/services/events/test_events_inputs.validation.json index 61a8dd3b9cb4f..6b53b82b29016 100644 --- a/tests/aws/services/events/test_events_inputs.validation.json +++ b/tests/aws/services/events/test_events_inputs.validation.json @@ -1,4 +1,22 @@ { + "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path": { + "last_validated_date": "2024-05-08T13:54:10+00:00" + }, + "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_max_level_depth": { + "last_validated_date": "2024-05-06T15:11:54+00:00" + }, + "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_multiple_targets": { + "last_validated_date": "2024-05-06T15:22:58+00:00" + }, + "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_nested": { + "last_validated_date": "2024-05-06T15:11:52+00:00" + }, + "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_nested[event_detail0]": { + "last_validated_date": "2024-05-08T13:54:42+00:00" + }, + "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_nested[event_detail1]": { + "last_validated_date": "2024-05-08T13:54:44+00:00" + }, "tests/aws/services/events/test_events_inputs.py::TestEventsInputTransformers::test_put_events_with_input_transformation_to_sqs": { "last_validated_date": "2024-03-26T15:48:35+00:00" } diff --git a/tests/aws/services/events/test_events_integrations.py b/tests/aws/services/events/test_events_integrations.py index 9c8659322d498..609cb09b2e218 100644 --- a/tests/aws/services/events/test_events_integrations.py +++ b/tests/aws/services/events/test_events_integrations.py @@ -634,9 +634,8 @@ def check_stream_status(): assert_valid_event(data) -@markers.aws.unknown +@markers.aws.needs_fixing # TODO: Reason add permission and correct policies @pytest.mark.parametrize("strategy", ["standard", "domain", "path"]) -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_trigger_event_on_ssm_change(monkeypatch, aws_client, clean_up, strategy): monkeypatch.setattr(config, "SQS_ENDPOINT_STRATEGY", strategy) From 094740e6ad9762eb89919009b713bae5ef666726 Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Mon, 13 May 2024 14:06:32 +0200 Subject: [PATCH 138/169] StepFunctions: Temporary Skip of Flaky Test Cases (#10807) --- .../aws/services/stepfunctions/v2/callback/test_callback.py | 2 ++ tests/aws/services/stepfunctions/v2/test_sfn_api.py | 5 ----- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/aws/services/stepfunctions/v2/callback/test_callback.py b/tests/aws/services/stepfunctions/v2/callback/test_callback.py index b071a78b7b074..feb440d99e081 100644 --- a/tests/aws/services/stepfunctions/v2/callback/test_callback.py +++ b/tests/aws/services/stepfunctions/v2/callback/test_callback.py @@ -499,6 +499,7 @@ def test_start_execution_sync_delegate_timeout( ) @markers.aws.validated + @pytest.mark.skip(reason="Skipped until flaky behaviour can be rectified.") def test_multiple_heartbeat_notifications( self, aws_client, @@ -546,6 +547,7 @@ def test_multiple_heartbeat_notifications( task_token_consumer_thread.join(timeout=300) @markers.aws.validated + @pytest.mark.skip(reason="Skipped until flaky behaviour can be rectified.") def test_multiple_executions_and_heartbeat_notifications( self, aws_client, diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api.py b/tests/aws/services/stepfunctions/v2/test_sfn_api.py index 3384ff235295c..fee59b76b4429 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api.py +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api.py @@ -13,7 +13,6 @@ from tests.aws.services.stepfunctions.templates.base.base_templates import BaseTemplate from tests.aws.services.stepfunctions.utils import ( await_execution_aborted, - await_execution_started, await_execution_success, await_execution_terminated, await_list_execution_status, @@ -508,10 +507,6 @@ def _check_stated_entered(events: HistoryEventList) -> bool: check_func=_check_stated_entered, ) - await_execution_started( - stepfunctions_client=aws_client.stepfunctions, execution_arn=execution_arn - ) - stop_res = aws_client.stepfunctions.stop_execution(executionArn=execution_arn) sfn_snapshot.match("stop_res", stop_res) From ecd113addafd9bcbeda3b38ba61e6645d38995c0 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Tue, 14 May 2024 08:13:02 +0200 Subject: [PATCH 139/169] Upgrade pinned Python dependencies (#10813) Co-authored-by: LocalStack Bot <localstack-bot@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- requirements-dev.txt | 16 ++++++++-------- requirements-runtime.txt | 8 ++++---- requirements-test.txt | 10 +++++----- requirements-typehint.txt | 30 +++++++++++++++--------------- 5 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b58e0898028a5..9b28e2a4ba735 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.4.3 + rev: v0.4.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/requirements-dev.txt b/requirements-dev.txt index 3c4c96dbf6259..3d81ca5948a5f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -37,7 +37,7 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.140.0 +aws-cdk-lib==2.141.0 # via localstack-core aws-sam-translator==1.88.0 # via @@ -92,7 +92,7 @@ cffi==1.16.0 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==0.87.1 +cfn-lint==0.87.2 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -214,7 +214,7 @@ jmespath==1.0.1 # via # boto3 # botocore -joserfc==0.9.0 +joserfc==0.10.0 # via moto-ext jpype1==1.5.0 # via localstack-core @@ -329,7 +329,7 @@ ply==3.11 # jsonpath-ng # jsonpath-rw # pandoc -pre-commit==3.7.0 +pre-commit==3.7.1 # via localstack-core (pyproject.toml) priority==1.3.0 # via @@ -359,7 +359,7 @@ pydantic-core==2.18.2 # via pydantic pygments==2.18.0 # via rich -pymongo==4.7.1 +pymongo==4.7.2 # via localstack-core pyopenssl==24.1.0 # via @@ -414,7 +414,7 @@ referencing==0.31.1 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2024.4.28 +regex==2024.5.10 # via cfn-lint requests==2.31.0 # via @@ -449,7 +449,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core (pyproject.toml) -ruff==0.4.3 +ruff==0.4.4 # via localstack-core (pyproject.toml) s3transfer==0.10.1 # via @@ -509,7 +509,7 @@ urllib3==2.2.1 # opensearch-py # requests # responses -virtualenv==20.26.1 +virtualenv==20.26.2 # via pre-commit websocket-client==1.8.0 # via diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 791f4f0bdf59d..4eeae9c925552 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -73,7 +73,7 @@ certifi==2024.2.2 # requests cffi==1.16.0 # via cryptography -cfn-lint==0.87.1 +cfn-lint==0.87.2 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -161,7 +161,7 @@ jmespath==1.0.1 # via # boto3 # botocore -joserfc==0.9.0 +joserfc==0.10.0 # via moto-ext jpype1==1.5.0 # via localstack-core (pyproject.toml) @@ -267,7 +267,7 @@ pydantic-core==2.18.2 # via pydantic pygments==2.18.0 # via rich -pymongo==4.7.1 +pymongo==4.7.2 # via localstack-core (pyproject.toml) pyopenssl==24.1.0 # via @@ -305,7 +305,7 @@ referencing==0.31.1 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2024.4.28 +regex==2024.5.10 # via cfn-lint requests==2.31.0 # via diff --git a/requirements-test.txt b/requirements-test.txt index 24d406ae51286..1c9fdcf639f2c 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -37,7 +37,7 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.140.0 +aws-cdk-lib==2.141.0 # via localstack-core (pyproject.toml) aws-sam-translator==1.88.0 # via @@ -90,7 +90,7 @@ certifi==2024.2.2 # requests cffi==1.16.0 # via cryptography -cfn-lint==0.87.1 +cfn-lint==0.87.2 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -198,7 +198,7 @@ jmespath==1.0.1 # via # boto3 # botocore -joserfc==0.9.0 +joserfc==0.10.0 # via moto-ext jpype1==1.5.0 # via localstack-core @@ -330,7 +330,7 @@ pydantic-core==2.18.2 # via pydantic pygments==2.18.0 # via rich -pymongo==4.7.1 +pymongo==4.7.2 # via localstack-core pyopenssl==24.1.0 # via @@ -382,7 +382,7 @@ referencing==0.31.1 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2024.4.28 +regex==2024.5.10 # via cfn-lint requests==2.31.0 # via diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 8f30010393827..8ffd9c7c26ea0 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -37,7 +37,7 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.140.0 +aws-cdk-lib==2.141.0 # via localstack-core aws-sam-translator==1.88.0 # via @@ -96,7 +96,7 @@ cffi==1.16.0 # via cryptography cfgv==3.4.0 # via pre-commit -cfn-lint==0.87.1 +cfn-lint==0.87.2 # via moto-ext charset-normalizer==3.3.2 # via requests @@ -218,7 +218,7 @@ jmespath==1.0.1 # via # boto3 # botocore -joserfc==0.9.0 +joserfc==0.10.0 # via moto-ext jpype1==1.5.0 # via localstack-core @@ -327,7 +327,7 @@ mypy-boto3-codecommit==1.34.6 # via boto3-stubs mypy-boto3-cognito-identity==1.34.0 # via boto3-stubs -mypy-boto3-cognito-idp==1.34.93 +mypy-boto3-cognito-idp==1.34.101 # via boto3-stubs mypy-boto3-dms==1.34.0 # via boto3-stubs @@ -337,9 +337,9 @@ mypy-boto3-dynamodb==1.34.97 # via boto3-stubs mypy-boto3-dynamodbstreams==1.34.0 # via boto3-stubs -mypy-boto3-ec2==1.34.97 +mypy-boto3-ec2==1.34.101 # via boto3-stubs -mypy-boto3-ecr==1.34.0 +mypy-boto3-ecr==1.34.101 # via boto3-stubs mypy-boto3-ecs==1.34.76 # via boto3-stubs @@ -359,7 +359,7 @@ mypy-boto3-emr-serverless==1.34.87 # via boto3-stubs mypy-boto3-es==1.34.36 # via boto3-stubs -mypy-boto3-events==1.34.17 +mypy-boto3-events==1.34.104 # via boto3-stubs mypy-boto3-firehose==1.34.69 # via boto3-stubs @@ -435,13 +435,13 @@ mypy-boto3-resourcegroupstaggingapi==1.34.0 # via boto3-stubs mypy-boto3-route53==1.34.31 # via boto3-stubs -mypy-boto3-route53resolver==1.34.95 +mypy-boto3-route53resolver==1.34.102 # via boto3-stubs mypy-boto3-s3==1.34.91 # via boto3-stubs mypy-boto3-s3control==1.34.83 # via boto3-stubs -mypy-boto3-sagemaker==1.34.98 +mypy-boto3-sagemaker==1.34.103 # via boto3-stubs mypy-boto3-sagemaker-runtime==1.34.0 # via boto3-stubs @@ -457,7 +457,7 @@ mypy-boto3-sesv2==1.34.98 # via boto3-stubs mypy-boto3-sns==1.34.44 # via boto3-stubs -mypy-boto3-sqs==1.34.0 +mypy-boto3-sqs==1.34.101 # via boto3-stubs mypy-boto3-ssm==1.34.91 # via boto3-stubs @@ -525,7 +525,7 @@ ply==3.11 # jsonpath-ng # jsonpath-rw # pandoc -pre-commit==3.7.0 +pre-commit==3.7.1 # via localstack-core priority==1.3.0 # via @@ -555,7 +555,7 @@ pydantic-core==2.18.2 # via pydantic pygments==2.18.0 # via rich -pymongo==4.7.1 +pymongo==4.7.2 # via localstack-core pyopenssl==24.1.0 # via @@ -610,7 +610,7 @@ referencing==0.31.1 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2024.4.28 +regex==2024.5.10 # via cfn-lint requests==2.31.0 # via @@ -645,7 +645,7 @@ rsa==4.7.2 # via awscli rstr==3.2.2 # via localstack-core -ruff==0.4.3 +ruff==0.4.4 # via localstack-core s3transfer==0.10.1 # via @@ -806,7 +806,7 @@ urllib3==2.2.1 # opensearch-py # requests # responses -virtualenv==20.26.1 +virtualenv==20.26.2 # via pre-commit websocket-client==1.8.0 # via From ecd7dc8797a19ccad35b03c703e72ea28c6ed26b Mon Sep 17 00:00:00 2001 From: Viren Nadkarni <viren.nadkarni@localstack.cloud> Date: Tue, 14 May 2024 14:16:48 +0530 Subject: [PATCH 140/169] Bump moto-ext to 5.0.6.post2 (#10742) Co-authored-by: Benjamin Simon <benjh.simon@gmail.com> --- localstack/services/apigateway/patches.py | 66 ++++------------------ localstack/services/apigateway/provider.py | 57 +++++++++++++++++++ pyproject.toml | 2 +- requirements-dev.txt | 2 +- requirements-runtime.txt | 2 +- requirements-test.txt | 2 +- requirements-typehint.txt | 2 +- 7 files changed, 74 insertions(+), 59 deletions(-) diff --git a/localstack/services/apigateway/patches.py b/localstack/services/apigateway/patches.py index b533b38ce57ec..315050b21a28d 100644 --- a/localstack/services/apigateway/patches.py +++ b/localstack/services/apigateway/patches.py @@ -37,50 +37,25 @@ def apigateway_models_Stage_init( apigateway_models_Stage_init_orig = apigateway_models.Stage.__init__ apigateway_models.Stage.__init__ = apigateway_models_Stage_init - @patch(APIGatewayResponse.integrations) - def apigateway_response_integrations(fn, self, request, *args, **kwargs): - result = fn(self, request, *args, **kwargs) - - if self.method not in ["PUT", "PATCH"]: - return result + @patch(APIGatewayResponse.put_integration) + def apigateway_put_integration(fn, self, *args, **kwargs): + # TODO: verify if this patch is still necessary, this might have been fixed upstream + fn(self, *args, **kwargs) url_path_parts = self.path.split("/") function_id = url_path_parts[2] resource_id = url_path_parts[4] method_type = url_path_parts[6] - integration = self.backend.get_integration(function_id, resource_id, method_type) - if not integration: - return result - - if self.method == "PUT": - timeout_milliseconds = self._get_param("timeoutInMillis") - cache_key_parameters = self._get_param("cacheKeyParameters") or [] - content_handling = self._get_param("contentHandling") - integration.cache_namespace = resource_id - integration.timeout_in_millis = timeout_milliseconds - integration.cache_key_parameters = cache_key_parameters - integration.content_handling = content_handling - return 201, {}, json.dumps(integration.to_json()) - - if self.method == "PATCH": - patch_operations = self._get_param("patchOperations") - apply_json_patch_safe(integration.to_json(), patch_operations, in_place=True) - # fix data types - if integration.timeout_in_millis: - integration.timeout_in_millis = int(integration.timeout_in_millis) - if skip_verification := (integration.tls_config or {}).get("insecureSkipVerification"): - integration.tls_config["insecureSkipVerification"] = str_to_bool(skip_verification) - return 200, {}, json.dumps(integration.to_json()) - - return result - def backend_update_deployment(self, function_id, deployment_id, patch_operations): - rest_api = self.get_rest_api(function_id) - deployment = rest_api.get_deployment(deployment_id) - deployment = deployment.to_json() or {} - apply_json_patch_safe(deployment, patch_operations, in_place=True) - return deployment + timeout_milliseconds = self._get_param("timeoutInMillis") + cache_key_parameters = self._get_param("cacheKeyParameters") or [] + content_handling = self._get_param("contentHandling") + integration.cache_namespace = resource_id + integration.timeout_in_millis = timeout_milliseconds + integration.cache_key_parameters = cache_key_parameters + integration.content_handling = content_handling + return 201, {}, json.dumps(integration.to_json()) # define json-patch operations for backend models @@ -140,9 +115,6 @@ def apigateway_models_resource_get_integration(fn, self, method_type): raise NoIntegrationDefined() return resource_method.method_integration - if not hasattr(apigateway_models.APIGatewayBackend, "update_deployment"): - apigateway_models.APIGatewayBackend.update_deployment = backend_update_deployment - @patch(apigateway_models.RestAPI.to_dict) def apigateway_models_rest_api_to_dict(fn, self): resp = fn(self) @@ -172,20 +144,6 @@ def apigateway_models_stage_to_json(fn, self): return result - @patch(APIGatewayResponse.individual_deployment) - def individual_deployment(fn, self, request, full_url, headers, *args, **kwargs): - result = fn(self, request, full_url, headers, *args, **kwargs) - if self.method == "PATCH": - url_path_parts = self.path.split("/") - function_id = url_path_parts[2] - deployment_id = url_path_parts[4] - patch_operations = self._get_param("patchOperations") - deployment = self.backend.update_deployment( - function_id, deployment_id, patch_operations - ) - return 201, {}, json.dumps(deployment) - return result - @patch(apigateway_models.APIGatewayBackend.create_rest_api) def create_rest_api(fn, self, *args, tags=None, **kwargs): """ diff --git a/localstack/services/apigateway/provider.py b/localstack/services/apigateway/provider.py index 565e1d1d31dba..6a5a840eedd59 100644 --- a/localstack/services/apigateway/provider.py +++ b/localstack/services/apigateway/provider.py @@ -32,6 +32,7 @@ CreateAuthorizerRequest, CreateRestApiRequest, CreateStageRequest, + Deployment, DocumentationPart, DocumentationPartIds, DocumentationPartLocation, @@ -1064,6 +1065,32 @@ def _patch_stage_response(self, response: dict): if not response.get("variables"): response.pop("variables", None) + def update_deployment( + self, + context: RequestContext, + rest_api_id: String, + deployment_id: String, + patch_operations: ListOfPatchOperation = None, + **kwargs, + ) -> Deployment: + moto_rest_api = get_moto_rest_api(context, rest_api_id) + try: + deployment = moto_rest_api.get_deployment(deployment_id) + except KeyError: + raise NotFoundException("Invalid Deployment identifier specified") + + for patch_operation in patch_operations: + # TODO: add validation for unsupported paths + # see https://docs.aws.amazon.com/apigateway/latest/api/patch-operations.html#UpdateDeployment-Patch + if ( + patch_operation.get("path") == "/description" + and patch_operation.get("op") == "replace" + ): + deployment.description = patch_operation["value"] + + deployment_response: Deployment = deployment.to_json() or {} + return deployment_response + # authorizers @handler("CreateAuthorizer", expand=False) @@ -1896,6 +1923,36 @@ def put_integration( return response + def update_integration( + self, + context: RequestContext, + rest_api_id: String, + resource_id: String, + http_method: String, + patch_operations: ListOfPatchOperation = None, + **kwargs, + ) -> Integration: + moto_rest_api = get_moto_rest_api(context=context, rest_api_id=rest_api_id) + resource = moto_rest_api.resources.get(resource_id) + if not resource: + raise NotFoundException("Invalid Resource identifier specified") + + method = resource.resource_methods.get(http_method) + if not method: + raise NotFoundException("Invalid Integration identifier specified") + + integration = method.method_integration + _patch_api_gateway_entity(integration, patch_operations) + + # fix data types + if integration.timeout_in_millis: + integration.timeout_in_millis = int(integration.timeout_in_millis) + if skip_verification := (integration.tls_config or {}).get("insecureSkipVerification"): + integration.tls_config["insecureSkipVerification"] = str_to_bool(skip_verification) + + integration_dict: Integration = integration.to_json() + return integration_dict + def delete_integration( self, context: RequestContext, diff --git a/pyproject.toml b/pyproject.toml index 17f5d8cef1405..2b9af5cc8a599 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,7 +90,7 @@ runtime = [ "json5>=0.9.11", "jsonpath-ng>=1.6.1", "jsonpath-rw>=1.4.0", - "moto-ext[all]==5.0.5.post1", + "moto-ext[all]==5.0.6.post2", "opensearch-py>=2.4.1", "pymongo>=4.2.0", "pyopenssl>=23.0.0", diff --git a/requirements-dev.txt b/requirements-dev.txt index 3d81ca5948a5f..ed612615091d8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -275,7 +275,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext==5.0.5.post1 +moto-ext==5.0.6.post2 # via localstack-core mpmath==1.3.0 # via sympy diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 4eeae9c925552..3657526f80515 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -212,7 +212,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext==5.0.5.post1 +moto-ext==5.0.6.post2 # via localstack-core (pyproject.toml) mpmath==1.3.0 # via sympy diff --git a/requirements-test.txt b/requirements-test.txt index 1c9fdcf639f2c..3ac1f7c2bf030 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -259,7 +259,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext==5.0.5.post1 +moto-ext==5.0.6.post2 # via localstack-core mpmath==1.3.0 # via sympy diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 8ffd9c7c26ea0..4aaf9a535379b 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -279,7 +279,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext==5.0.5.post1 +moto-ext==5.0.6.post2 # via localstack-core mpmath==1.3.0 # via sympy From e69d60f3bb551b7bc0af081e797b1df94a528533 Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Tue, 14 May 2024 14:52:10 +0200 Subject: [PATCH 141/169] fix GitHub error reporting by disabling colors (#10815) --- .github/workflows/tests-cli.yml | 2 -- .github/workflows/tests-podman.yml | 2 -- .github/workflows/tests-pro-integration.yml | 2 -- .github/workflows/tests-s3-image.yml | 2 -- Makefile | 2 +- 5 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/tests-cli.yml b/.github/workflows/tests-cli.yml index 1fbf8f274650c..42f9656e54997 100644 --- a/.github/workflows/tests-cli.yml +++ b/.github/workflows/tests-cli.yml @@ -58,8 +58,6 @@ concurrency: cancel-in-progress: true env: - # Enable colors in tests running outside of docker (not in a tty - https://github.com/pytest-dev/pytest/pull/7462) - PY_COLORS: "1" # Configure PyTest log level PYTEST_LOGLEVEL: "${{ inputs.PYTEST_LOGLEVEL || 'WARNING' }}" # Set non-job-specific environment variables for pytest-tinybird diff --git a/.github/workflows/tests-podman.yml b/.github/workflows/tests-podman.yml index d5165502e85e4..edd8aeb2926d7 100644 --- a/.github/workflows/tests-podman.yml +++ b/.github/workflows/tests-podman.yml @@ -15,8 +15,6 @@ on: default: WARNING env: - # Enable colors in tests running outside of docker (not in a tty - https://github.com/pytest-dev/pytest/pull/7462) - PY_COLORS: "1" # Configure PyTest log level PYTEST_LOGLEVEL: "${{ inputs.PYTEST_LOGLEVEL || 'WARNING' }}" # Set non-job-specific environment variables for pytest-tinybird diff --git a/.github/workflows/tests-pro-integration.yml b/.github/workflows/tests-pro-integration.yml index 9e526a7a477f1..a62c54286bef5 100644 --- a/.github/workflows/tests-pro-integration.yml +++ b/.github/workflows/tests-pro-integration.yml @@ -92,8 +92,6 @@ concurrency: cancel-in-progress: true env: - # Enable colors in tests running outside of docker (not in a tty - https://github.com/pytest-dev/pytest/pull/7462) - PY_COLORS: "1" # Configure PyTest log level PYTEST_LOGLEVEL: "${{ inputs.PYTEST_LOGLEVEL || 'WARNING' }}" # Set non-job-specific environment variables for pytest-tinybird diff --git a/.github/workflows/tests-s3-image.yml b/.github/workflows/tests-s3-image.yml index 65b44a47a0b22..3c2754f64d3af 100644 --- a/.github/workflows/tests-s3-image.yml +++ b/.github/workflows/tests-s3-image.yml @@ -62,8 +62,6 @@ concurrency: cancel-in-progress: true env: - # Enable colors in tests running outside of docker (not in a tty - https://github.com/pytest-dev/pytest/pull/7462) - PY_COLORS: "1" # Configure PyTest log level PYTEST_LOGLEVEL: "${{ inputs.PYTEST_LOGLEVEL || 'WARNING' }}" # Set non-job-specific environment variables for pytest-tinybird diff --git a/Makefile b/Makefile index a3c90aa492113..4e1df728147db 100644 --- a/Makefile +++ b/Makefile @@ -195,7 +195,7 @@ docker-run-tests-s3-only: ## Initializes the test environment and runs the te # g++ is a workaround to fix the JPype1 compile error on ARM Linux "gcc: fatal error: cannot execute ‘cc1plus’" because the test dependencies include the runtime dependencies. docker run -e LOCALSTACK_INTERNAL_TEST_COLLECT_METRIC=1 --entrypoint= -v `pwd`/requirements-test.txt:/opt/code/localstack/requirements-test.txt -v `pwd`/tests/:/opt/code/localstack/tests/ -v `pwd`/target/:/opt/code/localstack/target/ -v /var/run/docker.sock:/var/run/docker.sock -v /tmp/localstack:/var/lib/localstack \ $(IMAGE_NAME) \ - bash -c "apt-get update && apt-get install -y g++ && make install-test && apt-get install -y --no-install-recommends gnupg && mkdir -p /etc/apt/keyrings && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main\" > /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install -y --no-install-recommends nodejs && DEBUG=$(DEBUG) PY_COLORS=1 PYTEST_LOGLEVEL=$(PYTEST_LOGLEVEL) PYTEST_ARGS='$(PYTEST_ARGS)' TEST_PATH='$(TEST_PATH)' TINYBIRD_PYTEST_ARGS='$(TINYBIRD_PYTEST_ARGS)' TINYBIRD_DATASOURCE='$(TINYBIRD_DATASOURCE)' TINYBIRD_TOKEN='$(TINYBIRD_TOKEN)' TINYBIRD_URL='$(TINYBIRD_URL)' CI_COMMIT_BRANCH='$(CI_COMMIT_BRANCH)' CI_COMMIT_SHA='$(CI_COMMIT_SHA)' CI_JOB_URL='$(CI_JOB_URL)' CI_JOB_NAME='$(CI_JOB_NAME)' CI_JOB_ID='$(CI_JOB_ID)' CI='$(CI)' make test" + bash -c "apt-get update && apt-get install -y g++ && make install-test && apt-get install -y --no-install-recommends gnupg && mkdir -p /etc/apt/keyrings && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main\" > /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install -y --no-install-recommends nodejs && DEBUG=$(DEBUG) PYTEST_LOGLEVEL=$(PYTEST_LOGLEVEL) PYTEST_ARGS='$(PYTEST_ARGS)' TEST_PATH='$(TEST_PATH)' TINYBIRD_PYTEST_ARGS='$(TINYBIRD_PYTEST_ARGS)' TINYBIRD_DATASOURCE='$(TINYBIRD_DATASOURCE)' TINYBIRD_TOKEN='$(TINYBIRD_TOKEN)' TINYBIRD_URL='$(TINYBIRD_URL)' CI_COMMIT_BRANCH='$(CI_COMMIT_BRANCH)' CI_COMMIT_SHA='$(CI_COMMIT_SHA)' CI_JOB_URL='$(CI_JOB_URL)' CI_JOB_NAME='$(CI_JOB_NAME)' CI_JOB_ID='$(CI_JOB_ID)' CI='$(CI)' make test" docker-run: ## Run Docker image locally From 3c4c46383d8a8d85dffd29f05da978605d468133 Mon Sep 17 00:00:00 2001 From: Max <max.hoheiser@gmail.com> Date: Tue, 14 May 2024 19:59:00 +0200 Subject: [PATCH 142/169] Feature/eventbridge v2 add input transformer (#10789) --- localstack/services/events/provider.py | 7 +- localstack/services/events/target.py | 119 +++++++- .../aws/services/events/test_events_inputs.py | 267 +++++++++++++++++- .../events/test_events_inputs.snapshot.json | 197 ++++++++++--- .../events/test_events_inputs.validation.json | 46 ++- 5 files changed, 558 insertions(+), 78 deletions(-) diff --git a/localstack/services/events/provider.py b/localstack/services/events/provider.py index 80d9c8e7bb8ec..b2f71bbdb6646 100644 --- a/localstack/services/events/provider.py +++ b/localstack/services/events/provider.py @@ -423,8 +423,9 @@ def put_targets( rule_service = self.get_rule_service(context, rule, event_bus_name) failed_entries = rule_service.add_targets(targets) rule_arn = rule_service.arn + rule_name = rule_service.rule.name for target in targets: # TODO only add successful targets - self.create_target_sender(target, region, account_id, rule_arn) + self.create_target_sender(target, region, account_id, rule_arn, rule_name) response = PutTargetsResponse( FailedEntryCount=len(failed_entries), FailedEntries=failed_entries @@ -568,10 +569,10 @@ def create_rule_service( return rule_service def create_target_sender( - self, target: Target, region: str, account_id: str, rule_arn: Arn + self, target: Target, region: str, account_id: str, rule_arn: Arn, rule_name: RuleName ) -> TargetSender: target_sender = TargetSenderFactory( - target, region, account_id, rule_arn + target, region, account_id, rule_arn, rule_name ).get_target_sender() self._target_sender_store[target_sender.arn] = target_sender return target_sender diff --git a/localstack/services/events/target.py b/localstack/services/events/target.py index 0937bd9680ac6..ac02dfcdd405a 100644 --- a/localstack/services/events/target.py +++ b/localstack/services/events/target.py @@ -1,17 +1,15 @@ import json import logging +import re import uuid from abc import ABC, abstractmethod +from typing import Any, Set from botocore.client import BaseClient -from localstack.aws.api.events import ( - Arn, - Target, - TargetInputPath, -) +from localstack.aws.api.events import Arn, InputTransformer, RuleName, Target, TargetInputPath from localstack.aws.connect import connect_to -from localstack.services.events.models import FormattedEvent, TransformedEvent +from localstack.services.events.models import FormattedEvent, TransformedEvent, ValidationException from localstack.utils import collections from localstack.utils.aws.arns import ( extract_service_from_arn, @@ -25,6 +23,20 @@ LOG = logging.getLogger(__name__) +# https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-input.html#eb-transform-input-predefined +AWS_PREDEFINED_PLACEHOLDERS_STRING_VALUES = { + "aws.events.rule-arn", + "aws.events.rule-name", + "aws.events.event.ingestion-time", +} +AWS_PREDEFINED_PLACEHOLDERS_JSON_VALUES = {"aws.events.event", "aws.events.event.json"} + +PREDEFINED_PLACEHOLDERS: Set[str] = AWS_PREDEFINED_PLACEHOLDERS_STRING_VALUES.union( + AWS_PREDEFINED_PLACEHOLDERS_JSON_VALUES +) + +TRANSFORMER_PLACEHOLDER_PATTERN = re.compile(r"<(.*?)>") + def transform_event_with_target_input_path( input_path: TargetInputPath, event: FormattedEvent @@ -33,6 +45,38 @@ def transform_event_with_target_input_path( return formatted_event +def get_template_replacements( + input_transformer: InputTransformer, event: FormattedEvent +) -> dict[str, Any]: + """Extracts values from the event using the input paths map keys and places them in the input template dict.""" + template_replacements = {} + transformer_path_map = input_transformer.get("InputPathsMap", {}) + for placeholder, transformer_path in transformer_path_map.items(): + if placeholder in PREDEFINED_PLACEHOLDERS: + continue + value = extract_jsonpath(event, transformer_path) + if not value: + value = "" # default value is empty string + template_replacements[placeholder] = value + return template_replacements + + +def replace_template_placeholders( + template: str, replacements: dict[str, Any], is_json: bool +) -> TransformedEvent: + """Replace placeholders defined by <key> in the template with the values from the replacements dict. + Can handle single template string or template dict.""" + + def replace_placeholder(match): + key = match.group(1) + value = replacements.get(key, match.group(0)) # handle non defined placeholders + return json.dumps(value) if is_json else value + + formatted_template = TRANSFORMER_PLACEHOLDER_PATTERN.sub(replace_placeholder, template) + + return json.loads(formatted_template) if is_json else formatted_template[1:-1] + + class TargetSender(ABC): def __init__( self, @@ -40,12 +84,14 @@ def __init__( region: str, account_id: str, rule_arn: Arn, + rule_name: RuleName, service: str, ): self.target = target self.region = region self.account_id = account_id self.rule_arn = rule_arn + self.rule_name = rule_name self.service = service self._validate_input(target) @@ -70,12 +116,34 @@ def process_event(self, event: FormattedEvent): """Processes the event and send it to the target.""" if input_path := self.target.get("InputPath"): event = transform_event_with_target_input_path(input_path, event) + if input_transformer := self.target.get("InputTransformer"): + event = self.transform_event_with_target_input_transformer(input_transformer, event) self.send_event(event) + def transform_event_with_target_input_transformer( + self, input_transformer: InputTransformer, event: FormattedEvent + ) -> TransformedEvent: + input_template = input_transformer["InputTemplate"] + template_replacements = get_template_replacements(input_transformer, event) + predefined_template_replacements = self._get_predefined_template_replacements(event) + template_replacements.update(predefined_template_replacements) + + is_json_format = input_template.strip().startswith(("{")) + populated_template = replace_template_placeholders( + input_template, template_replacements, is_json_format + ) + + return populated_template + def _validate_input(self, target: Target): - """Provide a default implementation that does nothing if no specific validation is needed.""" + """Provide a default implementation extended for each target based on specifications.""" # TODO add For Lambda and Amazon SNS resources, EventBridge relies on resource-based policies. - pass + if "InputPath" in target and "InputTransformer" in target: + raise ValidationException( + f"Only one of Input, InputPath, or InputTransformer must be provided for target {target.get('Id')}." + ) + if input_transformer := target.get("InputTransformer"): + self._validate_input_transformer(input_transformer) def _initialize_client(self) -> BaseClient: """Initializes internal botocore client. @@ -96,6 +164,33 @@ def _initialize_client(self) -> BaseClient: ) return client + def _validate_input_transformer(self, input_transformer: InputTransformer): + if "InputTemplate" not in input_transformer: + raise ValueError("InputTemplate is required for InputTransformer") + input_template = input_transformer["InputTemplate"] + input_paths_map = input_transformer.get("InputPathsMap", {}) + placeholders = TRANSFORMER_PLACEHOLDER_PATTERN.findall(input_template) + for placeholder in placeholders: + if placeholder not in input_paths_map and placeholder not in PREDEFINED_PLACEHOLDERS: + raise ValidationException( + f"InputTemplate for target {self.target.get('Id')} contains invalid placeholder {placeholder}." + ) + + def _get_predefined_template_replacements(self, event: FormattedEvent) -> dict[str, Any]: + """Extracts predefined values from the event.""" + predefined_template_replacements = {} + predefined_template_replacements["aws.events.rule-arn"] = self.rule_arn + predefined_template_replacements["aws.events.rule-name"] = self.rule_name + predefined_template_replacements["aws.events.event.ingestion-time"] = event["time"] + predefined_template_replacements["aws.events.event"] = { + "detailType" if k == "detail-type" else k: v # detail-type is is returned as detailType + for k, v in event.items() + if k != "detail" # detail is not part of .event placeholder + } + predefined_template_replacements["aws.events.event.json"] = event + + return predefined_template_replacements + TargetSenderDict = dict[Arn, TargetSender] @@ -192,7 +287,6 @@ def send_event(self, event): def _validate_input(self, target: Target): super()._validate_input(target) - # TODO add validated test to check if RoleArn is mandatory if not collections.get_safe(target, "$.RoleArn"): raise ValueError("RoleArn is required for Kinesis target") if not collections.get_safe(target, "$.KinesisParameters.PartitionKeyPath"): @@ -301,11 +395,14 @@ class TargetSenderFactory: # TODO custom endpoints via http target } - def __init__(self, target: Target, region: str, account_id: str, rule_arn: Arn): + def __init__( + self, target: Target, region: str, account_id: str, rule_arn: Arn, rule_name: RuleName + ): self.target = target self.region = region self.account_id = account_id self.rule_arn = rule_arn + self.rule_name = rule_name def get_target_sender(self) -> TargetSender: service = extract_service_from_arn(self.target["Arn"]) @@ -314,6 +411,6 @@ def get_target_sender(self) -> TargetSender: else: raise Exception(f"Unsupported target for Service: {service}") target_sender = target_sender_class( - self.target, self.region, self.account_id, self.rule_arn, service + self.target, self.region, self.account_id, self.rule_arn, self.rule_name, service ) return target_sender diff --git a/tests/aws/services/events/test_events_inputs.py b/tests/aws/services/events/test_events_inputs.py index 6619ec4c76b51..9888073053dce 100644 --- a/tests/aws/services/events/test_events_inputs.py +++ b/tests/aws/services/events/test_events_inputs.py @@ -3,6 +3,7 @@ import json import pytest +from botocore.client import Config from localstack.testing.pytest import markers from localstack.utils.strings import short_uid @@ -16,7 +17,59 @@ } -class TestEventsInputPath: +INPUT_TEMPLATE_PREDEFINE_VARIABLES_STR = '"Message containing all pre defined variables <aws.events.rule-arn> <aws.events.rule-name> <aws.events.event.ingestion-time>"' +INPUT_TEMPLATE_PREDEFINED_VARIABLES_JSON = '{"originalEvent": <aws.events.event>, "originalEventJson": <aws.events.event.json>}' # important to not quote the predefined variables + + +@markers.aws.validated +@pytest.mark.skipif( + not is_v2_provider(), + reason="V1 provider does not support this feature", +) +def test_put_event_input_path_and_input_transfomer( + create_sqs_events_target, events_create_event_bus, events_put_rule, aws_client, snapshot +): + _, queue_arn = create_sqs_events_target() + bus_name = f"test-bus-{short_uid()}" + events_create_event_bus(Name=bus_name) + + rule_name = f"test-rule-{short_uid()}" + events_put_rule( + Name=rule_name, + EventBusName=bus_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + ) + + target_id = f"target-{short_uid()}" + input_path_map = { + "detail-type": "$.detail-type", + "timestamp": "$.time", + "command": "$.detail.command", + } + input_template = '{"detailType": <detail-type>, "time": <timestamp>, "command": <command>}' + input_transformer = { + "InputPathsMap": input_path_map, + "InputTemplate": input_template, + } + with pytest.raises(Exception) as exception: + aws_client.events.put_targets( + Rule=rule_name, + EventBusName=bus_name, + Targets=[ + { + "Id": target_id, + "Arn": queue_arn, + "InputTransformer": input_transformer, + "InputPath": "$.detail", + }, + ], + ) + + snapshot.add_transformer(snapshot.transform.regex(target_id, "<target-id>")) + snapshot.match("missing-key-exception", exception) + + +class TestInputPath: @markers.aws.validated def test_put_events_with_input_path(self, put_events_with_filter_to_sqs, snapshot): entries1 = [ @@ -152,19 +205,82 @@ def test_put_events_with_input_path_multiple_targets( snapshot.match("message-queue-2", messages_queue_2) -class TestEventsInputTransformers: +class TestInputTransformer: + @markers.aws.validated + @pytest.mark.parametrize( + "input_template", + [ + '"Event of <detail-type> type, at time <timestamp>, info extracted from detail <command>"', + '"{[/Check with special starting characters for event of <detail-type> type"', + ], + ) + def test_put_events_with_input_transformer_input_template_string( + self, input_template, put_events_with_filter_to_sqs, snapshot + ): + entries = [ + { + "Source": TEST_EVENT_PATTERN["source"][0], + "DetailType": TEST_EVENT_PATTERN["detail-type"][0], + "Detail": json.dumps(EVENT_DETAIL), + } + ] + entries_asserts = [(entries, True)] + + # input transformer with all keys in template present in message + input_path_map = { + "detail-type": "$.detail-type", + "timestamp": "$.time", + "command": "$.detail.command", + } + input_template = input_template + input_transformer_match_all = { + "InputPathsMap": input_path_map, + "InputTemplate": input_template, + } + messages_match_all = put_events_with_filter_to_sqs( + pattern=TEST_EVENT_PATTERN, + entries_asserts=entries_asserts, + input_transformer=input_transformer_match_all, + ) + + # input transformer with keys in template missing from message + input_path_map_missing_key = { + "detail-type": "$.detail-type", + "timestamp": "$.time", + "command": "$.detail.notinmessage", + } + input_transformer_not_match_all = { + "InputPathsMap": input_path_map_missing_key, + "InputTemplate": input_template, + } + messages_not_match_all = put_events_with_filter_to_sqs( + pattern=TEST_EVENT_PATTERN, + entries_asserts=entries_asserts, + input_transformer=input_transformer_not_match_all, + ) + + snapshot.add_transformer( + [ + snapshot.transform.key_value("MD5OfBody"), + snapshot.transform.key_value("ReceiptHandle"), + ] + ) + snapshot.match("custom-variables-match-all", messages_match_all) + snapshot.match("custom-variables-not-match-all", messages_not_match_all) + @markers.aws.validated - @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") - def test_put_events_with_input_transformation_to_sqs( + @pytest.mark.skipif( + not is_v2_provider(), + reason="V1 provider does not support this feature", + ) + def test_put_events_with_input_transformer_input_template_json( self, put_events_with_filter_to_sqs, snapshot ): - pattern = {"detail-type": ["customerCreated"]} - event_detail = {"command": "display-message", "payload": "baz"} entries = [ { - "Source": "com.mycompany.myapp", - "DetailType": "customerCreated", - "Detail": json.dumps(event_detail), + "Source": TEST_EVENT_PATTERN["source"][0], + "DetailType": TEST_EVENT_PATTERN["detail-type"][0], + "Detail": json.dumps(EVENT_DETAIL), } ] entries_asserts = [(entries, True)] @@ -175,13 +291,13 @@ def test_put_events_with_input_transformation_to_sqs( "timestamp": "$.time", "command": "$.detail.command", } - input_template = '"Event of <detail-type> type, at time <timestamp>, info extracted from detail <command>"' + input_template = '{"detailType": <detail-type>, "time": <timestamp>, "command": <command>}' input_transformer_match_all = { "InputPathsMap": input_path_map, "InputTemplate": input_template, } messages_match_all = put_events_with_filter_to_sqs( - pattern=pattern, + pattern=TEST_EVENT_PATTERN, entries_asserts=entries_asserts, input_transformer=input_transformer_match_all, ) @@ -197,7 +313,7 @@ def test_put_events_with_input_transformation_to_sqs( "InputTemplate": input_template, } messages_not_match_all = put_events_with_filter_to_sqs( - pattern=pattern, + pattern=TEST_EVENT_PATTERN, entries_asserts=entries_asserts, input_transformer=input_transformer_not_match_all, ) @@ -210,3 +326,130 @@ def test_put_events_with_input_transformation_to_sqs( ) snapshot.match("custom-variables-match-all", messages_match_all) snapshot.match("custom-variables-not-match-all", messages_not_match_all) + + @markers.aws.validated + @pytest.mark.skipif( + not is_v2_provider(), + reason="V1 provider does not support this feature", + ) + def test_put_events_with_input_transformer_missing_keys( + self, + create_sqs_events_target, + events_create_event_bus, + events_put_rule, + aws_client_factory, + aws_client, + snapshot, + ): + _, queue_arn = create_sqs_events_target() + + bus_name = f"test-bus-{short_uid()}" + events_create_event_bus(Name=bus_name) + + rule_name = f"test-rule-{short_uid()}" + events_put_rule( + Name=rule_name, + EventBusName=bus_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + ) + + events_client = aws_client_factory(config=Config(parameter_validation=False)).events + target_id = f"target-{short_uid()}" + input_path_map = { + "detail-type": "$.detail-type", + "timestamp": "$.time", + "command": "$.detail.command", + } + # input template with not defined key + input_template = '"Event of <detail-type> type, with not defined key <notdefinedkey>"' + input_transformer = { + "InputPathsMap": input_path_map, + "InputTemplate": input_template, + } + + with pytest.raises(Exception) as exception: + events_client.put_targets( + Rule=rule_name, + EventBusName=bus_name, + Targets=[ + {"Id": target_id, "Arn": queue_arn, "InputTransformer": input_transformer}, + ], + ) + + snapshot.add_transformer(snapshot.transform.regex(target_id, "<target-id>")) + snapshot.match("missing-key-exception", exception) + + @markers.aws.validated + @pytest.mark.skipif( + not is_v2_provider(), + reason="V1 provider does not support this feature", + ) + @pytest.mark.parametrize( + "input_template", + [INPUT_TEMPLATE_PREDEFINE_VARIABLES_STR, INPUT_TEMPLATE_PREDEFINED_VARIABLES_JSON], + ) + def test_input_transformer_predefined_variables( + self, + input_template, + create_sqs_events_target, + events_create_event_bus, + events_put_rule, + aws_client, + snapshot, + ): + # https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-transform-target-input.html#eb-transform-input-predefined + + # prepare target queues + queue_url, queue_arn = create_sqs_events_target() + + bus_name = f"test-bus-{short_uid()}" + events_create_event_bus(Name=bus_name) + + rule_name = f"test-rule-/slash-{short_uid()}" + events_put_rule( + Name=rule_name, + EventBusName=bus_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + ) + + # no input path map required for predefined variables + input_transformer = { + "InputTemplate": input_template, + } + + target_id = f"target-{short_uid()}" + aws_client.events.put_targets( + Rule=rule_name, + EventBusName=bus_name, + Targets=[ + {"Id": target_id, "Arn": queue_arn, "InputTransformer": input_transformer}, + ], + ) + + aws_client.events.put_events( + Entries=[ + { + "EventBusName": bus_name, + "Source": TEST_EVENT_PATTERN["source"][0], + "DetailType": TEST_EVENT_PATTERN["detail-type"][0], + "Detail": json.dumps(EVENT_DETAIL), + } + ] + ) + + messages = sqs_collect_messages(aws_client, queue_url, min_events=1, retries=3) + + snapshot.add_transformer( + [ + snapshot.transform.regex(bus_name, "<bus-name>"), + snapshot.transform.regex(rule_name, "<rule-name>"), + snapshot.transform.key_value("MD5OfBody"), + snapshot.transform.key_value("ReceiptHandle"), + snapshot.transform.jsonpath( + "$.messages[*].Body.originalEvent.time", + value_replacement="<ingestion-time>", + reference_replacement=False, + ), + ] + ) + snapshot.match("messages", messages) diff --git a/tests/aws/services/events/test_events_inputs.snapshot.json b/tests/aws/services/events/test_events_inputs.snapshot.json index c282663105c32..9f9b2a97f4031 100644 --- a/tests/aws/services/events/test_events_inputs.snapshot.json +++ b/tests/aws/services/events/test_events_inputs.snapshot.json @@ -1,27 +1,25 @@ { - "tests/aws/services/events/test_events_inputs.py::TestEventsInputTransformers::test_put_events_with_input_transformation_to_sqs": { - "recorded-date": "26-03-2024, 15:48:35", + "tests/aws/services/events/test_events_inputs.py::TestInputPath::test_put_events_with_input_path": { + "recorded-date": "13-05-2024, 12:27:07", "recorded-content": { - "custom-variables-match-all": [ + "message": [ { "MessageId": "<uuid:1>", "ReceiptHandle": "<receipt-handle:1>", "MD5OfBody": "<m-d5-of-body:1>", - "Body": "\"Event of customerCreated type, at time date, info extracted from detail display-message\"" - } - ], - "custom-variables-not-match-all": [ - { - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:2>", - "MD5OfBody": "<m-d5-of-body:2>", - "Body": "\"Event of customerCreated type, at time date, info extracted from detail \"" + "Body": { + "command": "update-account", + "payload": { + "acc_id": "0a787ecb-4015", + "sf_id": "baz" + } + } } ] } }, - "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path": { - "recorded-date": "08-05-2024, 13:54:10", + "tests/aws/services/events/test_events_inputs.py::TestInputPath::test_put_events_with_input_path_nested[event_detail0]": { + "recorded-date": "13-05-2024, 12:27:09", "recorded-content": { "message": [ { @@ -29,18 +27,15 @@ "ReceiptHandle": "<receipt-handle:1>", "MD5OfBody": "<m-d5-of-body:1>", "Body": { - "command": "update-account", - "payload": { - "acc_id": "0a787ecb-4015", - "sf_id": "baz" - } + "acc_id": "0a787ecb-4015", + "sf_id": "baz" } } ] } }, - "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_nested": { - "recorded-date": "06-05-2024, 15:11:52", + "tests/aws/services/events/test_events_inputs.py::TestInputPath::test_put_events_with_input_path_nested[event_detail1]": { + "recorded-date": "13-05-2024, 12:27:11", "recorded-content": { "message": [ { @@ -49,14 +44,17 @@ "MD5OfBody": "<m-d5-of-body:1>", "Body": { "acc_id": "0a787ecb-4015", - "sf_id": "baz" + "payload": { + "message": "baz", + "id": "123" + } } } ] } }, - "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_max_level_depth": { - "recorded-date": "06-05-2024, 15:11:54", + "tests/aws/services/events/test_events_inputs.py::TestInputPath::test_put_events_with_input_path_max_level_depth": { + "recorded-date": "13-05-2024, 12:27:13", "recorded-content": { "message": [ { @@ -68,8 +66,8 @@ ] } }, - "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_multiple_targets": { - "recorded-date": "06-05-2024, 15:22:58", + "tests/aws/services/events/test_events_inputs.py::TestInputPath::test_put_events_with_input_path_multiple_targets": { + "recorded-date": "13-05-2024, 12:27:16", "recorded-content": { "message-queue-1": [ { @@ -111,39 +109,162 @@ ] } }, - "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_nested[event_detail0]": { - "recorded-date": "08-05-2024, 13:54:42", + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_put_events_with_input_transformer_input_template_string": { + "recorded-date": "13-05-2024, 12:27:20", "recorded-content": { - "message": [ + "custom-variables-match-all": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": "\"Event of core.update-account-command type, at time date, info extracted from detail update-account\"" + } + ], + "custom-variables-not-match-all": [ + { + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>", + "MD5OfBody": "<m-d5-of-body:2>", + "Body": "\"Event of core.update-account-command type, at time date, info extracted from detail \"" + } + ] + } + }, + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_put_events_with_input_transformer_input_template_json": { + "recorded-date": "13-05-2024, 12:27:25", + "recorded-content": { + "custom-variables-match-all": [ { "MessageId": "<uuid:1>", "ReceiptHandle": "<receipt-handle:1>", "MD5OfBody": "<m-d5-of-body:1>", "Body": { - "acc_id": "0a787ecb-4015", - "sf_id": "baz" + "detailType": "core.update-account-command", + "time": "date", + "command": "update-account" + } + } + ], + "custom-variables-not-match-all": [ + { + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>", + "MD5OfBody": "<m-d5-of-body:2>", + "Body": { + "detailType": "core.update-account-command", + "time": "date", + "command": "" } } ] } }, - "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_nested[event_detail1]": { - "recorded-date": "08-05-2024, 13:54:44", + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_put_events_with_input_transformer_missing_keys": { + "recorded-date": "13-05-2024, 12:27:28", "recorded-content": { - "message": [ + "missing-key-exception": "<ExceptionInfo ClientError('An error occurred (ValidationException) when calling the PutTargets operation: InputTemplate for target <target-id> contains invalid placeholder notdefinedkey.') tblen=3>" + } + }, + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_input_transformer_predefined_variables[\"Message containing all pre defined variables <aws.events.rule-arn> <aws.events.rule-name> <aws.events.event.ingestion-time>\"]": { + "recorded-date": "13-05-2024, 12:27:30", + "recorded-content": { + "messages": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": "\"Message containing all pre defined variables arn:aws:events:<region>:111111111111:rule/<bus-name>/<rule-name> <rule-name> date\"" + } + ] + } + }, + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_input_transformer_predefined_variables[{\"originalEvent\": <aws.events.event>, \"originalEventJson\": <aws.events.event.json>}]": { + "recorded-date": "13-05-2024, 12:27:33", + "recorded-content": { + "messages": [ { "MessageId": "<uuid:1>", "ReceiptHandle": "<receipt-handle:1>", "MD5OfBody": "<m-d5-of-body:1>", "Body": { - "acc_id": "0a787ecb-4015", - "payload": { - "message": "baz", - "id": "123" + "originalEvent": { + "id": "<uuid:2>", + "account": "111111111111", + "detailType": "core.update-account-command", + "time": "<ingestion-time>", + "source": "core.update-account-command", + "region": "<region>", + "resources": [], + "version": "0" + }, + "originalEventJson": { + "version": "0", + "id": "<uuid:2>", + "detail-type": "core.update-account-command", + "source": "core.update-account-command", + "account": "111111111111", + "time": "date", + "region": "<region>", + "resources": [], + "detail": { + "command": "update-account", + "payload": { + "acc_id": "0a787ecb-4015", + "sf_id": "baz" + } + } } } } ] } + }, + "tests/aws/services/events/test_events_inputs.py::test_put_event_input_path_and_input_transfomer": { + "recorded-date": "13-05-2024, 13:01:15", + "recorded-content": { + "missing-key-exception": "<ExceptionInfo ClientError('An error occurred (ValidationException) when calling the PutTargets operation: Only one of Input, InputPath, or InputTransformer must be provided for target <target-id>.') tblen=3>" + } + }, + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_put_events_with_input_transformer_input_template_string[\"Event of <detail-type> type, at time <timestamp>, info extracted from detail <command>\"]": { + "recorded-date": "14-05-2024, 17:01:50", + "recorded-content": { + "custom-variables-match-all": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": "\"Event of core.update-account-command type, at time date, info extracted from detail update-account\"" + } + ], + "custom-variables-not-match-all": [ + { + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>", + "MD5OfBody": "<m-d5-of-body:2>", + "Body": "\"Event of core.update-account-command type, at time date, info extracted from detail \"" + } + ] + } + }, + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_put_events_with_input_transformer_input_template_string[\"{[/Check with special starting characters for event of <detail-type> type\"]": { + "recorded-date": "14-05-2024, 17:01:54", + "recorded-content": { + "custom-variables-match-all": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": "\"{[/Check with special starting characters for event of core.update-account-command type\"" + } + ], + "custom-variables-not-match-all": [ + { + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": "\"{[/Check with special starting characters for event of core.update-account-command type\"" + } + ] + } } } diff --git a/tests/aws/services/events/test_events_inputs.validation.json b/tests/aws/services/events/test_events_inputs.validation.json index 6b53b82b29016..7b59b3b4ffc59 100644 --- a/tests/aws/services/events/test_events_inputs.validation.json +++ b/tests/aws/services/events/test_events_inputs.validation.json @@ -1,23 +1,41 @@ { - "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path": { - "last_validated_date": "2024-05-08T13:54:10+00:00" + "tests/aws/services/events/test_events_inputs.py::TestInputPath::test_put_events_with_input_path": { + "last_validated_date": "2024-05-13T12:27:07+00:00" }, - "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_max_level_depth": { - "last_validated_date": "2024-05-06T15:11:54+00:00" + "tests/aws/services/events/test_events_inputs.py::TestInputPath::test_put_events_with_input_path_max_level_depth": { + "last_validated_date": "2024-05-13T12:27:13+00:00" }, - "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_multiple_targets": { - "last_validated_date": "2024-05-06T15:22:58+00:00" + "tests/aws/services/events/test_events_inputs.py::TestInputPath::test_put_events_with_input_path_multiple_targets": { + "last_validated_date": "2024-05-13T12:27:16+00:00" }, - "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_nested": { - "last_validated_date": "2024-05-06T15:11:52+00:00" + "tests/aws/services/events/test_events_inputs.py::TestInputPath::test_put_events_with_input_path_nested[event_detail0]": { + "last_validated_date": "2024-05-13T12:27:09+00:00" }, - "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_nested[event_detail0]": { - "last_validated_date": "2024-05-08T13:54:42+00:00" + "tests/aws/services/events/test_events_inputs.py::TestInputPath::test_put_events_with_input_path_nested[event_detail1]": { + "last_validated_date": "2024-05-13T12:27:11+00:00" }, - "tests/aws/services/events/test_events_inputs.py::TestEventsInputPath::test_put_events_with_input_path_nested[event_detail1]": { - "last_validated_date": "2024-05-08T13:54:44+00:00" + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_input_transformer_predefined_variables[\"Message containing all pre defined variables <aws.events.rule-arn> <aws.events.rule-name> <aws.events.event.ingestion-time>\"]": { + "last_validated_date": "2024-05-13T12:27:30+00:00" }, - "tests/aws/services/events/test_events_inputs.py::TestEventsInputTransformers::test_put_events_with_input_transformation_to_sqs": { - "last_validated_date": "2024-03-26T15:48:35+00:00" + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_input_transformer_predefined_variables[{\"originalEvent\": <aws.events.event>, \"originalEventJson\": <aws.events.event.json>}]": { + "last_validated_date": "2024-05-13T12:27:33+00:00" + }, + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_put_events_with_input_transformer_input_template_json": { + "last_validated_date": "2024-05-13T12:27:25+00:00" + }, + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_put_events_with_input_transformer_input_template_string": { + "last_validated_date": "2024-05-13T12:27:20+00:00" + }, + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_put_events_with_input_transformer_input_template_string[\"Event of <detail-type> type, at time <timestamp>, info extracted from detail <command>\"]": { + "last_validated_date": "2024-05-14T17:01:49+00:00" + }, + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_put_events_with_input_transformer_input_template_string[\"{[/Check with special starting characters for event of <detail-type> type\"]": { + "last_validated_date": "2024-05-14T17:01:54+00:00" + }, + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_put_events_with_input_transformer_missing_keys": { + "last_validated_date": "2024-05-13T12:27:28+00:00" + }, + "tests/aws/services/events/test_events_inputs.py::test_put_event_input_path_and_input_transfomer": { + "last_validated_date": "2024-05-13T13:01:15+00:00" } } From cac2d03d20ec740d54a06478a9f80e9d84d08ff3 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Wed, 15 May 2024 14:51:37 +0200 Subject: [PATCH 143/169] fix pagination for SNS ListSubscriptions and ListSubscriptionsByTopic (#10810) --- localstack/services/sns/provider.py | 34 +++++++++++++-- tests/aws/services/sns/test_sns.py | 41 +++++++++++++++++++ tests/aws/services/sns/test_sns.snapshot.json | 20 +++++++++ .../aws/services/sns/test_sns.validation.json | 3 ++ 4 files changed, 94 insertions(+), 4 deletions(-) diff --git a/localstack/services/sns/provider.py b/localstack/services/sns/provider.py index eca0a21275647..32a80fb7643e7 100644 --- a/localstack/services/sns/provider.py +++ b/localstack/services/sns/provider.py @@ -1,3 +1,4 @@ +import base64 import json import logging from typing import Dict, List @@ -73,8 +74,8 @@ extract_region_from_arn, parse_arn, ) -from localstack.utils.collections import select_from_typed_dict -from localstack.utils.strings import short_uid +from localstack.utils.collections import PaginatedList, select_from_typed_dict +from localstack.utils.strings import short_uid, to_bytes, to_str # set up logger LOG = logging.getLogger(__name__) @@ -464,7 +465,17 @@ def list_subscriptions( subscriptions = [ select_from_typed_dict(Subscription, sub) for sub in list(store.subscriptions.values()) ] - return ListSubscriptionsResponse(Subscriptions=subscriptions) + paginated_subscriptions = PaginatedList(subscriptions) + page, next_token = paginated_subscriptions.get_page( + token_generator=lambda x: get_next_page_token_from_arn(x["SubscriptionArn"]), + page_size=100, + next_token=next_token, + ) + + response = ListSubscriptionsResponse(Subscriptions=page) + if next_token: + response["NextToken"] = next_token + return response def list_subscriptions_by_topic( self, context: RequestContext, topic_arn: topicARN, next_token: nextToken = None, **kwargs @@ -474,7 +485,18 @@ def list_subscriptions_by_topic( store = self.get_store(parsed_topic_arn["account"], parsed_topic_arn["region"]) sns_subscriptions = store.get_topic_subscriptions(topic_arn) subscriptions = [select_from_typed_dict(Subscription, sub) for sub in sns_subscriptions] - return ListSubscriptionsByTopicResponse(Subscriptions=subscriptions) + + paginated_subscriptions = PaginatedList(subscriptions) + page, next_token = paginated_subscriptions.get_page( + token_generator=lambda x: get_next_page_token_from_arn(x["SubscriptionArn"]), + page_size=100, + next_token=next_token, + ) + + response = ListSubscriptionsResponse(Subscriptions=page) + if next_token: + response["NextToken"] = next_token + return response def publish( self, @@ -999,6 +1021,10 @@ def get_region_from_subscription_token(token: str) -> str: raise InvalidParameterException("Invalid parameter: Token") +def get_next_page_token_from_arn(resource_arn: str) -> str: + return to_str(base64.b64encode(to_bytes(resource_arn))) + + def register_sns_api_resource(router: Router): """Register the retrospection endpoints as internal LocalStack endpoints.""" router.add(SNSServicePlatformEndpointMessagesApiResource()) diff --git a/tests/aws/services/sns/test_sns.py b/tests/aws/services/sns/test_sns.py index 0393a2b01f4ff..f8bdaf01ed2e3 100644 --- a/tests/aws/services/sns/test_sns.py +++ b/tests/aws/services/sns/test_sns.py @@ -822,6 +822,47 @@ def test_list_subscriptions( assert all((sub["TopicArn"], sub["Endpoint"]) in sorting_list for sub in all_subs) + @markers.aws.validated + def test_list_subscriptions_by_topic_pagination( + self, sns_create_topic, sns_subscription, snapshot, aws_client + ): + # ordering of the listing seems to be not consistent, so we can transform its value + snapshot.add_transformers_list( + [ + snapshot.transform.key_value("Endpoint"), + snapshot.transform.key_value("NextToken"), + ] + ) + + base_phone_number = "+12312312" + topic_arn = sns_create_topic()["TopicArn"] + for phone_suffix in range(101): + phone_number = f"{base_phone_number}{phone_suffix}" + sns_subscription(TopicArn=topic_arn, Protocol="sms", Endpoint=phone_number) + + response = aws_client.sns.list_subscriptions_by_topic(TopicArn=topic_arn) + # not snapshotting the results, it contains 100 entries + assert "NextToken" in response + # seems to be b64 encoded + assert response["NextToken"].endswith("==") + assert len(response["Subscriptions"]) == 100 + # keep the page 1 subscriptions ARNs + page_1_subs = {sub["SubscriptionArn"] for sub in response["Subscriptions"]} + + response = aws_client.sns.list_subscriptions_by_topic( + TopicArn=topic_arn, NextToken=response["NextToken"] + ) + snapshot.match("list-sub-per-topic-page-2", response) + assert "NextToken" not in response + assert len(response["Subscriptions"]) == 1 + # assert that the last Subscription is not present in page 1 + assert response["Subscriptions"][0]["SubscriptionArn"] not in page_1_subs + + response = aws_client.sns.list_subscriptions() + # not snapshotting because there might be subscriptions outside the topic, this is all the requester subs + assert "NextToken" in response + assert len(response["Subscriptions"]) <= 100 + @markers.aws.validated def test_subscribe_idempotency( self, aws_client, sns_create_topic, sqs_create_queue, sqs_get_queue_arn, snapshot diff --git a/tests/aws/services/sns/test_sns.snapshot.json b/tests/aws/services/sns/test_sns.snapshot.json index 6002a58044676..73e4e694abaaf 100644 --- a/tests/aws/services/sns/test_sns.snapshot.json +++ b/tests/aws/services/sns/test_sns.snapshot.json @@ -5255,5 +5255,25 @@ ] } } + }, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_list_subscriptions_by_topic_pagination": { + "recorded-date": "13-05-2024, 12:50:25", + "recorded-content": { + "list-sub-per-topic-page-2": { + "Subscriptions": [ + { + "Endpoint": "<endpoint:1>", + "Owner": "111111111111", + "Protocol": "sms", + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:2>:<resource:1>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:2>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } } } diff --git a/tests/aws/services/sns/test_sns.validation.json b/tests/aws/services/sns/test_sns.validation.json index c07a5b165442d..dd3357dd6e3eb 100644 --- a/tests/aws/services/sns/test_sns.validation.json +++ b/tests/aws/services/sns/test_sns.validation.json @@ -92,6 +92,9 @@ "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_list_subscriptions": { "last_validated_date": "2023-08-25T14:23:53+00:00" }, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_list_subscriptions_by_topic_pagination": { + "last_validated_date": "2024-05-13T12:50:03+00:00" + }, "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_not_found_error_on_set_subscription_attributes": { "last_validated_date": "2023-08-24T21:27:55+00:00" }, From 9d80628c01a28920b619501fa3190193566883ce Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Wed, 15 May 2024 17:12:40 +0200 Subject: [PATCH 144/169] fix SNS cross-account issues with Subscriptions (#10819) --- localstack/services/sns/filter.py | 431 +++++ localstack/services/sns/provider.py | 99 +- localstack/services/sns/publisher.py | 217 +-- tests/aws/services/sns/test_sns.py | 1088 +----------- tests/aws/services/sns/test_sns.snapshot.json | 1308 +------------- .../aws/services/sns/test_sns.validation.json | 41 +- .../services/sns/test_sns_filter_policy.py | 1557 +++++++++++++++++ .../sns/test_sns_filter_policy.snapshot.json | 1533 ++++++++++++++++ .../test_sns_filter_policy.validation.json | 59 + tests/unit/test_sns.py | 74 +- 10 files changed, 3837 insertions(+), 2570 deletions(-) create mode 100644 localstack/services/sns/filter.py create mode 100644 tests/aws/services/sns/test_sns_filter_policy.py create mode 100644 tests/aws/services/sns/test_sns_filter_policy.snapshot.json create mode 100644 tests/aws/services/sns/test_sns_filter_policy.validation.json diff --git a/localstack/services/sns/filter.py b/localstack/services/sns/filter.py new file mode 100644 index 0000000000000..799ea91f38f36 --- /dev/null +++ b/localstack/services/sns/filter.py @@ -0,0 +1,431 @@ +import json +import typing as t + +from localstack.aws.api.sns import InvalidParameterException + + +class SubscriptionFilter: + def check_filter_policy_on_message_attributes( + self, filter_policy: dict, message_attributes: dict + ): + for criteria, conditions in filter_policy.items(): + if not self._evaluate_filter_policy_conditions_on_attribute( + conditions, + message_attributes.get(criteria), + field_exists=criteria in message_attributes, + ): + return False + + return True + + def check_filter_policy_on_message_body(self, filter_policy: dict, message_body: str): + try: + body = json.loads(message_body) + if not isinstance(body, dict): + return False + except json.JSONDecodeError: + # Filter policies for the message body assume that the message payload is a well-formed JSON object. + # See https://docs.aws.amazon.com/sns/latest/dg/sns-message-filtering.html + return False + + return self._evaluate_nested_filter_policy_on_dict(filter_policy, payload=body) + + def _evaluate_nested_filter_policy_on_dict(self, filter_policy, payload: dict) -> bool: + """ + This method evaluates the filter policy against the JSON decoded payload. + Although it's not documented anywhere, AWS allows `.` in the fields name in the filter policy and the payload, + and will evaluate them. However, it's not JSONPath compatible: + Example: + Policy: `{"field1.field2": "value1"}` + This policy will match both `{"field1.field2": "value1"}` and {"field1: {"field2": "value1"}}`, unlike JSONPath + for which `.` points to a child node. + This might show they are flattening the both dictionaries to a single level for an easier matching without + recursion. + :param filter_policy: a dict, starting at the FilterPolicy + :param payload: a dict, starting at the MessageBody + :return: True if the payload respect the filter policy, otherwise False + """ + flat_policy = self._flatten_dict(filter_policy) + flat_payloads = self._flatten_dict_with_list(payload) + for key, values in flat_policy.items(): + if not any( + self._evaluate_condition( + flat_payload.get(key), condition, field_exists=key in flat_payload + ) + for condition in values + for flat_payload in flat_payloads + ): + return False + return True + + def _evaluate_filter_policy_conditions_on_attribute( + self, conditions, attribute, field_exists: bool + ): + if not isinstance(conditions, list): + conditions = [conditions] + + tpe = attribute.get("DataType") or attribute.get("Type") if attribute else None + val = attribute.get("StringValue") or attribute.get("Value") if attribute else None + if attribute is not None and tpe == "String.Array": + try: + values = json.loads(val) + except ValueError: + return False + for value in values: + for condition in conditions: + if self._evaluate_condition(value, condition, field_exists): + return True + else: + for condition in conditions: + value = val or None + if self._evaluate_condition(value, condition, field_exists): + return True + + return False + + def _evaluate_condition(self, value, condition, field_exists: bool): + if not isinstance(condition, dict): + return field_exists and value == condition + elif (must_exist := condition.get("exists")) is not None: + # if must_exists is True then field_exists must be True + # if must_exists is False then fields_exists must be False + return must_exist == field_exists + elif value is None: + # the remaining conditions require the value to not be None + return False + elif anything_but := condition.get("anything-but"): + # TODO: support with `prefix` + # https://docs.aws.amazon.com/sns/latest/dg/string-value-matching.html#string-anything-but-matching-prefix + return value not in anything_but + elif prefix := (condition.get("prefix")): + return value.startswith(prefix) + elif numeric_condition := condition.get("numeric"): + return self._evaluate_numeric_condition(numeric_condition, value) + return False + + @staticmethod + def _evaluate_numeric_condition(conditions, value): + try: + # try if the value is numeric + value = float(value) + except ValueError: + # the value is not numeric, the condition is False + return False + + for i in range(0, len(conditions), 2): + operator = conditions[i] + operand = float(conditions[i + 1]) + + if operator == "=": + if value != operand: + return False + elif operator == ">": + if value <= operand: + return False + elif operator == "<": + if value >= operand: + return False + elif operator == ">=": + if value < operand: + return False + elif operator == "<=": + if value > operand: + return False + + return True + + @staticmethod + def _flatten_dict(nested_dict: dict): + """ + Takes a dictionary as input and will output the dictionary on a single level. + Input: + `{"field1": {"field2: {"field3: "val1", "field4": "val2"}}}` + Output: + `{ + "field1.field2.field3": "val1", + "field1.field2.field4": "val1" + }` + :param nested_dict: a (nested) dictionary + :return: a list of flattened dictionaries with no nested dict or list inside, flattened to a + single level, one list item for every list item encountered + """ + flatten = {} + + def _traverse(_policy: dict, parent_key=None): + for key, values in _policy.items(): + flattened_parent_key = key if not parent_key else f"{parent_key}.{key}" + if not isinstance(values, dict): + flatten[flattened_parent_key] = values + else: + _traverse(values, parent_key=flattened_parent_key) + + _traverse(nested_dict) + return flatten + + @staticmethod + def _flatten_dict_with_list(nested_dict: dict) -> list[dict]: + """ + Takes a dictionary as input and will output the dictionary on a single level. + The dictionary can have lists containing other dictionaries, and one root level entry will be created for every + item in a list. + Input: + `{"field1": { + "field2: [ + {"field3: "val1", "field4": "val2"}, + {"field3: "val3", "field4": "val4"}, + } + ]}` + Output: + `[ + { + "field1.field2.field3": "val1", + "field1.field2.field4": "val2" + }, + { + "field1.field2.field3": "val3", + "field1.field2.field4": "val4" + }, + ]` + :param nested_dict: a (nested) dictionary + :return: flatten_dict: a dictionary with no nested dict inside, flattened to a single level + """ + flattened = [] + current_object = {} + + def _traverse(_object, parent_key=None): + if isinstance(_object, dict): + for key, values in _object.items(): + flattened_parent_key = key if not parent_key else f"{parent_key}.{key}" + _traverse(values, flattened_parent_key) + + # we don't have to worry about `parent_key` being None for list or any other type, because we have a check + # that the first object is always a dict, thus setting a parent key on first iteration + elif isinstance(_object, list): + for value in _object: + if isinstance(value, (dict, list)): + _traverse(value, parent_key=parent_key) + else: + current_object[parent_key] = value + + if current_object: + flattened.append({**current_object}) + current_object.clear() + else: + current_object[parent_key] = _object + + _traverse(nested_dict) + + # if the payload did not have any list, we manually append the current object + if not flattened: + flattened.append(current_object) + + return flattened + + +class FilterPolicyValidator: + def __init__(self, scope: str, is_subscribe_call: bool): + self.scope = scope + self.error_prefix = ( + "Invalid parameter: Attributes Reason: " if is_subscribe_call else "Invalid parameter: " + ) + + def validate_filter_policy(self, filter_policy: dict[str, t.Any]): + # # A filter policy can have a maximum of five attribute names. For a nested policy, only parent keys are counted. + if len(filter_policy.values()) > 5: + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: Filter policy can not have more than 5 keys" + ) + + aggregated_rules, combinations = self.aggregate_rules(filter_policy) + # For the complexity of the filter policy, the total combination of values must not exceed 150. + # https://docs.aws.amazon.com/sns/latest/dg/subscription-filter-policy-constraints.html + if combinations > 150: + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: Filter policy is too complex" + ) + + for rules in aggregated_rules: + for rule in rules: + self._validate_rule(rule) + + def aggregate_rules(self, filter_policy: dict[str, t.Any]) -> tuple[list[list[t.Any]], int]: + """ + This method evaluate the filter policy recursively, and returns only a list of lists of rules. + It also calculates the combinations of rules, calculated depending on the nesting of the rules. + Example: + nested_filter_policy = { + "key_a": { + "key_b": { + "key_c": ["value_one", "value_two", "value_three", "value_four"] + } + }, + "key_d": { + "key_e": ["value_one", "value_two", "value_three"] + } + } + This function then iterates on the values of the top level keys of the filter policy: ("key_a", "key_d") + If the iterated value is not a list, it means it is a nested property. If the scope is `MessageBody`, it is + allowed, we call this method on the value, adding a level to the depth to keep track on how deep the key is. + If the value is a list, it means it contains rules: we will append this list of rules in _rules, and + calculate the combinations it adds. + For the example filter policy containing nested properties, we calculate it this way + The first array has four values in a three-level nested key, and the second has three values in a two-level + nested key. 3 x 4 x 2 x 3 = 72 + The return value would be: + [["value_one", "value_two", "value_three", "value_four"], ["value_one", "value_two", "value_three"]] + It allows us to later iterate of the list of rules in an easy way, to verify its conditions only. + + :param filter_policy: a dict, starting at the FilterPolicy + :return: a tuple with a list of lists of rules and the calculated number of combinations + """ + + def _inner( + policy_elements: dict[str, t.Any], depth: int = 1, combinations: int = 1 + ) -> tuple[list[list[t.Any]], int]: + _rules = [] + for key, _value in policy_elements.items(): + if isinstance(_value, dict): + # From AWS docs: "unlike attribute-based policies, payload-based policies support property nesting." + sub_rules, combinations = _inner( + _value, depth=depth + 1, combinations=combinations + ) + _rules.extend(sub_rules) + elif isinstance(_value, list): + current_combination = 0 + if key == "$or": + for val in _value: + sub_rules, or_combinations = _inner( + val, depth=depth, combinations=combinations + ) + _rules.extend(sub_rules) + current_combination += or_combinations + + combinations = current_combination + else: + _rules.append(_value) + combinations = combinations * len(_value) * depth + else: + raise InvalidParameterException( + f'{self.error_prefix}FilterPolicy: "{key}" must be an object or an array' + ) + + if self.scope == "MessageAttributes" and depth > 1: + raise InvalidParameterException( + f"{self.error_prefix}Filter policy scope MessageAttributes does not support nested filter policy" + ) + + return _rules, combinations + + return _inner(filter_policy) + + def _validate_rule(self, rule: t.Any) -> None: + match rule: + case None | str() | bool(): + return + + case int() | float(): + # TODO: AWS says they support only from -10^9 to 10^9 but seems to accept it, so we just return + # if rule <= -1000000000 or rule >= 1000000000: + # raise "" + return + + case {**kwargs}: + if len(kwargs) != 1: + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: Only one key allowed in match expression" + ) + + operator, value = None, None + for k, v in kwargs.items(): + operator, value = k, v + + if operator in ( + "anything-but", + "equals-ignore-case", + "prefix", + "suffix", + ): + if not isinstance(value, str): + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: {operator} match pattern must be a string" + ) + return + + elif operator == "exists": + if not isinstance(value, bool): + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: exists match pattern must be either true or false." + ) + return + + elif operator == "numeric": + self._validate_numeric_condition(value) + + else: + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: Unrecognized match type {operator}" + ) + + case _: + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: Match value must be String, number, true, false, or null" + ) + + def _validate_numeric_condition(self, value): + if not value: + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: Invalid member in numeric match: ]" + ) + num_values = value[::-1] + + operator = num_values.pop() + if not isinstance(operator, str): + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: Invalid member in numeric match: {operator}" + ) + elif operator not in ("<", "<=", "=", ">", ">="): + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: Unrecognized numeric range operator: {operator}" + ) + + value = num_values.pop() if num_values else None + if not isinstance(value, (int, float)): + exc_operator = "equals" if operator == "=" else operator + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: Value of {exc_operator} must be numeric" + ) + + if not num_values: + return + + if operator not in (">", ">="): + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: Too many elements in numeric expression" + ) + + second_operator = num_values.pop() + if not isinstance(second_operator, str): + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: Bad value in numeric range: {second_operator}" + ) + elif second_operator not in ("<", "<="): + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: Bad numeric range operator: {second_operator}" + ) + + second_value = num_values.pop() if num_values else None + if not isinstance(second_value, (int, float)): + exc_operator = "equals" if second_operator == "=" else second_operator + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: Value of {exc_operator} must be numeric" + ) + + elif second_value <= value: + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: Bottom must be less than top" + ) + + elif num_values: + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: Too many terms in numeric range expression" + ) diff --git a/localstack/services/sns/provider.py b/localstack/services/sns/provider.py index 32a80fb7643e7..370cd4b163a2b 100644 --- a/localstack/services/sns/provider.py +++ b/localstack/services/sns/provider.py @@ -2,6 +2,7 @@ import json import logging from typing import Dict, List +from uuid import uuid4 from botocore.utils import InvalidArnException from moto.core.utils import camelcase_to_pascal, underscores_to_camelcase @@ -9,7 +10,7 @@ from moto.sns.models import MAXIMUM_MESSAGE_LENGTH, SNSBackend, Topic from moto.sns.utils import is_e164 -from localstack.aws.api import CommonServiceException, RequestContext +from localstack.aws.api import RequestContext from localstack.aws.api.sns import ( AmazonResourceName, BatchEntryIdsNotDistinctException, @@ -32,10 +33,8 @@ PublishBatchResponse, PublishBatchResultEntry, PublishResponse, - SetSubscriptionAttributesInput, SnsApi, String, - SubscribeInput, SubscribeResponse, Subscription, SubscriptionAttributesMap, @@ -58,10 +57,11 @@ from localstack.constants import AWS_REGION_US_EAST_1, DEFAULT_AWS_ACCOUNT_ID from localstack.http import Request, Response, Router, route from localstack.services.edge import ROUTER -from localstack.services.moto import call_moto, call_moto_with_request +from localstack.services.moto import call_moto from localstack.services.plugins import ServiceLifecycleHook from localstack.services.sns import constants as sns_constants from localstack.services.sns.certificate import SNS_SERVER_CERT +from localstack.services.sns.filter import FilterPolicyValidator from localstack.services.sns.models import SnsMessage, SnsStore, SnsSubscription, sns_stores from localstack.services.sns.publisher import ( PublishDispatcher, @@ -274,24 +274,17 @@ def set_subscription_attributes( ) if attribute_name == "RawMessageDelivery": attribute_value = attribute_value.lower() - try: - request = SetSubscriptionAttributesInput( - SubscriptionArn=subscription_arn, - AttributeName=attribute_name, - AttributeValue=attribute_value, - ) - call_moto_with_request(context, service_request=request) - except CommonServiceException as e: - # Moto errors don't send the "Type": "Sender" field in their SNS exception - if e.code == "InvalidParameter": - raise InvalidParameterException(e.message) - raise - - if attribute_name == "FilterPolicy": - store = self.get_store(account_id=context.account_id, region_name=context.region) - store.subscription_filter_policy[subscription_arn] = ( - json.loads(attribute_value) if attribute_value else None - ) + + elif attribute_name == "FilterPolicy": + filter_policy = json.loads(attribute_value) if attribute_value else None + if filter_policy: + validator = FilterPolicyValidator( + scope=sub.get("FilterPolicyScope", "MessageAttributes"), + is_subscribe_call=False, + ) + validator.validate_filter_policy(filter_policy) + + store.subscription_filter_policy[subscription_arn] = filter_policy sub[attribute_name] = attribute_value @@ -636,6 +629,10 @@ def subscribe( return_subscription_arn: boolean = None, **kwargs, ) -> SubscribeResponse: + # TODO: check validation ordering + parsed_topic_arn = parse_and_validate_topic_arn(topic_arn) + store = self.get_store(account_id=parsed_topic_arn["account"], region_name=context.region) + if not endpoint: # TODO: check AWS behaviour (because endpoint is optional) raise NotFoundException("Endpoint not specified in subscription") @@ -656,6 +653,14 @@ def subscribe( except InvalidArnException: raise InvalidParameterException("Invalid parameter: SQS endpoint ARN") + elif protocol == "application": + # TODO: this is taken from moto, validate it + moto_backend = self.get_moto_backend( + account_id=parsed_topic_arn["account"], region_name=context.region + ) + if endpoint not in moto_backend.platform_endpoints: + raise NotFoundException("Endpoint does not exist") + if ".fifo" in endpoint and ".fifo" not in topic_arn: raise InvalidParameterException( "Invalid parameter: Invalid parameter: Endpoint Reason: FIFO SQS Queues can not be subscribed to standard SNS topics" @@ -670,27 +675,6 @@ def subscribe( endpoint=endpoint, is_subscribe_call=True, ) - if attributes and "RawMessageDelivery" in attributes: - # Moto does not lower case the value, so we need to override the request - attrs_copy = { - **attributes, - "RawMessageDelivery": attributes["RawMessageDelivery"].lower(), - } - request = SubscribeInput( - TopicArn=topic_arn, - Protocol=protocol, - Endpoint=endpoint, - Attributes=attrs_copy, - ReturnSubscriptionArn=return_subscription_arn, - ) - moto_response = call_moto_with_request(context, service_request=request) - else: - moto_response = call_moto(context) - - subscription_arn = moto_response.get("SubscriptionArn") - parsed_topic_arn = parse_and_validate_topic_arn(topic_arn) - - store = self.get_store(account_id=parsed_topic_arn["account"], region_name=context.region) # An endpoint may only be subscribed to a topic once. Subsequent # subscribe calls do nothing (subscribe is idempotent), except if its attributes are different. @@ -711,6 +695,7 @@ def subscribe( principal = sns_constants.DUMMY_SUBSCRIPTION_PRINCIPAL.replace( "{{account_id}}", context.account_id ) + subscription_arn = create_subscription_arn(topic_arn) subscription = SnsSubscription( # http://docs.aws.amazon.com/cli/latest/reference/sns/get-subscription-attributes.html TopicArn=topic_arn, @@ -726,22 +711,34 @@ def subscribe( if attributes: subscription.update(attributes) if "FilterPolicy" in attributes: + filter_policy = ( + json.loads(attributes["FilterPolicy"]) if attributes["FilterPolicy"] else None + ) + if filter_policy: + validator = FilterPolicyValidator( + scope=attributes.get("FilterPolicyScope", "MessageAttributes"), + is_subscribe_call=True, + ) + validator.validate_filter_policy(filter_policy) + store.subscription_filter_policy[subscription_arn] = ( json.loads(attributes["FilterPolicy"]) if attributes["FilterPolicy"] else None ) + if raw_msg_delivery := attributes.get("RawMessageDelivery"): subscription["RawMessageDelivery"] = raw_msg_delivery.lower() store.subscriptions[subscription_arn] = subscription - topic_subscription = store.topic_subscriptions.setdefault(topic_arn, []) - topic_subscription.append(subscription_arn) + topic_subscriptions = store.topic_subscriptions.setdefault(topic_arn, []) + topic_subscriptions.append(subscription_arn) # store the token and subscription arn # TODO: the token is a 288 hex char string subscription_token = encode_subscription_token_with_region(region=context.region) store.subscription_tokens[subscription_token] = subscription_arn + response_subscription_arn = subscription_arn # Send out confirmation message for HTTP(S), fix for https://github.com/localstack/localstack/issues/881 if protocol in ["http", "https"]: message_ctx = SnsMessage( @@ -760,6 +757,9 @@ def subscribe( topic_arn=topic_arn, subscription_arn=subscription_arn, ) + if not return_subscription_arn: + response_subscription_arn = "pending confirmation" + elif protocol not in ["email", "email-json"]: # Only HTTP(S) and email subscriptions are not auto validated # Except if the endpoint and the topic are not in the same AWS account, then you'd need to manually confirm @@ -770,7 +770,8 @@ def subscribe( # if parsed_topic_arn["account"] == endpoint account (depending on the type, SQS, lambda, parse the arn) subscription["PendingConfirmation"] = "false" subscription["ConfirmationWasAuthenticated"] = "true" - return SubscribeResponse(SubscriptionArn=subscription_arn) + + return SubscribeResponse(SubscriptionArn=response_subscription_arn) def tag_resource( self, context: RequestContext, resource_arn: AmazonResourceName, tags: TagList, **kwargs @@ -998,6 +999,12 @@ def parse_and_validate_topic_arn(topic_arn: str | None) -> ArnData: ) +def create_subscription_arn(topic_arn: str) -> str: + # This is the format of a Subscription ARN + # arn:aws:sns:us-west-2:123456789012:my-topic:8a21d249-4329-4871-acc6-7be709c6ea7f + return f"{topic_arn}:{uuid4()}" + + def encode_subscription_token_with_region(region: str) -> str: """ Create a 64 characters Subscription Token with the region encoded diff --git a/localstack/services/sns/publisher.py b/localstack/services/sns/publisher.py index 245ecdbee5592..07792b0187329 100644 --- a/localstack/services/sns/publisher.py +++ b/localstack/services/sns/publisher.py @@ -22,6 +22,7 @@ from localstack.config import external_service_url from localstack.services.sns import constants as sns_constants from localstack.services.sns.certificate import SNS_SERVER_PRIVATE_KEY +from localstack.services.sns.filter import SubscriptionFilter from localstack.services.sns.models import ( SnsApplicationPlatforms, SnsMessage, @@ -1100,222 +1101,6 @@ def create_unsubscribe_url(external_url, subscription_arn): return f"{external_url}/?Action=Unsubscribe&SubscriptionArn={subscription_arn}" -class SubscriptionFilter: - def check_filter_policy_on_message_attributes( - self, filter_policy: Dict, message_attributes: Dict - ): - for criteria, conditions in filter_policy.items(): - if not self._evaluate_filter_policy_conditions_on_attribute( - conditions, - message_attributes.get(criteria), - field_exists=criteria in message_attributes, - ): - return False - - return True - - def check_filter_policy_on_message_body(self, filter_policy: dict, message_body: str): - try: - body = json.loads(message_body) - if not isinstance(body, dict): - return False - except json.JSONDecodeError: - # Filter policies for the message body assume that the message payload is a well-formed JSON object. - # See https://docs.aws.amazon.com/sns/latest/dg/sns-message-filtering.html - return False - - return self._evaluate_nested_filter_policy_on_dict(filter_policy, payload=body) - - def _evaluate_nested_filter_policy_on_dict(self, filter_policy, payload: dict) -> bool: - """ - This method evaluates the filter policy against the JSON decoded payload. - Although it's not documented anywhere, AWS allows `.` in the fields name in the filter policy and the payload, - and will evaluate them. However, it's not JSONPath compatible: - Example: - Policy: `{"field1.field2": "value1"}` - This policy will match both `{"field1.field2": "value1"}` and {"field1: {"field2": "value1"}}`, unlike JSONPath - for which `.` points to a child node. - This might show they are flattening the both dictionaries to a single level for an easier matching without - recursion. - :param filter_policy: a dict, starting at the FilterPolicy - :param payload: a dict, starting at the MessageBody - :return: True if the payload respect the filter policy, otherwise False - """ - flat_policy = self._flatten_dict(filter_policy) - flat_payloads = self._flatten_dict_with_list(payload) - for key, values in flat_policy.items(): - if not any( - self._evaluate_condition( - flat_payload.get(key), condition, field_exists=key in flat_payload - ) - for condition in values - for flat_payload in flat_payloads - ): - return False - return True - - def _evaluate_filter_policy_conditions_on_attribute( - self, conditions, attribute, field_exists: bool - ): - if not isinstance(conditions, list): - conditions = [conditions] - - tpe = attribute.get("DataType") or attribute.get("Type") if attribute else None - val = attribute.get("StringValue") or attribute.get("Value") if attribute else None - if attribute is not None and tpe == "String.Array": - try: - values = json.loads(val) - except ValueError: - return False - for value in values: - for condition in conditions: - if self._evaluate_condition(value, condition, field_exists): - return True - else: - for condition in conditions: - value = val or None - if self._evaluate_condition(value, condition, field_exists): - return True - - return False - - def _evaluate_condition(self, value, condition, field_exists: bool): - if not isinstance(condition, dict): - return field_exists and value == condition - elif (must_exist := condition.get("exists")) is not None: - # if must_exists is True then field_exists must be True - # if must_exists is False then fields_exists must be False - return must_exist == field_exists - elif value is None: - # the remaining conditions require the value to not be None - return False - elif anything_but := condition.get("anything-but"): - return value not in anything_but - elif prefix := (condition.get("prefix")): - return value.startswith(prefix) - elif numeric_condition := condition.get("numeric"): - return self._evaluate_numeric_condition(numeric_condition, value) - return False - - @staticmethod - def _evaluate_numeric_condition(conditions, value): - try: - # try if the value is numeric - value = float(value) - except ValueError: - # the value is not numeric, the condition is False - return False - - for i in range(0, len(conditions), 2): - operator = conditions[i] - operand = float(conditions[i + 1]) - - if operator == "=": - if value != operand: - return False - elif operator == ">": - if value <= operand: - return False - elif operator == "<": - if value >= operand: - return False - elif operator == ">=": - if value < operand: - return False - elif operator == "<=": - if value > operand: - return False - - return True - - @staticmethod - def _flatten_dict(nested_dict: dict): - """ - Takes a dictionary as input and will output the dictionary on a single level. - Input: - `{"field1": {"field2: {"field3: "val1", "field4": "val2"}}}` - Output: - `{ - "field1.field2.field3": "val1", - "field1.field2.field4": "val1" - }` - :param nested_dict: a (nested) dictionary - :return: a list of flattened dictionaries with no nested dict or list inside, flattened to a - single level, one list item for every list item encountered - """ - flatten = {} - - def _traverse(_policy: dict, parent_key=None): - for key, values in _policy.items(): - flattened_parent_key = key if not parent_key else f"{parent_key}.{key}" - if not isinstance(values, dict): - flatten[flattened_parent_key] = values - else: - _traverse(values, parent_key=flattened_parent_key) - - _traverse(nested_dict) - return flatten - - @staticmethod - def _flatten_dict_with_list(nested_dict: dict) -> list[dict]: - """ - Takes a dictionary as input and will output the dictionary on a single level. - The dictionary can have lists containing other dictionaries, and one root level entry will be created for every - item in a list. - Input: - `{"field1": { - "field2: [ - {"field3: "val1", "field4": "val2"}, - {"field3: "val3", "field4": "val4"}, - } - ]}` - Output: - `[ - { - "field1.field2.field3": "val1", - "field1.field2.field4": "val2" - }, - { - "field1.field2.field3": "val3", - "field1.field2.field4": "val4" - }, - ]` - :param nested_dict: a (nested) dictionary - :return: flatten_dict: a dictionary with no nested dict inside, flattened to a single level - """ - flattened = [] - current_object = {} - - def _traverse(_object, parent_key=None): - if isinstance(_object, dict): - for key, values in _object.items(): - flattened_parent_key = key if not parent_key else f"{parent_key}.{key}" - _traverse(values, flattened_parent_key) - - # we don't have to worry about `parent_key` being None for list or any other type, because we have a check - # that the first object is always a dict, thus setting a parent key on first iteration - elif isinstance(_object, list): - for value in _object: - if isinstance(value, (dict, list)): - _traverse(value, parent_key=parent_key) - else: - current_object[parent_key] = value - - if current_object: - flattened.append({**current_object}) - current_object.clear() - else: - current_object[parent_key] = _object - - _traverse(nested_dict) - - # if the payload did not have any list, we manually append the current object - if not flattened: - flattened.append(current_object) - - return flattened - - class PublishDispatcher: """ The PublishDispatcher is responsible for dispatching the publishing of SNS messages asynchronously to worker diff --git a/tests/aws/services/sns/test_sns.py b/tests/aws/services/sns/test_sns.py index f8bdaf01ed2e3..df0516928c9e8 100644 --- a/tests/aws/services/sns/test_sns.py +++ b/tests/aws/services/sns/test_sns.py @@ -1,6 +1,5 @@ import base64 import contextlib -import copy import json import logging import queue @@ -2842,1055 +2841,6 @@ def check_subscription(): retry(check_subscription, retries=PUBLICATION_RETRIES, sleep=PUBLICATION_TIMEOUT) -class TestSNSFilter: - @pytest.fixture - def sns_create_sqs_subscription_with_filter_policy( - self, sns_create_sqs_subscription, aws_client - ): - def _inner(topic_arn: str, queue_url: str, filter_scope: str, filter_policy: dict): - subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) - subscription_arn = subscription["SubscriptionArn"] - - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicyScope", - AttributeValue=filter_scope, - ) - - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicy", - AttributeValue=json.dumps(filter_policy), - ) - - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="RawMessageDelivery", - AttributeValue="true", - ) - return subscription_arn - - yield _inner - - @markers.aws.validated - def test_filter_policy( - self, sqs_create_queue, sns_create_topic, sns_create_sqs_subscription, snapshot, aws_client - ): - topic_arn = sns_create_topic()["TopicArn"] - queue_url = sqs_create_queue() - subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) - subscription_arn = subscription["SubscriptionArn"] - - filter_policy = {"attr1": [{"numeric": [">", 0, "<=", 100]}]} - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicy", - AttributeValue=json.dumps(filter_policy), - ) - - response_attributes = aws_client.sns.get_subscription_attributes( - SubscriptionArn=subscription_arn - ) - snapshot.match("subscription-attributes", response_attributes) - - response_0 = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=1 - ) - snapshot.match("messages-0", response_0) - # get number of messages - num_msgs_0 = len(response_0.get("Messages", [])) - - # publish message that satisfies the filter policy, assert that message is received - message = "This is a test message" - message_attributes = {"attr1": {"DataType": "Number", "StringValue": "99"}} - aws_client.sns.publish( - TopicArn=topic_arn, - Message=message, - MessageAttributes=message_attributes, - ) - - response_1 = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 - ) - snapshot.match("messages-1", response_1) - - num_msgs_1 = len(response_1["Messages"]) - assert num_msgs_1 == (num_msgs_0 + 1) - - # publish message that does not satisfy the filter policy, assert that message is not received - message = "This is another test message" - aws_client.sns.publish( - TopicArn=topic_arn, - Message=message, - MessageAttributes={"attr1": {"DataType": "Number", "StringValue": "111"}}, - ) - - response_2 = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 - ) - snapshot.match("messages-2", response_2) - num_msgs_2 = len(response_2["Messages"]) - assert num_msgs_2 == num_msgs_1 - - # remove all messages from the queue - receipt_handle = response_1["Messages"][0]["ReceiptHandle"] - aws_client.sqs.delete_message(QueueUrl=queue_url, ReceiptHandle=receipt_handle) - - # test with a property value set to null with an OR operator with anything-but - filter_policy = json.dumps({"attr1": [None, {"anything-but": "whatever"}]}) - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicy", - AttributeValue=filter_policy, - ) - - def get_filter_policy(): - subscription_attrs = aws_client.sns.get_subscription_attributes( - SubscriptionArn=subscription_arn - ) - return subscription_attrs["Attributes"]["FilterPolicy"] - - # wait for the new filter policy to be in effect - poll_condition(lambda: get_filter_policy() == filter_policy, timeout=4) - response_attributes_2 = aws_client.sns.get_subscription_attributes( - SubscriptionArn=subscription_arn - ) - snapshot.match("subscription-attributes-2", response_attributes_2) - - # publish message that does not satisfy the filter policy, assert that message is not received - message = "This the test message for null" - aws_client.sns.publish( - TopicArn=topic_arn, - Message=message, - ) - - response_3 = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 - ) - snapshot.match("messages-3", response_3) - assert "Messages" not in response_3 or response_3["Messages"] == [] - - # unset the filter policy - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicy", - AttributeValue="", - ) - - def check_no_filter_policy(): - subscription_attrs = aws_client.sns.get_subscription_attributes( - SubscriptionArn=subscription_arn - ) - return "FilterPolicy" not in subscription_attrs["Attributes"] - - poll_condition(check_no_filter_policy, timeout=4) - - # publish message that does not satisfy the previous filter policy, but assert that the message is received now - message = "This the test message for null" - aws_client.sns.publish( - TopicArn=topic_arn, - Message=message, - ) - - response_4 = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 - ) - snapshot.match("messages-4", response_4) - - @markers.aws.validated - def test_exists_filter_policy( - self, sqs_create_queue, sns_create_topic, sns_create_sqs_subscription, snapshot, aws_client - ): - topic_arn = sns_create_topic()["TopicArn"] - queue_url = sqs_create_queue() - subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) - subscription_arn = subscription["SubscriptionArn"] - - filter_policy = {"store": [{"exists": True}]} - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicy", - AttributeValue=json.dumps(filter_policy), - ) - - response_attributes = aws_client.sns.get_subscription_attributes( - SubscriptionArn=subscription_arn - ) - snapshot.match("subscription-attributes-policy-1", response_attributes) - - response_0 = aws_client.sqs.receive_message(QueueUrl=queue_url, VisibilityTimeout=0) - snapshot.match("messages-0", response_0) - # get number of messages - num_msgs_0 = len(response_0.get("Messages", [])) - - # publish message that satisfies the filter policy, assert that message is received - message_1 = "message-1" - aws_client.sns.publish( - TopicArn=topic_arn, - Message=message_1, - MessageAttributes={ - "store": {"DataType": "Number", "StringValue": "99"}, - "def": {"DataType": "Number", "StringValue": "99"}, - }, - ) - response_1 = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 - ) - snapshot.match("messages-1", response_1) - num_msgs_1 = len(response_1["Messages"]) - assert num_msgs_1 == (num_msgs_0 + 1) - - # publish message that does not satisfy the filter policy, assert that message is not received - message_2 = "message-2" - aws_client.sns.publish( - TopicArn=topic_arn, - Message=message_2, - MessageAttributes={"attr1": {"DataType": "Number", "StringValue": "111"}}, - ) - - response_2 = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 - ) - snapshot.match("messages-2", response_2) - num_msgs_2 = len(response_2["Messages"]) - assert num_msgs_2 == num_msgs_1 - - # delete first message - aws_client.sqs.delete_message( - QueueUrl=queue_url, ReceiptHandle=response_1["Messages"][0]["ReceiptHandle"] - ) - - # test with exist operator set to false. - filter_policy = json.dumps({"store": [{"exists": False}]}) - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicy", - AttributeValue=filter_policy, - ) - - def get_filter_policy(): - subscription_attrs = aws_client.sns.get_subscription_attributes( - SubscriptionArn=subscription_arn - ) - return subscription_attrs["Attributes"]["FilterPolicy"] - - # wait for the new filter policy to be in effect - poll_condition(lambda: get_filter_policy() == filter_policy, timeout=4) - response_attributes_2 = aws_client.sns.get_subscription_attributes( - SubscriptionArn=subscription_arn - ) - snapshot.match("subscription-attributes-policy-2", response_attributes_2) - - # publish message that satisfies the filter policy, assert that message is received - message_3 = "message-3" - aws_client.sns.publish( - TopicArn=topic_arn, - Message=message_3, - MessageAttributes={"def": {"DataType": "Number", "StringValue": "99"}}, - ) - - response_3 = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 - ) - snapshot.match("messages-3", response_3) - num_msgs_3 = len(response_3["Messages"]) - assert num_msgs_3 == num_msgs_1 - - # publish message that does not satisfy the filter policy, assert that message is not received - message_4 = "message-4" - aws_client.sns.publish( - TopicArn=topic_arn, - Message=message_4, - MessageAttributes={ - "store": {"DataType": "Number", "StringValue": "99"}, - "def": {"DataType": "Number", "StringValue": "99"}, - }, - ) - - response_4 = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 - ) - snapshot.match("messages-4", response_4) - num_msgs_4 = len(response_4["Messages"]) - assert num_msgs_4 == num_msgs_3 - - @markers.aws.validated - def test_exists_filter_policy_attributes_array( - self, - sqs_create_queue, - sns_create_topic, - sns_create_sqs_subscription_with_filter_policy, - snapshot, - aws_client, - ): - topic_arn = sns_create_topic()["TopicArn"] - queue_url = sqs_create_queue() - filter_policy = {"store": ["value1"]} - subscription_arn = sns_create_sqs_subscription_with_filter_policy( - topic_arn=topic_arn, - queue_url=queue_url, - filter_scope="MessageAttributes", - filter_policy=filter_policy, - ) - - response_attributes = aws_client.sns.get_subscription_attributes( - SubscriptionArn=subscription_arn - ) - snapshot.match("subscription-attributes-policy", response_attributes) - - response_0 = aws_client.sqs.receive_message(QueueUrl=queue_url, VisibilityTimeout=0) - snapshot.match("messages-init", response_0) - - # publish message that satisfies the filter policy, assert that message is received - message = "message-1" - aws_client.sns.publish( - TopicArn=topic_arn, - Message=message, - MessageAttributes={ - "store": {"DataType": "String", "StringValue": "value1"}, - }, - ) - response_1 = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 - ) - aws_client.sqs.delete_message( - QueueUrl=queue_url, ReceiptHandle=response_1["Messages"][0]["ReceiptHandle"] - ) - snapshot.match("messages-1", response_1) - - # publish message that satisfies the filter policy but with String.Array - message = "message-2" - aws_client.sns.publish( - TopicArn=topic_arn, - Message=message, - MessageAttributes={ - "store": { - "DataType": "String.Array", - "StringValue": json.dumps(["value1", "value2"]), - }, - }, - ) - response_2 = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 - ) - aws_client.sqs.delete_message( - QueueUrl=queue_url, ReceiptHandle=response_2["Messages"][0]["ReceiptHandle"] - ) - snapshot.match("messages-2", response_2) - - # publish message that does not satisfy the filter policy with String.Array - message = "message-3" - aws_client.sns.publish( - TopicArn=topic_arn, - Message=message, - MessageAttributes={ - "store": { - "DataType": "String.Array", - "StringValue": json.dumps(["value2", "value3"]), - }, - }, - ) - response_3 = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 - ) - snapshot.match("messages-3", response_3) - - @markers.aws.validated - def test_set_subscription_filter_policy_scope( - self, sqs_create_queue, sns_create_topic, sns_create_sqs_subscription, snapshot, aws_client - ): - topic_arn = sns_create_topic()["TopicArn"] - queue_url = sqs_create_queue() - subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) - subscription_arn = subscription["SubscriptionArn"] - - # we fetch the default subscription attributes - # note: the FilterPolicyScope is not present in the response - subscription_attrs = aws_client.sns.get_subscription_attributes( - SubscriptionArn=subscription_arn - ) - snapshot.match("sub-attrs-default", subscription_attrs) - - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicyScope", - AttributeValue="MessageBody", - ) - - # we fetch the subscription attributes after setting the FilterPolicyScope - # note: the FilterPolicyScope is still not present in the response - subscription_attrs = aws_client.sns.get_subscription_attributes( - SubscriptionArn=subscription_arn - ) - snapshot.match("sub-attrs-filter-scope-body", subscription_attrs) - - # we try to set random values to the FilterPolicyScope - with pytest.raises(ClientError) as e: - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicyScope", - AttributeValue="RandomValue", - ) - - snapshot.match("sub-attrs-filter-scope-error", e.value.response) - - # we try to set a FilterPolicy to see if it will show the FilterPolicyScope in the attributes - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicy", - AttributeValue=json.dumps({"attr": ["match-this"]}), - ) - # the FilterPolicyScope is now present in the attributes - subscription_attrs = aws_client.sns.get_subscription_attributes( - SubscriptionArn=subscription_arn - ) - snapshot.match("sub-attrs-after-setting-policy", subscription_attrs) - - @markers.aws.validated - def test_sub_filter_policy_nested_property( - self, sqs_create_queue, sns_create_topic, sns_create_sqs_subscription, snapshot, aws_client - ): - topic_arn = sns_create_topic()["TopicArn"] - queue_url = sqs_create_queue() - subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) - subscription_arn = subscription["SubscriptionArn"] - - # see https://aws.amazon.com/blogs/compute/introducing-payload-based-message-filtering-for-amazon-sns/ - nested_filter_policy = {"object": {"key": [{"prefix": "auto-"}]}} - with pytest.raises(ClientError) as e: - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicy", - AttributeValue=json.dumps(nested_filter_policy), - ) - snapshot.match("sub-filter-policy-nested-error", e.value.response) - - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicyScope", - AttributeValue="MessageBody", - ) - - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicy", - AttributeValue=json.dumps(nested_filter_policy), - ) - - # the FilterPolicyScope is now present in the attributes - subscription_attrs = aws_client.sns.get_subscription_attributes( - SubscriptionArn=subscription_arn - ) - snapshot.match("sub-attrs-after-setting-nested-policy", subscription_attrs) - - @markers.aws.validated - @markers.snapshot.skip_snapshot_verify( - paths=[ - "$.sub-filter-policy-rule-no-list.Error.Message", # message contains java trace in AWS, assert instead - ] - ) - def test_sub_filter_policy_nested_property_constraints( - self, sqs_create_queue, sns_create_topic, sns_create_sqs_subscription, snapshot, aws_client - ): - # https://docs.aws.amazon.com/sns/latest/dg/subscription-filter-policy-constraints.html - topic_arn = sns_create_topic()["TopicArn"] - queue_url = sqs_create_queue() - subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) - subscription_arn = subscription["SubscriptionArn"] - - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicyScope", - AttributeValue="MessageBody", - ) - - nested_filter_policy = { - "key_a": { - "key_b": {"key_c": ["value_one", "value_two", "value_three", "value_four"]}, - }, - "key_d": {"key_e": ["value_one", "value_two", "value_three"]}, - "key_f": ["value_one", "value_two", "value_three"], - } - # The first array has four values in a three-level nested key, and the second has three values in a two-level - # nested key. The total combination is calculated as follows: - # 3 x 4 x 2 x 3 x 1 x 3 = 216 - with pytest.raises(ClientError) as e: - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicy", - AttributeValue=json.dumps(nested_filter_policy), - ) - snapshot.match("sub-filter-policy-nested-error-too-many-combinations", e.value.response) - - flat_filter_policy = { - "key_a": ["value_one"], - "key_b": ["value_two"], - "key_c": ["value_three"], - "key_d": ["value_four"], - "key_e": ["value_five"], - "key_f": ["value_six"], - } - # A filter policy can have a maximum of five attribute names. For a nested policy, only parent keys are counted. - with pytest.raises(ClientError) as e: - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicy", - AttributeValue=json.dumps(flat_filter_policy), - ) - snapshot.match("sub-filter-policy-max-attr-keys", e.value.response) - - flat_filter_policy = {"key_a": "value_one"} - # Rules should be contained in a list - with pytest.raises(ClientError) as e: - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicy", - AttributeValue=json.dumps(flat_filter_policy), - ) - snapshot.match("sub-filter-policy-rule-no-list", e.value.response) - assert e.value.response["Error"]["Message"].startswith( - 'Invalid parameter: FilterPolicy: "key_a" must be an object or an array' - ) - - @markers.aws.validated - @pytest.mark.parametrize("raw_message_delivery", [True, False]) - def test_filter_policy_on_message_body( - self, - sqs_create_queue, - sns_create_topic, - sns_create_sqs_subscription, - snapshot, - raw_message_delivery, - aws_client, - ): - topic_arn = sns_create_topic()["TopicArn"] - queue_url = sqs_create_queue() - subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) - subscription_arn = subscription["SubscriptionArn"] - # see https://aws.amazon.com/blogs/compute/introducing-payload-based-message-filtering-for-amazon-sns/ - nested_filter_policy = { - "object": { - "key": [{"prefix": "auto-"}, "hardcodedvalue"], - "nested_key": [{"exists": False}], - }, - "test": [{"exists": False}], - } - - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicyScope", - AttributeValue="MessageBody", - ) - - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicy", - AttributeValue=json.dumps(nested_filter_policy), - ) - - if raw_message_delivery: - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="RawMessageDelivery", - AttributeValue="true", - ) - - response = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=1 - ) - snapshot.match("recv-init", response) - # assert there are no messages in the queue - assert "Messages" not in response or response["Messages"] == [] - - # publish messages that satisfies the filter policy, assert that messages are received - messages = [ - {"object": {"key": "auto-test"}}, - {"object": {"key": "hardcodedvalue"}}, - ] - for i, message in enumerate(messages): - aws_client.sns.publish( - TopicArn=topic_arn, - Message=json.dumps(message), - ) - - response = aws_client.sqs.receive_message( - QueueUrl=queue_url, - VisibilityTimeout=0, - WaitTimeSeconds=5 if is_aws_cloud() else 2, - ) - snapshot.match(f"recv-passed-msg-{i}", response) - receipt_handle = response["Messages"][0]["ReceiptHandle"] - aws_client.sqs.delete_message(QueueUrl=queue_url, ReceiptHandle=receipt_handle) - - # publish messages that do not satisfy the filter policy, assert those messages are not received - messages = [ - {"object": {"key": "test-auto"}}, - {"object": {"key": "auto-test"}, "test": "just-exists"}, - {"object": {"key": "auto-test", "nested_key": "just-exists"}}, - {"object": {"test": "auto-test"}}, - {"test": "auto-test"}, - ] - for message in messages: - aws_client.sns.publish( - TopicArn=topic_arn, - Message=json.dumps(message), - ) - - response = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=5 if is_aws_cloud() else 2 - ) - # assert there are no messages in the queue - assert "Messages" not in response or response["Messages"] == [] - - # publish message that does not satisfy the filter policy as it's not even JSON, or not a JSON object - message = "Regular string message" - aws_client.sns.publish( - TopicArn=topic_arn, - Message=message, - ) - aws_client.sns.publish( - TopicArn=topic_arn, - Message=json.dumps(message), # send it JSON encoded, but not an object - ) - - response = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=2 - ) - # assert there are no messages in the queue - assert "Messages" not in response or response["Messages"] == [] - - @markers.aws.validated - def test_filter_policy_for_batch( - self, sqs_create_queue, sns_create_topic, sns_create_sqs_subscription, snapshot, aws_client - ): - topic_arn = sns_create_topic()["TopicArn"] - queue_url_with_filter = sqs_create_queue() - subscription_with_filter = sns_create_sqs_subscription( - topic_arn=topic_arn, queue_url=queue_url_with_filter - ) - subscription_with_filter_arn = subscription_with_filter["SubscriptionArn"] - - queue_url_no_filter = sqs_create_queue() - subscription_no_filter = sns_create_sqs_subscription( - topic_arn=topic_arn, queue_url=queue_url_no_filter - ) - subscription_no_filter_arn = subscription_no_filter["SubscriptionArn"] - - filter_policy = {"attr1": [{"numeric": [">", 0, "<=", 100]}]} - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_with_filter_arn, - AttributeName="FilterPolicy", - AttributeValue=json.dumps(filter_policy), - ) - - response_attributes = aws_client.sns.get_subscription_attributes( - SubscriptionArn=subscription_with_filter_arn - ) - snapshot.match("subscription-attributes-with-filter", response_attributes) - - response_attributes = aws_client.sns.get_subscription_attributes( - SubscriptionArn=subscription_no_filter_arn - ) - snapshot.match("subscription-attributes-no-filter", response_attributes) - - sqs_wait_time = 4 if is_aws_cloud() else 1 - - response_before_publish_no_filter = aws_client.sqs.receive_message( - QueueUrl=queue_url_with_filter, VisibilityTimeout=0, WaitTimeSeconds=sqs_wait_time - ) - snapshot.match("messages-no-filter-before-publish", response_before_publish_no_filter) - - response_before_publish_filter = aws_client.sqs.receive_message( - QueueUrl=queue_url_with_filter, VisibilityTimeout=0, WaitTimeSeconds=sqs_wait_time - ) - snapshot.match("messages-with-filter-before-publish", response_before_publish_filter) - - # publish message that satisfies the filter policy, assert that message is received - message = "This is a test message" - message_attributes = {"attr1": {"DataType": "Number", "StringValue": "99"}} - aws_client.sns.publish_batch( - TopicArn=topic_arn, - PublishBatchRequestEntries=[ - { - "Id": "1", - "Message": message, - "MessageAttributes": message_attributes, - } - ], - ) - - response_after_publish_no_filter = aws_client.sqs.receive_message( - QueueUrl=queue_url_no_filter, VisibilityTimeout=0, WaitTimeSeconds=sqs_wait_time - ) - snapshot.match("messages-no-filter-after-publish-ok", response_after_publish_no_filter) - aws_client.sqs.delete_message( - QueueUrl=queue_url_no_filter, - ReceiptHandle=response_after_publish_no_filter["Messages"][0]["ReceiptHandle"], - ) - - response_after_publish_filter = aws_client.sqs.receive_message( - QueueUrl=queue_url_with_filter, VisibilityTimeout=0, WaitTimeSeconds=sqs_wait_time - ) - snapshot.match("messages-with-filter-after-publish-ok", response_after_publish_filter) - aws_client.sqs.delete_message( - QueueUrl=queue_url_with_filter, - ReceiptHandle=response_after_publish_filter["Messages"][0]["ReceiptHandle"], - ) - - # publish message that does not satisfy the filter policy, assert that message is not received by the - # subscription with the filter and received by the other - aws_client.sns.publish_batch( - TopicArn=topic_arn, - PublishBatchRequestEntries=[ - { - "Id": "1", - "Message": "This is another test message", - "MessageAttributes": {"attr1": {"DataType": "Number", "StringValue": "111"}}, - } - ], - ) - - response_after_publish_no_filter = aws_client.sqs.receive_message( - QueueUrl=queue_url_no_filter, VisibilityTimeout=0, WaitTimeSeconds=sqs_wait_time - ) - # there should be 1 message in the queue, latest sent - snapshot.match("messages-no-filter-after-publish-ok-1", response_after_publish_no_filter) - - response_after_publish_filter = aws_client.sqs.receive_message( - QueueUrl=queue_url_with_filter, VisibilityTimeout=0, WaitTimeSeconds=sqs_wait_time - ) - # there should be no messages in this queue - snapshot.match("messages-with-filter-after-publish-filtered", response_after_publish_filter) - - @markers.aws.validated - def test_filter_policy_on_message_body_dot_attribute( - self, - sqs_create_queue, - sns_create_topic, - sns_create_sqs_subscription, - snapshot, - aws_client, - ): - topic_arn = sns_create_topic()["TopicArn"] - queue_url = sqs_create_queue() - subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) - subscription_arn = subscription["SubscriptionArn"] - - nested_filter_policy = json.dumps( - { - "object.nested": ["string.value"], - } - ) - - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicyScope", - AttributeValue="MessageBody", - ) - - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicy", - AttributeValue=nested_filter_policy, - ) - - def get_filter_policy(): - subscription_attrs = aws_client.sns.get_subscription_attributes( - SubscriptionArn=subscription_arn - ) - return subscription_attrs["Attributes"]["FilterPolicy"] - - # wait for the new filter policy to be in effect - poll_condition(lambda: get_filter_policy() == nested_filter_policy, timeout=4) - - response = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=1 - ) - snapshot.match("recv-init", response) - # assert there are no messages in the queue - assert "Messages" not in response or response["Messages"] == [] - - def _verify_and_snapshot_sqs_messages(msg_to_send: list[dict], snapshot_prefix: str): - for i, _message in enumerate(msg_to_send): - aws_client.sns.publish( - TopicArn=topic_arn, - Message=json.dumps(_message), - ) - - _response = aws_client.sqs.receive_message( - QueueUrl=queue_url, - VisibilityTimeout=0, - WaitTimeSeconds=5 if is_aws_cloud() else 2, - ) - snapshot.match(f"{snapshot_prefix}-{i}", _response) - receipt_handle = _response["Messages"][0]["ReceiptHandle"] - aws_client.sqs.delete_message(QueueUrl=queue_url, ReceiptHandle=receipt_handle) - - # publish messages that satisfies the filter policy, assert that messages are received - messages = [ - {"object": {"nested": "string.value"}}, - {"object.nested": "string.value"}, - ] - _verify_and_snapshot_sqs_messages(messages, snapshot_prefix="recv-nested-msg") - - # publish messages that do not satisfy the filter policy, assert those messages are not received - messages = [ - {"object": {"nested": "test-auto"}}, - ] - for message in messages: - aws_client.sns.publish( - TopicArn=topic_arn, - Message=json.dumps(message), - ) - - response = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=5 if is_aws_cloud() else 2 - ) - # assert there are no messages in the queue - assert "Messages" not in response or response["Messages"] == [] - - # assert with more nesting - deep_nested_filter_policy = json.dumps( - { - "object.nested.test": ["string.value"], - } - ) - - aws_client.sns.set_subscription_attributes( - SubscriptionArn=subscription_arn, - AttributeName="FilterPolicy", - AttributeValue=deep_nested_filter_policy, - ) - # wait for the new filter policy to be in effect - poll_condition(lambda: get_filter_policy() == deep_nested_filter_policy, timeout=4) - - messages = [ - {"object": {"nested": {"test": "string.value"}}}, - {"object.nested.test": "string.value"}, - {"object.nested": {"test": "string.value"}}, - {"object": {"nested.test": "string.value"}}, - ] - _verify_and_snapshot_sqs_messages(messages, snapshot_prefix="recv-deep-nested-msg") - # publish messages that do not satisfy the filter policy, assert those messages are not received - messages = [ - {"object": {"nested": {"test": "string.notvalue"}}}, - ] - for message in messages: - aws_client.sns.publish( - TopicArn=topic_arn, - Message=json.dumps(message), - ) - - response = aws_client.sqs.receive_message( - QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=5 if is_aws_cloud() else 2 - ) - # assert there are no messages in the queue - assert "Messages" not in response or response["Messages"] == [] - - @markers.aws.validated - def test_filter_policy_on_message_body_array_attributes( - self, - sqs_create_queue, - sns_create_topic, - sns_create_sqs_subscription_with_filter_policy, - snapshot, - aws_client, - ): - topic_arn = sns_create_topic()["TopicArn"] - queue_url_1 = sqs_create_queue() - queue_url_2 = sqs_create_queue() - - filter_policy_1 = {"headers": {"route-to": ["queue1"]}} - sns_create_sqs_subscription_with_filter_policy( - topic_arn=topic_arn, - queue_url=queue_url_1, - filter_scope="MessageBody", - filter_policy=filter_policy_1, - ) - - filter_policy_2 = {"headers": {"route-to": ["queue2"]}} - sns_create_sqs_subscription_with_filter_policy( - topic_arn=topic_arn, - queue_url=queue_url_2, - filter_scope="MessageBody", - filter_policy=filter_policy_2, - ) - - queues = [queue_url_1, queue_url_2] - - # publish messages that satisfies the filter policy, assert that messages are received - messages = [ - {"headers": {"route-to": ["queue3"]}}, - {"headers": {"route-to": ["queue1"]}}, - {"headers": {"route-to": ["queue2"]}}, - {"headers": {"route-to": ["queue1", "queue2"]}}, - ] - for i, message in enumerate(messages): - aws_client.sns.publish( - TopicArn=topic_arn, - Message=json.dumps(message), - ) - - def get_messages(_queue_url: str, _recv_messages: list): - # due to the random nature of receiving SQS messages, we need to consolidate a single object to match - sqs_response = aws_client.sqs.receive_message( - QueueUrl=_queue_url, - WaitTimeSeconds=1, - VisibilityTimeout=0, - MessageAttributeNames=["All"], - AttributeNames=["All"], - ) - for _message in sqs_response["Messages"]: - _recv_messages.append(_message) - aws_client.sqs.delete_message( - QueueUrl=_queue_url, ReceiptHandle=_message["ReceiptHandle"] - ) - - assert len(_recv_messages) == 2 - - for i, queue_url in enumerate(queues): - recv_messages = [] - retry( - get_messages, - retries=10, - sleep=0.1, - _queue_url=queue_url, - _recv_messages=recv_messages, - ) - # we need to sort the list (the order does not matter as we're not using FIFO) - recv_messages.sort(key=itemgetter("Body")) - snapshot.match(f"messages-queue-{i}", {"Messages": recv_messages}) - - @markers.aws.validated - def test_filter_policy_on_message_body_array_of_object_attributes( - self, - sqs_create_queue, - sns_create_topic, - sns_create_sqs_subscription_with_filter_policy, - snapshot, - aws_client, - ): - # example from https://aws.amazon.com/blogs/compute/introducing-payload-based-message-filtering-for-amazon-sns/ - topic_arn = sns_create_topic()["TopicArn"] - queue_url = sqs_create_queue() - # complex filter policy with different level of nesting - filter_policy = { - "Records": { - "s3": {"object": {"key": [{"prefix": "auto-"}]}}, - "eventName": [{"prefix": "ObjectCreated:"}], - } - } - - sns_create_sqs_subscription_with_filter_policy( - topic_arn=topic_arn, - queue_url=queue_url, - filter_scope="MessageBody", - filter_policy=filter_policy, - ) - - # stripped down events - s3_event_auto_insurance_created = { - "Records": [ - { - "eventSource": "aws:s3", - "eventTime": "2022-11-21T03:41:29.743Z", - "eventName": "ObjectCreated:Put", - "s3": { - "bucket": { - "name": "insurance-bucket-demo", - "arn": "arn:aws:s3:::insurance-bucket-demo", - }, - "object": { - "key": "auto-insurance-2314.xml", - "size": 17, - }, - }, - } - ] - } - # copy the object to modify it - s3_event_auto_insurance_removed = copy.deepcopy(s3_event_auto_insurance_created) - s3_event_auto_insurance_removed["Records"][0]["eventName"] = "ObjectRemoved:Delete" - - # copy the object to modify it - s3_event_home_insurance_created = copy.deepcopy(s3_event_auto_insurance_created) - s3_event_home_insurance_created["Records"][0]["s3"]["object"]["key"] = ( - "home-insurance-2314.xml" - ) - - # stripped down events - s3_event_multiple_records = { - "Records": [ - { - "eventSource": "aws:s3", - "eventName": "ObjectCreated:Put", - "s3": { - # this object is a list of list of dict, and it works in AWS - "object": [ - [ - { - "key": "auto-insurance-2314.xml", - "size": 17, - } - ] - ], - }, - }, - { - "eventSource": "aws:s3", - "eventName": "ObjectRemoved:Delete", - "s3": { - "object": { - "key": "home-insurance-2314.xml", - "size": 17, - } - }, - }, - ] - } - - messages = [ - s3_event_multiple_records, - s3_event_auto_insurance_removed, - s3_event_home_insurance_created, - s3_event_auto_insurance_created, - ] - for i, message in enumerate(messages): - aws_client.sns.publish( - TopicArn=topic_arn, - Message=json.dumps(message), - ) - - def get_messages(_queue_url: str, _received_messages: list): - # due to the random nature of receiving SQS messages, we need to consolidate a single object to match - sqs_response = aws_client.sqs.receive_message( - QueueUrl=_queue_url, - WaitTimeSeconds=1, - VisibilityTimeout=0, - MessageAttributeNames=["All"], - AttributeNames=["All"], - ) - for _message in sqs_response["Messages"]: - _received_messages.append(_message) - aws_client.sqs.delete_message( - QueueUrl=_queue_url, ReceiptHandle=_message["ReceiptHandle"] - ) - - assert len(_received_messages) == 2 - - received_messages = [] - retry( - get_messages, - retries=10, - sleep=0.1, - _queue_url=queue_url, - _received_messages=received_messages, - ) - # we need to sort the list (the order does not matter as we're not using FIFO) - received_messages.sort(key=itemgetter("Body")) - snapshot.match("messages", {"Messages": received_messages}) - - class TestSNSPlatformEndpoint: @markers.aws.only_localstack def test_subscribe_platform_endpoint( @@ -4197,6 +3147,35 @@ def test_publish_wrong_phone_format( class TestSNSSubscriptionHttp: + @markers.aws.validated + def test_http_subscription_response( + self, + sns_create_topic, + sns_subscription, + aws_client, + snapshot, + ): + topic_arn = sns_create_topic()["TopicArn"] + snapshot.match("topic-arn", {"TopicArn": topic_arn}) + + # we need to hit whatever URL, even external, the publishing is async, but we need an endpoint who won't + # confirm the subscription + subscription = sns_subscription( + TopicArn=topic_arn, + Protocol="http", + Endpoint="http://example.com", + ReturnSubscriptionArn=False, + ) + snapshot.match("subscription", subscription) + + subscription_with_arn = sns_subscription( + TopicArn=topic_arn, + Protocol="http", + Endpoint="http://example.com", + ReturnSubscriptionArn=True, + ) + snapshot.match("subscription-with-arn", subscription_with_arn) + @markers.aws.manual_setup_required def test_redrive_policy_http_subscription( self, sns_create_topic, sqs_create_queue, sqs_get_queue_arn, sns_subscription, aws_client @@ -4765,6 +3744,15 @@ def test_cross_account_access(self, sns_primary_client, sns_secondary_client): subscriptions = [s["SubscriptionArn"] for s in response["Subscriptions"]] assert subscription_arn in subscriptions + response = sns_primary_client.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="RawMessageDelivery", + AttributeValue="true", + ) + assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 + response = sns_primary_client.get_subscription_attributes(SubscriptionArn=subscription_arn) + assert response["Attributes"]["RawMessageDelivery"] == "true" + assert sns_secondary_client.delete_topic(TopicArn=topic_arn) @markers.aws.only_localstack diff --git a/tests/aws/services/sns/test_sns.snapshot.json b/tests/aws/services/sns/test_sns.snapshot.json index 73e4e694abaaf..7eb1125759218 100644 --- a/tests/aws/services/sns/test_sns.snapshot.json +++ b/tests/aws/services/sns/test_sns.snapshot.json @@ -3233,801 +3233,6 @@ } } }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy": { - "recorded-date": "25-01-2024, 18:07:57", - "recorded-content": { - "subscription-attributes": { - "Attributes": { - "ConfirmationWasAuthenticated": "true", - "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", - "FilterPolicy": { - "attr1": [ - { - "numeric": [ - ">", - 0, - "<=", - 100 - ] - } - ] - }, - "FilterPolicyScope": "MessageAttributes", - "Owner": "111111111111", - "PendingConfirmation": "false", - "Protocol": "sqs", - "RawMessageDelivery": "false", - "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-0": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-1": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:1>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", - "Message": "This is a test message", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "MessageAttributes": { - "attr1": { - "Type": "Number", - "Value": "99" - } - } - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-2": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:1>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", - "Message": "This is a test message", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "MessageAttributes": { - "attr1": { - "Type": "Number", - "Value": "99" - } - } - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:2>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "subscription-attributes-2": { - "Attributes": { - "ConfirmationWasAuthenticated": "true", - "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", - "FilterPolicy": { - "attr1": [ - null, - { - "anything-but": "whatever" - } - ] - }, - "FilterPolicyScope": "MessageAttributes", - "Owner": "111111111111", - "PendingConfirmation": "false", - "Protocol": "sqs", - "RawMessageDelivery": "false", - "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-3": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-4": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:3>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", - "Message": "This the test message for null", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>" - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:4>", - "ReceiptHandle": "<receipt-handle:3>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_exists_filter_policy": { - "recorded-date": "07-12-2023, 10:17:55", - "recorded-content": { - "subscription-attributes-policy-1": { - "Attributes": { - "ConfirmationWasAuthenticated": "true", - "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", - "FilterPolicy": { - "store": [ - { - "exists": true - } - ] - }, - "FilterPolicyScope": "MessageAttributes", - "Owner": "111111111111", - "PendingConfirmation": "false", - "Protocol": "sqs", - "RawMessageDelivery": "false", - "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-0": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-1": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:1>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", - "Message": "message-1", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "MessageAttributes": { - "def": { - "Type": "Number", - "Value": "99" - }, - "store": { - "Type": "Number", - "Value": "99" - } - } - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-2": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:1>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", - "Message": "message-1", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "MessageAttributes": { - "def": { - "Type": "Number", - "Value": "99" - }, - "store": { - "Type": "Number", - "Value": "99" - } - } - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:2>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "subscription-attributes-policy-2": { - "Attributes": { - "ConfirmationWasAuthenticated": "true", - "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", - "FilterPolicy": { - "store": [ - { - "exists": false - } - ] - }, - "FilterPolicyScope": "MessageAttributes", - "Owner": "111111111111", - "PendingConfirmation": "false", - "Protocol": "sqs", - "RawMessageDelivery": "false", - "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-3": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:3>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", - "Message": "message-3", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "MessageAttributes": { - "def": { - "Type": "Number", - "Value": "99" - } - } - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:4>", - "ReceiptHandle": "<receipt-handle:3>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-4": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:3>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", - "Message": "message-3", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "MessageAttributes": { - "def": { - "Type": "Number", - "Value": "99" - } - } - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:4>", - "ReceiptHandle": "<receipt-handle:4>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_set_subscription_filter_policy_scope": { - "recorded-date": "07-12-2023, 10:17:58", - "recorded-content": { - "sub-attrs-default": { - "Attributes": { - "ConfirmationWasAuthenticated": "true", - "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", - "Owner": "111111111111", - "PendingConfirmation": "false", - "Protocol": "sqs", - "RawMessageDelivery": "false", - "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "sub-attrs-filter-scope-body": { - "Attributes": { - "ConfirmationWasAuthenticated": "true", - "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", - "Owner": "111111111111", - "PendingConfirmation": "false", - "Protocol": "sqs", - "RawMessageDelivery": "false", - "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "sub-attrs-filter-scope-error": { - "Error": { - "Code": "InvalidParameter", - "Message": "Invalid parameter: FilterPolicyScope: Invalid value [RandomValue]. Please use either MessageBody or MessageAttributes", - "Type": "Sender" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - }, - "sub-attrs-after-setting-policy": { - "Attributes": { - "ConfirmationWasAuthenticated": "true", - "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", - "FilterPolicy": { - "attr": [ - "match-this" - ] - }, - "FilterPolicyScope": "MessageBody", - "Owner": "111111111111", - "PendingConfirmation": "false", - "Protocol": "sqs", - "RawMessageDelivery": "false", - "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_sub_filter_policy_nested_property": { - "recorded-date": "07-12-2023, 10:18:00", - "recorded-content": { - "sub-filter-policy-nested-error": { - "Error": { - "Code": "InvalidParameter", - "Message": "Invalid parameter: Filter policy scope MessageAttributes does not support nested filter policy", - "Type": "Sender" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - }, - "sub-attrs-after-setting-nested-policy": { - "Attributes": { - "ConfirmationWasAuthenticated": "true", - "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", - "FilterPolicy": { - "object": { - "key": [ - { - "prefix": "auto-" - } - ] - } - }, - "FilterPolicyScope": "MessageBody", - "Owner": "111111111111", - "PendingConfirmation": "false", - "Protocol": "sqs", - "RawMessageDelivery": "false", - "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_sub_filter_policy_nested_property_constraints": { - "recorded-date": "07-12-2023, 10:18:03", - "recorded-content": { - "sub-filter-policy-nested-error-too-many-combinations": { - "Error": { - "Code": "InvalidParameter", - "Message": "Invalid parameter: FilterPolicy: Filter policy is too complex", - "Type": "Sender" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - }, - "sub-filter-policy-max-attr-keys": { - "Error": { - "Code": "InvalidParameter", - "Message": "Invalid parameter: FilterPolicy: Filter policy can not have more than 5 keys", - "Type": "Sender" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - }, - "sub-filter-policy-rule-no-list": { - "Error": { - "Code": "InvalidParameter", - "Message": "Invalid parameter: FilterPolicy: \"key_a\" must be an object or an array\n at [Source: (String)\"{\"key_a\":\"value_one\"}\"; line: 1, column: 11]", - "Type": "Sender" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 400 - } - } - } - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body[True]": { - "recorded-date": "07-12-2023, 10:18:17", - "recorded-content": { - "recv-init": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "recv-passed-msg-0": { - "Messages": [ - { - "Body": { - "object": { - "key": "auto-test" - } - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "recv-passed-msg-1": { - "Messages": [ - { - "Body": { - "object": { - "key": "hardcodedvalue" - } - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:2>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body[False]": { - "recorded-date": "07-12-2023, 10:18:31", - "recorded-content": { - "recv-init": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "recv-passed-msg-0": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:1>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", - "Message": "{\"object\": {\"key\": \"auto-test\"}}", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "recv-passed-msg-1": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:3>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", - "Message": "{\"object\": {\"key\": \"hardcodedvalue\"}}", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:4>", - "ReceiptHandle": "<receipt-handle:2>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_for_batch": { - "recorded-date": "07-12-2023, 10:18:50", - "recorded-content": { - "subscription-attributes-with-filter": { - "Attributes": { - "ConfirmationWasAuthenticated": "true", - "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", - "FilterPolicy": { - "attr1": [ - { - "numeric": [ - ">", - 0, - "<=", - 100 - ] - } - ] - }, - "FilterPolicyScope": "MessageAttributes", - "Owner": "111111111111", - "PendingConfirmation": "false", - "Protocol": "sqs", - "RawMessageDelivery": "false", - "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "subscription-attributes-no-filter": { - "Attributes": { - "ConfirmationWasAuthenticated": "true", - "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:5>", - "Owner": "111111111111", - "PendingConfirmation": "false", - "Protocol": "sqs", - "RawMessageDelivery": "false", - "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:6>", - "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-no-filter-before-publish": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-with-filter-before-publish": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-no-filter-after-publish-ok": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:1>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", - "Message": "This is a test message", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:6>", - "MessageAttributes": { - "attr1": { - "Type": "Number", - "Value": "99" - } - } - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-with-filter-after-publish-ok": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:1>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", - "Message": "This is a test message", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "MessageAttributes": { - "attr1": { - "Type": "Number", - "Value": "99" - } - } - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:3>", - "ReceiptHandle": "<receipt-handle:2>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-no-filter-after-publish-ok-1": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:4>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", - "Message": "This is another test message", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:6>", - "MessageAttributes": { - "attr1": { - "Type": "Number", - "Value": "111" - } - } - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:5>", - "ReceiptHandle": "<receipt-handle:3>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-with-filter-after-publish-filtered": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, - "tests/aws/services/sns/test_sns.py::TestSNSSMS::test_subscribe_sms_endpoint": { - "recorded-date": "25-08-2023, 00:20:07", - "recorded-content": { - "subscribe-sms-endpoint": { - "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:3>:<resource:1>", - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "subscribe-sms-attrs": { - "Attributes": { - "ConfirmationWasAuthenticated": "true", - "Endpoint": "+123123123", - "Owner": "111111111111", - "PendingConfirmation": "false", - "Protocol": "sms", - "RawMessageDelivery": "false", - "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:3>:<resource:1>", - "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:2>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:3>" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - } - }, "tests/aws/services/sns/test_sns.py::TestSNSSMS::test_publish_wrong_phone_format": { "recorded-date": "25-08-2023, 00:20:12", "recorded-content": { @@ -4606,213 +3811,58 @@ } }, "confirm-subscribe": { - "ConfirmSubscriptionResponse": { - "ConfirmSubscriptionResult": { - "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - }, - "http-message": { - "Message": "test_external_http_endpoint", - "MessageId": "<uuid:2>", - "Signature": "<signature>", - "SignatureVersion": "1", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "Timestamp": "date", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", - "Type": "Notification", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" - }, - "http-message-headers": { - "Accept-Encoding": "gzip,deflate", - "Connection": "connection", - "Content-Length": "content--length", - "Content-Type": "text/plain; charset=UTF-8", - "Host": "<host:1>", - "User-Agent": "Amazon Simple Notification Service Agent", - "X-Amz-Sns-Message-Id": "<uuid:2>", - "X-Amz-Sns-Message-Type": "Notification", - "X-Amz-Sns-Subscription-Arn": "arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>", - "X-Amz-Sns-Topic-Arn": "arn:aws:sns:<region>:111111111111:<resource:1>" - }, - "unsubscribe-response": { - "UnsubscribeResponse": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - } - }, - "unsubscribe-request": { - "Message": "You have chosen to deactivate subscription arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>.\nTo cancel this operation and restore the subscription, visit the SubscribeURL included in this message.", - "MessageId": "<uuid:3>", - "Signature": "<signature>", - "SignatureVersion": "1", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "SubscribeURL": "<subscribe-domain>/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:<region>:111111111111:<resource:1>&Token=<token:2>", - "Timestamp": "date", - "Token": "<token:2>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", - "Type": "UnsubscribeConfirmation" - } - } - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body_dot_attribute": { - "recorded-date": "07-12-2023, 10:19:19", - "recorded-content": { - "recv-init": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "recv-nested-msg-0": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:1>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", - "Message": "{\"object\": {\"nested\": \"string.value\"}}", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:1>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "recv-nested-msg-1": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:3>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", - "Message": "{\"object.nested\": \"string.value\"}", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:4>", - "ReceiptHandle": "<receipt-handle:2>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "recv-deep-nested-msg-0": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:5>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", - "Message": "{\"object\": {\"nested\": {\"test\": \"string.value\"}}}", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:6>", - "ReceiptHandle": "<receipt-handle:3>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "recv-deep-nested-msg-1": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:7>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", - "Message": "{\"object.nested.test\": \"string.value\"}", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:8>", - "ReceiptHandle": "<receipt-handle:4>" - } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "recv-deep-nested-msg-2": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:9>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", - "Message": "{\"object.nested\": {\"test\": \"string.value\"}}", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:10>", - "ReceiptHandle": "<receipt-handle:5>" + "ConfirmSubscriptionResponse": { + "ConfirmSubscriptionResult": { + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 } }, - "recv-deep-nested-msg-3": { - "Messages": [ - { - "Body": { - "Type": "Notification", - "MessageId": "<uuid:11>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", - "Message": "{\"object\": {\"nested.test\": \"string.value\"}}", - "Timestamp": "date", - "SignatureVersion": "1", - "Signature": "<signature>", - "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", - "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:12>", - "ReceiptHandle": "<receipt-handle:6>" + "http-message": { + "Message": "test_external_http_endpoint", + "MessageId": "<uuid:2>", + "Signature": "<signature>", + "SignatureVersion": "1", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "Timestamp": "date", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", + "Type": "Notification", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" + }, + "http-message-headers": { + "Accept-Encoding": "gzip,deflate", + "Connection": "connection", + "Content-Length": "content--length", + "Content-Type": "text/plain; charset=UTF-8", + "Host": "<host:1>", + "User-Agent": "Amazon Simple Notification Service Agent", + "X-Amz-Sns-Message-Id": "<uuid:2>", + "X-Amz-Sns-Message-Type": "Notification", + "X-Amz-Sns-Subscription-Arn": "arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>", + "X-Amz-Sns-Topic-Arn": "arn:aws:sns:<region>:111111111111:<resource:1>" + }, + "unsubscribe-response": { + "UnsubscribeResponse": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 } - ], - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 } + }, + "unsubscribe-request": { + "Message": "You have chosen to deactivate subscription arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>.\nTo cancel this operation and restore the subscription, visit the SubscribeURL included in this message.", + "MessageId": "<uuid:3>", + "Signature": "<signature>", + "SignatureVersion": "1", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "SubscribeURL": "<subscribe-domain>/?Action=ConfirmSubscription&TopicArn=arn:aws:sns:<region>:111111111111:<resource:1>&Token=<token:2>", + "Timestamp": "date", + "Token": "<token:2>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", + "Type": "UnsubscribeConfirmation" } } }, @@ -5023,67 +4073,41 @@ } } }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_exists_filter_policy_attributes_array": { - "recorded-date": "17-04-2024, 18:10:46", + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_list_subscriptions_by_topic_pagination": { + "recorded-date": "13-05-2024, 12:50:25", "recorded-content": { - "subscription-attributes-policy": { - "Attributes": { - "ConfirmationWasAuthenticated": "true", - "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", - "FilterPolicy": { - "store": [ - "value1" - ] - }, - "FilterPolicyScope": "MessageAttributes", - "Owner": "111111111111", - "PendingConfirmation": "false", - "Protocol": "sqs", - "RawMessageDelivery": "true", - "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", - "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" - }, - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-init": { - "ResponseMetadata": { - "HTTPHeaders": {}, - "HTTPStatusCode": 200 - } - }, - "messages-1": { - "Messages": [ + "list-sub-per-topic-page-2": { + "Subscriptions": [ { - "Body": "message-1", - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" + "Endpoint": "<endpoint:1>", + "Owner": "111111111111", + "Protocol": "sms", + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:2>:<resource:1>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:2>" } ], "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } + } + } + }, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionHttp::test_http_subscription_response": { + "recorded-date": "13-05-2024, 21:28:15", + "recorded-content": { + "topic-arn": { + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>" }, - "messages-2": { - "Messages": [ - { - "Body": "message-2", - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:2>" - } - ], + "subscription": { + "SubscriptionArn": "pending confirmation", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 } }, - "messages-3": { + "subscription-with-arn": { + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>", "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 @@ -5091,184 +4115,28 @@ } } }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body_array_attributes": { - "recorded-date": "17-04-2024, 21:33:10", + "tests/aws/services/sns/test_sns.py::TestSNSSMS::test_subscribe_sms_endpoint": { + "recorded-date": "14-05-2024, 19:34:12", "recorded-content": { - "messages-queue-0": { - "Messages": [ - { - "Attributes": { - "ApproximateFirstReceiveTimestamp": "timestamp", - "ApproximateReceiveCount": "1", - "SenderId": "<sender-id>", - "SentTimestamp": "timestamp" - }, - "Body": { - "headers": { - "route-to": [ - "queue1", - "queue2" - ] - } - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" - }, - { - "Attributes": { - "ApproximateFirstReceiveTimestamp": "timestamp", - "ApproximateReceiveCount": "1", - "SenderId": "<sender-id>", - "SentTimestamp": "timestamp" - }, - "Body": { - "headers": { - "route-to": [ - "queue1" - ] - } - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:2>" - } - ] + "subscribe-sms-endpoint": { + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:3>:<resource:1>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } }, - "messages-queue-1": { - "Messages": [ - { - "Attributes": { - "ApproximateFirstReceiveTimestamp": "timestamp", - "ApproximateReceiveCount": "1", - "SenderId": "<sender-id>", - "SentTimestamp": "timestamp" - }, - "Body": { - "headers": { - "route-to": [ - "queue1", - "queue2" - ] - } - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:3>", - "ReceiptHandle": "<receipt-handle:3>" - }, - { - "Attributes": { - "ApproximateFirstReceiveTimestamp": "timestamp", - "ApproximateReceiveCount": "1", - "SenderId": "<sender-id>", - "SentTimestamp": "timestamp" - }, - "Body": { - "headers": { - "route-to": [ - "queue2" - ] - } - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:4>", - "ReceiptHandle": "<receipt-handle:4>" - } - ] - } - } - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body_array_of_object_attributes": { - "recorded-date": "17-04-2024, 21:32:41", - "recorded-content": { - "messages": { - "Messages": [ - { - "Attributes": { - "ApproximateFirstReceiveTimestamp": "timestamp", - "ApproximateReceiveCount": "1", - "SenderId": "<sender-id>", - "SentTimestamp": "timestamp" - }, - "Body": { - "Records": [ - { - "eventSource": "aws:s3", - "eventName": "ObjectCreated:Put", - "s3": { - "object": [ - [ - { - "key": "auto-insurance-2314.xml", - "size": 17 - } - ] - ] - } - }, - { - "eventSource": "aws:s3", - "eventName": "ObjectRemoved:Delete", - "s3": { - "object": { - "key": "home-insurance-2314.xml", - "size": 17 - } - } - } - ] - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>" - }, - { - "Attributes": { - "ApproximateFirstReceiveTimestamp": "timestamp", - "ApproximateReceiveCount": "1", - "SenderId": "<sender-id>", - "SentTimestamp": "timestamp" - }, - "Body": { - "Records": [ - { - "eventSource": "aws:s3", - "eventTime": "date", - "eventName": "ObjectCreated:Put", - "s3": { - "bucket": { - "name": "<resource:1>", - "arn": "arn:aws:s3:::<resource:1>" - }, - "object": { - "key": "auto-insurance-2314.xml", - "size": 17 - } - } - } - ] - }, - "MD5OfBody": "<md5-hash>", - "MessageId": "<uuid:2>", - "ReceiptHandle": "<receipt-handle:2>" - } - ] - } - } - }, - "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_list_subscriptions_by_topic_pagination": { - "recorded-date": "13-05-2024, 12:50:25", - "recorded-content": { - "list-sub-per-topic-page-2": { - "Subscriptions": [ - { - "Endpoint": "<endpoint:1>", - "Owner": "111111111111", - "Protocol": "sms", - "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:2>:<resource:1>", - "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:2>" - } - ], + "subscribe-sms-attrs": { + "Attributes": { + "ConfirmationWasAuthenticated": "true", + "Endpoint": "+123123123", + "Owner": "111111111111", + "PendingConfirmation": "false", + "Protocol": "sms", + "RawMessageDelivery": "false", + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:3>:<resource:1>", + "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:2>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:3>" + }, "ResponseMetadata": { "HTTPHeaders": {}, "HTTPStatusCode": 200 diff --git a/tests/aws/services/sns/test_sns.validation.json b/tests/aws/services/sns/test_sns.validation.json index dd3357dd6e3eb..3851b9122493f 100644 --- a/tests/aws/services/sns/test_sns.validation.json +++ b/tests/aws/services/sns/test_sns.validation.json @@ -1,40 +1,4 @@ { - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_exists_filter_policy": { - "last_validated_date": "2023-11-09T20:04:02+00:00" - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_exists_filter_policy_attributes_array": { - "last_validated_date": "2024-04-17T18:10:45+00:00" - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy": { - "last_validated_date": "2024-01-25T18:07:57+00:00" - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_for_batch": { - "last_validated_date": "2023-11-09T20:01:32+00:00" - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body[False]": { - "last_validated_date": "2023-11-09T19:58:42+00:00" - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body[True]": { - "last_validated_date": "2023-11-09T19:58:29+00:00" - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body_array_attributes": { - "last_validated_date": "2024-04-17T21:33:09+00:00" - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body_array_of_object_attributes": { - "last_validated_date": "2024-04-17T21:32:40+00:00" - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_filter_policy_on_message_body_dot_attribute": { - "last_validated_date": "2023-11-09T17:50:59+00:00" - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_set_subscription_filter_policy_scope": { - "last_validated_date": "2023-08-24T22:15:12+00:00" - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_sub_filter_policy_nested_property": { - "last_validated_date": "2023-08-24T22:15:14+00:00" - }, - "tests/aws/services/sns/test_sns.py::TestSNSFilter::test_sub_filter_policy_nested_property_constraints": { - "last_validated_date": "2023-08-24T22:18:18+00:00" - }, "tests/aws/services/sns/test_sns.py::TestSNSPublishCrud::test_empty_sns_message": { "last_validated_date": "2023-08-24T20:31:48+00:00" }, @@ -84,7 +48,7 @@ "last_validated_date": "2023-08-24T22:20:12+00:00" }, "tests/aws/services/sns/test_sns.py::TestSNSSMS::test_subscribe_sms_endpoint": { - "last_validated_date": "2023-08-24T22:20:07+00:00" + "last_validated_date": "2024-05-14T19:34:11+00:00" }, "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_create_subscriptions_with_attributes": { "last_validated_date": "2024-03-29T19:44:42+00:00" @@ -119,6 +83,9 @@ "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_validate_set_sub_attributes": { "last_validated_date": "2024-03-29T19:30:23+00:00" }, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionHttp::test_http_subscription_response": { + "last_validated_date": "2024-05-13T21:28:15+00:00" + }, "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionHttp::test_subscribe_external_http_endpoint[False]": { "last_validated_date": "2023-10-11T22:47:29+00:00" }, diff --git a/tests/aws/services/sns/test_sns_filter_policy.py b/tests/aws/services/sns/test_sns_filter_policy.py new file mode 100644 index 0000000000000..f2ace9a283a60 --- /dev/null +++ b/tests/aws/services/sns/test_sns_filter_policy.py @@ -0,0 +1,1557 @@ +import copy +import json +from operator import itemgetter + +import pytest +from botocore.exceptions import ClientError + +from localstack.testing.aws.util import is_aws_cloud +from localstack.testing.pytest import markers +from localstack.utils.sync import poll_condition, retry + + +@pytest.fixture(autouse=True) +def sns_snapshot_transformer(snapshot): + snapshot.add_transformer(snapshot.transform.sns_api()) + + +@pytest.fixture +def sns_create_sqs_subscription_with_filter_policy(sns_create_sqs_subscription, aws_client): + def _inner(topic_arn: str, queue_url: str, filter_scope: str, filter_policy: dict): + subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) + subscription_arn = subscription["SubscriptionArn"] + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicyScope", + AttributeValue=filter_scope, + ) + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=json.dumps(filter_policy), + ) + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="RawMessageDelivery", + AttributeValue="true", + ) + return subscription_arn + + yield _inner + + +class TestSNSFilterPolicyCrud: + @markers.aws.validated + def test_set_subscription_filter_policy_scope( + self, sqs_create_queue, sns_create_topic, sns_create_sqs_subscription, snapshot, aws_client + ): + topic_arn = sns_create_topic()["TopicArn"] + queue_url = sqs_create_queue() + subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) + subscription_arn = subscription["SubscriptionArn"] + + # we fetch the default subscription attributes + # note: the FilterPolicyScope is not present in the response + subscription_attrs = aws_client.sns.get_subscription_attributes( + SubscriptionArn=subscription_arn + ) + snapshot.match("sub-attrs-default", subscription_attrs) + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicyScope", + AttributeValue="MessageBody", + ) + + # we fetch the subscription attributes after setting the FilterPolicyScope + # note: the FilterPolicyScope is still not present in the response + subscription_attrs = aws_client.sns.get_subscription_attributes( + SubscriptionArn=subscription_arn + ) + snapshot.match("sub-attrs-filter-scope-body", subscription_attrs) + + # we try to set random values to the FilterPolicyScope + with pytest.raises(ClientError) as e: + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicyScope", + AttributeValue="RandomValue", + ) + + snapshot.match("sub-attrs-filter-scope-error", e.value.response) + + # we try to set a FilterPolicy to see if it will show the FilterPolicyScope in the attributes + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=json.dumps({"attr": ["match-this"]}), + ) + # the FilterPolicyScope is now present in the attributes + subscription_attrs = aws_client.sns.get_subscription_attributes( + SubscriptionArn=subscription_arn + ) + snapshot.match("sub-attrs-after-setting-policy", subscription_attrs) + + @markers.aws.validated + def test_sub_filter_policy_nested_property( + self, sqs_create_queue, sns_create_topic, sns_create_sqs_subscription, snapshot, aws_client + ): + topic_arn = sns_create_topic()["TopicArn"] + queue_url = sqs_create_queue() + subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) + subscription_arn = subscription["SubscriptionArn"] + + # see https://aws.amazon.com/blogs/compute/introducing-payload-based-message-filtering-for-amazon-sns/ + nested_filter_policy = {"object": {"key": [{"prefix": "auto-"}]}} + with pytest.raises(ClientError) as e: + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=json.dumps(nested_filter_policy), + ) + snapshot.match("sub-filter-policy-nested-error", e.value.response) + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicyScope", + AttributeValue="MessageBody", + ) + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=json.dumps(nested_filter_policy), + ) + + # the FilterPolicyScope is now present in the attributes + subscription_attrs = aws_client.sns.get_subscription_attributes( + SubscriptionArn=subscription_arn + ) + snapshot.match("sub-attrs-after-setting-nested-policy", subscription_attrs) + + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + paths=[ + "$.sub-filter-policy-rule-no-list.Error.Message", # message contains java trace in AWS, assert instead + ] + ) + def test_sub_filter_policy_nested_property_constraints( + self, sqs_create_queue, sns_create_topic, sns_create_sqs_subscription, snapshot, aws_client + ): + # https://docs.aws.amazon.com/sns/latest/dg/subscription-filter-policy-constraints.html + topic_arn = sns_create_topic()["TopicArn"] + queue_url = sqs_create_queue() + subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) + subscription_arn = subscription["SubscriptionArn"] + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicyScope", + AttributeValue="MessageBody", + ) + + nested_filter_policy = { + "key_a": { + "key_b": {"key_c": ["value_one", "value_two", "value_three", "value_four"]}, + }, + "key_d": {"key_e": ["value_one", "value_two", "value_three"]}, + "key_f": ["value_one", "value_two", "value_three"], + } + # The first array has four values in a three-level nested key, and the second has three values in a two-level + # nested key. The total combination is calculated as follows: + # 3 x 4 x 2 x 3 x 1 x 3 = 216 + with pytest.raises(ClientError) as e: + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=json.dumps(nested_filter_policy), + ) + snapshot.match("sub-filter-policy-nested-error-too-many-combinations", e.value.response) + + flat_filter_policy = { + "key_a": ["value_one"], + "key_b": ["value_two"], + "key_c": ["value_three"], + "key_d": ["value_four"], + "key_e": ["value_five"], + "key_f": ["value_six"], + } + # A filter policy can have a maximum of five attribute names. For a nested policy, only parent keys are counted. + with pytest.raises(ClientError) as e: + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=json.dumps(flat_filter_policy), + ) + snapshot.match("sub-filter-policy-max-attr-keys", e.value.response) + + flat_filter_policy = {"key_a": "value_one"} + # Rules should be contained in a list + with pytest.raises(ClientError) as e: + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=json.dumps(flat_filter_policy), + ) + snapshot.match("sub-filter-policy-rule-no-list", e.value.response) + assert e.value.response["Error"]["Message"].startswith( + 'Invalid parameter: FilterPolicy: "key_a" must be an object or an array' + ) + + +class TestSNSFilterPolicyAttributes: + @markers.aws.validated + def test_filter_policy( + self, sqs_create_queue, sns_create_topic, sns_create_sqs_subscription, snapshot, aws_client + ): + topic_arn = sns_create_topic()["TopicArn"] + queue_url = sqs_create_queue() + subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) + subscription_arn = subscription["SubscriptionArn"] + + filter_policy = {"attr1": [{"numeric": [">", 0, "<=", 100]}]} + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=json.dumps(filter_policy), + ) + + response_attributes = aws_client.sns.get_subscription_attributes( + SubscriptionArn=subscription_arn + ) + snapshot.match("subscription-attributes", response_attributes) + + response_0 = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=1 + ) + snapshot.match("messages-0", response_0) + # get number of messages + num_msgs_0 = len(response_0.get("Messages", [])) + + # publish message that satisfies the filter policy, assert that message is received + message = "This is a test message" + message_attributes = {"attr1": {"DataType": "Number", "StringValue": "99"}} + aws_client.sns.publish( + TopicArn=topic_arn, + Message=message, + MessageAttributes=message_attributes, + ) + + response_1 = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 + ) + snapshot.match("messages-1", response_1) + + num_msgs_1 = len(response_1["Messages"]) + assert num_msgs_1 == (num_msgs_0 + 1) + + # publish message that does not satisfy the filter policy, assert that message is not received + message = "This is another test message" + aws_client.sns.publish( + TopicArn=topic_arn, + Message=message, + MessageAttributes={"attr1": {"DataType": "Number", "StringValue": "111"}}, + ) + + response_2 = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 + ) + snapshot.match("messages-2", response_2) + num_msgs_2 = len(response_2["Messages"]) + assert num_msgs_2 == num_msgs_1 + + # remove all messages from the queue + receipt_handle = response_1["Messages"][0]["ReceiptHandle"] + aws_client.sqs.delete_message(QueueUrl=queue_url, ReceiptHandle=receipt_handle) + + # test with a property value set to null with an OR operator with anything-but + filter_policy = json.dumps({"attr1": [None, {"anything-but": "whatever"}]}) + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=filter_policy, + ) + + def get_filter_policy(): + subscription_attrs = aws_client.sns.get_subscription_attributes( + SubscriptionArn=subscription_arn + ) + return subscription_attrs["Attributes"]["FilterPolicy"] + + # wait for the new filter policy to be in effect + poll_condition(lambda: get_filter_policy() == filter_policy, timeout=4) + response_attributes_2 = aws_client.sns.get_subscription_attributes( + SubscriptionArn=subscription_arn + ) + snapshot.match("subscription-attributes-2", response_attributes_2) + + # publish message that does not satisfy the filter policy, assert that message is not received + message = "This the test message for null" + aws_client.sns.publish( + TopicArn=topic_arn, + Message=message, + ) + + response_3 = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 + ) + snapshot.match("messages-3", response_3) + assert "Messages" not in response_3 or response_3["Messages"] == [] + + # unset the filter policy + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue="", + ) + + def check_no_filter_policy(): + subscription_attrs = aws_client.sns.get_subscription_attributes( + SubscriptionArn=subscription_arn + ) + return "FilterPolicy" not in subscription_attrs["Attributes"] + + poll_condition(check_no_filter_policy, timeout=4) + + # publish message that does not satisfy the previous filter policy, but assert that the message is received now + message = "This the test message for null" + aws_client.sns.publish( + TopicArn=topic_arn, + Message=message, + ) + + response_4 = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 + ) + snapshot.match("messages-4", response_4) + + @markers.aws.validated + def test_exists_filter_policy( + self, sqs_create_queue, sns_create_topic, sns_create_sqs_subscription, snapshot, aws_client + ): + topic_arn = sns_create_topic()["TopicArn"] + queue_url = sqs_create_queue() + subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) + subscription_arn = subscription["SubscriptionArn"] + + filter_policy = {"store": [{"exists": True}]} + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=json.dumps(filter_policy), + ) + + response_attributes = aws_client.sns.get_subscription_attributes( + SubscriptionArn=subscription_arn + ) + snapshot.match("subscription-attributes-policy-1", response_attributes) + + response_0 = aws_client.sqs.receive_message(QueueUrl=queue_url, VisibilityTimeout=0) + snapshot.match("messages-0", response_0) + # get number of messages + num_msgs_0 = len(response_0.get("Messages", [])) + + # publish message that satisfies the filter policy, assert that message is received + message_1 = "message-1" + aws_client.sns.publish( + TopicArn=topic_arn, + Message=message_1, + MessageAttributes={ + "store": {"DataType": "Number", "StringValue": "99"}, + "def": {"DataType": "Number", "StringValue": "99"}, + }, + ) + response_1 = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 + ) + snapshot.match("messages-1", response_1) + num_msgs_1 = len(response_1["Messages"]) + assert num_msgs_1 == (num_msgs_0 + 1) + + # publish message that does not satisfy the filter policy, assert that message is not received + message_2 = "message-2" + aws_client.sns.publish( + TopicArn=topic_arn, + Message=message_2, + MessageAttributes={"attr1": {"DataType": "Number", "StringValue": "111"}}, + ) + + response_2 = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 + ) + snapshot.match("messages-2", response_2) + num_msgs_2 = len(response_2["Messages"]) + assert num_msgs_2 == num_msgs_1 + + # delete first message + aws_client.sqs.delete_message( + QueueUrl=queue_url, ReceiptHandle=response_1["Messages"][0]["ReceiptHandle"] + ) + + # test with exist operator set to false. + filter_policy = json.dumps({"store": [{"exists": False}]}) + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=filter_policy, + ) + + def get_filter_policy(): + subscription_attrs = aws_client.sns.get_subscription_attributes( + SubscriptionArn=subscription_arn + ) + return subscription_attrs["Attributes"]["FilterPolicy"] + + # wait for the new filter policy to be in effect + poll_condition(lambda: get_filter_policy() == filter_policy, timeout=4) + response_attributes_2 = aws_client.sns.get_subscription_attributes( + SubscriptionArn=subscription_arn + ) + snapshot.match("subscription-attributes-policy-2", response_attributes_2) + + # publish message that satisfies the filter policy, assert that message is received + message_3 = "message-3" + aws_client.sns.publish( + TopicArn=topic_arn, + Message=message_3, + MessageAttributes={"def": {"DataType": "Number", "StringValue": "99"}}, + ) + + response_3 = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 + ) + snapshot.match("messages-3", response_3) + num_msgs_3 = len(response_3["Messages"]) + assert num_msgs_3 == num_msgs_1 + + # publish message that does not satisfy the filter policy, assert that message is not received + message_4 = "message-4" + aws_client.sns.publish( + TopicArn=topic_arn, + Message=message_4, + MessageAttributes={ + "store": {"DataType": "Number", "StringValue": "99"}, + "def": {"DataType": "Number", "StringValue": "99"}, + }, + ) + + response_4 = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 + ) + snapshot.match("messages-4", response_4) + num_msgs_4 = len(response_4["Messages"]) + assert num_msgs_4 == num_msgs_3 + + @markers.aws.validated + def test_exists_filter_policy_attributes_array( + self, + sqs_create_queue, + sns_create_topic, + sns_create_sqs_subscription_with_filter_policy, + snapshot, + aws_client, + ): + topic_arn = sns_create_topic()["TopicArn"] + queue_url = sqs_create_queue() + filter_policy = {"store": ["value1"]} + subscription_arn = sns_create_sqs_subscription_with_filter_policy( + topic_arn=topic_arn, + queue_url=queue_url, + filter_scope="MessageAttributes", + filter_policy=filter_policy, + ) + + response_attributes = aws_client.sns.get_subscription_attributes( + SubscriptionArn=subscription_arn + ) + snapshot.match("subscription-attributes-policy", response_attributes) + + response_0 = aws_client.sqs.receive_message(QueueUrl=queue_url, VisibilityTimeout=0) + snapshot.match("messages-init", response_0) + + # publish message that satisfies the filter policy, assert that message is received + message = "message-1" + aws_client.sns.publish( + TopicArn=topic_arn, + Message=message, + MessageAttributes={ + "store": {"DataType": "String", "StringValue": "value1"}, + }, + ) + response_1 = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 + ) + aws_client.sqs.delete_message( + QueueUrl=queue_url, ReceiptHandle=response_1["Messages"][0]["ReceiptHandle"] + ) + snapshot.match("messages-1", response_1) + + # publish message that satisfies the filter policy but with String.Array + message = "message-2" + aws_client.sns.publish( + TopicArn=topic_arn, + Message=message, + MessageAttributes={ + "store": { + "DataType": "String.Array", + "StringValue": json.dumps(["value1", "value2"]), + }, + }, + ) + response_2 = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 + ) + aws_client.sqs.delete_message( + QueueUrl=queue_url, ReceiptHandle=response_2["Messages"][0]["ReceiptHandle"] + ) + snapshot.match("messages-2", response_2) + + # publish message that does not satisfy the filter policy with String.Array + message = "message-3" + aws_client.sns.publish( + TopicArn=topic_arn, + Message=message, + MessageAttributes={ + "store": { + "DataType": "String.Array", + "StringValue": json.dumps(["value2", "value3"]), + }, + }, + ) + response_3 = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=4 + ) + snapshot.match("messages-3", response_3) + + +class TestSNSFilterPolicyBody: + @markers.aws.validated + @pytest.mark.parametrize("raw_message_delivery", [True, False]) + def test_filter_policy_on_message_body( + self, + sqs_create_queue, + sns_create_topic, + sns_create_sqs_subscription, + snapshot, + raw_message_delivery, + aws_client, + ): + topic_arn = sns_create_topic()["TopicArn"] + queue_url = sqs_create_queue() + subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) + subscription_arn = subscription["SubscriptionArn"] + # see https://aws.amazon.com/blogs/compute/introducing-payload-based-message-filtering-for-amazon-sns/ + nested_filter_policy = { + "object": { + "key": [{"prefix": "auto-"}, "hardcodedvalue"], + "nested_key": [{"exists": False}], + }, + "test": [{"exists": False}], + } + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicyScope", + AttributeValue="MessageBody", + ) + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=json.dumps(nested_filter_policy), + ) + + if raw_message_delivery: + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="RawMessageDelivery", + AttributeValue="true", + ) + + response = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=1 + ) + snapshot.match("recv-init", response) + # assert there are no messages in the queue + assert "Messages" not in response or response["Messages"] == [] + + # publish messages that satisfies the filter policy, assert that messages are received + messages = [ + {"object": {"key": "auto-test"}}, + {"object": {"key": "hardcodedvalue"}}, + ] + for i, message in enumerate(messages): + aws_client.sns.publish( + TopicArn=topic_arn, + Message=json.dumps(message), + ) + + response = aws_client.sqs.receive_message( + QueueUrl=queue_url, + VisibilityTimeout=0, + WaitTimeSeconds=5 if is_aws_cloud() else 2, + ) + snapshot.match(f"recv-passed-msg-{i}", response) + receipt_handle = response["Messages"][0]["ReceiptHandle"] + aws_client.sqs.delete_message(QueueUrl=queue_url, ReceiptHandle=receipt_handle) + + # publish messages that do not satisfy the filter policy, assert those messages are not received + messages = [ + {"object": {"key": "test-auto"}}, + {"object": {"key": "auto-test"}, "test": "just-exists"}, + {"object": {"key": "auto-test", "nested_key": "just-exists"}}, + {"object": {"test": "auto-test"}}, + {"test": "auto-test"}, + ] + for message in messages: + aws_client.sns.publish( + TopicArn=topic_arn, + Message=json.dumps(message), + ) + + response = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=5 if is_aws_cloud() else 2 + ) + # assert there are no messages in the queue + assert "Messages" not in response or response["Messages"] == [] + + # publish message that does not satisfy the filter policy as it's not even JSON, or not a JSON object + message = "Regular string message" + aws_client.sns.publish( + TopicArn=topic_arn, + Message=message, + ) + aws_client.sns.publish( + TopicArn=topic_arn, + Message=json.dumps(message), # send it JSON encoded, but not an object + ) + + response = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=2 + ) + # assert there are no messages in the queue + assert "Messages" not in response or response["Messages"] == [] + + @markers.aws.validated + def test_filter_policy_for_batch( + self, sqs_create_queue, sns_create_topic, sns_create_sqs_subscription, snapshot, aws_client + ): + topic_arn = sns_create_topic()["TopicArn"] + queue_url_with_filter = sqs_create_queue() + subscription_with_filter = sns_create_sqs_subscription( + topic_arn=topic_arn, queue_url=queue_url_with_filter + ) + subscription_with_filter_arn = subscription_with_filter["SubscriptionArn"] + + queue_url_no_filter = sqs_create_queue() + subscription_no_filter = sns_create_sqs_subscription( + topic_arn=topic_arn, queue_url=queue_url_no_filter + ) + subscription_no_filter_arn = subscription_no_filter["SubscriptionArn"] + + filter_policy = {"attr1": [{"numeric": [">", 0, "<=", 100]}]} + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_with_filter_arn, + AttributeName="FilterPolicy", + AttributeValue=json.dumps(filter_policy), + ) + + response_attributes = aws_client.sns.get_subscription_attributes( + SubscriptionArn=subscription_with_filter_arn + ) + snapshot.match("subscription-attributes-with-filter", response_attributes) + + response_attributes = aws_client.sns.get_subscription_attributes( + SubscriptionArn=subscription_no_filter_arn + ) + snapshot.match("subscription-attributes-no-filter", response_attributes) + + sqs_wait_time = 4 if is_aws_cloud() else 1 + + response_before_publish_no_filter = aws_client.sqs.receive_message( + QueueUrl=queue_url_with_filter, VisibilityTimeout=0, WaitTimeSeconds=sqs_wait_time + ) + snapshot.match("messages-no-filter-before-publish", response_before_publish_no_filter) + + response_before_publish_filter = aws_client.sqs.receive_message( + QueueUrl=queue_url_with_filter, VisibilityTimeout=0, WaitTimeSeconds=sqs_wait_time + ) + snapshot.match("messages-with-filter-before-publish", response_before_publish_filter) + + # publish message that satisfies the filter policy, assert that message is received + message = "This is a test message" + message_attributes = {"attr1": {"DataType": "Number", "StringValue": "99"}} + aws_client.sns.publish_batch( + TopicArn=topic_arn, + PublishBatchRequestEntries=[ + { + "Id": "1", + "Message": message, + "MessageAttributes": message_attributes, + } + ], + ) + + response_after_publish_no_filter = aws_client.sqs.receive_message( + QueueUrl=queue_url_no_filter, VisibilityTimeout=0, WaitTimeSeconds=sqs_wait_time + ) + snapshot.match("messages-no-filter-after-publish-ok", response_after_publish_no_filter) + aws_client.sqs.delete_message( + QueueUrl=queue_url_no_filter, + ReceiptHandle=response_after_publish_no_filter["Messages"][0]["ReceiptHandle"], + ) + + response_after_publish_filter = aws_client.sqs.receive_message( + QueueUrl=queue_url_with_filter, VisibilityTimeout=0, WaitTimeSeconds=sqs_wait_time + ) + snapshot.match("messages-with-filter-after-publish-ok", response_after_publish_filter) + aws_client.sqs.delete_message( + QueueUrl=queue_url_with_filter, + ReceiptHandle=response_after_publish_filter["Messages"][0]["ReceiptHandle"], + ) + + # publish message that does not satisfy the filter policy, assert that message is not received by the + # subscription with the filter and received by the other + aws_client.sns.publish_batch( + TopicArn=topic_arn, + PublishBatchRequestEntries=[ + { + "Id": "1", + "Message": "This is another test message", + "MessageAttributes": {"attr1": {"DataType": "Number", "StringValue": "111"}}, + } + ], + ) + + response_after_publish_no_filter = aws_client.sqs.receive_message( + QueueUrl=queue_url_no_filter, VisibilityTimeout=0, WaitTimeSeconds=sqs_wait_time + ) + # there should be 1 message in the queue, latest sent + snapshot.match("messages-no-filter-after-publish-ok-1", response_after_publish_no_filter) + + response_after_publish_filter = aws_client.sqs.receive_message( + QueueUrl=queue_url_with_filter, VisibilityTimeout=0, WaitTimeSeconds=sqs_wait_time + ) + # there should be no messages in this queue + snapshot.match("messages-with-filter-after-publish-filtered", response_after_publish_filter) + + @markers.aws.validated + def test_filter_policy_on_message_body_dot_attribute( + self, + sqs_create_queue, + sns_create_topic, + sns_create_sqs_subscription, + snapshot, + aws_client, + ): + topic_arn = sns_create_topic()["TopicArn"] + queue_url = sqs_create_queue() + subscription = sns_create_sqs_subscription(topic_arn=topic_arn, queue_url=queue_url) + subscription_arn = subscription["SubscriptionArn"] + + nested_filter_policy = json.dumps( + { + "object.nested": ["string.value"], + } + ) + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicyScope", + AttributeValue="MessageBody", + ) + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=nested_filter_policy, + ) + + def get_filter_policy(): + subscription_attrs = aws_client.sns.get_subscription_attributes( + SubscriptionArn=subscription_arn + ) + return subscription_attrs["Attributes"]["FilterPolicy"] + + # wait for the new filter policy to be in effect + poll_condition(lambda: get_filter_policy() == nested_filter_policy, timeout=4) + + response = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=1 + ) + snapshot.match("recv-init", response) + # assert there are no messages in the queue + assert "Messages" not in response or response["Messages"] == [] + + def _verify_and_snapshot_sqs_messages(msg_to_send: list[dict], snapshot_prefix: str): + for i, _message in enumerate(msg_to_send): + aws_client.sns.publish( + TopicArn=topic_arn, + Message=json.dumps(_message), + ) + + _response = aws_client.sqs.receive_message( + QueueUrl=queue_url, + VisibilityTimeout=0, + WaitTimeSeconds=5 if is_aws_cloud() else 2, + ) + snapshot.match(f"{snapshot_prefix}-{i}", _response) + receipt_handle = _response["Messages"][0]["ReceiptHandle"] + aws_client.sqs.delete_message(QueueUrl=queue_url, ReceiptHandle=receipt_handle) + + # publish messages that satisfies the filter policy, assert that messages are received + messages = [ + {"object": {"nested": "string.value"}}, + {"object.nested": "string.value"}, + ] + _verify_and_snapshot_sqs_messages(messages, snapshot_prefix="recv-nested-msg") + + # publish messages that do not satisfy the filter policy, assert those messages are not received + messages = [ + {"object": {"nested": "test-auto"}}, + ] + for message in messages: + aws_client.sns.publish( + TopicArn=topic_arn, + Message=json.dumps(message), + ) + + response = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=5 if is_aws_cloud() else 2 + ) + # assert there are no messages in the queue + assert "Messages" not in response or response["Messages"] == [] + + # assert with more nesting + deep_nested_filter_policy = json.dumps( + { + "object.nested.test": ["string.value"], + } + ) + + aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=deep_nested_filter_policy, + ) + # wait for the new filter policy to be in effect + poll_condition(lambda: get_filter_policy() == deep_nested_filter_policy, timeout=4) + + messages = [ + {"object": {"nested": {"test": "string.value"}}}, + {"object.nested.test": "string.value"}, + {"object.nested": {"test": "string.value"}}, + {"object": {"nested.test": "string.value"}}, + ] + _verify_and_snapshot_sqs_messages(messages, snapshot_prefix="recv-deep-nested-msg") + # publish messages that do not satisfy the filter policy, assert those messages are not received + messages = [ + {"object": {"nested": {"test": "string.notvalue"}}}, + ] + for message in messages: + aws_client.sns.publish( + TopicArn=topic_arn, + Message=json.dumps(message), + ) + + response = aws_client.sqs.receive_message( + QueueUrl=queue_url, VisibilityTimeout=0, WaitTimeSeconds=5 if is_aws_cloud() else 2 + ) + # assert there are no messages in the queue + assert "Messages" not in response or response["Messages"] == [] + + @markers.aws.validated + def test_filter_policy_on_message_body_array_attributes( + self, + sqs_create_queue, + sns_create_topic, + sns_create_sqs_subscription_with_filter_policy, + snapshot, + aws_client, + ): + topic_arn = sns_create_topic()["TopicArn"] + queue_url_1 = sqs_create_queue() + queue_url_2 = sqs_create_queue() + + filter_policy_1 = {"headers": {"route-to": ["queue1"]}} + sns_create_sqs_subscription_with_filter_policy( + topic_arn=topic_arn, + queue_url=queue_url_1, + filter_scope="MessageBody", + filter_policy=filter_policy_1, + ) + + filter_policy_2 = {"headers": {"route-to": ["queue2"]}} + sns_create_sqs_subscription_with_filter_policy( + topic_arn=topic_arn, + queue_url=queue_url_2, + filter_scope="MessageBody", + filter_policy=filter_policy_2, + ) + + queues = [queue_url_1, queue_url_2] + + # publish messages that satisfies the filter policy, assert that messages are received + messages = [ + {"headers": {"route-to": ["queue3"]}}, + {"headers": {"route-to": ["queue1"]}}, + {"headers": {"route-to": ["queue2"]}}, + {"headers": {"route-to": ["queue1", "queue2"]}}, + ] + for i, message in enumerate(messages): + aws_client.sns.publish( + TopicArn=topic_arn, + Message=json.dumps(message), + ) + + def get_messages(_queue_url: str, _recv_messages: list): + # due to the random nature of receiving SQS messages, we need to consolidate a single object to match + sqs_response = aws_client.sqs.receive_message( + QueueUrl=_queue_url, + WaitTimeSeconds=1, + VisibilityTimeout=0, + MessageAttributeNames=["All"], + AttributeNames=["All"], + ) + for _message in sqs_response["Messages"]: + _recv_messages.append(_message) + aws_client.sqs.delete_message( + QueueUrl=_queue_url, ReceiptHandle=_message["ReceiptHandle"] + ) + + assert len(_recv_messages) == 2 + + for i, queue_url in enumerate(queues): + recv_messages = [] + retry( + get_messages, + retries=10, + sleep=0.1, + _queue_url=queue_url, + _recv_messages=recv_messages, + ) + # we need to sort the list (the order does not matter as we're not using FIFO) + recv_messages.sort(key=itemgetter("Body")) + snapshot.match(f"messages-queue-{i}", {"Messages": recv_messages}) + + @markers.aws.validated + def test_filter_policy_on_message_body_array_of_object_attributes( + self, + sqs_create_queue, + sns_create_topic, + sns_create_sqs_subscription_with_filter_policy, + snapshot, + aws_client, + ): + # example from https://aws.amazon.com/blogs/compute/introducing-payload-based-message-filtering-for-amazon-sns/ + topic_arn = sns_create_topic()["TopicArn"] + queue_url = sqs_create_queue() + # complex filter policy with different level of nesting + filter_policy = { + "Records": { + "s3": {"object": {"key": [{"prefix": "auto-"}]}}, + "eventName": [{"prefix": "ObjectCreated:"}], + } + } + + sns_create_sqs_subscription_with_filter_policy( + topic_arn=topic_arn, + queue_url=queue_url, + filter_scope="MessageBody", + filter_policy=filter_policy, + ) + + # stripped down events + s3_event_auto_insurance_created = { + "Records": [ + { + "eventSource": "aws:s3", + "eventTime": "2022-11-21T03:41:29.743Z", + "eventName": "ObjectCreated:Put", + "s3": { + "bucket": { + "name": "insurance-bucket-demo", + "arn": "arn:aws:s3:::insurance-bucket-demo", + }, + "object": { + "key": "auto-insurance-2314.xml", + "size": 17, + }, + }, + } + ] + } + # copy the object to modify it + s3_event_auto_insurance_removed = copy.deepcopy(s3_event_auto_insurance_created) + s3_event_auto_insurance_removed["Records"][0]["eventName"] = "ObjectRemoved:Delete" + + # copy the object to modify it + s3_event_home_insurance_created = copy.deepcopy(s3_event_auto_insurance_created) + s3_event_home_insurance_created["Records"][0]["s3"]["object"]["key"] = ( + "home-insurance-2314.xml" + ) + + # stripped down events + s3_event_multiple_records = { + "Records": [ + { + "eventSource": "aws:s3", + "eventName": "ObjectCreated:Put", + "s3": { + # this object is a list of list of dict, and it works in AWS + "object": [ + [ + { + "key": "auto-insurance-2314.xml", + "size": 17, + } + ] + ], + }, + }, + { + "eventSource": "aws:s3", + "eventName": "ObjectRemoved:Delete", + "s3": { + "object": { + "key": "home-insurance-2314.xml", + "size": 17, + } + }, + }, + ] + } + + messages = [ + s3_event_multiple_records, + s3_event_auto_insurance_removed, + s3_event_home_insurance_created, + s3_event_auto_insurance_created, + ] + for i, message in enumerate(messages): + aws_client.sns.publish( + TopicArn=topic_arn, + Message=json.dumps(message), + ) + + def get_messages(_queue_url: str, _received_messages: list): + # due to the random nature of receiving SQS messages, we need to consolidate a single object to match + sqs_response = aws_client.sqs.receive_message( + QueueUrl=_queue_url, + WaitTimeSeconds=1, + VisibilityTimeout=0, + MessageAttributeNames=["All"], + AttributeNames=["All"], + ) + for _message in sqs_response["Messages"]: + _received_messages.append(_message) + aws_client.sqs.delete_message( + QueueUrl=_queue_url, ReceiptHandle=_message["ReceiptHandle"] + ) + + assert len(_received_messages) == 2 + + received_messages = [] + retry( + get_messages, + retries=10, + sleep=0.1, + _queue_url=queue_url, + _received_messages=received_messages, + ) + # we need to sort the list (the order does not matter as we're not using FIFO) + received_messages.sort(key=itemgetter("Body")) + snapshot.match("messages", {"Messages": received_messages}) + + @markers.aws.validated + @pytest.mark.skip("Not yet supported by LocalStack") + def test_filter_policy_on_message_body_or_attribute( + self, + sqs_create_queue, + sns_create_topic, + sns_create_sqs_subscription_with_filter_policy, + snapshot, + aws_client, + ): + topic_arn = sns_create_topic()["TopicArn"] + queue_url = sqs_create_queue() + + filter_policy = { + "$or": [ + {"metricName": ["CPUUtilization", "ReadLatency"]}, + {"namespace": ["AWS/EC2", "AWS/ES"]}, + ], + "detail": { + "scope": ["Service"], + "$or": [ + {"source": ["aws.cloudwatch"]}, + {"type": ["CloudWatch Alarm State Change"]}, + ], + }, + } + sns_create_sqs_subscription_with_filter_policy( + topic_arn=topic_arn, + queue_url=queue_url, + filter_scope="MessageBody", + filter_policy=filter_policy, + ) + + # publish messages that satisfies the filter policy, assert that messages are received + messages = [ + # not passing + # wrong value for `metricName` + { + "metricName": "CPUUtilization", + "detail": {"scope": "aws.cloudwatch", "type": "CloudWatch Alarm State Change"}, + }, + # wrong value for `detail.type` + { + "metricName": "CPUUtilization", + "detail": {"scope": "Service", "type": "CPUUtilization"}, + }, + # missing value for `detail.scope` + {"metricName": "CPUUtilization", "detail": {"type": "CloudWatch Alarm State Change"}}, + # missing value for `detail.type` or `detail.source` + {"metricName": "CPUUtilization", "detail": {"scope": "Service"}}, + # missing value for `detail.scope` AND `detail.source` or `detail.type` + {"metricName": "CPUUtilization", "scope": "Service"}, + # passing + { + "metricName": "CPUUtilization", + "detail": {"scope": "Service", "source": "aws.cloudwatch"}, + }, + { + "metricName": "ReadLatency", + "detail": {"scope": "Service", "source": "aws.cloudwatch"}, + }, + {"namespace": "AWS/EC2", "detail": {"scope": "Service", "source": "aws.cloudwatch"}}, + {"namespace": "AWS/ES", "detail": {"scope": "Service", "source": "aws.cloudwatch"}}, + { + "metricName": "CPUUtilization", + "detail": {"scope": "Service", "type": "CloudWatch Alarm State Change"}, + }, + { + "metricName": "AWS/EC2", + "detail": {"scope": "Service", "type": "CloudWatch Alarm State Change"}, + }, + { + "namespace": "CPUUtilization", + "detail": {"scope": "Service", "type": "CloudWatch Alarm State Change"}, + }, + ] + for message in messages: + aws_client.sns.publish( + TopicArn=topic_arn, + Message=json.dumps(message), + ) + + def get_messages(_queue_url: str, _recv_messages: list): + # due to the random nature of receiving SQS messages, we need to consolidate a single object to match + sqs_response = aws_client.sqs.receive_message( + QueueUrl=_queue_url, + WaitTimeSeconds=1, + VisibilityTimeout=0, + MessageAttributeNames=["All"], + AttributeNames=["All"], + ) + for _message in sqs_response["Messages"]: + _recv_messages.append(_message) + aws_client.sqs.delete_message( + QueueUrl=_queue_url, ReceiptHandle=_message["ReceiptHandle"] + ) + + assert len(_recv_messages) == 7 + + recv_messages = [] + retry( + get_messages, + retries=10, + sleep=0.1, + _queue_url=queue_url, + _recv_messages=recv_messages, + ) + # we need to sort the list (the order does not matter as we're not using FIFO) + recv_messages.sort(key=itemgetter("Body")) + snapshot.match("messages-queue", {"Messages": recv_messages}) + + +class TestSNSFilterPolicyConditions: + @staticmethod + def _add_normalized_field_to_snapshot(error_dict): + error_dict["Error"]["_normalized"] = error_dict["Error"]["Message"].split("\n")[0] + + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify( + # AWS adds JSON position error: `\n at [Source: (String)"{"key":[["value"]]}"; line: 1, column: 10]` + paths=["$..Error.Message"] + ) + def test_validate_policy( + self, + sns_create_topic, + sns_subscription, + snapshot, + aws_client, + ): + phone_number = "+123123123" + topic_arn = sns_create_topic()["TopicArn"] + + def _subscribe(policy: dict): + sns_subscription( + TopicArn=topic_arn, + Protocol="sms", + Endpoint=phone_number, + Attributes={"FilterPolicy": json.dumps(policy)}, + ) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [["value"]]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-condition-list", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"wrong-operator": True}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-condition-operator", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"suffix": "value", "prefix": "value2"}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-condition-two-operators", e.value.response) + + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify(paths=["$..Error.Message"]) + def test_validate_policy_string_operators( + self, + sns_create_topic, + sns_subscription, + snapshot, + aws_client, + ): + phone_number = "+123123123" + topic_arn = sns_create_topic()["TopicArn"] + + def _subscribe(policy: dict): + sns_subscription( + TopicArn=topic_arn, + Protocol="sms", + Endpoint=phone_number, + Attributes={"FilterPolicy": json.dumps(policy)}, + ) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"suffix": 100}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-condition-is-numeric", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"suffix": None}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-condition-is-none", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": {"suffix": "value"}} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-condition-is-not-list-and-operator", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": {"suffix": "value", "prefix": "value"}} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-condition-is-not-list-two-ops", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": {"not-an-operator": "value"}} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-condition-is-not-list-and-no-operator", e.value.response) + + # TODO: add `cidr` string operator + + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify(paths=["$..Error.Message"]) + def test_validate_policy_numeric_operator( + self, + sns_create_topic, + sns_subscription, + snapshot, + aws_client, + ): + phone_number = "+123123123" + topic_arn = sns_create_topic()["TopicArn"] + + def _subscribe(policy: dict): + sns_subscription( + TopicArn=topic_arn, + Protocol="sms", + Endpoint=phone_number, + Attributes={"FilterPolicy": json.dumps(policy)}, + ) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"numeric": []}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-numeric-empty", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"numeric": ["operator"]}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-numeric-wrong-operator", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"numeric": [1, "<="]}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-numeric-operator-order", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"numeric": ["=", "000"]}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-numeric-type", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"numeric": [">="]}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-numeric-missing-value", e.value.response) + + # dealing with range numeric + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"numeric": ["<", 100, ">", 10]}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-numeric-wrong-range-order", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"numeric": [">=", 1, ">", 2]}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-numeric-wrong-range-operators", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"numeric": [">", 3, "<", 1]}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-numeric-wrong-value-order", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"numeric": [">", 20, "<="]}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-numeric-missing-range-value", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"numeric": [">", 20, 30]}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-numeric-missing-range-operator", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"numeric": [">", 20, "test"]}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-numeric-wrong-second-range-operator", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"numeric": [">", "20", "<", "30"]}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-numeric-wrong-range-value-1-type", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"numeric": [">", 20, "<", "30"]}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-numeric-wrong-range-value-2-type", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"numeric": [">", 20, "<", 30, "<", 50]}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-numeric-too-many-range", e.value.response) + + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify(paths=["$..Error.Message"]) + def test_validate_policy_exists_operator( + self, + sns_create_topic, + sns_subscription, + snapshot, + aws_client, + ): + phone_number = "+123123123" + topic_arn = sns_create_topic()["TopicArn"] + + def _subscribe(policy: dict): + sns_subscription( + TopicArn=topic_arn, + Protocol="sms", + Endpoint=phone_number, + Attributes={"FilterPolicy": json.dumps(policy)}, + ) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"exists": None}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-condition-none", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"exists": "no"}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-condition-string", e.value.response) + + @markers.aws.validated + def test_policy_complexity( + self, + sns_create_topic, + sns_subscription, + snapshot, + aws_client, + ): + phone_number = "+123123123" + topic_arn = sns_create_topic()["TopicArn"] + + def _subscribe(policy: dict): + sns_subscription( + TopicArn=topic_arn, + Protocol="sms", + Endpoint=phone_number, + Attributes={"FilterPolicy": json.dumps(policy)}, + ) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [f"value{i}" for i in range(151)]} + _subscribe(filter_policy) + snapshot.match("error-complexity-in-one-condition", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = { + "key1": [f"value{i}" for i in range(100)], + "key2": [f"value{i}" for i in range(51)], + } + _subscribe(filter_policy) + snapshot.match("error-complexity-in-two-conditions", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = { + "key1": ["value1"], + "key2": ["value2"], + "key3": ["value3"], + "key4": ["value4"], + "key5": ["value5"], + "key6": ["value6"], + } + _subscribe(filter_policy) + snapshot.match("error-complexity-too-many-fields", e.value.response) + + @markers.aws.validated + def test_policy_complexity_with_or( + self, + sns_create_topic, + sns_subscription, + snapshot, + aws_client, + ): + phone_number = "+123123123" + topic_arn = sns_create_topic()["TopicArn"] + + def _subscribe(policy: dict, scope: str): + attributes = {"FilterPolicy": json.dumps(policy)} + if scope: + attributes["FilterPolicyScope"] = scope + + return sns_subscription( + TopicArn=topic_arn, + Protocol="sms", + Endpoint=phone_number, + Attributes=attributes, + ) + + with pytest.raises(ClientError) as e: + # (source * metricName) + (source * metricType * metricId) + (source * metricType * spaceId) + # = (4 * 6) + (4 * 4 * 4) + (4 * 4 * 4) + # = 24 + 64 + 64 + # = 152 + filter_policy = { + "source": ["aws.cloudwatch", "aws.events", "aws.test", "aws.test2"], + "$or": [ + {"metricName": ["CPUUtilization", "ReadLatency", "t1", "t2", "t3", "t4"]}, + { + "metricType": ["MetricType", "TestType", "TestType2", "TestType3"], + "$or": [{"metricId": [1234, 4321, 5678, 9012]}, {"spaceId": [1, 2, 3, 4]}], + }, + ], + } + + _subscribe(filter_policy, scope="MessageAttributes") + snapshot.match("error-complexity-or-flat", e.value.response) + + with pytest.raises(ClientError) as e: + # ("metricName" AND ("detail"."scope" AND "detail"."source") + # OR + # ("metricName" AND ("detail"."scope" AND "detail"."type") + # OR + # ("namespace" AND ("detail"."scope" AND "detail"."source") + # OR + # ("namespace" AND ("detail"."scope" AND "detail"."type") + # (3 * 4 * 2) + (3 * 4 * 6) + (2 * 4 * 2) + (2 * 4 * 6) + # = 24 + 72 + 16 + 48 + # = 160 + filter_policy = { + "$or": [ + {"metricName": ["CPUUtilization", "ReadLatency", "TestValue"]}, + {"namespace": ["AWS/EC2", "AWS/ES"]}, + ], + "detail": { + "scope": ["Service", "Test"], + "$or": [ + {"source": ["aws.cloudwatch"]}, + {"type": ["CloudWatch Alarm State Change", "TestValue", "TestValue2"]}, + ], + }, + } + + _subscribe(filter_policy, scope="MessageBody") + snapshot.match("error-complexity-or-nested", e.value.response) + + # (source * metricName) + (source * metricType * metricId) + (source * metricType * spaceId) + # = (3 * 6) + (3 * 4 * 4) + (3 * 4 * 7) + # = 18 + 48 + 84 + # = 150 + filter_policy = { + "source": ["aws.cloudwatch", "aws.events", "aws.test"], + "$or": [ + { + "metricName": [ + "CPUUtilization", + "ReadLatency", + "TestVal", + "TestVal2", + "TestVal3", + "TestVal4", + ] + }, + { + "metricType": ["MetricType", "TestType", "TestType2", "TestType3"], + "$or": [ + {"metricId": [1234, 4321, 5678, 9012]}, + {"spaceId": [1, 2, 3, 4, 5, 6, 7]}, + ], + }, + ], + } + response = _subscribe(filter_policy, scope="MessageAttributes") + assert "SubscriptionArn" in response diff --git a/tests/aws/services/sns/test_sns_filter_policy.snapshot.json b/tests/aws/services/sns/test_sns_filter_policy.snapshot.json new file mode 100644 index 0000000000000..63b9df5dbede5 --- /dev/null +++ b/tests/aws/services/sns/test_sns_filter_policy.snapshot.json @@ -0,0 +1,1533 @@ +{ + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy": { + "recorded-date": "14-05-2024, 16:51:02", + "recorded-content": { + "error-condition-list": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Match value must be String, number, true, false, or null\n at [Source: (String)\"{\"key\":[[\"value\"]]}\"; line: 1, column: 10]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Match value must be String, number, true, false, or null" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-condition-operator": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Unrecognized match type wrong-operator\n at [Source: (String)\"{\"key\":[{\"wrong-operator\":true}]}\"; line: 1, column: 31]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Unrecognized match type wrong-operator" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-condition-two-operators": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Only one key allowed in match expression\n at [Source: (String)\"{\"key\":[{\"suffix\":\"value\",\"prefix\":\"value2\"}]}\"; line: 1, column: 37]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Only one key allowed in match expression" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_string_operators": { + "recorded-date": "14-05-2024, 16:51:03", + "recorded-content": { + "error-condition-is-numeric": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: suffix match pattern must be a string\n at [Source: (String)\"{\"key\":[{\"suffix\":100}]}\"; line: 1, column: 22]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: suffix match pattern must be a string" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-condition-is-none": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: suffix match pattern must be a string\n at [Source: (String)\"{\"key\":[{\"suffix\":null}]}\"; line: 1, column: 23]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: suffix match pattern must be a string" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-condition-is-not-list-and-operator": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: \"suffix\" must be an object or an array\n at [Source: (String)\"{\"key\":{\"suffix\":\"value\"}}\"; line: 1, column: 19]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: \"suffix\" must be an object or an array" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-condition-is-not-list-two-ops": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: \"suffix\" must be an object or an array\n at [Source: (String)\"{\"key\":{\"suffix\":\"value\",\"prefix\":\"value\"}}\"; line: 1, column: 19]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: \"suffix\" must be an object or an array" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-condition-is-not-list-and-no-operator": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: \"not-an-operator\" must be an object or an array\n at [Source: (String)\"{\"key\":{\"not-an-operator\":\"value\"}}\"; line: 1, column: 28]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: \"not-an-operator\" must be an object or an array" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_numeric_operator": { + "recorded-date": "14-05-2024, 16:51:06", + "recorded-content": { + "error-numeric-empty": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Invalid member in numeric match: ]\n at [Source: (String)\"{\"key\":[{\"numeric\":[]}]}\"; line: 1, column: 22]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Invalid member in numeric match: ]" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-numeric-wrong-operator": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Unrecognized numeric range operator: operator\n at [Source: (String)\"{\"key\":[{\"numeric\":[\"operator\"]}]}\"; line: 1, column: 32]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Unrecognized numeric range operator: operator" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-numeric-operator-order": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Invalid member in numeric match: 1\n at [Source: (String)\"{\"key\":[{\"numeric\":[1,\"<=\"]}]}\"; line: 1, column: 22]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Invalid member in numeric match: 1" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-numeric-type": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Value of equals must be numeric\n at [Source: (String)\"{\"key\":[{\"numeric\":[\"=\",\"000\"]}]}\"; line: 1, column: 26]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Value of equals must be numeric" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-numeric-missing-value": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Value of >= must be numeric\n at [Source: (String)\"{\"key\":[{\"numeric\":[\">=\"]}]}\"; line: 1, column: 26]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Value of >= must be numeric" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-numeric-wrong-range-order": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Too many elements in numeric expression\n at [Source: (String)\"{\"key\":[{\"numeric\":[\"<\",100,\">\",10]}]}\"; line: 1, column: 30]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Too many elements in numeric expression" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-numeric-wrong-range-operators": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Bad numeric range operator: >\n at [Source: (String)\"{\"key\":[{\"numeric\":[\">=\",1,\">\",2]}]}\"; line: 1, column: 31]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Bad numeric range operator: >" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-numeric-wrong-value-order": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Bottom must be less than top\n at [Source: (String)\"{\"key\":[{\"numeric\":[\">\",3,\"<\",1]}]}\"; line: 1, column: 33]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Bottom must be less than top" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-numeric-missing-range-value": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Value of <= must be numeric\n at [Source: (String)\"{\"key\":[{\"numeric\":[\">\",20,\"<=\"]}]}\"; line: 1, column: 33]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Value of <= must be numeric" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-numeric-missing-range-operator": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Bad value in numeric range: 30\n at [Source: (String)\"{\"key\":[{\"numeric\":[\">\",20,30]}]}\"; line: 1, column: 30]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Bad value in numeric range: 30" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-numeric-wrong-second-range-operator": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Bad numeric range operator: test\n at [Source: (String)\"{\"key\":[{\"numeric\":[\">\",20,\"test\"]}]}\"; line: 1, column: 34]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Bad numeric range operator: test" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-numeric-wrong-range-value-1-type": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Value of > must be numeric\n at [Source: (String)\"{\"key\":[{\"numeric\":[\">\",\"20\",\"<\",\"30\"]}]}\"; line: 1, column: 26]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Value of > must be numeric" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-numeric-wrong-range-value-2-type": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Value of < must be numeric\n at [Source: (String)\"{\"key\":[{\"numeric\":[\">\",20,\"<\",\"30\"]}]}\"; line: 1, column: 33]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Value of < must be numeric" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-numeric-too-many-range": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Too many terms in numeric range expression\n at [Source: (String)\"{\"key\":[{\"numeric\":[\">\",20,\"<\",30,\"<\",50]}]}\"; line: 1, column: 36]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Too many terms in numeric range expression" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_exists_operator": { + "recorded-date": "14-05-2024, 16:51:07", + "recorded-content": { + "error-condition-none": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: exists match pattern must be either true or false.\n at [Source: (String)\"{\"key\":[{\"exists\":null}]}\"; line: 1, column: 23]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: exists match pattern must be either true or false." + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-condition-string": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: exists match pattern must be either true or false.\n at [Source: (String)\"{\"key\":[{\"exists\":\"no\"}]}\"; line: 1, column: 20]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: exists match pattern must be either true or false." + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_policy_complexity": { + "recorded-date": "14-05-2024, 16:51:07", + "recorded-content": { + "error-complexity-in-one-condition": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Filter policy is too complex", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-complexity-in-two-conditions": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Filter policy is too complex", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-complexity-too-many-fields": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Filter policy can not have more than 5 keys", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyCrud::test_sub_filter_policy_nested_property": { + "recorded-date": "14-05-2024, 16:49:14", + "recorded-content": { + "sub-filter-policy-nested-error": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Filter policy scope MessageAttributes does not support nested filter policy", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "sub-attrs-after-setting-nested-policy": { + "Attributes": { + "ConfirmationWasAuthenticated": "true", + "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", + "FilterPolicy": { + "object": { + "key": [ + { + "prefix": "auto-" + } + ] + } + }, + "FilterPolicyScope": "MessageBody", + "Owner": "111111111111", + "PendingConfirmation": "false", + "Protocol": "sqs", + "RawMessageDelivery": "false", + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_policy_complexity_with_or": { + "recorded-date": "14-05-2024, 16:51:09", + "recorded-content": { + "error-complexity-or-flat": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Filter policy is too complex", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-complexity-or-nested": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Filter policy is too complex", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyCrud::test_set_subscription_filter_policy_scope": { + "recorded-date": "14-05-2024, 16:49:12", + "recorded-content": { + "sub-attrs-default": { + "Attributes": { + "ConfirmationWasAuthenticated": "true", + "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", + "Owner": "111111111111", + "PendingConfirmation": "false", + "Protocol": "sqs", + "RawMessageDelivery": "false", + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "sub-attrs-filter-scope-body": { + "Attributes": { + "ConfirmationWasAuthenticated": "true", + "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", + "Owner": "111111111111", + "PendingConfirmation": "false", + "Protocol": "sqs", + "RawMessageDelivery": "false", + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "sub-attrs-filter-scope-error": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: FilterPolicyScope: Invalid value [RandomValue]. Please use either MessageBody or MessageAttributes", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "sub-attrs-after-setting-policy": { + "Attributes": { + "ConfirmationWasAuthenticated": "true", + "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", + "FilterPolicy": { + "attr": [ + "match-this" + ] + }, + "FilterPolicyScope": "MessageBody", + "Owner": "111111111111", + "PendingConfirmation": "false", + "Protocol": "sqs", + "RawMessageDelivery": "false", + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyCrud::test_sub_filter_policy_nested_property_constraints": { + "recorded-date": "14-05-2024, 16:49:16", + "recorded-content": { + "sub-filter-policy-nested-error-too-many-combinations": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: FilterPolicy: Filter policy is too complex", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "sub-filter-policy-max-attr-keys": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: FilterPolicy: Filter policy can not have more than 5 keys", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "sub-filter-policy-rule-no-list": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: FilterPolicy: \"key_a\" must be an object or an array\n at [Source: (String)\"{\"key_a\":\"value_one\"}\"; line: 1, column: 11]", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyAttributes::test_filter_policy": { + "recorded-date": "14-05-2024, 16:49:29", + "recorded-content": { + "subscription-attributes": { + "Attributes": { + "ConfirmationWasAuthenticated": "true", + "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", + "FilterPolicy": { + "attr1": [ + { + "numeric": [ + ">", + 0, + "<=", + 100 + ] + } + ] + }, + "FilterPolicyScope": "MessageAttributes", + "Owner": "111111111111", + "PendingConfirmation": "false", + "Protocol": "sqs", + "RawMessageDelivery": "false", + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-0": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-1": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:1>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", + "Message": "This is a test message", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "MessageAttributes": { + "attr1": { + "Type": "Number", + "Value": "99" + } + } + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:1>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-2": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:1>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", + "Message": "This is a test message", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "MessageAttributes": { + "attr1": { + "Type": "Number", + "Value": "99" + } + } + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "subscription-attributes-2": { + "Attributes": { + "ConfirmationWasAuthenticated": "true", + "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", + "FilterPolicy": { + "attr1": [ + null, + { + "anything-but": "whatever" + } + ] + }, + "FilterPolicyScope": "MessageAttributes", + "Owner": "111111111111", + "PendingConfirmation": "false", + "Protocol": "sqs", + "RawMessageDelivery": "false", + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-3": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-4": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:3>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", + "Message": "This the test message for null", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>" + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:4>", + "ReceiptHandle": "<receipt-handle:3>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyAttributes::test_exists_filter_policy": { + "recorded-date": "14-05-2024, 16:49:37", + "recorded-content": { + "subscription-attributes-policy-1": { + "Attributes": { + "ConfirmationWasAuthenticated": "true", + "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", + "FilterPolicy": { + "store": [ + { + "exists": true + } + ] + }, + "FilterPolicyScope": "MessageAttributes", + "Owner": "111111111111", + "PendingConfirmation": "false", + "Protocol": "sqs", + "RawMessageDelivery": "false", + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-0": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-1": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:1>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", + "Message": "message-1", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "MessageAttributes": { + "def": { + "Type": "Number", + "Value": "99" + }, + "store": { + "Type": "Number", + "Value": "99" + } + } + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:1>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-2": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:1>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", + "Message": "message-1", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "MessageAttributes": { + "def": { + "Type": "Number", + "Value": "99" + }, + "store": { + "Type": "Number", + "Value": "99" + } + } + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "subscription-attributes-policy-2": { + "Attributes": { + "ConfirmationWasAuthenticated": "true", + "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", + "FilterPolicy": { + "store": [ + { + "exists": false + } + ] + }, + "FilterPolicyScope": "MessageAttributes", + "Owner": "111111111111", + "PendingConfirmation": "false", + "Protocol": "sqs", + "RawMessageDelivery": "false", + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-3": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:3>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", + "Message": "message-3", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "MessageAttributes": { + "def": { + "Type": "Number", + "Value": "99" + } + } + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:4>", + "ReceiptHandle": "<receipt-handle:3>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-4": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:3>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", + "Message": "message-3", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "MessageAttributes": { + "def": { + "Type": "Number", + "Value": "99" + } + } + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:4>", + "ReceiptHandle": "<receipt-handle:4>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyAttributes::test_exists_filter_policy_attributes_array": { + "recorded-date": "14-05-2024, 16:49:44", + "recorded-content": { + "subscription-attributes-policy": { + "Attributes": { + "ConfirmationWasAuthenticated": "true", + "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", + "FilterPolicy": { + "store": [ + "value1" + ] + }, + "FilterPolicyScope": "MessageAttributes", + "Owner": "111111111111", + "PendingConfirmation": "false", + "Protocol": "sqs", + "RawMessageDelivery": "true", + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-init": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-1": { + "Messages": [ + { + "Body": "message-1", + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-2": { + "Messages": [ + { + "Body": "message-2", + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-3": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body[True]": { + "recorded-date": "14-05-2024, 16:49:57", + "recorded-content": { + "recv-init": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "recv-passed-msg-0": { + "Messages": [ + { + "Body": { + "object": { + "key": "auto-test" + } + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "recv-passed-msg-1": { + "Messages": [ + { + "Body": { + "object": { + "key": "hardcodedvalue" + } + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body[False]": { + "recorded-date": "14-05-2024, 16:50:08", + "recorded-content": { + "recv-init": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "recv-passed-msg-0": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:1>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", + "Message": "{\"object\": {\"key\": \"auto-test\"}}", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:1>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "recv-passed-msg-1": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:3>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", + "Message": "{\"object\": {\"key\": \"hardcodedvalue\"}}", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:4>", + "ReceiptHandle": "<receipt-handle:2>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_for_batch": { + "recorded-date": "14-05-2024, 16:50:25", + "recorded-content": { + "subscription-attributes-with-filter": { + "Attributes": { + "ConfirmationWasAuthenticated": "true", + "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:1>", + "FilterPolicy": { + "attr1": [ + { + "numeric": [ + ">", + 0, + "<=", + 100 + ] + } + ] + }, + "FilterPolicyScope": "MessageAttributes", + "Owner": "111111111111", + "PendingConfirmation": "false", + "Protocol": "sqs", + "RawMessageDelivery": "false", + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "subscription-attributes-no-filter": { + "Attributes": { + "ConfirmationWasAuthenticated": "true", + "Endpoint": "arn:aws:sqs:<region>:111111111111:<resource:5>", + "Owner": "111111111111", + "PendingConfirmation": "false", + "Protocol": "sqs", + "RawMessageDelivery": "false", + "SubscriptionArn": "arn:aws:sns:<region>:111111111111:<resource:4>:<resource:6>", + "SubscriptionPrincipal": "arn:aws:iam::111111111111:user/<resource:3>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-no-filter-before-publish": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-with-filter-before-publish": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-no-filter-after-publish-ok": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:1>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", + "Message": "This is a test message", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:6>", + "MessageAttributes": { + "attr1": { + "Type": "Number", + "Value": "99" + } + } + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:1>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-with-filter-after-publish-ok": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:1>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", + "Message": "This is a test message", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:2>", + "MessageAttributes": { + "attr1": { + "Type": "Number", + "Value": "99" + } + } + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:3>", + "ReceiptHandle": "<receipt-handle:2>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-no-filter-after-publish-ok-1": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:4>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:4>", + "Message": "This is another test message", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:4>:<resource:6>", + "MessageAttributes": { + "attr1": { + "Type": "Number", + "Value": "111" + } + } + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:5>", + "ReceiptHandle": "<receipt-handle:3>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-with-filter-after-publish-filtered": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body_dot_attribute": { + "recorded-date": "14-05-2024, 16:50:50", + "recorded-content": { + "recv-init": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "recv-nested-msg-0": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:1>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", + "Message": "{\"object\": {\"nested\": \"string.value\"}}", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:1>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "recv-nested-msg-1": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:3>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", + "Message": "{\"object.nested\": \"string.value\"}", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:4>", + "ReceiptHandle": "<receipt-handle:2>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "recv-deep-nested-msg-0": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:5>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", + "Message": "{\"object\": {\"nested\": {\"test\": \"string.value\"}}}", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:6>", + "ReceiptHandle": "<receipt-handle:3>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "recv-deep-nested-msg-1": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:7>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", + "Message": "{\"object.nested.test\": \"string.value\"}", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:8>", + "ReceiptHandle": "<receipt-handle:4>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "recv-deep-nested-msg-2": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:9>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", + "Message": "{\"object.nested\": {\"test\": \"string.value\"}}", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:10>", + "ReceiptHandle": "<receipt-handle:5>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "recv-deep-nested-msg-3": { + "Messages": [ + { + "Body": { + "Type": "Notification", + "MessageId": "<uuid:11>", + "TopicArn": "arn:aws:sns:<region>:111111111111:<resource:1>", + "Message": "{\"object\": {\"nested.test\": \"string.value\"}}", + "Timestamp": "date", + "SignatureVersion": "1", + "Signature": "<signature>", + "SigningCertURL": "<cert-domain>/SimpleNotificationService-<signing-cert-file:1>", + "UnsubscribeURL": "<unsubscribe-domain>/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:<region>:111111111111:<resource:1>:<resource:2>" + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:12>", + "ReceiptHandle": "<receipt-handle:6>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body_array_attributes": { + "recorded-date": "14-05-2024, 16:50:55", + "recorded-content": { + "messages-queue-0": { + "Messages": [ + { + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "1", + "SenderId": "<sender-id>", + "SentTimestamp": "timestamp" + }, + "Body": { + "headers": { + "route-to": [ + "queue1", + "queue2" + ] + } + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>" + }, + { + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "1", + "SenderId": "<sender-id>", + "SentTimestamp": "timestamp" + }, + "Body": { + "headers": { + "route-to": [ + "queue1" + ] + } + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>" + } + ] + }, + "messages-queue-1": { + "Messages": [ + { + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "1", + "SenderId": "<sender-id>", + "SentTimestamp": "timestamp" + }, + "Body": { + "headers": { + "route-to": [ + "queue1", + "queue2" + ] + } + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:3>", + "ReceiptHandle": "<receipt-handle:3>" + }, + { + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "1", + "SenderId": "<sender-id>", + "SentTimestamp": "timestamp" + }, + "Body": { + "headers": { + "route-to": [ + "queue2" + ] + } + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:4>", + "ReceiptHandle": "<receipt-handle:4>" + } + ] + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body_array_of_object_attributes": { + "recorded-date": "14-05-2024, 16:50:58", + "recorded-content": { + "messages": { + "Messages": [ + { + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "1", + "SenderId": "<sender-id>", + "SentTimestamp": "timestamp" + }, + "Body": { + "Records": [ + { + "eventSource": "aws:s3", + "eventName": "ObjectCreated:Put", + "s3": { + "object": [ + [ + { + "key": "auto-insurance-2314.xml", + "size": 17 + } + ] + ] + } + }, + { + "eventSource": "aws:s3", + "eventName": "ObjectRemoved:Delete", + "s3": { + "object": { + "key": "home-insurance-2314.xml", + "size": 17 + } + } + } + ] + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>" + }, + { + "Attributes": { + "ApproximateFirstReceiveTimestamp": "timestamp", + "ApproximateReceiveCount": "1", + "SenderId": "<sender-id>", + "SentTimestamp": "timestamp" + }, + "Body": { + "Records": [ + { + "eventSource": "aws:s3", + "eventTime": "date", + "eventName": "ObjectCreated:Put", + "s3": { + "bucket": { + "name": "<resource:1>", + "arn": "arn:aws:s3:::<resource:1>" + }, + "object": { + "key": "auto-insurance-2314.xml", + "size": 17 + } + } + } + ] + }, + "MD5OfBody": "<md5-hash>", + "MessageId": "<uuid:2>", + "ReceiptHandle": "<receipt-handle:2>" + } + ] + } + } + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body_or_attribute": { + "recorded-date": "14-05-2024, 16:51:02", + "recorded-content": {} + } +} diff --git a/tests/aws/services/sns/test_sns_filter_policy.validation.json b/tests/aws/services/sns/test_sns_filter_policy.validation.json new file mode 100644 index 0000000000000..67fd226d3fb71 --- /dev/null +++ b/tests/aws/services/sns/test_sns_filter_policy.validation.json @@ -0,0 +1,59 @@ +{ + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyAttributes::test_exists_filter_policy": { + "last_validated_date": "2024-05-14T16:49:36+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyAttributes::test_exists_filter_policy_attributes_array": { + "last_validated_date": "2024-05-14T16:49:44+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyAttributes::test_filter_policy": { + "last_validated_date": "2024-05-14T16:49:28+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_for_batch": { + "last_validated_date": "2024-05-14T16:50:24+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body[False]": { + "last_validated_date": "2024-05-14T16:50:08+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body[True]": { + "last_validated_date": "2024-05-14T16:49:56+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body_array_attributes": { + "last_validated_date": "2024-05-14T16:50:54+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body_array_of_object_attributes": { + "last_validated_date": "2024-05-14T16:50:58+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body_dot_attribute": { + "last_validated_date": "2024-05-14T16:50:49+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body_or_attribute": { + "last_validated_date": "2024-05-14T16:51:01+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_policy_complexity": { + "last_validated_date": "2024-05-14T16:51:07+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_policy_complexity_with_or": { + "last_validated_date": "2024-05-14T16:51:08+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy": { + "last_validated_date": "2024-05-14T16:51:02+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_exists_operator": { + "last_validated_date": "2024-05-14T16:51:06+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_numeric_operator": { + "last_validated_date": "2024-05-14T16:51:06+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_string_operators": { + "last_validated_date": "2024-05-14T16:51:03+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyCrud::test_set_subscription_filter_policy_scope": { + "last_validated_date": "2024-05-14T16:49:11+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyCrud::test_sub_filter_policy_nested_property": { + "last_validated_date": "2024-05-14T16:49:13+00:00" + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyCrud::test_sub_filter_policy_nested_property_constraints": { + "last_validated_date": "2024-05-14T16:49:15+00:00" + } +} diff --git a/tests/unit/test_sns.py b/tests/unit/test_sns.py index 4dcb46580f492..9de1298f1be34 100644 --- a/tests/unit/test_sns.py +++ b/tests/unit/test_sns.py @@ -8,6 +8,7 @@ import pytest from localstack.aws.api.sns import InvalidParameterException +from localstack.services.sns.filter import FilterPolicyValidator, SubscriptionFilter from localstack.services.sns.models import SnsMessage from localstack.services.sns.provider import ( encode_subscription_token_with_region, @@ -15,7 +16,6 @@ is_raw_message_delivery, ) from localstack.services.sns.publisher import ( - SubscriptionFilter, compute_canonical_string, create_sns_message_body, ) @@ -691,3 +691,75 @@ def test_canonical_string_calculation(self): canonical_string == f"Message\ntest content\nMessageId\nabdcdef\nSubscribeURL\nhttp://randomurl.com\nTimestamp\n{timestamp}\nToken\nrandomtoken\nTopicArn\narn\nType\nSubscriptionConfirmation\n" ) + + def test_filter_policy_complexity(self): + # examples taken from https://docs.aws.amazon.com/sns/latest/dg/subscription-filter-policy-constraints.html + # and https://docs.aws.amazon.com/sns/latest/dg/and-or-logic.html + validator_flat = FilterPolicyValidator(scope="MessageAttributes", is_subscribe_call=True) + validator_nested = FilterPolicyValidator(scope="MessageBody", is_subscribe_call=True) + + filter_policy = { + "key_a": { + "key_b": {"key_c": ["value_one", "value_two", "value_three", "value_four"]}, + }, + "key_d": {"key_e": ["value_one", "value_two", "value_three"]}, + "key_f": ["value_one", "value_two", "value_three"], + } + rules, combinations = validator_nested.aggregate_rules(filter_policy) + assert combinations == 216 + + filter_policy = { + "source": ["aws.cloudwatch", "aws.events", "aws.test", "aws.test2"], + "$or": [ + {"metricName": ["CPUUtilization", "ReadLatency", "t1", "t2", "t3", "t4"]}, + { + "metricType": ["MetricType", "TestType", "TestType2", "TestType3"], + "$or": [{"metricId": [1234, 4321, 5678, 9012]}, {"spaceId": [1, 2, 3, 4]}], + }, + ], + } + + rules, combinations = validator_flat.aggregate_rules(filter_policy) + assert combinations == 152 + + filter_policy = { + "$or": [ + {"metricName": ["CPUUtilization", "ReadLatency", "TestValue"]}, + {"namespace": ["AWS/EC2", "AWS/ES"]}, + ], + "detail": { + "scope": ["Service", "Test"], + "$or": [ + {"source": ["aws.cloudwatch"]}, + {"type": ["CloudWatch Alarm State Change", "TestValue", "TestValue2"]}, + ], + }, + } + + rules, combinations = validator_nested.aggregate_rules(filter_policy) + assert combinations == 160 + + filter_policy = { + "source": ["aws.cloudwatch", "aws.events", "aws.test"], + "$or": [ + { + "metricName": [ + "CPUUtilization", + "ReadLatency", + "TestVal", + "TestVal2", + "TestVal3", + "TestVal4", + ] + }, + { + "metricType": ["MetricType", "TestType", "TestType2", "TestType3"], + "$or": [ + {"metricId": [1234, 4321, 5678, 9012]}, + {"spaceId": [1, 2, 3, 4, 5, 6, 7]}, + ], + }, + ], + } + rules, combinations = validator_flat.aggregate_rules(filter_policy) + assert combinations == 150 From e450aac56c451907ea87ebb05daca51ae565917a Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Thu, 16 May 2024 00:25:39 +0200 Subject: [PATCH 145/169] fix ListObjectVersions field order (#10829) --- localstack/aws/api/s3/__init__.py | 2 +- localstack/aws/spec-patches.json | 13 +++++++++++++ tests/aws/services/s3/test_s3.py | 16 ++++++++++++++++ tests/aws/services/s3/test_s3.validation.json | 2 +- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/localstack/aws/api/s3/__init__.py b/localstack/aws/api/s3/__init__.py index 174e8a6cb38ed..1a4cd9e3fbacc 100644 --- a/localstack/aws/api/s3/__init__.py +++ b/localstack/aws/api/s3/__init__.py @@ -2588,7 +2588,6 @@ class ListObjectVersionsOutput(TypedDict, total=False): VersionIdMarker: Optional[VersionIdMarker] NextKeyMarker: Optional[NextKeyMarker] NextVersionIdMarker: Optional[NextVersionIdMarker] - Versions: Optional[ObjectVersionList] DeleteMarkers: Optional[DeleteMarkers] Name: Optional[BucketName] Prefix: Optional[Prefix] @@ -2597,6 +2596,7 @@ class ListObjectVersionsOutput(TypedDict, total=False): CommonPrefixes: Optional[CommonPrefixList] EncodingType: Optional[EncodingType] RequestCharged: Optional[RequestCharged] + Versions: Optional[ObjectVersionList] OptionalObjectAttributesList = List[OptionalObjectAttributes] diff --git a/localstack/aws/spec-patches.json b/localstack/aws/spec-patches.json index e47e706d3e6c7..33bdf35120f03 100644 --- a/localstack/aws/spec-patches.json +++ b/localstack/aws/spec-patches.json @@ -1210,6 +1210,19 @@ "documentation": "<p>Your proposed upload exceeds the maximum allowed size</p>", "exception": true } + }, + { + "op": "remove", + "path": "/shapes/ListObjectVersionsOutput/members/Versions" + }, + { + "op": "add", + "path": "/shapes/ListObjectVersionsOutput/members/Versions", + "value": { + "shape":"ObjectVersionList", + "documentation":"<p>Container for version information.</p>", + "locationName":"Version" + } } ] } diff --git a/tests/aws/services/s3/test_s3.py b/tests/aws/services/s3/test_s3.py index f9f506f0e5c29..9f7316ae198cd 100644 --- a/tests/aws/services/s3/test_s3.py +++ b/tests/aws/services/s3/test_s3.py @@ -4835,6 +4835,22 @@ def get_xml_content(http_response_content: bytes) -> bytes: key_name = "test-key" aws_client.s3.put_object(Bucket=s3_bucket, Key=key_name, Tagging="tag1=tag1") + # Lists all objects versions in a bucket + list_objects_version_url = f"{bucket_url}?versions" + resp = s3_http_client.get(list_objects_version_url, headers=headers) + assert b'<?xml version="1.0" encoding="UTF-8"?>\n' in get_xml_content(resp.content) + resp_dict = xmltodict.parse(resp.content) + assert "ListVersionsResult" in resp_dict + assert ( + resp_dict["ListVersionsResult"]["@xmlns"] == "http://s3.amazonaws.com/doc/2006-03-01/" + ) + # same as ListObjects + list_objects_versions_tags = list(resp_dict["ListVersionsResult"].keys()) + assert list_objects_versions_tags.index("Name") < list_objects_versions_tags.index( + "Version" + ) + assert list_objects_versions_tags[-1] == "Version" + # GetObjectTagging get_object_tagging_url = f"{bucket_url}/{key_name}?tagging" resp = s3_http_client.get(get_object_tagging_url, headers=headers) diff --git a/tests/aws/services/s3/test_s3.validation.json b/tests/aws/services/s3/test_s3.validation.json index 6caabeedee35e..1a6a9bbd8d75e 100644 --- a/tests/aws/services/s3/test_s3.validation.json +++ b/tests/aws/services/s3/test_s3.validation.json @@ -237,7 +237,7 @@ "last_validated_date": "2024-03-21T08:05:13+00:00" }, "tests/aws/services/s3/test_s3.py::TestS3::test_response_structure": { - "last_validated_date": "2024-04-24T18:48:32+00:00" + "last_validated_date": "2024-05-15T16:13:26+00:00" }, "tests/aws/services/s3/test_s3.py::TestS3::test_s3_analytics_configurations": { "last_validated_date": "2023-08-03T02:25:40+00:00" From eb0f96175118674cb4c15f7a792ec267300deb46 Mon Sep 17 00:00:00 2001 From: Marccio Silva <marccio.silva@localstack.cloud> Date: Wed, 15 May 2024 20:49:42 -0300 Subject: [PATCH 146/169] APIGW: Default to an empty dict when the provided body for an API Gateway step function is an empty string (#10816) --- localstack/services/apigateway/templates.py | 2 +- tests/unit/test_templating.py | 23 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/localstack/services/apigateway/templates.py b/localstack/services/apigateway/templates.py index 9dc1422b9afdd..90ae240d32941 100644 --- a/localstack/services/apigateway/templates.py +++ b/localstack/services/apigateway/templates.py @@ -190,7 +190,7 @@ def prepare_namespace(self, variables) -> Dict[str, Any]: namespace["stageVariables"] = stage_var input_var = variables.get("input") or {} variables = { - "input": VelocityInput(input_var.get("body"), input_var.get("params")), + "input": VelocityInput(input_var.get("body") or {}, input_var.get("params")), "util": VelocityUtilApiGateway(), } namespace.update(variables) diff --git a/tests/unit/test_templating.py b/tests/unit/test_templating.py index f93ff996aed34..67872019162e7 100644 --- a/tests/unit/test_templating.py +++ b/tests/unit/test_templating.py @@ -80,6 +80,21 @@ } """ +APIGW_TEMPLATE_BODY_FORWARDING_ONLY = """ +## Template that attempts to forward the request body to the execution input of +## the state machine. + +#set($inputString = '') +#set($allParams = $input.params()) +{ + #set($inputString = "$inputString,@@body@@: $input.body") + #set($inputString = "$inputString}") + #set($inputString = $inputString.replaceAll("@@",'"')) + #set($len = $inputString.length() - 1) + "input": "{$util.escapeJavaScript($inputString.substring(1,$len)).replaceAll("\\'","'")}" +} +""" + class TestMessageTransformationBasic: def test_return_macro(self): @@ -182,6 +197,14 @@ def test_array_size(self): result = ApiGatewayVtlTemplate().render_vtl(template, variables) assert result == " 2" + def test_template_rendering_with_empty_string_body(self): + template = APIGW_TEMPLATE_BODY_FORWARDING_ONLY + variables = {"input": {"body": ""}} + result = ApiGatewayVtlTemplate().render_vtl(template, variables) + result = re.sub(r"\s+", " ", result).strip() + result = json.loads(result) + assert result == {"input": '{"body": {}}'} + def test_message_transformation(self): template = APIGW_TEMPLATE_TRANSFORM_KINESIS records = [ From 01fc64511ae19bf68ffcd9569101082c1735e8d1 Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Thu, 16 May 2024 09:26:09 +0200 Subject: [PATCH 147/169] update test durations (#10833) --- .test_durations | 3375 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 3374 insertions(+), 1 deletion(-) diff --git a/.test_durations b/.test_durations index fb29c3a3decf8..94d5f162327c3 100644 --- a/.test_durations +++ b/.test_durations @@ -968,8 +968,14 @@ "tests/aws/services/lambda_/test_lambda_developer_tools.py::TestHotReloading::test_hot_reloading[nodejs18.x]": 3.06303496099963, "tests/aws/services/lambda_/test_lambda_developer_tools.py::TestHotReloading::test_hot_reloading[python3.9]": 2.900148914000056, "tests/aws/services/lambda_/test_lambda_developer_tools.py::TestHotReloading::test_hot_reloading_publish_version": 0.17520181300005788, - "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_deletion_event_source_mapping_with_dynamodb": 6.989548434999961, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_deletion_event_source_mapping_with_dynamodb": 65.70240591700002, "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_disabled_dynamodb_event_source_mapping": 15.30626196799949, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[content_filter_type]": 12.029870920999997, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[content_multiple_filters]": 11.95623391300012, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[content_or_filter]": 11.960007893999887, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[date_time_conversion]": 11.981410234999998, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[exists_filter_type]": 11.984267058, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[insert_same_entry_twice]": 42.08338016800002, "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put10-None-filter0-1]": 13.56093944200029, "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put11-item_to_put21-filter1-2]": 13.692982211000071, "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put12-item_to_put22-filter2-1]": 13.578544705999775, @@ -977,7 +983,3374 @@ "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put14-item_to_put24-filter4-0]": 13.460648560000209, "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put15-item_to_put25-filter5-0]": 13.429647027999636, "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put16-item_to_put26-filter6-1]": 13.461413060000268, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[prefix_filter]": 12.653915643999994, "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_source_mapping": 12.534212509000099, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_source_mapping_with_on_failure_destination_config": 11.636363534999987, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_invalid_event_filter[[{\"eventName\": [\"INSERT\"=123}]]": 3.859730550999984, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_invalid_event_filter[single-string]": 3.911510849000024, + "tests/aws/services/lambda_/test_lambda_integration_kinesis.py::TestKinesisEventFiltering::test_kinesis_event_filtering_json_pattern": 8.907987791000096, + "tests/aws/services/lambda_/test_lambda_integration_kinesis.py::TestKinesisSource::test_create_kinesis_event_source_mapping": 11.998650312000109, + "tests/aws/services/lambda_/test_lambda_integration_kinesis.py::TestKinesisSource::test_create_kinesis_event_source_mapping_multiple_lambdas_single_kinesis_event_stream": 19.028468417, + "tests/aws/services/lambda_/test_lambda_integration_kinesis.py::TestKinesisSource::test_disable_kinesis_event_source_mapping": 29.023371538999868, + "tests/aws/services/lambda_/test_lambda_integration_kinesis.py::TestKinesisSource::test_duplicate_event_source_mappings": 3.0375691499999675, + "tests/aws/services/lambda_/test_lambda_integration_kinesis.py::TestKinesisSource::test_kinesis_event_source_mapping_with_async_invocation": 0.0008571649999566944, + "tests/aws/services/lambda_/test_lambda_integration_kinesis.py::TestKinesisSource::test_kinesis_event_source_mapping_with_on_failure_destination_config": 9.161241069999846, + "tests/aws/services/lambda_/test_lambda_integration_kinesis.py::TestKinesisSource::test_kinesis_event_source_trim_horizon": 26.048679450999998, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::TestSQSEventSourceMapping::test_duplicate_event_source_mappings": 2.380562894000036, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::TestSQSEventSourceMapping::test_event_source_mapping_default_batch_size": 3.3538331000002017, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::TestSQSEventSourceMapping::test_sqs_event_filter[filter0-item_matching0-item_not_matching0]": 6.365141879000021, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::TestSQSEventSourceMapping::test_sqs_event_filter[filter1-item_matching1-item_not_matching1]": 6.369409483000027, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::TestSQSEventSourceMapping::test_sqs_event_filter[filter2-item_matching2-item_not_matching2]": 6.39507826800002, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::TestSQSEventSourceMapping::test_sqs_event_filter[filter3-item_matching3-item_not_matching3]": 6.374382959999934, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::TestSQSEventSourceMapping::test_sqs_event_filter[filter4-item_matching4-this is a test string]": 6.370342953999852, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::TestSQSEventSourceMapping::test_sqs_event_filter[filter5-item_matching5-item_not_matching5]": 6.369974540000044, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::TestSQSEventSourceMapping::test_sqs_event_filter[filter6-item_matching6-item_not_matching6]": 6.376985823999917, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::TestSQSEventSourceMapping::test_sqs_event_filter[filter7-item_matching7-item_not_matching7]": 6.3664332079999895, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::TestSQSEventSourceMapping::test_sqs_event_source_mapping": 6.370081586999845, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::TestSQSEventSourceMapping::test_sqs_event_source_mapping_update": 26.482445556000016, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::TestSQSEventSourceMapping::test_sqs_invalid_event_filter[None]": 1.3132066260000101, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::TestSQSEventSourceMapping::test_sqs_invalid_event_filter[invalid_filter2]": 1.2997808979999945, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::TestSQSEventSourceMapping::test_sqs_invalid_event_filter[invalid_filter3]": 1.3092748799998617, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::TestSQSEventSourceMapping::test_sqs_invalid_event_filter[simple string]": 1.9570565240001088, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::test_failing_lambda_retries_after_visibility_timeout": 29.40644923299999, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::test_fifo_message_group_parallelism": 63.308914815000094, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::test_message_body_and_attributes_passed_correctly": 3.8218953639999427, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::test_redrive_policy_with_failing_lambda": 16.00115070000004, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::test_report_batch_item_failures": 19.58526717100017, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::test_report_batch_item_failures_empty_json_batch_succeeds": 8.740799162999906, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::test_report_batch_item_failures_invalid_result_json_batch_fails": 13.162712597999871, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::test_report_batch_item_failures_on_lambda_error": 9.114919099000076, + "tests/aws/services/lambda_/test_lambda_integration_sqs.py::test_sqs_queue_as_lambda_dead_letter_queue": 2.122293002000106, + "tests/aws/services/lambda_/test_lambda_integration_xray.py::test_traceid_outside_handler[Active]": 2.485993255000153, + "tests/aws/services/lambda_/test_lambda_integration_xray.py::test_traceid_outside_handler[PassThrough]": 2.4820509969999875, + "tests/aws/services/lambda_/test_lambda_legacy.py::TestGolangRuntimes::test_golang_lambda": 0.0007217120000859722, + "tests/aws/services/lambda_/test_lambda_legacy.py::TestLambdaLegacyProvider::test_add_lambda_multiple_permission": 0.0009570170000188227, + "tests/aws/services/lambda_/test_lambda_legacy.py::TestLambdaLegacyProvider::test_add_lambda_permission": 0.0007694139999330218, + "tests/aws/services/lambda_/test_lambda_legacy.py::TestLambdaLegacyProvider::test_create_lambda_function": 0.0007493119996979658, + "tests/aws/services/lambda_/test_lambda_legacy.py::TestLambdaLegacyProvider::test_update_lambda_with_layers": 0.0010462190002726857, + "tests/aws/services/lambda_/test_lambda_legacy.py::TestRubyRuntimes::test_ruby_lambda_running_in_docker": 0.0007264120004037977, + "tests/aws/services/lambda_/test_lambda_legacy.py::test_logging_in_local_executor[logging]": 0.0017789310004445724, + "tests/aws/services/lambda_/test_lambda_legacy.py::test_logging_in_local_executor[print]": 0.0010278189997734444, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestGoProvidedRuntimes::test_manual_endpoint_injection[provided.al2023]": 1.702215986000283, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestGoProvidedRuntimes::test_manual_endpoint_injection[provided.al2]": 1.7181690739998885, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestGoProvidedRuntimes::test_uncaught_exception_invoke[provided.al2023]": 2.7687620990002415, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestGoProvidedRuntimes::test_uncaught_exception_invoke[provided.al2]": 2.7384266670001125, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestJavaRuntimes::test_java_custom_handler_method_specification[cloud.localstack.sample.LambdaHandlerWithInterfaceAndCustom-INTERFACE]": 1.922735049999801, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestJavaRuntimes::test_java_custom_handler_method_specification[cloud.localstack.sample.LambdaHandlerWithInterfaceAndCustom::handleRequest-INTERFACE]": 2.9149042660001214, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestJavaRuntimes::test_java_custom_handler_method_specification[cloud.localstack.sample.LambdaHandlerWithInterfaceAndCustom::handleRequestCustom-CUSTOM]": 2.9099716429998352, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestJavaRuntimes::test_java_lambda_subscribe_sns_topic": 8.06448247000003, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestJavaRuntimes::test_java_runtime_with_lib": 11.561693197999944, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestJavaRuntimes::test_serializable_input_object[java11]": 2.3378456939999523, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestJavaRuntimes::test_serializable_input_object[java17]": 2.155391525000141, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestJavaRuntimes::test_serializable_input_object[java21]": 2.3944248340001195, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestJavaRuntimes::test_serializable_input_object[java8.al2]": 2.3755543530000978, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestJavaRuntimes::test_serializable_input_object[java8]": 3.4628529380001964, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestJavaRuntimes::test_stream_handler[java11]": 1.6247334380000211, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestJavaRuntimes::test_stream_handler[java17]": 7.594286626999747, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestJavaRuntimes::test_stream_handler[java21]": 7.941220542999872, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestJavaRuntimes::test_stream_handler[java8.al2]": 3.607198140000037, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestJavaRuntimes::test_stream_handler[java8]": 2.2008104649994493, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestNodeJSRuntimes::test_invoke_nodejs_es6_lambda[nodejs14.x]": 5.1264774329997636, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestNodeJSRuntimes::test_invoke_nodejs_es6_lambda[nodejs16.x]": 10.56440412500001, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestNodeJSRuntimes::test_invoke_nodejs_es6_lambda[nodejs18.x]": 10.587688369000034, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestNodeJSRuntimes::test_invoke_nodejs_es6_lambda[nodejs20.x]": 10.56366535299992, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_handler_in_submodule[python3.10]": 15.65542199499987, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_handler_in_submodule[python3.11]": 1.5735545780000848, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_handler_in_submodule[python3.12]": 1.6136287119998087, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_handler_in_submodule[python3.7]": 2.036244644999897, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_handler_in_submodule[python3.8]": 1.59418438900002, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_handler_in_submodule[python3.9]": 1.6511087590001807, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_python_runtime_correct_versions[python3.10]": 1.47626534799997, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_python_runtime_correct_versions[python3.11]": 1.4681422940000175, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_python_runtime_correct_versions[python3.12]": 1.4645522490000076, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_python_runtime_correct_versions[python3.7]": 0.001176621000013256, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_python_runtime_correct_versions[python3.8]": 1.4997710860000097, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_python_runtime_correct_versions[python3.9]": 2.2143838790000245, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_python_runtime_unhandled_errors[python3.10]": 0.0009830170001805527, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_python_runtime_unhandled_errors[python3.11]": 0.0007296129997484968, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_python_runtime_unhandled_errors[python3.7]": 0.0011284209999757877, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_python_runtime_unhandled_errors[python3.8]": 0.0009532170001875784, + "tests/aws/services/lambda_/test_lambda_runtimes.py::TestPythonRuntimes::test_python_runtime_unhandled_errors[python3.9]": 0.0012389220000841306, + "tests/aws/services/lambda_/test_lambda_whitebox.py::TestDockerExecutors::test_additional_docker_flags": 0.0007339130002037564, + "tests/aws/services/lambda_/test_lambda_whitebox.py::TestDockerExecutors::test_code_updated_on_redeployment": 0.0011782199999288423, + "tests/aws/services/lambda_/test_lambda_whitebox.py::TestDockerExecutors::test_destroy_idle_containers": 0.0007428140002048167, + "tests/aws/services/lambda_/test_lambda_whitebox.py::TestDockerExecutors::test_logresult_more_than_4k_characters": 0.0013273240001581144, + "tests/aws/services/lambda_/test_lambda_whitebox.py::TestDockerExecutors::test_prime_and_destroy_containers": 0.0007460139995600912, + "tests/aws/services/lambda_/test_lambda_whitebox.py::TestFunctionStates::test_invoke_failure_when_state_pending": 0.0007421129994327202, + "tests/aws/services/lambda_/test_lambda_whitebox.py::TestLambdaFallbackUrl::test_adding_fallback_function_name_in_headers": 0.0007912129999567696, + "tests/aws/services/lambda_/test_lambda_whitebox.py::TestLambdaFallbackUrl::test_forward_to_fallback_url_dynamodb": 0.002531645000090066, + "tests/aws/services/lambda_/test_lambda_whitebox.py::TestLambdaFallbackUrl::test_forward_to_fallback_url_http": 0.0007415129998662451, + "tests/aws/services/lambda_/test_lambda_whitebox.py::TestLocalExecutors::test_python3_runtime_multiple_create_with_conflicting_module": 0.0007397130007120722, + "tests/aws/services/logs/test_logs.py::TestCloudWatchLogs::test_create_and_delete_log_group": 0.02595307699971272, + "tests/aws/services/logs/test_logs.py::TestCloudWatchLogs::test_create_and_delete_log_stream": 0.12053234399991197, + "tests/aws/services/logs/test_logs.py::TestCloudWatchLogs::test_delivery_logs_for_sns": 1.0319462599998133, + "tests/aws/services/logs/test_logs.py::TestCloudWatchLogs::test_filter_log_events_response_header": 0.01697640900033548, + "tests/aws/services/logs/test_logs.py::TestCloudWatchLogs::test_list_tags_log_group": 0.20526764300029754, + "tests/aws/services/logs/test_logs.py::TestCloudWatchLogs::test_metric_filters": 0.000940899999704925, + "tests/aws/services/logs/test_logs.py::TestCloudWatchLogs::test_put_events_multi_bytes_msg": 0.017708558999856905, + "tests/aws/services/logs/test_logs.py::TestCloudWatchLogs::test_put_subscription_filter_firehose": 0.3247550979999687, + "tests/aws/services/logs/test_logs.py::TestCloudWatchLogs::test_put_subscription_filter_kinesis": 2.2748052300000836, + "tests/aws/services/logs/test_logs.py::TestCloudWatchLogs::test_put_subscription_filter_lambda": 1.638770268999906, + "tests/aws/services/opensearch/test_opensearch.py::TestCustomBackendManager::test_custom_backend": 0.0840780910000376, + "tests/aws/services/opensearch/test_opensearch.py::TestCustomBackendManager::test_custom_backend_with_custom_endpoint": 0.0824167790001411, + "tests/aws/services/opensearch/test_opensearch.py::TestEdgeProxiedOpensearchCluster::test_custom_endpoint": 16.79398734700021, + "tests/aws/services/opensearch/test_opensearch.py::TestEdgeProxiedOpensearchCluster::test_custom_endpoint_disabled": 16.242747867999924, + "tests/aws/services/opensearch/test_opensearch.py::TestEdgeProxiedOpensearchCluster::test_route_through_edge": 16.37831340599996, + "tests/aws/services/opensearch/test_opensearch.py::TestMultiClusterManager::test_multi_cluster": 34.43233357400027, + "tests/aws/services/opensearch/test_opensearch.py::TestMultiplexingClusterManager::test_multiplexing_cluster": 17.110790998000084, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_cloudformation_deployment": 23.23072848700008, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_create_domain": 17.7984975520003, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_create_domain_with_invalid_custom_endpoint": 0.008081975000095554, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_create_domain_with_invalid_name": 0.01183344500009298, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_create_existing_domain_causes_exception": 16.766494731999956, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_create_indices": 17.534822351999992, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_describe_domains": 16.742763427, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_domain_version": 16.258703616000048, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_endpoint_strategy_path": 16.754736436000258, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_endpoint_strategy_port": 16.708731869999838, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_exception_header_field": 0.004439039000089906, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_get_compatible_version_for_domain": 16.852524319000167, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_get_compatible_versions": 0.004999407000013889, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_get_document": 17.688502238999945, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_gzip_responses": 16.81011892399988, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_list_versions": 0.043879693000008047, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_search": 17.627307255999995, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_security_plugin": 26.44022615400013, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_security_plugin[OpenSearch_1.3]": 41.457079351001084, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_security_plugin[OpenSearch_2.5]": 25.165472549000697, + "tests/aws/services/opensearch/test_opensearch.py::TestOpensearchProvider::test_update_domain_config": 16.748448025000243, + "tests/aws/services/opensearch/test_opensearch.py::TestSingletonClusterManager::test_endpoint_strategy_port_singleton_cluster": 15.92595610100011, + "tests/aws/services/redshift/test_redshift.py::TestRedshift::test_cluster_security_groups": 0.014143037999701846, + "tests/aws/services/redshift/test_redshift.py::TestRedshift::test_create_clusters": 21.73816540499979, + "tests/aws/services/resource_groups/test_resource_groups.py::TestResourceGroups::test_cloudformation_query": 0.000853177999943, + "tests/aws/services/resource_groups/test_resource_groups.py::TestResourceGroups::test_create_group": 0.1889994159998878, + "tests/aws/services/resource_groups/test_resource_groups.py::TestResourceGroups::test_resource_groups_different_region": 0.0008102369999960501, + "tests/aws/services/resource_groups/test_resource_groups.py::TestResourceGroups::test_resource_groups_tag_query": 0.001025752000259672, + "tests/aws/services/resource_groups/test_resource_groups.py::TestResourceGroups::test_resource_type_filters": 0.0008125210001708183, + "tests/aws/services/resource_groups/test_resource_groups.py::TestResourceGroups::test_search_resources": 0.00080630000002202, + "tests/aws/services/resourcegroupstaggingapi/test_rgsa.py::TestRGSAIntegrations::test_get_resources": 0.2464910929998041, + "tests/aws/services/route53/test_route53.py::TestRoute53::test_associate_vpc_with_hosted_zone": 0.10904181600017182, + "tests/aws/services/route53/test_route53.py::TestRoute53::test_create_hosted_zone": 0.19886758599977838, + "tests/aws/services/route53/test_route53.py::TestRoute53::test_create_hosted_zone_in_non_existent_vpc": 0.07197241200015014, + "tests/aws/services/route53/test_route53.py::TestRoute53::test_create_private_hosted_zone": 0.21319679800012636, + "tests/aws/services/route53/test_route53.py::TestRoute53::test_crud_health_check": 0.026342915999975958, + "tests/aws/services/route53/test_route53.py::TestRoute53::test_reusable_delegation_sets": 0.057584607999842774, + "tests/aws/services/route53resolver/test_route53resolver.py::TestRoute53Resolver::test_associate_and_disassociate_resolver_rule": 0.1721123310001076, + "tests/aws/services/route53resolver/test_route53resolver.py::TestRoute53Resolver::test_create_resolver_endpoint[INBOUND-5]": 0.17053669300003094, + "tests/aws/services/route53resolver/test_route53resolver.py::TestRoute53Resolver::test_create_resolver_endpoint[OUTBOUND-10]": 0.10379138899997997, + "tests/aws/services/route53resolver/test_route53resolver.py::TestRoute53Resolver::test_create_resolver_query_log_config": 0.18117351600017173, + "tests/aws/services/route53resolver/test_route53resolver.py::TestRoute53Resolver::test_create_resolver_rule": 0.13737414999991415, + "tests/aws/services/route53resolver/test_route53resolver.py::TestRoute53Resolver::test_create_resolver_rule_with_invalid_direction": 0.11006163399997604, + "tests/aws/services/route53resolver/test_route53resolver.py::TestRoute53Resolver::test_delete_non_existent_resolver_endpoint": 0.0323987889998989, + "tests/aws/services/route53resolver/test_route53resolver.py::TestRoute53Resolver::test_delete_non_existent_resolver_query_log_config": 0.053972608999856675, + "tests/aws/services/route53resolver/test_route53resolver.py::TestRoute53Resolver::test_delete_non_existent_resolver_rule": 0.031093627000018387, + "tests/aws/services/route53resolver/test_route53resolver.py::TestRoute53Resolver::test_delete_resolver_endpoint": 0.10699914699989677, + "tests/aws/services/route53resolver/test_route53resolver.py::TestRoute53Resolver::test_disassociate_non_existent_association": 0.03071384300005775, + "tests/aws/services/route53resolver/test_route53resolver.py::TestRoute53Resolver::test_list_firewall_domain_lists": 0.06519145399965964, + "tests/aws/services/route53resolver/test_route53resolver.py::TestRoute53Resolver::test_multipe_create_resolver_rule": 0.14904397799978142, + "tests/aws/services/route53resolver/test_route53resolver.py::TestRoute53Resolver::test_multiple_create_resolver_endpoint_with_same_req_id": 0.10790325099992515, + "tests/aws/services/route53resolver/test_route53resolver.py::TestRoute53Resolver::test_route53resolver_bad_create_endpoint_security_groups": 0.07934029699981693, + "tests/aws/services/route53resolver/test_route53resolver.py::TestRoute53Resolver::test_update_resolver_endpoint": 0.11408705600024405, + "tests/aws/services/s3/test_s3.py::TestS3::test_access_bucket_different_region": 0.0008444830000371439, + "tests/aws/services/s3/test_s3.py::TestS3::test_bucket_availability": 0.012571356999842465, + "tests/aws/services/s3/test_s3.py::TestS3::test_bucket_does_not_exist": 0.18055564099972798, + "tests/aws/services/s3/test_s3.py::TestS3::test_bucket_exists": 0.07944859100007307, + "tests/aws/services/s3/test_s3.py::TestS3::test_bucket_name_with_dots": 0.17243385400001898, + "tests/aws/services/s3/test_s3.py::TestS3::test_bucket_operation_between_regions": 0.17176189499991779, + "tests/aws/services/s3/test_s3.py::TestS3::test_complete_multipart_parts_checksum": 0.0008320189999722061, + "tests/aws/services/s3/test_s3.py::TestS3::test_complete_multipart_parts_order": 0.3930535740000778, + "tests/aws/services/s3/test_s3.py::TestS3::test_copy_in_place_with_bucket_encryption": 0.07368279299998903, + "tests/aws/services/s3/test_s3.py::TestS3::test_copy_object_kms": 1.2340223849998893, + "tests/aws/services/s3/test_s3.py::TestS3::test_copy_object_special_character": 0.20994696700017812, + "tests/aws/services/s3/test_s3.py::TestS3::test_copy_object_special_character_plus_for_space": 0.03031858900021689, + "tests/aws/services/s3/test_s3.py::TestS3::test_create_bucket_head_bucket": 0.21556773200018142, + "tests/aws/services/s3/test_s3.py::TestS3::test_create_bucket_via_host_name": 0.17039942400015207, + "tests/aws/services/s3/test_s3.py::TestS3::test_create_bucket_with_existing_name": 0.14540928400015218, + "tests/aws/services/s3/test_s3.py::TestS3::test_delete_bucket_no_such_bucket": 0.007347107000214237, + "tests/aws/services/s3/test_s3.py::TestS3::test_delete_bucket_with_content": 0.24393912999994427, + "tests/aws/services/s3/test_s3.py::TestS3::test_delete_keys_in_versioned_bucket": 0.1753236110000671, + "tests/aws/services/s3/test_s3.py::TestS3::test_delete_non_existing_keys": 0.02429594900036136, + "tests/aws/services/s3/test_s3.py::TestS3::test_delete_non_existing_keys_in_non_existing_bucket": 0.008301830000164045, + "tests/aws/services/s3/test_s3.py::TestS3::test_delete_non_existing_keys_quiet": 0.02373944300029507, + "tests/aws/services/s3/test_s3.py::TestS3::test_delete_object_tagging": 0.03363434200014126, + "tests/aws/services/s3/test_s3.py::TestS3::test_delete_objects_encoding": 0.03928287100029593, + "tests/aws/services/s3/test_s3.py::TestS3::test_different_location_constraint": 0.2095073909997609, + "tests/aws/services/s3/test_s3.py::TestS3::test_download_fileobj_multiple_range_requests": 0.7690595730000496, + "tests/aws/services/s3/test_s3.py::TestS3::test_empty_bucket_fixture": 0.04274887600013244, + "tests/aws/services/s3/test_s3.py::TestS3::test_etag_on_get_object_call": 0.1513049969998974, + "tests/aws/services/s3/test_s3.py::TestS3::test_get_bucket_notification_configuration_no_such_bucket": 0.00793128300006174, + "tests/aws/services/s3/test_s3.py::TestS3::test_get_bucket_versioning_order": 0.24253961500016885, + "tests/aws/services/s3/test_s3.py::TestS3::test_get_object_after_deleted_in_versioned_bucket": 0.03605609999999615, + "tests/aws/services/s3/test_s3.py::TestS3::test_get_object_attributes": 0.10899202199993852, + "tests/aws/services/s3/test_s3.py::TestS3::test_get_object_attributes_versioned": 0.1709445839996988, + "tests/aws/services/s3/test_s3.py::TestS3::test_get_object_content_length_with_virtual_host[False]": 0.0554823850000048, + "tests/aws/services/s3/test_s3.py::TestS3::test_get_object_content_length_with_virtual_host[True]": 0.0678535090000878, + "tests/aws/services/s3/test_s3.py::TestS3::test_get_object_no_such_bucket": 0.008159171999977843, + "tests/aws/services/s3/test_s3.py::TestS3::test_get_object_part": 0.10805107700002736, + "tests/aws/services/s3/test_s3.py::TestS3::test_get_object_with_anon_credentials": 0.1665430779999042, + "tests/aws/services/s3/test_s3.py::TestS3::test_get_range_object_headers": 0.026823085999922114, + "tests/aws/services/s3/test_s3.py::TestS3::test_head_object_fields": 0.029467410999814092, + "tests/aws/services/s3/test_s3.py::TestS3::test_invalid_range_error": 0.027327843999955803, + "tests/aws/services/s3/test_s3.py::TestS3::test_list_multipart_uploads_parameters": 0.23237182999946526, + "tests/aws/services/s3/test_s3.py::TestS3::test_list_objects_v2_with_prefix": 0.33396188500046264, + "tests/aws/services/s3/test_s3.py::TestS3::test_list_objects_versions_with_prefix": 0.611541760998989, + "tests/aws/services/s3/test_s3.py::TestS3::test_list_objects_with_prefix[%2F]": 0.2966503179995925, + "tests/aws/services/s3/test_s3.py::TestS3::test_list_objects_with_prefix[/]": 0.3220028720006667, + "tests/aws/services/s3/test_s3.py::TestS3::test_metadata_header_character_decoding": 0.149148322000201, + "tests/aws/services/s3/test_s3.py::TestS3::test_multipart_and_list_parts": 0.10118624499978068, + "tests/aws/services/s3/test_s3.py::TestS3::test_multipart_complete_multipart_too_small": 0.05868280599975151, + "tests/aws/services/s3/test_s3.py::TestS3::test_multipart_complete_multipart_wrong_part": 0.02950325699998757, + "tests/aws/services/s3/test_s3.py::TestS3::test_multipart_copy_object_etag": 0.03805932299997039, + "tests/aws/services/s3/test_s3.py::TestS3::test_multipart_no_such_upload": 0.02650789499966777, + "tests/aws/services/s3/test_s3.py::TestS3::test_multipart_overwrite_key": 0.041848286999993434, + "tests/aws/services/s3/test_s3.py::TestS3::test_multipart_parts_checksum_exceptions": 0.025507528000161983, + "tests/aws/services/s3/test_s3.py::TestS3::test_object_with_slashes_in_key[False]": 0.0667188449999685, + "tests/aws/services/s3/test_s3.py::TestS3::test_object_with_slashes_in_key[True]": 0.0661683019998236, + "tests/aws/services/s3/test_s3.py::TestS3::test_precondition_failed_error": 0.04244986800017614, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_and_get_bucket_policy": 0.02906347900034234, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_and_get_object_with_content_language_disposition": 0.30814267200003087, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_and_get_object_with_hash_prefix": 0.15480821500000275, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_and_get_object_with_utf8_key": 0.15826209099986954, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_bucket_inventory_config_order": 0.04532737500016992, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_get_object_special_character[a/%F0%9F%98%80/]": 0.1598185690002083, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_get_object_special_character[file%2Fname]": 0.156305785999848, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_get_object_special_character[test key//]": 0.15186199900017527, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_get_object_special_character[test key/]": 0.15188800500004618, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_get_object_special_character[test%123/]": 0.15130464200024107, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_get_object_special_character[test%123]": 0.15376027199999953, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_get_object_special_character[test%percent]": 0.15327811499992094, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_get_object_special_character[test@key/]": 0.15901567899982183, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_acl_on_delete_marker": 0.17212949999998273, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_checksum[CRC32C]": 0.040309309999884135, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_checksum[CRC32]": 0.04062836000025527, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_checksum[SHA1]": 0.0390652560001854, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_checksum[SHA256]": 0.04009843800008639, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_chunked_newlines": 0.024820141000191143, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_chunked_newlines_no_sig": 0.02379619999987881, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_chunked_newlines_with_checksum": 0.2988038599999072, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class[DEEP_ARCHIVE-False]": 0.02838139699997555, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class[DEEP_ARCHIVE]": 0.06793593600013992, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class[GLACIER-False]": 0.029590455999823462, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class[GLACIER]": 0.0738647440002751, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class[GLACIER_IR-True]": 0.02963472900000852, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class[GLACIER_IR]": 0.06832904299972142, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class[INTELLIGENT_TIERING-True]": 0.02907704300014302, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class[INTELLIGENT_TIERING]": 0.06720872400001099, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class[ONEZONE_IA-True]": 0.029620994999959294, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class[ONEZONE_IA]": 0.07376414300051692, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class[REDUCED_REDUNDANCY-True]": 0.02982950999989953, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class[REDUCED_REDUNDANCY]": 0.07696750200102542, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class[STANDARD-True]": 0.03322424399993906, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class[STANDARD]": 0.07454755699836824, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class[STANDARD_IA-True]": 0.02911832900031186, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class[STANDARD_IA]": 0.07096339099916804, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_storage_class_outposts": 0.023625632999937807, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_tagging_empty_list": 0.03894807899996522, + "tests/aws/services/s3/test_s3.py::TestS3::test_put_object_with_md5_and_chunk_signature": 0.02580482399957873, + "tests/aws/services/s3/test_s3.py::TestS3::test_putobject_with_multiple_keys": 0.14863079100018695, + "tests/aws/services/s3/test_s3.py::TestS3::test_range_header_body_length": 0.03145822500005124, + "tests/aws/services/s3/test_s3.py::TestS3::test_range_key_not_exists": 0.021709190999899874, + "tests/aws/services/s3/test_s3.py::TestS3::test_region_header_exists": 0.16927184399992257, + "tests/aws/services/s3/test_s3.py::TestS3::test_resource_object_with_slashes_in_key": 0.11590947799959395, + "tests/aws/services/s3/test_s3.py::TestS3::test_response_structure": 0.05545736400017631, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_analytics_configurations": 0.0651161130001583, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_batch_delete_objects": 0.1831474419998358, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_batch_delete_objects_using_requests_with_acl": 0.0009515050001027703, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_batch_delete_public_objects_using_requests": 0.23182589700013523, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_bucket_acl": 0.04611973799978841, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_bucket_acl_exceptions": 0.05925105600022107, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_checksum_with_content_encoding": 0.03445450200001687, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_content_type_and_metadata": 0.1654999179997958, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_metadata_directive_copy": 0.17002690899994377, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_metadata_replace": 0.15956735600002503, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_object_in_place": 0.17392152299998997, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_object_in_place_metadata_directive": 0.18090643700043074, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_object_in_place_storage_class": 0.15787487199986572, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_object_in_place_versioned": 0.1846154120000847, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_object_in_place_website_redirect_location": 0.15725379600007727, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_object_in_place_with_encryption": 0.2563496010000108, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_object_preconditions": 3.1695702139998048, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_object_storage_class": 0.1718964810002035, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_object_with_checksum[CRC32C]": 0.15787364599987086, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_object_with_checksum[CRC32]": 0.15947277299983398, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_object_with_checksum[SHA1]": 1.1511908910003967, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_object_with_checksum[SHA256]": 0.15700394399982542, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_object_wrong_format": 0.13760296000032213, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_tagging_directive[COPY]": 0.15736198700005843, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_tagging_directive[None]": 0.15585942799975783, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_copy_tagging_directive[REPLACE]": 0.15560937399959585, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_delete_object_with_version_id": 0.23721239799965588, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_delete_objects_trailing_slash": 0.022664267999971344, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_download_object_with_lambda": 4.075501564000206, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_get_object_checksum[CRC32C]": 0.038245825000331024, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_get_object_checksum[CRC32]": 0.045851445999915086, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_get_object_checksum[None]": 0.03746060399998896, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_get_object_checksum[SHA1]": 0.036964356000453336, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_get_object_checksum[SHA256]": 0.03797900499989737, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_get_object_header_overrides": 0.028416144000175336, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_get_object_headers": 0.05187765899995611, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_get_object_preconditions[get_object]": 3.1927356210001108, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_get_object_preconditions[head_object]": 3.174930300000142, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_hostname_with_subdomain": 0.008280257000023994, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_intelligent_tier_config": 0.04974543900016215, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_invalid_content_md5": 0.06163451099973827, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_inventory_report_crud": 0.04903089999993426, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_lambda_integration": 14.578819952000003, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_list_objects_empty_marker": 0.3535436590000245, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_multipart_upload_acls": 0.061669891000065036, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_multipart_upload_sse": 0.06617755700017369, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_object_acl": 0.050186812000220016, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_object_acl_exceptions": 0.06752740899969467, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_object_expiry": 3.185596599000064, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_put_inventory_report_exceptions": 0.0449175999997351, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_put_more_than_1000_items": 4.02771765600005, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_put_object_versioned": 0.29251398099972903, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_raw_request_routing": 0.039789918999986185, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_request_payer": 0.024346354000272186, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_request_payer_exceptions": 0.023695555000131208, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_sse_bucket_key_default": 0.07521719299984397, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_sse_default_kms_key": 0.0009583619998920767, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_sse_validate_kms_key": 1.0065991209999083, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_sse_validate_kms_key_state": 0.09683218100008162, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_timestamp_precision": 0.03567324299979191, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_upload_download_gzip": 0.05935954700021284, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_uppercase_bucket_name": 0.19895948400017005, + "tests/aws/services/s3/test_s3.py::TestS3::test_s3_uppercase_key_names": 0.029774667999845406, + "tests/aws/services/s3/test_s3.py::TestS3::test_set_external_hostname": 0.04232186399985949, + "tests/aws/services/s3/test_s3.py::TestS3::test_upload_big_file": 0.32653044399989994, + "tests/aws/services/s3/test_s3.py::TestS3::test_upload_file_multipart": 0.1594533350000802, + "tests/aws/services/s3/test_s3.py::TestS3::test_upload_file_with_xml_preamble": 0.14791502200023388, + "tests/aws/services/s3/test_s3.py::TestS3::test_upload_part_chunked_cancelled_valid_etag": 0.03914462300008381, + "tests/aws/services/s3/test_s3.py::TestS3::test_upload_part_chunked_newlines_valid_etag": 0.029916879999746016, + "tests/aws/services/s3/test_s3.py::TestS3::test_url_encoded_key[False]": 0.04442478700002539, + "tests/aws/services/s3/test_s3.py::TestS3::test_url_encoded_key[True]": 0.05384118300025875, + "tests/aws/services/s3/test_s3.py::TestS3::test_virtual_host_proxy_does_not_decode_gzip": 0.03280378799991013, + "tests/aws/services/s3/test_s3.py::TestS3::test_virtual_host_proxying_headers": 0.03362476299980699, + "tests/aws/services/s3/test_s3.py::TestS3BucketLifecycle::test_bucket_lifecycle_configuration_date": 0.031049444000245785, + "tests/aws/services/s3/test_s3.py::TestS3BucketLifecycle::test_bucket_lifecycle_configuration_object_expiry": 0.034549490999779664, + "tests/aws/services/s3/test_s3.py::TestS3BucketLifecycle::test_bucket_lifecycle_configuration_object_expiry_versioned": 0.04912481599990315, + "tests/aws/services/s3/test_s3.py::TestS3BucketLifecycle::test_bucket_lifecycle_multiple_rules": 0.03653829799986852, + "tests/aws/services/s3/test_s3.py::TestS3BucketLifecycle::test_bucket_lifecycle_object_size_rules": 0.03719948000025397, + "tests/aws/services/s3/test_s3.py::TestS3BucketLifecycle::test_bucket_lifecycle_tag_rules": 0.0598728659997505, + "tests/aws/services/s3/test_s3.py::TestS3BucketLifecycle::test_delete_bucket_lifecycle_configuration": 0.034183619999794246, + "tests/aws/services/s3/test_s3.py::TestS3BucketLifecycle::test_delete_lifecycle_configuration_on_bucket_deletion": 0.03419870399989122, + "tests/aws/services/s3/test_s3.py::TestS3BucketLifecycle::test_lifecycle_expired_object_delete_marker": 0.0309315740000784, + "tests/aws/services/s3/test_s3.py::TestS3BucketLifecycle::test_object_expiry_after_bucket_lifecycle_configuration": 0.03687289200001942, + "tests/aws/services/s3/test_s3.py::TestS3BucketLifecycle::test_put_bucket_lifecycle_conf_exc": 0.04226695500005917, + "tests/aws/services/s3/test_s3.py::TestS3BucketLogging::test_put_bucket_logging": 0.04200616599996465, + "tests/aws/services/s3/test_s3.py::TestS3BucketLogging::test_put_bucket_logging_accept_wrong_grants": 0.03634416299996701, + "tests/aws/services/s3/test_s3.py::TestS3BucketLogging::test_put_bucket_logging_cross_locations": 0.0460881160001918, + "tests/aws/services/s3/test_s3.py::TestS3BucketLogging::test_put_bucket_logging_wrong_target": 0.03344005600024502, + "tests/aws/services/s3/test_s3.py::TestS3BucketPolicies::test_access_to_bucket_not_denied": 0.0008905180000056134, + "tests/aws/services/s3/test_s3.py::TestS3BucketReplication::test_replication_config": 0.21599105299969779, + "tests/aws/services/s3/test_s3.py::TestS3BucketReplication::test_replication_config_without_filter": 0.20940194599984352, + "tests/aws/services/s3/test_s3.py::TestS3DeepArchive::test_s3_get_deep_archive_object_restore": 0.177004628000077, + "tests/aws/services/s3/test_s3.py::TestS3DeepArchive::test_storage_class_deep_archive": 0.04610290699974939, + "tests/aws/services/s3/test_s3.py::TestS3MultiAccounts::test_cross_account_access": 0.03894403799995416, + "tests/aws/services/s3/test_s3.py::TestS3MultiAccounts::test_cross_account_copy_object": 0.027292557000009765, + "tests/aws/services/s3/test_s3.py::TestS3MultiAccounts::test_shared_bucket_namespace": 0.11909633099980965, + "tests/aws/services/s3/test_s3.py::TestS3ObjectLockLegalHold::test_delete_locked_object": 0.03618522400006441, + "tests/aws/services/s3/test_s3.py::TestS3ObjectLockLegalHold::test_put_get_object_legal_hold": 0.03931917399995655, + "tests/aws/services/s3/test_s3.py::TestS3ObjectLockLegalHold::test_put_object_legal_hold_exc": 0.04774070200028291, + "tests/aws/services/s3/test_s3.py::TestS3ObjectLockLegalHold::test_put_object_with_legal_hold": 0.031036720999964018, + "tests/aws/services/s3/test_s3.py::TestS3ObjectLockLegalHold::test_s3_copy_object_legal_hold": 0.1601275610000812, + "tests/aws/services/s3/test_s3.py::TestS3ObjectLockLegalHold::test_s3_legal_hold_lock_versioned": 0.17136415300001318, + "tests/aws/services/s3/test_s3.py::TestS3ObjectLockRetention::test_bucket_config_default_retention": 0.038059975999885864, + "tests/aws/services/s3/test_s3.py::TestS3ObjectLockRetention::test_object_lock_delete_markers": 0.03681764000043586, + "tests/aws/services/s3/test_s3.py::TestS3ObjectLockRetention::test_object_lock_extend_duration": 0.03662758199993732, + "tests/aws/services/s3/test_s3.py::TestS3ObjectLockRetention::test_s3_copy_object_retention_lock": 0.16774228500025856, + "tests/aws/services/s3/test_s3.py::TestS3ObjectLockRetention::test_s3_object_retention": 6.0504608609999195, + "tests/aws/services/s3/test_s3.py::TestS3ObjectLockRetention::test_s3_object_retention_exc": 0.06669252400024561, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_policy_conditions_validation_eq": 0.11603300599995237, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_policy_conditions_validation_starts_with": 0.10075066399986099, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_policy_validation_size": 0.07298861799972656, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_with_file_as_string": 0.11250162700002875, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_with_files": 0.07103505600002791, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_with_metadata": 0.0785369410000385, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_with_storage_class": 0.10738687100001698, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_with_tags[invalid]": 0.05485572700013108, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_with_tags[list]": 0.053584596000064266, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_with_tags[notxml]": 0.05051998199996888, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_with_tags[single]": 0.05470348900007593, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_object_with_wrong_content_type": 0.0539259879999463, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_request_expires": 3.0511759119999624, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_request_malformed_policy[s3]": 0.05490022899994074, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_request_malformed_policy[s3v4]": 0.05300438699987353, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_request_missing_fields[s3]": 0.05802954200021304, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_request_missing_fields[s3v4]": 0.05832565599985173, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_request_missing_signature[s3]": 0.9696146500000395, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_post_request_missing_signature[s3v4]": 0.05271031999996012, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_presigned_post_with_different_user_credentials": 0.07024664000005032, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_s3_presigned_post_success_action_redirect": 0.03188035200014383, + "tests/aws/services/s3/test_s3.py::TestS3PresignedPost::test_s3_presigned_post_success_action_status_201_response": 0.023127749999957814, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_delete_has_empty_content_length_header": 0.030030392000071515, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_get_object_ignores_request_body": 0.02660295999999107, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_get_request_expires_ignored_if_validation_disabled": 3.0379890199997135, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_head_has_correct_content_length_header": 0.026148497999884057, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_pre_signed_url_forward_slash_bucket": 0.05228405999991992, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presign_check_signature_validation_for_port_permutation": 0.037050614999770914, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presign_with_additional_query_params": 0.04598192499997822, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_signature_authentication[s3-False]": 0.09132331499995416, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_signature_authentication[s3-True]": 0.09447678499986978, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_signature_authentication[s3v4-False]": 0.094482785000082, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_signature_authentication[s3v4-True]": 0.11133343899996362, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_signature_authentication_expired[s3-False]": 2.060159720999991, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_signature_authentication_expired[s3-True]": 2.0666266099999575, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_signature_authentication_expired[s3v4-False]": 2.061713847999954, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_signature_authentication_expired[s3v4-True]": 2.233442279999963, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_signature_authentication_multi_part[s3-False]": 0.04527683000014804, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_signature_authentication_multi_part[s3-True]": 0.050234832999876744, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_signature_authentication_multi_part[s3v4-False]": 0.045335983999848395, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_signature_authentication_multi_part[s3v4-True]": 0.05185513500032357, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_v4_signed_headers_in_qs": 1.870505059999914, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_v4_x_amz_in_qs": 7.349661120999826, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_with_different_user_credentials": 0.07601835900027254, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_with_session_token": 0.03966746200012494, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_put_object": 0.14993645399999878, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_put_object_with_md5_and_chunk_signature_bad_headers[s3-False]": 0.03251577600008204, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_put_object_with_md5_and_chunk_signature_bad_headers[s3-True]": 0.05828376300019045, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_put_object_with_md5_and_chunk_signature_bad_headers[s3v4-False]": 0.03424069600009716, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_put_object_with_md5_and_chunk_signature_bad_headers[s3v4-True]": 0.05811605099984263, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_put_url_metadata": 0.34379176200036454, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_put_url_metadata_with_sig_s3[False]": 0.18562190600005124, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_put_url_metadata_with_sig_s3[True]": 0.18400622000035582, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_put_url_metadata_with_sig_s3v4[False]": 0.20126454399996874, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_put_url_metadata_with_sig_s3v4[True]": 0.19234603300014896, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_copy_md5": 0.040715014999932464, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_get_response_case_sensitive_headers": 0.02849139499994635, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_get_response_case_sensitive_headers[False]": 0.07302654500017525, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_get_response_case_sensitive_headers[True]": 0.06450209899958281, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_get_response_content_type_same_as_upload_and_range": 0.030856036999921344, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_get_response_default_content_type": 0.025383523999835234, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_get_response_header_overrides[s3]": 0.03280259699999988, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_get_response_header_overrides[s3v4]": 0.032839863000390324, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_ignored_special_headers": 0.06239843599996675, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_presign_url_encoding[s3]": 0.04170091799983311, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_presign_url_encoding[s3v4]": 0.06576129700010824, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_presigned_url_expired[s3]": 3.0704600560000017, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_presigned_url_expired[s3v4]": 3.072771702999944, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_put_presigned_url_missing_sig_param[s3]": 0.06097023799998169, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_put_presigned_url_missing_sig_param[s3v4]": 0.05974114200012082, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_put_presigned_url_same_header_and_qs_parameter": 0.0692228300001716, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_put_presigned_url_with_different_headers[s3]": 1.1819602480002231, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_s3_put_presigned_url_with_different_headers[s3v4]": 0.08278233100008947, + "tests/aws/services/s3/test_s3.py::TestS3Routing::test_access_favicon_via_aws_endpoints[s3.amazonaws.com-False]": 0.028081906000124945, + "tests/aws/services/s3/test_s3.py::TestS3Routing::test_access_favicon_via_aws_endpoints[s3.amazonaws.com-True]": 0.027891226000065217, + "tests/aws/services/s3/test_s3.py::TestS3Routing::test_access_favicon_via_aws_endpoints[s3.us-west-2.amazonaws.com-False]": 0.027429019999999582, + "tests/aws/services/s3/test_s3.py::TestS3Routing::test_access_favicon_via_aws_endpoints[s3.us-west-2.amazonaws.com-True]": 0.029874363999851994, + "tests/aws/services/s3/test_s3.py::TestS3StaticWebsiteHosting::test_crud_website_configuration": 0.032045570000036605, + "tests/aws/services/s3/test_s3.py::TestS3StaticWebsiteHosting::test_object_website_redirect_location": 0.11914758699981576, + "tests/aws/services/s3/test_s3.py::TestS3StaticWebsiteHosting::test_routing_rules_conditions": 0.29651797600013197, + "tests/aws/services/s3/test_s3.py::TestS3StaticWebsiteHosting::test_routing_rules_empty_replace_prefix": 0.22917792499970346, + "tests/aws/services/s3/test_s3.py::TestS3StaticWebsiteHosting::test_routing_rules_order": 0.11396053099997516, + "tests/aws/services/s3/test_s3.py::TestS3StaticWebsiteHosting::test_routing_rules_redirects": 0.05400631599991357, + "tests/aws/services/s3/test_s3.py::TestS3StaticWebsiteHosting::test_s3_static_website_hosting": 0.246066454999891, + "tests/aws/services/s3/test_s3.py::TestS3StaticWebsiteHosting::test_s3_static_website_index": 0.04722603599998365, + "tests/aws/services/s3/test_s3.py::TestS3StaticWebsiteHosting::test_validate_website_configuration": 0.06588514100030807, + "tests/aws/services/s3/test_s3.py::TestS3StaticWebsiteHosting::test_website_hosting_404": 0.07299656299983326, + "tests/aws/services/s3/test_s3.py::TestS3StaticWebsiteHosting::test_website_hosting_http_methods": 0.053014340999880005, + "tests/aws/services/s3/test_s3.py::TestS3StaticWebsiteHosting::test_website_hosting_index_lookup": 0.09359444599976996, + "tests/aws/services/s3/test_s3.py::TestS3StaticWebsiteHosting::test_website_hosting_no_such_website": 0.0540555120001045, + "tests/aws/services/s3/test_s3.py::TestS3StaticWebsiteHosting::test_website_hosting_redirect_all": 0.10678906300017843, + "tests/aws/services/s3/test_s3.py::TestS3TerraformRawRequests::test_terraform_request_sequence": 0.02771876999986489, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketAccelerateConfiguration::test_bucket_acceleration_configuration_crud": 0.027679746000103478, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketAccelerateConfiguration::test_bucket_acceleration_configuration_exc": 0.03468928799998139, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketCRUD::test_delete_bucket_with_objects": 0.1424336699997184, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketCRUD::test_delete_versioned_bucket_with_objects": 0.15228032100003475, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketEncryption::test_s3_bucket_encryption_sse_kms": 0.07130188700011786, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketEncryption::test_s3_bucket_encryption_sse_kms_aws_managed_key": 0.08716104800009816, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketEncryption::test_s3_bucket_encryption_sse_s3": 0.02989484599993375, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketEncryption::test_s3_default_bucket_encryption": 0.026092837999840413, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketEncryption::test_s3_default_bucket_encryption_exc": 0.15499515100032113, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketObjectTagging::test_bucket_tagging_crud": 0.042423131000077774, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketObjectTagging::test_bucket_tagging_exc": 0.024114091999990706, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketObjectTagging::test_object_tagging_crud": 0.04724012099995889, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketObjectTagging::test_object_tagging_exc": 0.12288103200012301, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketObjectTagging::test_object_tagging_versioned": 0.0492778659997839, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketObjectTagging::test_object_tags_delete_or_overwrite_object": 0.039122755999869696, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketObjectTagging::test_put_object_with_tags": 0.08984807599995293, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketObjectTagging::test_tagging_validation": 0.08420947599961437, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketOwnershipControls::test_bucket_ownership_controls_exc": 0.03251271800036193, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketOwnershipControls::test_crud_bucket_ownership_controls": 0.04639949300008084, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketPolicy::test_bucket_policy_crud": 0.03360225200026434, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketPolicy::test_bucket_policy_exc": 0.026792801000055988, + "tests/aws/services/s3/test_s3_api.py::TestS3BucketVersioning::test_bucket_versioning_crud": 0.04679682799996954, + "tests/aws/services/s3/test_s3_api.py::TestS3Multipart::test_upload_part_copy_no_copy_source_range": 0.058450855000273805, + "tests/aws/services/s3/test_s3_api.py::TestS3Multipart::test_upload_part_copy_range": 0.10832586500009711, + "tests/aws/services/s3/test_s3_api.py::TestS3ObjectCRUD::test_delete_object": 0.02603761599993959, + "tests/aws/services/s3/test_s3_api.py::TestS3ObjectCRUD::test_delete_object_locked": 0.0006253110004763585, + "tests/aws/services/s3/test_s3_api.py::TestS3ObjectCRUD::test_delete_object_on_suspended_bucket": 0.1825024409999969, + "tests/aws/services/s3/test_s3_api.py::TestS3ObjectCRUD::test_delete_object_versioned": 0.1859175239999331, + "tests/aws/services/s3/test_s3_api.py::TestS3ObjectCRUD::test_delete_objects": 0.024547177999920677, + "tests/aws/services/s3/test_s3_api.py::TestS3ObjectCRUD::test_delete_objects_versioned": 0.15771815299990521, + "tests/aws/services/s3/test_s3_api.py::TestS3ObjectCRUD::test_get_object_range": 0.0952603639998415, + "tests/aws/services/s3/test_s3_api.py::TestS3ObjectCRUD::test_get_object_with_version_unversioned_bucket": 0.14878322499976093, + "tests/aws/services/s3/test_s3_api.py::TestS3ObjectCRUD::test_list_object_versions_order_unversioned": 0.15818690799983415, + "tests/aws/services/s3/test_s3_api.py::TestS3ObjectCRUD::test_put_object_on_suspended_bucket": 0.19201336900027854, + "tests/aws/services/s3/test_s3_api.py::TestS3ObjectLock::test_delete_object_with_no_locking": 0.02963538700009849, + "tests/aws/services/s3/test_s3_api.py::TestS3ObjectLock::test_disable_versioning_on_locked_bucket": 0.018553677999989304, + "tests/aws/services/s3/test_s3_api.py::TestS3ObjectLock::test_get_object_lock_configuration_exc": 0.02117681699996865, + "tests/aws/services/s3/test_s3_api.py::TestS3ObjectLock::test_get_put_object_lock_configuration": 0.027144476000103168, + "tests/aws/services/s3/test_s3_api.py::TestS3ObjectLock::test_put_object_lock_configuration_exc": 0.03149759500001892, + "tests/aws/services/s3/test_s3_api.py::TestS3ObjectLock::test_put_object_lock_configuration_on_existing_bucket": 0.03492399100014154, + "tests/aws/services/s3/test_s3_api.py::TestS3PublicAccessBlock::test_crud_public_access_block": 0.03130948900002295, + "tests/aws/services/s3/test_s3_concurrency.py::TestParallelBucketCreation::test_parallel_bucket_creation": 2.1221967719998247, + "tests/aws/services/s3/test_s3_concurrency.py::TestParallelBucketCreation::test_parallel_object_creation_and_listing": 0.09557316900009027, + "tests/aws/services/s3/test_s3_concurrency.py::TestParallelBucketCreation::test_parallel_object_creation_and_read": 1.2534191739998732, + "tests/aws/services/s3/test_s3_concurrency.py::TestParallelBucketCreation::test_parallel_object_read_range": 1.258672338000224, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_cors_expose_headers": 0.09270225099976415, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_cors_http_get_no_config": 0.04126001599979645, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_cors_http_options_no_config": 0.08711141999970096, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_cors_http_options_non_existent_bucket": 0.0608739530002822, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_cors_http_options_non_existent_bucket_ls_allowed": 0.045298533000050156, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_cors_list_buckets": 0.025539367999954266, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_cors_match_headers": 0.29475895900009164, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_cors_match_methods": 0.26366661899987776, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_cors_match_origins": 0.24094455200020093, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_cors_no_config_localstack_allowed": 0.040130355999963285, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_cors_options_fails_partial_origin": 0.15398718699998426, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_cors_options_match_partial_origin": 0.2281597379997038, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_delete_cors": 0.06732132900015131, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_get_cors": 0.0545451039999989, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_put_cors": 0.053042221000168865, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_put_cors_default_values": 0.16521918400007962, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_put_cors_empty_origin": 0.055182067000032475, + "tests/aws/services/s3/test_s3_cors.py::TestS3Cors::test_put_cors_invalid_rules": 0.05097986500004481, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListMultipartUploads::test_list_multipart_uploads_marker_common_prefixes": 0.15737640900010774, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListMultipartUploads::test_list_multipart_uploads_parameters": 0.000874726000120063, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListMultipartUploads::test_list_multiparts_next_marker": 0.19527773699996942, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListMultipartUploads::test_list_multiparts_with_prefix_and_delimiter": 0.15951971900040007, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListMultipartUploads::test_s3_list_multiparts_timestamp_precision": 0.02167227199993249, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListObjectVersions::test_list_object_versions_pagination_common_prefixes": 0.18108247299983304, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListObjectVersions::test_list_objects_versions_markers": 1.3904474700000264, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListObjectVersions::test_list_objects_versions_with_prefix": 0.19136924300005376, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListObjectVersions::test_s3_list_object_versions_timestamp_precision": 0.02928905199996734, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListObjects::test_list_objects_marker_common_prefixes": 0.17405334599970956, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListObjects::test_list_objects_next_marker": 0.16597473299975718, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListObjects::test_list_objects_with_prefix[%2F]": 0.15319022800031235, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListObjects::test_list_objects_with_prefix[/]": 0.1442347059999065, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListObjects::test_list_objects_with_prefix[]": 0.14880843700007063, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListObjects::test_s3_list_objects_empty_marker": 0.13930606699977943, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListObjects::test_s3_list_objects_timestamp_precision[ListObjectsV2]": 0.028244094000001496, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListObjects::test_s3_list_objects_timestamp_precision[ListObjects]": 0.026377410999884887, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListObjectsV2::test_list_objects_v2_continuation_common_prefixes": 0.16676133200007826, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListObjectsV2::test_list_objects_v2_continuation_start_after": 0.20567220199995973, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListObjectsV2::test_list_objects_v2_with_prefix": 0.17127459299990733, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListObjectsV2::test_list_objects_v2_with_prefix_and_delimiter": 0.1627539789999446, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListParts::test_list_parts_empty_part_number_marker": 0.031274919999987105, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListParts::test_list_parts_pagination": 0.04528377700012243, + "tests/aws/services/s3/test_s3_list_operations.py::TestS3ListParts::test_s3_list_parts_timestamp_precision": 0.02453162399979192, + "tests/aws/services/s3/test_s3_notifications_eventbridge.py::TestS3NotificationsToEventBridge::test_object_created_put": 1.411489402000143, + "tests/aws/services/s3/test_s3_notifications_eventbridge.py::TestS3NotificationsToEventBridge::test_object_created_put_in_different_region": 1.2847460759999194, + "tests/aws/services/s3/test_s3_notifications_eventbridge.py::TestS3NotificationsToEventBridge::test_object_put_acl": 0.6277674240000124, + "tests/aws/services/s3/test_s3_notifications_eventbridge.py::TestS3NotificationsToEventBridge::test_restore_object": 0.5160318399998687, + "tests/aws/services/s3/test_s3_notifications_lambda.py::TestS3NotificationsToLambda::test_create_object_by_presigned_request_via_dynamodb": 5.397681808999778, + "tests/aws/services/s3/test_s3_notifications_lambda.py::TestS3NotificationsToLambda::test_create_object_put_via_dynamodb": 2.3364378680000755, + "tests/aws/services/s3/test_s3_notifications_lambda.py::TestS3NotificationsToLambda::test_invalid_lambda_arn": 0.35000167299995155, + "tests/aws/services/s3/test_s3_notifications_sns.py::TestS3NotificationsToSns::test_bucket_not_exist": 0.12324925600000824, + "tests/aws/services/s3/test_s3_notifications_sns.py::TestS3NotificationsToSns::test_bucket_notifications_with_filter": 1.2225160120003693, + "tests/aws/services/s3/test_s3_notifications_sns.py::TestS3NotificationsToSns::test_invalid_topic_arn": 0.07872946200018305, + "tests/aws/services/s3/test_s3_notifications_sns.py::TestS3NotificationsToSns::test_object_created_put": 1.3285793529998955, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_bucket_notification_with_invalid_filter_rules": 0.08338470399985454, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_delete_objects": 0.2573707330000161, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_filter_rules_case_insensitive": 0.032276622000381394, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_invalid_sqs_arn": 0.13150342800031467, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_key_encoding": 0.20382597899970278, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_multiple_invalid_sqs_arns": 0.19723941799998101, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_notifications_with_filter": 0.23854260799998883, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_object_created_and_object_removed": 0.2792585979996147, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_object_created_complete_multipart_upload": 0.2208458749998954, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_object_created_copy": 0.21704564699984985, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_object_created_put": 0.24416444799999226, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_object_created_put_with_presigned_url_upload": 0.296317290999923, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_object_put_acl": 1.2900663279997389, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_object_tagging_delete_event": 0.21721156900002825, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_object_tagging_put_event": 0.2154163409998091, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_restore_object": 0.26194822399997975, + "tests/aws/services/s3/test_s3_notifications_sqs.py::TestS3NotificationsToSQS::test_xray_header": 1.2052725700000337, + "tests/aws/services/s3control/test_s3control.py::test_lifecycle_public_access_block": 0.26942422699994495, + "tests/aws/services/s3control/test_s3control.py::test_public_access_block_validations": 0.025883503000386554, + "tests/aws/services/scheduler/test_scheduler.py::test_list_schedules": 0.03052626700014116, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_call_lists_secrets_multiple_times": 0.028724832000307288, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_call_lists_secrets_multiple_times_snapshots": 0.0008263889999398089, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_can_recreate_delete_secret": 0.017953591999912533, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_create_and_update_secret[Valid/_+=.@-Name-a1b2]": 0.02987345900010041, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_create_and_update_secret[Valid/_+=.@-Name-a1b2c3-]": 0.029470993000131784, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_create_and_update_secret[Valid/_+=.@-Name]": 0.029650823999872955, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_create_and_update_secret[s-c64bdc03]": 0.04745894899997438, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_create_multi_secrets": 0.03463572399982695, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_create_multi_secrets_snapshot": 0.0008299249998344749, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_create_secret_version_from_empty_secret": 0.015524965999929918, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_delete_non_existent_secret_returns_as_if_secret_exists": 0.0067076950001592195, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_deprecated_secret_version": 0.3201686080001309, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_deprecated_secret_version_stage": 0.07014801800005444, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_exp_raised_on_creation_of_secret_scheduled_for_deletion": 0.014302490000090984, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_get_random_exclude_characters_and_symbols": 0.004785353999977815, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_get_secret_value_errors": 0.013123964999977034, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_http_put_secret_value_custom_client_request_token_new_version_stages": 0.024400070999945456, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_http_put_secret_value_duplicate_req": 0.02144935100022849, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_http_put_secret_value_null_client_request_token_new_version_stages": 0.023712950000117416, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_http_put_secret_value_with_duplicate_client_request_token": 0.021837364000020898, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_http_put_secret_value_with_non_provided_client_request_token": 0.02110925200008751, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_invalid_secret_name[ Inv *?!]Name\\\\-]": 0.03188664100002825, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_invalid_secret_name[ Inv Name]": 0.03291309499991257, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_invalid_secret_name[ Inv*Name? ]": 0.03187982899976305, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_invalid_secret_name[Inv Name]": 0.038352073999931235, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_last_accessed_date": 0.019805153000106657, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_last_updated_date": 0.02872137000008479, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_list_secrets_filtering": 0.06642074600017622, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_no_client_request_token[CreateSecret]": 0.008236232000399468, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_no_client_request_token[PutSecretValue]": 0.008228549000250496, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_no_client_request_token[RotateSecret]": 0.008151250000082655, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_no_client_request_token[UpdateSecret]": 0.008246347999829595, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_non_versioning_version_stages_no_replacement": 0.07364406700003201, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_non_versioning_version_stages_replacement": 0.07410174799997549, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_put_secret_value_with_new_custom_client_request_token": 0.021349602999862327, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_put_secret_value_with_version_stages": 0.08053384999993796, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_resource_policy": 0.017648533999818028, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_invalid_lambda_arn": 0.1016011009999147, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_1": 0.0007240129998535849, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_2": 0.0007415129994114977, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[None]": 1.8706602069999008, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_rotate_secret_with_lambda_success[True]": 1.8519444920000296, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_secret_exists": 0.017670720999831246, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_secret_exists_snapshots": 0.017197094000039215, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_secret_not_found": 0.009676700000227356, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_secret_restore": 0.01543655000000399, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_secret_tags": 0.04726922699978786, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_update_secret_description": 0.03593351599988637, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_update_secret_version_stages_current_pending": 0.07731285599993498, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_update_secret_version_stages_current_pending_cycle": 0.09564089600030456, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_update_secret_version_stages_current_pending_cycle_custom_stages_1": 0.09644911300006243, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_update_secret_version_stages_current_pending_cycle_custom_stages_2": 0.1046868559997165, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_update_secret_version_stages_current_pending_cycle_custom_stages_3": 0.09268421199976729, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_update_secret_version_stages_current_previous": 0.07247022899991862, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_update_secret_version_stages_return_type": 0.01692657600028724, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManager::test_update_secret_with_non_provided_client_request_token": 0.017380048000404713, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManagerMultiAccounts::test_cross_account_access": 0.09175672100013799, + "tests/aws/services/secretsmanager/test_secretsmanager.py::TestSecretsManagerMultiAccounts::test_cross_account_access_non_default_key": 0.03611294200004522, + "tests/aws/services/ses/test_ses.py::TestSES::test_cannot_create_event_for_no_topic": 0.017559659999733412, + "tests/aws/services/ses/test_ses.py::TestSES::test_clone_receipt_rule_set": 0.17430315500018878, + "tests/aws/services/ses/test_ses.py::TestSES::test_creating_event_destination_without_configuration_set": 0.020224966000114364, + "tests/aws/services/ses/test_ses.py::TestSES::test_delete_template": 0.018942632000062076, + "tests/aws/services/ses/test_ses.py::TestSES::test_deleting_non_existent_configuration_set": 0.004796058000465564, + "tests/aws/services/ses/test_ses.py::TestSES::test_deleting_non_existent_configuration_set_event_destination": 0.010882755000011457, + "tests/aws/services/ses/test_ses.py::TestSES::test_get_identity_verification_attributes_for_domain": 0.0037563269997917814, + "tests/aws/services/ses/test_ses.py::TestSES::test_get_identity_verification_attributes_for_email": 0.008684999000024618, + "tests/aws/services/ses/test_ses.py::TestSES::test_invalid_tags_send_email[-]": 0.029302277000169852, + "tests/aws/services/ses/test_ses.py::TestSES::test_invalid_tags_send_email[-test]": 0.029983978000018396, + "tests/aws/services/ses/test_ses.py::TestSES::test_invalid_tags_send_email[test-]": 0.03019167000024936, + "tests/aws/services/ses/test_ses.py::TestSES::test_invalid_tags_send_email[test-test_invalid_value:123]": 0.030377677000160475, + "tests/aws/services/ses/test_ses.py::TestSES::test_invalid_tags_send_email[test_invalid_name:123-test]": 0.030404291999957422, + "tests/aws/services/ses/test_ses.py::TestSES::test_invalid_tags_send_email[test_invalid_name:123-test_invalid_value:123]": 0.03035158600005161, + "tests/aws/services/ses/test_ses.py::TestSES::test_invalid_tags_send_email[test_invalid_name_len]": 0.030045259999951668, + "tests/aws/services/ses/test_ses.py::TestSES::test_invalid_tags_send_email[test_invalid_value_len]": 0.0307329900001605, + "tests/aws/services/ses/test_ses.py::TestSES::test_invalid_tags_send_email[test_priority_name_value]": 0.030165220000299087, + "tests/aws/services/ses/test_ses.py::TestSES::test_list_templates": 0.05981394599984924, + "tests/aws/services/ses/test_ses.py::TestSES::test_sending_to_deleted_topic": 0.1581524310001896, + "tests/aws/services/ses/test_ses.py::TestSES::test_sent_message_counter": 0.03816016399991895, + "tests/aws/services/ses/test_ses.py::TestSES::test_ses_sns_topic_integration_send_email": 0.5158817690003161, + "tests/aws/services/ses/test_ses.py::TestSES::test_ses_sns_topic_integration_send_raw_email": 0.5020178089996534, + "tests/aws/services/ses/test_ses.py::TestSES::test_ses_sns_topic_integration_send_templated_email": 0.5077262350000638, + "tests/aws/services/ses/test_ses.py::TestSES::test_trying_to_delete_event_destination_from_non_existent_configuration_set": 0.030636029000106646, + "tests/aws/services/ses/test_ses.py::TestSESRetrospection::test_send_email_can_retrospect": 0.030538353000110874, + "tests/aws/services/ses/test_ses.py::TestSESRetrospection::test_send_templated_email_can_retrospect": 0.022852018999856227, + "tests/aws/services/sns/test_sns.py::TestSNSMultiAccounts::test_cross_account_access": 0.040458738999859634, + "tests/aws/services/sns/test_sns.py::TestSNSMultiAccounts::test_cross_account_publish_to_sqs": 0.2733160590000807, + "tests/aws/services/sns/test_sns.py::TestSNSPlatformEndpoint::test_create_platform_endpoint_check_idempotency": 0.0008611939999809692, + "tests/aws/services/sns/test_sns.py::TestSNSPlatformEndpoint::test_publish_disabled_endpoint": 0.041222678000167434, + "tests/aws/services/sns/test_sns.py::TestSNSPlatformEndpoint::test_publish_to_gcm": 0.023682039999812332, + "tests/aws/services/sns/test_sns.py::TestSNSPlatformEndpoint::test_publish_to_platform_endpoint_is_dispatched": 0.05265150400009588, + "tests/aws/services/sns/test_sns.py::TestSNSPlatformEndpoint::test_subscribe_platform_endpoint": 0.04053418200010128, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_attribute_raw_subscribe": 0.17819386799965287, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_create_duplicate_topic_check_idempotency": 0.10791121600050246, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_create_duplicate_topic_with_more_tags": 0.026419368999086146, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_create_platform_endpoint_check_idempotency": 0.0007917140010249568, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_create_subscriptions_with_attributes": 0.1158573599986994, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_create_topic_after_delete_with_new_tags": 0.04323926200140704, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_create_topic_test_arn": 0.2087973100005911, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_create_topic_with_attributes": 0.1620311810011117, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_dlq_external_http_endpoint[False]": 2.6566505609998785, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_dlq_external_http_endpoint[True]": 2.659784669000146, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_empty_or_wrong_message_attributes": 0.17563517700091325, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_empty_sns_message": 0.085872112999823, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_exists_filter_policy": 0.3416930749999665, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_filter_policy": 1.2344848459988498, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_filter_policy_for_batch": 3.4312493070001437, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_filter_policy_on_message_body[False]": 5.300850845999776, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_filter_policy_on_message_body[True]": 5.318018103001123, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_message_attributes_not_missing": 0.2218514079995657, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_message_attributes_prefixes": 0.16109672299899103, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_message_structure_json_exc": 0.04570650200093951, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_message_structure_json_to_sqs": 0.1902359340001567, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_message_to_fifo_sqs[False]": 1.2313058960007766, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_message_to_fifo_sqs[True]": 1.2046494280011757, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_multiple_subscriptions_http_endpoint": 1.7485074590003933, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_not_found_error_on_set_subscription_attributes": 0.2919607450012336, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_batch_exceptions": 0.056616498999574105, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_batch_messages_from_fifo_topic_to_fifo_queue[False]": 3.5997051609992923, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_batch_messages_from_fifo_topic_to_fifo_queue[True]": 3.5155896599999323, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_batch_messages_from_sns_to_sqs": 0.7291393510004127, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_batch_messages_without_topic": 0.023123011000279803, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_by_path_parameters": 0.12520062499970663, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_fifo_messages_to_dlq[False]": 1.518365509999967, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_fifo_messages_to_dlq[True]": 1.4893792339989886, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_message_before_subscribe_topic": 0.14992666300076962, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_message_by_target_arn": 0.18489378400136047, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_non_existent_target": 0.04595261700069386, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_sms": 0.0156877790004728, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_sms_endpoint": 0.11387452300004952, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_sqs_from_sns": 0.211081548999573, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_sqs_from_sns_with_xray_propagation": 0.12687063600060355, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_to_fifo_topic_deduplication_on_topic_level": 1.6709965239997473, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_to_fifo_topic_to_sqs_queue_no_content_dedup[False]": 0.29924183399998583, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_to_fifo_topic_to_sqs_queue_no_content_dedup[True]": 0.2855869940003686, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_to_fifo_with_target_arn": 0.036142431999905966, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_to_firehose_with_s3": 1.3128187080001226, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_to_gcm": 0.05968604600002436, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_to_platform_endpoint_is_dispatched": 0.16221984300045733, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_too_long_message": 0.057494208000207436, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_unicode_chars": 0.3638783690003038, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_with_empty_subject": 0.032047570000031556, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_wrong_arn_format": 0.016379689001041697, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_publish_wrong_phone_format": 0.03566063400012354, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_redrive_policy_http_subscription": 0.6909830730010071, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_redrive_policy_lambda_subscription": 1.2396847190002518, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_redrive_policy_sqs_queue_subscription[False]": 0.20141114899979584, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_redrive_policy_sqs_queue_subscription[True]": 0.2205831870005568, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_set_subscription_filter_policy_scope": 0.14700287699906767, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_sns_confirm_subscription_wrong_token": 0.08884195700011333, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_sns_topic_as_lambda_dead_letter_queue": 4.271508126000299, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_sqs_topic_subscription_confirmation": 0.05683871099972748, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_sub_filter_policy_nested_property": 0.1490920129999722, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_sub_filter_policy_nested_property_constraints": 0.1838193269995827, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_subscribe_external_http_endpoint[False]": 0.6442700520001381, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_subscribe_external_http_endpoint[True]": 0.6376740369987601, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_subscribe_platform_endpoint": 0.10574708000058308, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_subscribe_sms_endpoint": 0.10939544300072157, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_subscribe_sqs_queue": 0.18104011800096487, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_subscribe_to_sqs_with_queue_url": 0.036844848999862734, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_subscribe_with_invalid_protocol": 0.0331569900008617, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_subscription_after_failure_to_deliver": 1.5653773360008927, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_tags": 0.06361213099899032, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_topic_email_subscription_confirmation": 0.04612892000022839, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_unknown_topic_publish": 0.03989810999973997, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_unsubscribe_from_non_existing_subscription": 0.06899322600020241, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_validate_set_sub_attributes": 0.19093966499985981, + "tests/aws/services/sns/test_sns.py::TestSNSProvider::test_validations_for_fifo": 0.1546634250007628, + "tests/aws/services/sns/test_sns.py::TestSNSPublishCrud::test_empty_sns_message": 0.030412371000011262, + "tests/aws/services/sns/test_sns.py::TestSNSPublishCrud::test_message_structure_json_exc": 0.019997463999970932, + "tests/aws/services/sns/test_sns.py::TestSNSPublishCrud::test_publish_by_path_parameters": 0.047715391000110685, + "tests/aws/services/sns/test_sns.py::TestSNSPublishCrud::test_publish_message_before_subscribe_topic": 0.05412771600003907, + "tests/aws/services/sns/test_sns.py::TestSNSPublishCrud::test_publish_message_by_target_arn": 0.0716602590000548, + "tests/aws/services/sns/test_sns.py::TestSNSPublishCrud::test_publish_non_existent_target": 0.011008420000052865, + "tests/aws/services/sns/test_sns.py::TestSNSPublishCrud::test_publish_too_long_message": 0.01818866300004629, + "tests/aws/services/sns/test_sns.py::TestSNSPublishCrud::test_publish_with_empty_subject": 0.013907201999927565, + "tests/aws/services/sns/test_sns.py::TestSNSPublishCrud::test_publish_wrong_arn_format": 0.011521315999971193, + "tests/aws/services/sns/test_sns.py::TestSNSPublishCrud::test_topic_publish_another_region": 0.021717068000043582, + "tests/aws/services/sns/test_sns.py::TestSNSPublishCrud::test_unknown_topic_publish": 0.014189519999945333, + "tests/aws/services/sns/test_sns.py::TestSNSPublishDelivery::test_delivery_lambda": 1.6324158259999422, + "tests/aws/services/sns/test_sns.py::TestSNSRetrospectionEndpoints::test_publish_sms_can_retrospect": 0.0937248070001715, + "tests/aws/services/sns/test_sns.py::TestSNSRetrospectionEndpoints::test_publish_to_platform_endpoint_can_retrospect": 0.20715852100011034, + "tests/aws/services/sns/test_sns.py::TestSNSRetrospectionEndpoints::test_subscription_tokens_can_retrospect": 1.0382622530000845, + "tests/aws/services/sns/test_sns.py::TestSNSSMS::test_publish_sms": 0.006424030999824026, + "tests/aws/services/sns/test_sns.py::TestSNSSMS::test_publish_sms_endpoint": 0.05589591700004348, + "tests/aws/services/sns/test_sns.py::TestSNSSMS::test_publish_wrong_phone_format": 0.01914780900006008, + "tests/aws/services/sns/test_sns.py::TestSNSSMS::test_subscribe_sms_endpoint": 0.01936890799993307, + "tests/aws/services/sns/test_sns.py::TestSNSSubscription::test_python_lambda_subscribe_sns_topic": 4.1884100209999815, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_create_subscriptions_with_attributes": 0.02945889200009333, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_list_subscriptions": 0.12056238400009534, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_list_subscriptions_by_topic_pagination": 0.522808798999904, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_not_found_error_on_set_subscription_attributes": 0.23993268700019144, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_sns_confirm_subscription_wrong_token": 0.04279310000015357, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_subscribe_idempotency": 0.03867175100003806, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_subscribe_with_invalid_protocol": 0.011122281999632833, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_unsubscribe_from_non_existing_subscription": 0.03006639499949415, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_unsubscribe_idempotency": 0.031098475999897346, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_unsubscribe_wrong_arn_format": 0.10092819500027872, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_validate_set_sub_attributes": 0.09401082299973496, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionFirehose::test_publish_to_firehose_with_s3": 1.0872945379999237, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionHttp::test_dlq_external_http_endpoint[False]": 2.5676497000001746, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionHttp::test_dlq_external_http_endpoint[True]": 2.57041186299989, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionHttp::test_http_subscription_response": 0.026152429000148913, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionHttp::test_multiple_subscriptions_http_endpoint": 1.591597352000008, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionHttp::test_redrive_policy_http_subscription": 0.558946386000116, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionHttp::test_subscribe_external_http_endpoint[False]": 1.5626246209999408, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionHttp::test_subscribe_external_http_endpoint[True]": 1.5654814950000855, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionLambda::test_publish_lambda_verify_signature[1]": 4.071711646999574, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionLambda::test_publish_lambda_verify_signature[2]": 4.080145155999844, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionLambda::test_python_lambda_subscribe_sns_topic": 4.064932018000263, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionLambda::test_redrive_policy_lambda_subscription": 1.1218370289998347, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionLambda::test_sns_topic_as_lambda_dead_letter_queue": 2.1327326109999376, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSES::test_topic_email_subscription_confirmation": 0.02325889299982009, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_attribute_raw_subscribe": 0.05919351300008202, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_empty_or_wrong_message_attributes": 0.06075199600013548, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_message_attributes_not_missing": 0.07539093899981708, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_message_attributes_prefixes": 0.06387518099995759, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_message_structure_json_to_sqs": 0.06987836099983724, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_publish_batch_exceptions": 0.024227707999898485, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_publish_batch_messages_from_sns_to_sqs": 0.5167297389998566, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_publish_batch_messages_without_topic": 0.011559797000245453, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_publish_sqs_from_sns": 0.08041042800005016, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_publish_sqs_from_sns_with_xray_propagation": 0.046414783999807696, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_publish_sqs_verify_signature[1]": 0.05215448700005254, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_publish_sqs_verify_signature[2]": 0.05052770000020246, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_publish_unicode_chars": 0.08634944899995389, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_redrive_policy_sqs_queue_subscription[False]": 0.06764220799982468, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_redrive_policy_sqs_queue_subscription[True]": 0.06861053100010395, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_sqs_topic_subscription_confirmation": 0.025698145999967892, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_subscribe_sqs_queue": 0.08176274199990985, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_subscribe_to_sqs_with_queue_url": 0.016372696999951586, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQS::test_subscription_after_failure_to_deliver": 1.181798240000262, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQSFifo::test_fifo_topic_to_regular_sqs[False]": 0.09745574999988094, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQSFifo::test_fifo_topic_to_regular_sqs[True]": 0.09506587699979718, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQSFifo::test_message_to_fifo_sqs[False]": 1.0667427160001353, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQSFifo::test_message_to_fifo_sqs[True]": 1.0668579470000168, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQSFifo::test_publish_batch_messages_from_fifo_topic_to_fifo_queue[False]": 4.2618169889999535, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQSFifo::test_publish_batch_messages_from_fifo_topic_to_fifo_queue[True]": 3.2345848380002735, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQSFifo::test_publish_fifo_messages_to_dlq[False]": 1.2005628980000438, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQSFifo::test_publish_fifo_messages_to_dlq[True]": 1.2081016829999953, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQSFifo::test_publish_to_fifo_topic_deduplication_on_topic_level": 1.5734533370000463, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQSFifo::test_publish_to_fifo_topic_to_sqs_queue_no_content_dedup[False]": 0.0941796729998714, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQSFifo::test_publish_to_fifo_topic_to_sqs_queue_no_content_dedup[True]": 0.09481927000001633, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQSFifo::test_publish_to_fifo_with_target_arn": 0.014714383000182352, + "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionSQSFifo::test_validations_for_fifo": 0.07894570999997086, + "tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_duplicate_topic_check_idempotency": 0.02699840800005404, + "tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_duplicate_topic_with_more_tags": 0.0119885050000903, + "tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_topic_after_delete_with_new_tags": 0.017609147999792185, + "tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_topic_test_arn": 0.11040482999987944, + "tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_create_topic_with_attributes": 0.09266555400017751, + "tests/aws/services/sns/test_sns.py::TestSNSTopicCrud::test_tags": 0.03188910700009728, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyAttributes::test_exists_filter_policy": 0.11358178599971325, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyAttributes::test_exists_filter_policy_attributes_array": 4.101422049999883, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyAttributes::test_filter_policy": 5.119831603999955, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_for_batch": 3.145356450999998, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body[False]": 5.120975940000108, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body[True]": 5.123163857999998, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body_array_attributes": 0.3560024909997992, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body_array_of_object_attributes": 0.1897150799998144, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body_dot_attribute": 5.213960984999858, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body_or_attribute": 0.0008642290001716901, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_policy_complexity": 0.018908398999883502, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_policy_complexity_with_or": 0.02003021899986379, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy": 0.04330768600016199, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_exists_operator": 0.039413643999978376, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_numeric_operator": 0.07810704199982865, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_string_operators": 0.04850714400004108, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyCrud::test_set_subscription_filter_policy_scope": 0.044842895000101635, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyCrud::test_sub_filter_policy_nested_property": 0.04417262399988431, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyCrud::test_sub_filter_policy_nested_property_constraints": 0.06452931300009368, + "tests/aws/services/sqs/test_sqs.py::TestSQSMultiAccounts::test_cross_account_access[domain]": 0.03268827000010788, + "tests/aws/services/sqs/test_sqs.py::TestSQSMultiAccounts::test_cross_account_access[path]": 0.03255352200017114, + "tests/aws/services/sqs/test_sqs.py::TestSQSMultiAccounts::test_cross_account_access[standard]": 0.0342031299996961, + "tests/aws/services/sqs/test_sqs.py::TestSQSMultiAccounts::test_cross_account_get_queue_url[domain]": 0.009775619999800256, + "tests/aws/services/sqs/test_sqs.py::TestSQSMultiAccounts::test_cross_account_get_queue_url[path]": 0.009451693999835697, + "tests/aws/services/sqs/test_sqs.py::TestSQSMultiAccounts::test_cross_account_get_queue_url[standard]": 0.010517002999904435, + "tests/aws/services/sqs/test_sqs.py::TestSQSMultiAccounts::test_delete_queue_multi_account": 0.06963691199871391, + "tests/aws/services/sqs/test_sqs.py::TestSQSMultiAccounts::test_delete_queue_multi_account[sqs]": 0.036377287000050273, + "tests/aws/services/sqs/test_sqs.py::TestSQSMultiAccounts::test_delete_queue_multi_account[sqs_query]": 0.03752001599991672, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_approximate_number_of_messages_delayed": 3.113475202999325, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_approximate_number_of_messages_delayed[sqs]": 3.0500203909998618, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_approximate_number_of_messages_delayed[sqs_query]": 3.0523428980000062, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_batch_send_with_invalid_char_should_succeed": 0.22570910499962338, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_batch_send_with_invalid_char_should_succeed[sqs]": 0.03246745199999168, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_batch_send_with_invalid_char_should_succeed[sqs_query]": 0.08870465400013927, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_after_visibility_timeout_expiration[sqs]": 2.038525126999957, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_after_visibility_timeout_expiration[sqs_query]": 2.0400429740002437, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_batch_with_too_large_batch": 0.6592767229994934, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_batch_with_too_large_batch[sqs]": 0.5684927439999683, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_batch_with_too_large_batch[sqs_query]": 0.5638431259999379, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_not_permanent": 0.0749873300001127, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_not_permanent[sqs]": 0.03458542599969405, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_message_visibility_not_permanent[sqs_query]": 0.03584142799991241, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_visibility_on_deleted_message_raises_invalid_parameter_value": 0.08266863799963176, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_visibility_on_deleted_message_raises_invalid_parameter_value[sqs]": 0.03219068899988997, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_change_visibility_on_deleted_message_raises_invalid_parameter_value[sqs_query]": 0.03319501600026342, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_and_send_to_fifo_queue": 0.05527620799966826, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_and_send_to_fifo_queue[sqs]": 0.021490180999990116, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_and_send_to_fifo_queue[sqs_query]": 0.022876527999869722, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_and_update_queue_attributes": 0.05815273999905912, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_and_update_queue_attributes[sqs]": 0.027789158000132375, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_and_update_queue_attributes[sqs_query]": 0.03130028700024923, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_fifo_queue_with_different_attributes_raises_error": 0.02088837299925217, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_fifo_queue_with_different_attributes_raises_error[sqs]": 0.04613834799988581, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_fifo_queue_with_different_attributes_raises_error[sqs_query]": 0.04612207100012711, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_fifo_queue_with_same_attributes_is_idempotent": 0.011641634999932648, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_after_internal_attributes_changes_works": 0.0620510100006868, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_after_internal_attributes_changes_works[sqs]": 0.028827621999880648, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_after_internal_attributes_changes_works[sqs_query]": 0.030424479999965115, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_after_modified_attributes": 0.05413000599946827, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_after_modified_attributes[sqs]": 0.02127851999989616, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_after_modified_attributes[sqs_query]": 0.021805990999837377, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_after_send": 0.0990588429995114, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_after_send[sqs]": 0.039296420999789916, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_after_send[sqs_query]": 0.041004312000040954, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_and_get_attributes": 0.021370997999838437, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_and_get_attributes[sqs]": 0.01020247799988283, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_and_get_attributes[sqs_query]": 0.010386078999999881, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_recently_deleted": 0.02622248900024715, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_recently_deleted[sqs]": 0.012088291999816647, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_recently_deleted[sqs_query]": 0.012786261999963244, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_recently_deleted_cache": 1.5415261529997224, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_recently_deleted_cache[sqs]": 1.5177580099998522, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_recently_deleted_cache[sqs_query]": 1.5196553000000677, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_recently_deleted_can_be_disabled": 0.036890289000439225, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_recently_deleted_can_be_disabled[sqs]": 0.01430427300010706, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_recently_deleted_can_be_disabled[sqs_query]": 0.014778996999893934, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_default_arguments_works_with_modified_attributes": 0.04898681199938437, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_default_arguments_works_with_modified_attributes[sqs]": 0.021376445999976568, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_default_arguments_works_with_modified_attributes[sqs_query]": 0.021402698999963832, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_default_attributes_is_idempotent": 0.012304449999874123, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_different_attributes_raises_exception": 0.09479069500048354, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_different_attributes_raises_exception[sqs]": 0.07487468600015745, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_different_attributes_raises_exception[sqs_query]": 0.06638214299982792, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_same_attributes_is_idempotent": 0.011858274999895002, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_tags": 0.02110337700105447, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_tags[sqs]": 0.009430903000065882, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_tags[sqs_query]": 0.009887912000067445, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_without_attributes_is_idempotent": 0.011540658000058102, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_chain": 1.2516506249994563, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_chain[sqs]": 1.1571867190000376, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_chain[sqs_query]": 1.1596870880000552, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_config": 0.011533304000295175, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_execution_lambda_mapping_preserves_id": 0.0007261130003826111, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_execution_lambda_mapping_preserves_id[sqs]": 0.0008919179999793414, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_execution_lambda_mapping_preserves_id[sqs_query]": 0.0008205069998439285, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_list_sources": 0.05460059600045497, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_list_sources[sqs]": 0.019572496000137107, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_list_sources[sqs_query]": 0.019511097999838967, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_max_receive_count": 0.14739248799924098, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_max_receive_count[sqs]": 0.04279414400002679, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_max_receive_count[sqs_query]": 0.044368879000103334, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_message_attributes": 0.5934673139997813, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_with_fifo_and_content_based_deduplication": 0.10461060800116684, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_with_fifo_and_content_based_deduplication[sqs]": 0.046266397999943365, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_with_fifo_and_content_based_deduplication[sqs_query]": 0.04754926999999043, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_deduplication_interval": 0.0007031129998722463, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_deduplication_interval[sqs]": 0.0008385710000311519, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_deduplication_interval[sqs_query]": 0.0008043070001804153, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_from_lambda": 2.003266755000368, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_from_lambda[sqs]": 0.000823886000262064, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_from_lambda[sqs_query]": 0.00080687200011198, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[]": 0.10151553099876764, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[invalid:id]": 0.04661814100018091, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs-]": 0.03990372299995215, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs-invalid:id]": 0.023707296999873506, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs-testLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongId]": 0.02374172599979829, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs_query-]": 0.024030326999991303, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs_query-invalid:id]": 0.02426999899967086, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[sqs_query-testLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongId]": 0.023332760000130293, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_invalid_msg_id[testLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongIdtestLongId]": 0.04743145500015089, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_with_too_large_batch": 0.6636393839989978, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_with_too_large_batch[sqs]": 0.5718126439999196, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_batch_with_too_large_batch[sqs_query]": 0.5773700060001374, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_deletes_with_change_visibility_timeout": 0.10012760600056936, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_deletes_with_change_visibility_timeout[sqs]": 0.04667297200012399, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_deletes_with_change_visibility_timeout[sqs_query]": 0.05050197799982925, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_with_deleted_receipt_handle": 0.08865584199975274, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_with_deleted_receipt_handle[sqs]": 0.038195336999933716, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_with_deleted_receipt_handle[sqs_query]": 0.0391835269999774, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_with_illegal_receipt_handle": 0.020265451000341272, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_with_illegal_receipt_handle[sqs]": 0.009890600000062477, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_delete_message_with_illegal_receipt_handle[sqs_query]": 0.009798562000014499, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_disallow_queue_name_with_slashes": 0.004884111999899687, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_extend_message_visibility_timeout_set_in_queue": 6.858764821999102, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_extend_message_visibility_timeout_set_in_queue[sqs]": 6.927680897999835, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_extend_message_visibility_timeout_set_in_queue[sqs_query]": 6.9993876559999535, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_external_endpoint[sqs]": 0.04504773799999384, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_external_endpoint[sqs_query]": 0.022134800999765503, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_external_host_via_header_complete_message_lifecycle": 0.03583732700008113, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_external_hostname": 0.12520312099968578, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_external_hostname_via_host_header": 0.020906064999962837, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_approx_number_of_messages": 0.2097869819999687, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_approx_number_of_messages[sqs]": 0.09014269599992986, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_approx_number_of_messages[sqs_query]": 0.09325549299978775, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_high_throughput_after_creation[sqs]": 0.08081372700007705, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_high_throughput_after_creation[sqs_query]": 0.08333649899986995, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_regular_throughput_after_creation[sqs]": 0.08297471299988501, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_change_to_regular_throughput_after_creation[sqs_query]": 0.08592211699988184, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_content_based_message_deduplication_arrives_once": 1.0827966460001335, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_content_based_message_deduplication_arrives_once[sqs]": 1.036847484999953, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_content_based_message_deduplication_arrives_once[sqs_query]": 1.0392543670002397, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[False]": 1.1287900859997535, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[True]": 1.119681720000699, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[sqs-False]": 1.0553182910000487, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[sqs-True]": 1.0553432260001046, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[sqs_query-False]": 1.0541739920001874, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[sqs_query-True]": 1.0569754799998918, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[False]": 1.1109396149995518, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[True]": 1.1192495999985113, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[sqs-False]": 1.0492656559999887, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[sqs-True]": 1.0487868679999792, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[sqs_query-False]": 1.0553555500000584, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_not_on_message_group_id[sqs_query-True]": 1.0524696640000002, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_delete_message_with_expired_receipt_handle": 2.061263867000889, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_delete_message_with_expired_receipt_handle[sqs]": 0.023598179000146047, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_delete_message_with_expired_receipt_handle[sqs_query]": 0.02580162199978986, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_empty_message_groups_added_back_to_queue[sqs]": 0.05994012700011808, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_empty_message_groups_added_back_to_queue[sqs_query]": 0.06243222199987031, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_high_throughput_ordering[sqs]": 0.05596182499994029, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_high_throughput_ordering[sqs_query]": 0.05699056100024791, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_attributes": 0.1229283150005358, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_attributes[sqs]": 0.05640399500020976, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_attributes[sqs_query]": 0.0568996669999251, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_group_visibility": 2.038163958000041, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_group_visibility_after_change_message_visibility": 2.109731725999154, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_group_visibility_after_change_message_visibility[sqs]": 2.048667458000182, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_group_visibility_after_change_message_visibility[sqs_query]": 2.0628144800000427, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_group_visibility_after_delete": 0.2637410790002832, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_group_visibility_after_delete[sqs]": 0.10041579599987926, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_group_visibility_after_delete[sqs_query]": 0.10105107500021404, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_group_visibility_after_partial_delete": 0.21853323299910699, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_group_visibility_after_partial_delete[sqs]": 0.09299962699992648, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_group_visibility_after_partial_delete[sqs_query]": 0.10325228900001093, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_group_visibility_after_terminate_visibility_timeout": 0.11141877799946087, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_group_visibility_after_terminate_visibility_timeout[sqs]": 0.04715113900010692, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_message_group_visibility_after_terminate_visibility_timeout[sqs_query]": 0.048401029999922685, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_messages_in_order_after_timeout": 2.1308247999986634, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_messages_in_order_after_timeout[sqs]": 2.045801311999867, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_messages_in_order_after_timeout[sqs_query]": 2.0433494749997863, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_queue_requires_suffix": 0.005018026000243481, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_queue_send_message_with_delay_on_queue_works": 4.102350769000623, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_queue_send_message_with_delay_on_queue_works[sqs]": 4.044900906000066, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_queue_send_message_with_delay_on_queue_works[sqs_query]": 4.0468472550001025, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_queue_send_message_with_delay_seconds_fails": 0.06282973299948935, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_queue_send_message_with_delay_seconds_fails[sqs]": 0.05402591899996878, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_queue_send_message_with_delay_seconds_fails[sqs_query]": 0.054175426000256266, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_queue_send_multiple_messages_multiple_single_receives": 0.22624432600059663, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_queue_send_multiple_messages_multiple_single_receives[sqs]": 0.08999755300010293, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_queue_send_multiple_messages_multiple_single_receives[sqs_query]": 0.0934052929999325, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_receive_message_group_id_ordering": 0.10890403099983814, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_receive_message_group_id_ordering[sqs]": 0.04690187499977583, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_receive_message_group_id_ordering[sqs_query]": 0.048825499000031414, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_receive_message_visibility_timeout_shared_in_group": 2.175535993000267, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_receive_message_visibility_timeout_shared_in_group[sqs]": 2.076035137999952, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_receive_message_visibility_timeout_shared_in_group[sqs_query]": 2.0766808500000025, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_receive_message_with_zero_visibility_timeout": 0.1526925089992801, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_receive_message_with_zero_visibility_timeout[sqs]": 0.06253415299988774, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_receive_message_with_zero_visibility_timeout[sqs_query]": 0.06299412399994253, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_sequence_number_increases": 0.08425139599876275, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_sequence_number_increases[sqs]": 0.03431461200011654, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_sequence_number_increases[sqs_query]": 0.03500103499982288, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_set_content_based_deduplication_strategy": 0.06332204200043634, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_set_content_based_deduplication_strategy[sqs]": 0.029234391999807485, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_set_content_based_deduplication_strategy[sqs_query]": 0.029639800000268224, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_get_list_queues_with_query_auth": 0.007797396999876582, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_get_queue_url_contains_localstack_host[sqs]": 0.010391476999984661, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_get_queue_url_contains_localstack_host[sqs_query]": 0.01585919100011779, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_get_queue_url_contains_request_host": 0.03106108000065433, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_get_queue_url_multi_region": 0.037587354000606865, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_get_queue_url_multi_region[domain]": 0.01615873100013232, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_get_queue_url_multi_region[path]": 0.016163642999799777, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_get_queue_url_multi_region[standard]": 0.016794663999917248, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_get_specific_queue_attribute_response": 0.04710315900047135, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_get_specific_queue_attribute_response[sqs]": 0.018333586000153446, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_get_specific_queue_attribute_response[sqs_query]": 0.0193750620001083, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_inflight_message_requeue": 4.533913576000032, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_batch_id": 0.08848499599935167, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_batch_id[sqs]": 0.06010567099997388, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_batch_id[sqs_query]": 0.04819791299996723, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_dead_letter_arn_rejected_before_lookup": 0.008363220000092042, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_receipt_handle_should_return_error_message": 0.08628653100095107, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_receipt_handle_should_return_error_message[sqs]": 0.009626431000242519, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_receipt_handle_should_return_error_message[sqs_query]": 0.009689516000207732, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_string_attributes_cause_invalid_parameter_value_error": 0.020458062998841342, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_string_attributes_cause_invalid_parameter_value_error[sqs]": 0.00961061199973301, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_invalid_string_attributes_cause_invalid_parameter_value_error[sqs_query]": 0.009757889999946201, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_list_queue_tags": 0.029328928000722954, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_list_queue_tags[sqs]": 0.012044208999896, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_list_queue_tags[sqs_query]": 0.012307482999858621, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_list_queues": 0.03250914700015528, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_list_queues_multi_region_with_endpoint_strategy_domain": 0.02108497500012163, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_list_queues_multi_region_with_endpoint_strategy_standard": 0.02059817600002134, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_list_queues_multi_region_without_endpoint_strategy": 0.025170796000111295, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_marker_serialization_json_protocol[\"{\\\\\"foo\\\\\": \\\\\"ba\\\\rr\\\\\"}\"]": 0.028555536999874676, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_marker_serialization_json_protocol[{\"foo\": \"ba\\rr\", \"foo2\": \"ba"r"\"}]": 0.028915180000012697, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_deduplication_id_too_long": 0.05545606099985889, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_group_id_too_long": 0.054966978000265954, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_retention": 3.030907033999938, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_retention_fifo": 3.027739804999783, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_retention_with_inflight": 5.543747227000267, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_with_attributes_should_be_enqueued": 0.08188475299994025, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_with_attributes_should_be_enqueued[sqs]": 0.0214906510000219, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_with_attributes_should_be_enqueued[sqs_query]": 0.022320035000120697, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_with_carriage_return": 0.0006721109994032304, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_with_carriage_return[sqs]": 0.02218351100009386, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_message_with_carriage_return[sqs_query]": 0.02272391700012122, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_non_existent_queue": 0.05722115400021721, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_posting_to_fifo_requires_deduplicationid_group_id": 0.050711800999124534, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_posting_to_fifo_requires_deduplicationid_group_id[sqs]": 0.08113147400013077, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_posting_to_fifo_requires_deduplicationid_group_id[sqs_query]": 0.08312429900001916, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_posting_to_queue_via_queue_name": 0.030524040999807767, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_posting_to_queue_via_queue_name[sqs]": 0.016448279000087496, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_posting_to_queue_via_queue_name[sqs_query]": 0.016047678999939308, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_publish_get_delete_message": 0.07615877299940621, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_publish_get_delete_message[sqs]": 0.03279958999996779, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_publish_get_delete_message[sqs_query]": 0.033898592999776156, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_publish_get_delete_message_batch": 0.1372002740008611, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_publish_get_delete_message_batch[sqs]": 0.08917963499993675, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_publish_get_delete_message_batch[sqs_query]": 0.08408013700000083, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_purge_queue": 1.2073209269983636, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_purge_queue[sqs]": 1.0861358629999813, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_purge_queue[sqs_query]": 1.0893810279999343, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_purge_queue_clears_fifo_deduplication_cache": 0.08287246600048093, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_purge_queue_clears_fifo_deduplication_cache[sqs]": 0.031944353000199044, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_purge_queue_clears_fifo_deduplication_cache[sqs_query]": 0.03464323200000763, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_purge_queue_deletes_delayed_messages": 3.1389040359999854, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_purge_queue_deletes_delayed_messages[sqs]": 3.0605465050000475, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_purge_queue_deletes_delayed_messages[sqs_query]": 3.06174324699964, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_purge_queue_deletes_inflight_messages": 4.2691647330002525, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_purge_queue_deletes_inflight_messages[sqs]": 4.109823773000244, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_purge_queue_deletes_inflight_messages[sqs_query]": 4.111669342000141, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_queue_list_nonexistent_tags": 0.022357103001013456, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_queue_list_nonexistent_tags[sqs]": 0.009460196999725667, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_queue_list_nonexistent_tags[sqs_query]": 0.00952045300005011, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_after_visibility_timeout": 1.8763467960006892, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_after_visibility_timeout[sqs]": 1.8947787100000824, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_after_visibility_timeout[sqs_query]": 1.999156352999762, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_attribute_names_filters": 0.16818892499941285, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_attribute_names_filters[sqs]": 0.08238161899976149, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_attribute_names_filters[sqs_query]": 0.08461587199985843, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_attributes_timestamp_types": 0.046590070000092965, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_attributes_timestamp_types[sqs]": 0.023176424000212137, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_attributes_timestamp_types[sqs_query]": 0.022470725999937713, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_message_attribute_names_filters": 0.21333581099952426, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_message_attribute_names_filters[sqs]": 0.09389944500003367, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_message_attribute_names_filters[sqs_query]": 0.09619879900014894, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_wait_time_seconds_and_max_number_of_messages_does_not_block": 0.08673085099871969, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_wait_time_seconds_and_max_number_of_messages_does_not_block[sqs]": 0.032712536999952135, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_wait_time_seconds_and_max_number_of_messages_does_not_block[sqs_query]": 0.03431335400000535, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_with_visibility_timeout_updates_timeout": 0.07455602600111888, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_with_visibility_timeout_updates_timeout[sqs]": 0.03374559999997473, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_message_with_visibility_timeout_updates_timeout[sqs_query]": 0.03480411300029118, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_terminate_visibility_timeout": 0.11343380999915098, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_terminate_visibility_timeout[sqs]": 0.03496950599992488, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_receive_terminate_visibility_timeout[sqs_query]": 0.03402410700005021, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_redrive_policy_attribute_validity": 0.07858723200024542, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_redrive_policy_attribute_validity[sqs]": 0.026840456999707385, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_redrive_policy_attribute_validity[sqs_query]": 0.025017208000008395, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_remove_message_with_old_receipt_handle": 2.0672988679998525, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_remove_message_with_old_receipt_handle[sqs]": 2.030791995999607, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_remove_message_with_old_receipt_handle[sqs_query]": 2.0294363739999426, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_message_size": 0.07835912600012307, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_deduplication_id_for_fifo_queue": 0.09240626599967072, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_deduplication_id_for_fifo_queue[sqs]": 0.04744562099995164, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_deduplication_id_for_fifo_queue[sqs_query]": 0.04822700599993368, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_message_group_id_for_fifo_queue": 0.09068913500050257, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_message_group_id_for_fifo_queue[sqs]": 0.04809288199999173, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_missing_message_group_id_for_fifo_queue[sqs_query]": 0.04823496000017258, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_receive_multiple": 0.11693899200054148, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_receive_multiple[sqs]": 0.037991237000142064, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_batch_receive_multiple[sqs_query]": 0.038339030999850365, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_delay_and_wait_time": 1.755118742000377, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_delay_and_wait_time[sqs]": 1.1032571709999957, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_delay_and_wait_time[sqs_query]": 1.9990829159999066, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch": 6.39933515499979, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch[sqs]": 0.039817250999931275, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch[sqs_query]": 0.04233083199983412, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_empty_list": 0.041672745998766914, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_empty_list[sqs]": 0.009735314000181461, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_empty_list[sqs_query]": 0.009711664000178644, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents": 0.13878638300047896, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents[sqs]": 0.05072979599981409, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents[sqs_query]": 0.05469627100001162, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents_with_updated_maximum_message_size": 0.15418345700163627, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents_with_updated_maximum_message_size[sqs]": 0.040080426000031366, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_batch_with_oversized_contents_with_updated_maximum_message_size[sqs_query]": 0.04064618100005646, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_attributes": 0.05995539299965458, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_attributes[sqs]": 0.02210880399979942, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_attributes[sqs_query]": 0.02276007299997218, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_binary_attributes": 0.08058466999955272, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_binary_attributes[sqs]": 0.035974858000145105, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_binary_attributes[sqs_query]": 0.0364247769996382, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_delay_0_works_for_fifo": 0.05127901699870563, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_delay_0_works_for_fifo[sqs]": 0.021531750000121974, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_delay_0_works_for_fifo[sqs_query]": 0.02203261399972689, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_empty_string_attribute": 0.026364379999904486, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_empty_string_attribute[sqs]": 0.04778780400033611, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_empty_string_attribute[sqs_query]": 0.0474066159999893, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_invalid_fifo_parameters": 0.043515792999642144, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_invalid_fifo_parameters[sqs]": 0.01190717899999072, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_invalid_fifo_parameters[sqs_query]": 0.012408350999976392, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_invalid_payload_characters": 0.020227369999702205, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_invalid_payload_characters[sqs]": 0.009682809999958408, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_invalid_payload_characters[sqs_query]": 0.009587785000121585, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_invalid_string_attributes": 0.18136390800009394, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_invalid_string_attributes[sqs]": 0.046602694000057454, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_invalid_string_attributes[sqs_query]": 0.051583509999773014, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_updated_maximum_message_size": 0.1639930329984054, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_updated_maximum_message_size[sqs]": 0.05993195800010653, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_message_with_updated_maximum_message_size[sqs_query]": 0.06174828900020657, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_oversized_message": 0.12429992300076265, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_oversized_message[sqs]": 0.0579776669999319, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_oversized_message[sqs_query]": 0.053771439999991344, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_max_number_of_messages": 0.1053942670005199, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_max_number_of_messages[sqs]": 0.05695863199980522, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_max_number_of_messages[sqs_query]": 0.060014427999931286, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_message": 0.045617750000928936, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_message[sqs]": 0.02224203499986288, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_message[sqs_query]": 0.023913533000040843, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_message_encoded_content": 0.04483843600064574, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_message_encoded_content[sqs]": 0.021839063000015813, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_message_encoded_content[sqs_query]": 0.022149638999735544, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_message_multiple_queues": 0.032241051999790216, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sent_message_retains_attributes_after_receive": 0.06655621400022937, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sent_message_retains_attributes_after_receive[sqs]": 0.028392244999849936, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sent_message_retains_attributes_after_receive[sqs_query]": 0.028145195999968564, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sequence_number": 0.06434674100000848, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sequence_number[sqs]": 0.029839884999773858, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sequence_number[sqs_query]": 0.03001385599986861, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_empty_queue_policy": 0.08321791800062783, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_empty_queue_policy[sqs]": 0.020759500000167463, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_empty_queue_policy[sqs_query]": 0.022934165999913603, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_queue_policy": 0.06418426999971416, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_queue_policy[sqs]": 0.014152297999999064, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_queue_policy[sqs_query]": 0.015212256000040725, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_fifo": 0.054270890000225336, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_fifo[sqs]": 0.01098350200004461, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_fifo[sqs_query]": 0.011693371999626834, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_fifo_same_dedup_id_different_message_groups[sqs]": 0.06015446800029167, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_fifo_same_dedup_id_different_message_groups[sqs_query]": 0.05774919200007389, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_permission_lifecycle": 0.18162865900012548, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_permission_lifecycle[sqs]": 0.08538219299998673, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_permission_lifecycle[sqs_query]": 0.08797067199975572, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_kms_and_sqs_are_mutually_exclusive": 0.029410912999992433, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_kms_and_sqs_are_mutually_exclusive[sqs]": 0.011397673000146824, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_kms_and_sqs_are_mutually_exclusive[sqs_query]": 0.011469710000028499, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_queue_attributes": 0.07386828500057163, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_queue_attributes[sqs]": 0.03589629599991895, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sse_queue_attributes[sqs_query]": 0.03645360699988487, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_standard_queue_cannot_have_fifo_suffix": 0.004733005000161938, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_successive_purge_calls_fail": 0.11549744300009479, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_successive_purge_calls_fail[sqs]": 0.04994823999982145, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_successive_purge_calls_fail[sqs_query]": 0.04998897699988447, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_system_attributes_have_no_effect_on_attr_md5": 0.05540048299917544, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_system_attributes_have_no_effect_on_attr_md5[sqs]": 0.024525425999854633, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_system_attributes_have_no_effect_on_attr_md5[sqs_query]": 0.02513956699976916, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_tag_queue_overwrites_existing_tag": 0.035241831001258106, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_tag_queue_overwrites_existing_tag[sqs]": 0.014180965999912587, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_tag_queue_overwrites_existing_tag[sqs_query]": 0.015026950999754263, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_tag_untag_queue": 0.09757874400020228, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_tag_untag_queue[sqs]": 0.03680696700007502, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_tag_untag_queue[sqs_query]": 0.03905593400008911, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_tags_case_sensitive": 0.05310214900146093, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_tags_case_sensitive[sqs]": 0.011913083000081315, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_tags_case_sensitive[sqs_query]": 0.012372004000326342, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_terminate_visibility_timeout_after_receive": 0.07727887600049144, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_terminate_visibility_timeout_after_receive[sqs]": 0.036335950999728084, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_terminate_visibility_timeout_after_receive[sqs_query]": 0.03727729100000943, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_too_many_entries_in_batch_request": 0.08938691200091853, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_too_many_entries_in_batch_request[sqs]": 0.04881046200034689, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_too_many_entries_in_batch_request[sqs_query]": 0.04984621100015829, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_untag_queue_ignores_non_existing_tag": 0.08131345400124701, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_untag_queue_ignores_non_existing_tag[sqs]": 0.013833797999950548, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_untag_queue_ignores_non_existing_tag[sqs_query]": 0.014787430999831486, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_wait_time_seconds_queue_attribute_waits_correctly": 1.0472264270001688, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_wait_time_seconds_queue_attribute_waits_correctly[sqs]": 1.0227756629997202, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_wait_time_seconds_queue_attribute_waits_correctly[sqs_query]": 1.0223855539998112, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_wait_time_seconds_waits_correctly": 1.0440237689990681, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_wait_time_seconds_waits_correctly[sqs]": 1.0222788930000206, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_wait_time_seconds_waits_correctly[sqs_query]": 1.0233796779998556, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_endpoint_strategy_with_multi_region[domain]": 0.05613138600006096, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_endpoint_strategy_with_multi_region[off]": 0.04379505099996095, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_endpoint_strategy_with_multi_region[path]": 0.04277428999989752, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_endpoint_strategy_with_multi_region[standard]": 0.10493338099990979, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_create_queue_fails": 0.014007881999987148, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_delete_queue[domain]": 0.021083592999957546, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_delete_queue[path]": 0.01674849399978484, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_delete_queue[standard]": 0.017070789000172226, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_list_queues_fails": 0.03871699899991654, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_list_queues_fails_json_format": 0.013484913999946002, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_on_deleted_queue_fails": 0.039459685999645444, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_on_deleted_queue_fails[sqs]": 0.030970962999845142, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_on_deleted_queue_fails[sqs_query]": 0.020283203999952093, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_queue_attributes_all": 0.33317843700001504, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_queue_attributes_json_format": 0.01647130099968308, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_queue_attributes_of_fifo_queue": 0.015197317000229305, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_queue_attributes_with_invalid_arg_returns_error": 0.015584351999905266, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_queue_attributes_with_query_args": 0.17033550299993294, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_queue_attributes_works_without_authparams": 0.026139155000237224, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_queue_attributes_works_without_authparams[domain]": 0.18006216799972208, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_queue_attributes_works_without_authparams[path]": 0.01755802700017739, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_queue_attributes_works_without_authparams[standard]": 0.01512636300003578, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_queue_url_work_for_different_queue[domain]": 0.17884609399993678, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_queue_url_work_for_different_queue[path]": 0.019466791000013473, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_queue_url_work_for_different_queue[standard]": 0.020025544000191076, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_queue_url_works_for_same_queue[domain]": 0.014744131000043126, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_queue_url_works_for_same_queue[path]": 0.01468292599997767, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_queue_url_works_for_same_queue[standard]": 0.015890183999999863, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_send_and_receive_messages": 0.048631632999786234, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_without_query_json_format_returns_returns_xml": 0.011988033999841718, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_get_without_query_returns_unknown_operation": 0.012286336999750347, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_invalid_action_raises_exception": 0.012440062999985457, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_overwrite_queue_url_in_params": 0.01971714499995869, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_queue_url_format_path_strategy": 0.007660929000167016, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_send_message_via_queue_url_with_json_protocol": 1.0333510420000493, + "tests/aws/services/sqs/test_sqs.py::TestSqsQueryApi::test_valid_action_with_missing_parameter_raises_exception": 0.1715557410002475, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEdpoints::test_fifo_list_messages_as_botocore_endpoint_url": 0.08394846000101097, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEdpoints::test_list_messages_as_botocore_endpoint_url": 0.06470092500057945, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEdpoints::test_list_messages_as_json": 0.05885122400104592, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEdpoints::test_list_messages_has_no_side_effects": 0.08254263600065315, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEdpoints::test_list_messages_with_invalid_action_raises_error": 0.021256869999888295, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEdpoints::test_list_messages_with_invalid_queue_url": 0.014548954000019876, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEdpoints::test_list_messages_with_non_existent_queue": 0.018498120999538514, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEdpoints::test_list_messages_with_queue_url_in_path": 0.06479132799995568, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEdpoints::test_list_messages_without_queue_url": 0.018716624999797205, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_fifo_list_messages_as_botocore_endpoint_url[domain]": 0.035346937000213074, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_fifo_list_messages_as_botocore_endpoint_url[path]": 0.03604598700007955, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_fifo_list_messages_as_botocore_endpoint_url[standard]": 0.035123002999625896, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_as_botocore_endpoint_url[domain]": 0.027085598999974536, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_as_botocore_endpoint_url[path]": 0.027550155000199084, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_as_botocore_endpoint_url[standard]": 0.03063724499975251, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_as_json[domain]": 0.027052420999780225, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_as_json[path]": 0.026658532000055857, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_as_json[standard]": 0.02706645699981891, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_has_no_side_effects[domain]": 0.03991544300015448, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_has_no_side_effects[path]": 0.036442150999619116, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_has_no_side_effects[standard]": 0.03644535200032806, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_delayed_messages[domain]": 0.038080165000110355, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_delayed_messages[path]": 0.03893068000002131, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_delayed_messages[standard]": 0.038501486999848566, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_invalid_action_raises_error[domain]": 0.008574873000043226, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_invalid_action_raises_error[path]": 0.008649913999988712, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_invalid_action_raises_error[standard]": 0.010465882999824316, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_invalid_queue_url[domain]": 0.0074946389997876395, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_invalid_queue_url[path]": 0.007400321000432086, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_invalid_queue_url[standard]": 0.008978496000281666, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_invisible_messages[domain]": 0.04561035000006086, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_invisible_messages[path]": 0.04588309299992943, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_invisible_messages[standard]": 0.04743640200013033, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_non_existent_queue[domain]": 0.009909550999964267, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_non_existent_queue[path]": 0.00951931099984904, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_non_existent_queue[standard]": 0.009859917999847312, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_queue_url_in_path[domain]": 0.03053461299987248, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_queue_url_in_path[path]": 0.02921486999980516, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_with_queue_url_in_path[standard]": 0.030735902000060378, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_without_queue_url[domain]": 0.00642899500007843, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_without_queue_url[path]": 0.006811837000213927, + "tests/aws/services/sqs/test_sqs_backdoor.py::TestSqsDeveloperEndpoints::test_list_messages_without_queue_url[standard]": 0.006510200999855442, + "tests/aws/services/sqs/test_sqs_move_task.py::test_basic_move_task_workflow": 1.6236311469999691, + "tests/aws/services/sqs/test_sqs_move_task.py::test_cancel_with_invalid_source_arn_in_task_handle": 0.10979319900002338, + "tests/aws/services/sqs/test_sqs_move_task.py::test_cancel_with_invalid_task_handle": 0.018213765999689713, + "tests/aws/services/sqs/test_sqs_move_task.py::test_cancel_with_invalid_task_id_in_task_handle": 0.025171039000042583, + "tests/aws/services/sqs/test_sqs_move_task.py::test_destination_needs_to_exist": 0.0351739349998752, + "tests/aws/services/sqs/test_sqs_move_task.py::test_move_task_cancel": 1.2929352679998374, + "tests/aws/services/sqs/test_sqs_move_task.py::test_move_task_delete_destination_queue_while_running": 1.3078000929997415, + "tests/aws/services/sqs/test_sqs_move_task.py::test_move_task_with_throughput_limit": 3.1429878010001175, + "tests/aws/services/sqs/test_sqs_move_task.py::test_move_task_workflow_with_default_destination": 1.6090844070001822, + "tests/aws/services/sqs/test_sqs_move_task.py::test_move_task_workflow_with_multiple_sources_as_default_destination": 2.1782633719999467, + "tests/aws/services/sqs/test_sqs_move_task.py::test_source_needs_redrive_policy": 0.032069012999954793, + "tests/aws/services/sqs/test_sqs_move_task.py::test_start_multiple_move_tasks": 0.24996597400013343, + "tests/aws/services/ssm/test_ssm.py::TestSSM::test_describe_parameters": 0.03785447000018394, + "tests/aws/services/ssm/test_ssm.py::TestSSM::test_get_inexistent_maintenance_window": 0.004483727999968323, + "tests/aws/services/ssm/test_ssm.py::TestSSM::test_get_inexistent_secret": 0.010426013999904171, + "tests/aws/services/ssm/test_ssm.py::TestSSM::test_get_parameters_and_secrets": 0.04305401500027983, + "tests/aws/services/ssm/test_ssm.py::TestSSM::test_get_parameters_by_path_and_filter_by_labels": 0.021543559999827266, + "tests/aws/services/ssm/test_ssm.py::TestSSM::test_get_secret_parameter": 0.030831798999997773, + "tests/aws/services/ssm/test_ssm.py::TestSSM::test_hierarchical_parameter[/<param>//b//c]": 0.027822305000199776, + "tests/aws/services/ssm/test_ssm.py::TestSSM::test_hierarchical_parameter[<param>/b/c]": 0.020920113000101992, + "tests/aws/services/ssm/test_ssm.py::TestSSM::test_put_parameters": 0.04489287000001241, + "tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py::TestStateMachine::test_create_choice_state_machine": 0.001037112999938472, + "tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py::TestStateMachine::test_create_run_map_state_machine": 0.0007734079997590015, + "tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py::TestStateMachine::test_create_run_state_machine": 0.0007541940001374314, + "tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py::TestStateMachine::test_create_state_machines_in_parallel": 0.0007519279999996797, + "tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py::TestStateMachine::test_events_state_machine": 0.000787895999792454, + "tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py::TestStateMachine::test_intrinsic_functions": 0.0007454169999618898, + "tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py::TestStateMachine::test_try_catch_state_machine": 0.0007533109999258158, + "tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py::test_aws_sdk_task": 0.000754614000015863, + "tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py::test_aws_sdk_task_delete_s3_object": 0.0007320209999761573, + "tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py::test_default_logging_configuration": 0.0007526199997300864, + "tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py::test_multiregion_nested[statemachine_definition0-eu-central-1]": 0.0007591019998471893, + "tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py::test_multiregion_nested[statemachine_definition0-eu-west-1]": 0.0007520380002006277, + "tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py::test_multiregion_nested[statemachine_definition0-us-east-1]": 0.000902100000075734, + "tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py::test_multiregion_nested[statemachine_definition0-us-east-2]": 0.0007797410003149707, + "tests/aws/services/stepfunctions/v2/activities/test_activities.py::TestActivities::test_activity_task": 1.581069406999859, + "tests/aws/services/stepfunctions/v2/activities/test_activities.py::TestActivities::test_activity_task_failure": 2.268098620999581, + "tests/aws/services/stepfunctions/v2/activities/test_activities.py::TestActivities::test_activity_task_no_worker_name": 2.252678521999769, + "tests/aws/services/stepfunctions/v2/activities/test_activities.py::TestActivities::test_activity_task_on_deleted": 0.18066014700002597, + "tests/aws/services/stepfunctions/v2/activities/test_activities.py::TestActivities::test_activity_task_start_timeout": 5.361349271000108, + "tests/aws/services/stepfunctions/v2/activities/test_activities.py::TestActivities::test_activity_task_with_heartbeat": 6.298470980000275, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfApi::test_event_bridge_events_base": 0.0006677110004602582, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfApi::test_event_bridge_events_failure": 0.0007507120008085622, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfApi::test_state_fail": 0.000726412999938475, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfApi::test_state_fail_empty": 0.0007169130003603641, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_decl_version_1_0": 1.1875544209997315, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_event_bridge_events_base": 2.873166251000157, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_event_bridge_events_failure": 0.0008758799999668554, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_execution_dateformat": 1.0731538719999207, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_query_context_object_values": 1.313544944000114, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_state_fail": 1.2234482499998194, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_state_fail_empty": 0.21634349800001473, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_state_fail_intrinsic": 1.2071512899997288, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_state_fail_path": 1.186238239000204, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_state_pass_regex_json_path": 0.0008156079998116184, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_state_pass_regex_json_path_base": 1.1880675879999671, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_state_pass_result": 1.1778533180001887, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_state_pass_result_jsonpaths": 0.18451133500025207, + "tests/aws/services/stepfunctions/v2/base/test_base.py::TestSnfBase::test_state_pass_result_null_input_output_paths": 1.1956145330000254, + "tests/aws/services/stepfunctions/v2/base/test_wait.py::TestSfnWait::test_base_wait_seconds_path[-1.5]": 1.1760678949999601, + "tests/aws/services/stepfunctions/v2/base/test_wait.py::TestSfnWait::test_base_wait_seconds_path[-1]": 1.171361270999796, + "tests/aws/services/stepfunctions/v2/base/test_wait.py::TestSfnWait::test_base_wait_seconds_path[0]": 1.1884108339997965, + "tests/aws/services/stepfunctions/v2/base/test_wait.py::TestSfnWait::test_base_wait_seconds_path[1.5]": 1.1744278110002142, + "tests/aws/services/stepfunctions/v2/base/test_wait.py::TestSfnWait::test_base_wait_seconds_path[1]": 1.1967996499997753, + "tests/aws/services/stepfunctions/v2/base/test_wait.py::TestSfnWait::test_timestamp_too_far_in_future_boundary[24855]": 0.0007808019997810334, + "tests/aws/services/stepfunctions/v2/base/test_wait.py::TestSfnWait::test_timestamp_too_far_in_future_boundary[24856]": 0.000772205999965081, + "tests/aws/services/stepfunctions/v2/base/test_wait.py::TestSfnWait::test_wait_timestamppath[.000000Z]": 1.184188789000018, + "tests/aws/services/stepfunctions/v2/base/test_wait.py::TestSfnWait::test_wait_timestamppath[.000000]": 1.177737406999995, + "tests/aws/services/stepfunctions/v2/base/test_wait.py::TestSfnWait::test_wait_timestamppath[.00Z]": 1.1867328840000937, + "tests/aws/services/stepfunctions/v2/base/test_wait.py::TestSfnWait::test_wait_timestamppath[Z]": 1.181267285000331, + "tests/aws/services/stepfunctions/v2/base/test_wait.py::TestSfnWait::test_wait_timestamppath[]": 1.1793282539999836, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_multiple_executions_and_heartbeat_notifications": 0.000971108999920034, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_multiple_heartbeat_notifications": 0.001126149999890913, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sns_publish_wait_for_task_token": 1.2918096060000153, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_failure_in_wait_for_task_tok_no_error_field[SQS_PARALLEL_WAIT_FOR_TASK_TOKEN]": 0.0029991559997597506, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_failure_in_wait_for_task_tok_no_error_field[SQS_WAIT_FOR_TASK_TOKEN_CATCH]": 1.3096510780001154, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_failure_in_wait_for_task_token": 2.449071847000141, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_tok_with_heartbeat": 7.492445526999973, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_token": 2.444384345999879, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_token_call_chain": 4.689138891000084, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_token_no_token_parameter": 5.2692269110000325, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_sqs_wait_for_task_token_timeout": 5.2952526850001504, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_start_execution_sync": 1.3128721289999703, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_start_execution_sync2": 1.2980403839999326, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_start_execution_sync_delegate_failure": 1.302506222999682, + "tests/aws/services/stepfunctions/v2/callback/test_callback.py::TestCallback::test_start_execution_sync_delegate_timeout": 7.379907647999971, + "tests/aws/services/stepfunctions/v2/choice_operators/test_boolean_equals.py::TestBooleanEquals::test_boolean_equals": 38.38807722599995, + "tests/aws/services/stepfunctions/v2/choice_operators/test_boolean_equals.py::TestBooleanEquals::test_boolean_equals_path": 38.36422449099996, + "tests/aws/services/stepfunctions/v2/choice_operators/test_is_operators.py::TestIsOperators::test_is_boolean": 39.63425829200014, + "tests/aws/services/stepfunctions/v2/choice_operators/test_is_operators.py::TestIsOperators::test_is_null": 38.292058607999934, + "tests/aws/services/stepfunctions/v2/choice_operators/test_is_operators.py::TestIsOperators::test_is_numeric": 38.229499426000075, + "tests/aws/services/stepfunctions/v2/choice_operators/test_is_operators.py::TestIsOperators::test_is_present": 39.41626069299991, + "tests/aws/services/stepfunctions/v2/choice_operators/test_is_operators.py::TestIsOperators::test_is_string": 38.20996820799974, + "tests/aws/services/stepfunctions/v2/choice_operators/test_is_operators.py::TestIsOperators::test_is_timestamp": 0.0012135950000811135, + "tests/aws/services/stepfunctions/v2/choice_operators/test_numeric.py::TestNumerics::test_numeric_equals": 58.50505768999983, + "tests/aws/services/stepfunctions/v2/choice_operators/test_numeric.py::TestNumerics::test_numeric_equals_path": 59.73385648300018, + "tests/aws/services/stepfunctions/v2/choice_operators/test_numeric.py::TestNumerics::test_numeric_greater_than": 6.48524774399948, + "tests/aws/services/stepfunctions/v2/choice_operators/test_numeric.py::TestNumerics::test_numeric_greater_than_equals": 6.506651471000168, + "tests/aws/services/stepfunctions/v2/choice_operators/test_numeric.py::TestNumerics::test_numeric_greater_than_equals_path": 6.49997777300041, + "tests/aws/services/stepfunctions/v2/choice_operators/test_numeric.py::TestNumerics::test_numeric_greater_than_path": 6.493721504000405, + "tests/aws/services/stepfunctions/v2/choice_operators/test_numeric.py::TestNumerics::test_numeric_less_than": 6.440827941000407, + "tests/aws/services/stepfunctions/v2/choice_operators/test_numeric.py::TestNumerics::test_numeric_less_than_equals": 6.484112211000138, + "tests/aws/services/stepfunctions/v2/choice_operators/test_numeric.py::TestNumerics::test_numeric_less_than_equals_path": 6.455776926999988, + "tests/aws/services/stepfunctions/v2/choice_operators/test_numeric.py::TestNumerics::test_numeric_less_than_path": 6.487964764000026, + "tests/aws/services/stepfunctions/v2/choice_operators/test_string_operators.py::TestStrings::test_string_equals": 18.179056419000062, + "tests/aws/services/stepfunctions/v2/choice_operators/test_string_operators.py::TestStrings::test_string_equals_path": 3.313401985999917, + "tests/aws/services/stepfunctions/v2/choice_operators/test_string_operators.py::TestStrings::test_string_greater_than": 4.366883951000091, + "tests/aws/services/stepfunctions/v2/choice_operators/test_string_operators.py::TestStrings::test_string_greater_than_equals": 3.326705522999873, + "tests/aws/services/stepfunctions/v2/choice_operators/test_string_operators.py::TestStrings::test_string_greater_than_equals_path": 3.2975740280003265, + "tests/aws/services/stepfunctions/v2/choice_operators/test_string_operators.py::TestStrings::test_string_greater_than_path": 4.365840604000368, + "tests/aws/services/stepfunctions/v2/choice_operators/test_string_operators.py::TestStrings::test_string_less_than": 3.3036863760003143, + "tests/aws/services/stepfunctions/v2/choice_operators/test_string_operators.py::TestStrings::test_string_less_than_equals": 3.304035992000081, + "tests/aws/services/stepfunctions/v2/choice_operators/test_string_operators.py::TestStrings::test_string_less_than_equals_path": 3.3247687870002665, + "tests/aws/services/stepfunctions/v2/choice_operators/test_string_operators.py::TestStrings::test_string_less_than_path": 3.307400621999477, + "tests/aws/services/stepfunctions/v2/choice_operators/test_timestamp_operators.py::TestTimestamps::test_timestamp_equals": 18.241930911000054, + "tests/aws/services/stepfunctions/v2/choice_operators/test_timestamp_operators.py::TestTimestamps::test_timestamp_equals_path": 3.3171440390005955, + "tests/aws/services/stepfunctions/v2/choice_operators/test_timestamp_operators.py::TestTimestamps::test_timestamp_greater_than": 3.3094920039998215, + "tests/aws/services/stepfunctions/v2/choice_operators/test_timestamp_operators.py::TestTimestamps::test_timestamp_greater_than_equals": 3.2965894279996064, + "tests/aws/services/stepfunctions/v2/choice_operators/test_timestamp_operators.py::TestTimestamps::test_timestamp_greater_than_equals_path": 1.189993405000223, + "tests/aws/services/stepfunctions/v2/choice_operators/test_timestamp_operators.py::TestTimestamps::test_timestamp_greater_than_path": 1.1634008699998049, + "tests/aws/services/stepfunctions/v2/choice_operators/test_timestamp_operators.py::TestTimestamps::test_timestamp_less_than": 3.29629328899955, + "tests/aws/services/stepfunctions/v2/choice_operators/test_timestamp_operators.py::TestTimestamps::test_timestamp_less_than_equals": 3.29912990000048, + "tests/aws/services/stepfunctions/v2/choice_operators/test_timestamp_operators.py::TestTimestamps::test_timestamp_less_than_equals_path": 1.1861441119999654, + "tests/aws/services/stepfunctions/v2/choice_operators/test_timestamp_operators.py::TestTimestamps::test_timestamp_less_than_path": 1.1807483099996716, + "tests/aws/services/stepfunctions/v2/comments/test_comments.py::TestComments::test_comment_in_parameters": 1.193703875999745, + "tests/aws/services/stepfunctions/v2/comments/test_comments.py::TestComments::test_comments_as_per_docs": 7.325740523000604, + "tests/aws/services/stepfunctions/v2/error_handling/test_aws_sdk.py::TestAwsSdk::test_dynamodb_invalid_param": 0.0007792290002726077, + "tests/aws/services/stepfunctions/v2/error_handling/test_aws_sdk.py::TestAwsSdk::test_dynamodb_put_item_no_such_table": 1.215790036999806, + "tests/aws/services/stepfunctions/v2/error_handling/test_aws_sdk.py::TestAwsSdk::test_invalid_secret_name": 2.448101640999994, + "tests/aws/services/stepfunctions/v2/error_handling/test_aws_sdk.py::TestAwsSdk::test_no_such_bucket": 1.1992597569997088, + "tests/aws/services/stepfunctions/v2/error_handling/test_states_errors.py::TestStatesErrors::test_service_task_lambada_catch_state_all_data_limit_exceeded_on_large_utf8_response": 2.2863439269999617, + "tests/aws/services/stepfunctions/v2/error_handling/test_states_errors.py::TestStatesErrors::test_service_task_lambada_data_limit_exceeded_on_large_utf8_response": 2.2465974669994466, + "tests/aws/services/stepfunctions/v2/error_handling/test_states_errors.py::TestStatesErrors::test_start_large_input": 1.530559933999939, + "tests/aws/services/stepfunctions/v2/error_handling/test_states_errors.py::TestStatesErrors::test_task_lambda_catch_state_all_data_limit_exceeded_on_large_utf8_response": 2.2504034250005134, + "tests/aws/services/stepfunctions/v2/error_handling/test_states_errors.py::TestStatesErrors::test_task_lambda_data_limit_exceeded_on_large_utf8_response": 2.254222350999953, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_lambda.py::TestTaskLambda::test_no_such_function": 2.396685060999971, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_lambda.py::TestTaskLambda::test_no_such_function_catch": 2.3945846780002285, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_lambda.py::TestTaskLambda::test_raise_exception": 2.384854661999725, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_lambda.py::TestTaskLambda::test_raise_exception_catch": 2.4105281720003404, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_service_dynamodb.py::TestTaskServiceDynamoDB::test_invalid_param": 1.3636231880000196, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_service_dynamodb.py::TestTaskServiceDynamoDB::test_put_item_invalid_table_name": 1.3518128399996385, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_service_dynamodb.py::TestTaskServiceDynamoDB::test_put_item_no_such_table": 1.323458712000047, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_service_lambda.py::TestTaskServiceLambda::test_invoke_timeout": 7.324769266999738, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_service_lambda.py::TestTaskServiceLambda::test_no_such_function": 2.3080763839998326, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_service_lambda.py::TestTaskServiceLambda::test_no_such_function_catch": 2.2863915310003904, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_service_lambda.py::TestTaskServiceLambda::test_raise_exception": 2.312434774000394, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_service_lambda.py::TestTaskServiceLambda::test_raise_exception_catch": 2.3639906670005075, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sfn.py::TestTaskServiceSfn::test_start_execution_no_such_arn": 1.4654460910001035, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.py::TestTaskServiceSqs::test_send_message_empty_body": 0.000873415000114619, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.py::TestTaskServiceSqs::test_send_message_no_such_queue": 1.4334611460003543, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.py::TestTaskServiceSqs::test_send_message_no_such_queue_no_catch": 1.4003422490000048, + "tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.py::TestTaskServiceSqs::test_sqs_failure_in_wait_for_task_tok": 2.555363226999816, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_array.py::TestArray::test_array_0": 1.2199889970006552, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_array.py::TestArray::test_array_2": 8.434146888000214, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_array.py::TestArray::test_array_contains": 9.460614794000321, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_array.py::TestArray::test_array_get_item": 1.216947639999944, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_array.py::TestArray::test_array_length": 1.2160797030005597, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_array.py::TestArray::test_array_partition": 27.119619279000744, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_array.py::TestArray::test_array_range": 4.303945501000271, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_array.py::TestArray::test_array_unique": 1.2176358020005864, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_encode_decode.py::TestEncodeDecode::test_base_64_decode": 2.2437437920002594, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_encode_decode.py::TestEncodeDecode::test_base_64_encode": 2.2622606449995146, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_generic.py::TestGeneric::test_context_json_path": 1.2214020790001996, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_generic.py::TestGeneric::test_escape_sequence": 1.2156632830005947, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_generic.py::TestGeneric::test_format_1": 7.380699809000362, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_generic.py::TestGeneric::test_format_2": 8.417104329999802, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_generic.py::TestGeneric::test_nested_calls_1": 1.2162518750001254, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_generic.py::TestGeneric::test_nested_calls_2": 1.2196495909997793, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_hash_calculations.py::TestHashCalculations::test_hash": 5.332313623000118, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_json_manipulation.py::TestJsonManipulation::test_json_merge": 1.2260884150000493, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_json_manipulation.py::TestJsonManipulation::test_json_to_string": 8.4062853370001, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_json_manipulation.py::TestJsonManipulation::test_string_to_json": 10.456882701999348, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_math_operations.py::TestMathOperations::test_math_add": 21.76808898599984, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_math_operations.py::TestMathOperations::test_math_random": 3.326230664999912, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_math_operations.py::TestMathOperations::test_math_random_seeded": 1.253720784000052, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_string_operations.py::TestStringOperations::test_string_split": 6.370577980000235, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_string_operations.py::TestStringOperations::test_string_split_context_object": 1.2178767149998748, + "tests/aws/services/stepfunctions/v2/intrinsic_functions/test_unique_id_generation.py::TestUniqueIdGeneration::test_uuid": 2.5889601159997255, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_catch_empty": 2.3558964560002096, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_catch_states_runtime": 2.3632258370003, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_choice_aws_docs_scenario": 1.2069031429996357, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_choice_singleton_composite": 1.1992686240000694, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_choice_unsorted_parameters[{\"result\": {\"done\": false}}]": 1.1884016740000334, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_choice_unsorted_parameters[{\"result\": {\"done\": true}}]": 1.2120603539997319, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_lambda_empty_retry": 2.252503187000002, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_lambda_invoke_with_retry_base": 9.315040353999393, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_lambda_invoke_with_retry_extended_input": 9.343001625000397, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_lambda_service_invoke_with_retry_extended_input": 9.356471779999993, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_base_csv_headers_decl": 1.2138864369999283, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_base_csv_headers_first_line": 1.215963560000091, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_base_json": 1.2323048329999438, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_base_json_max_items": 1.2272414510002818, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_base_list_objects_v2": 1.2221612069997718, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_csv_first_row_extra_fields": 1.2084288619998915, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_csv_headers_decl_duplicate_headers": 1.2069600559998435, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_csv_headers_decl_extra_fields": 1.2105379800000264, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_csv_headers_first_row_typed_headers": 1.2073853099996086, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_csv_max_items[0]": 1.2141664949999722, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_csv_max_items[100000000]": 1.2086669790000997, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_csv_max_items[2]": 2.372079364000001, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_csv_max_items_paths[-1]": 1.2138068599992948, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_csv_max_items_paths[0]": 1.2055681470001218, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_csv_max_items_paths[1.5]": 0.005257128000266675, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_csv_max_items_paths[100000000]": 1.2028547869995236, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_csv_max_items_paths[100000001]": 1.203308210999694, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_csv_max_items_paths[2]": 1.2015331489997152, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_item_reader_json_no_json_list_object": 1.2111264160002975, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state": 1.2975446669997837, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_break_condition": 1.2379961259998709, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_break_condition_legacy": 1.2297554579995449, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_catch": 1.222552118999829, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_catch_empty_fail": 1.2060905759999514, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_catch_legacy": 1.2458250780000526, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_config_distributed_item_selector": 1.218579131000297, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_config_distributed_parameters": 1.2234537459999046, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_config_distributed_reentrant": 2.4542520400000285, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_config_distributed_reentrant_lambda": 2.271195718000399, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_config_inline_item_selector": 1.211645438999767, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_config_inline_parameters": 1.2260339499994188, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_item_selector": 1.3367018730000382, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_item_selector_singleton": 1.2398872809999375, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_legacy": 1.2960783649996301, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_legacy_config_distributed": 1.290593460999844, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_legacy_config_distributed_item_selector": 1.2908480739997685, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_legacy_config_distributed_parameters": 1.3081260079998174, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_legacy_config_inline": 1.2900898599996253, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_legacy_config_inline_item_selector": 1.3051163450004424, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_legacy_config_inline_parameters": 1.2996267159996933, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_legacy_reentrant": 1.2661131479999312, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_nested": 1.3298727469996265, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_no_processor_config": 1.2773236290004206, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_parameters_legacy": 1.3422970339997846, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_parameters_singleton_legacy": 1.2304549310001676, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_retry": 4.218390730000465, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_retry_legacy": 3.238210966999304, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_map_state_retry_multiple_retriers": 7.243803931999992, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_max_concurrency_path[0]": 1.2449121059999015, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_max_concurrency_path[1]": 1.2456964090006295, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_max_concurrency_path[NoNumber]": 1.2320119500000146, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_max_concurrency_path[max_concurrency_value0]": 1.2585487750002358, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_max_concurrency_path_negative": 1.2647854000001644, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_parallel_state": 1.383457627000098, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_parallel_state_catch": 1.2711525869999605, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_parallel_state_fail": 1.2420030410003164, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_parallel_state_nested": 1.3792856200002461, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_parallel_state_order": 1.3008275910005977, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_parallel_state_retry": 4.267274529000133, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_retry_interval_features": 5.263610362000236, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_retry_interval_features_jitter_none": 4.246687922000547, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_retry_interval_features_max_attempts_zero": 2.249921763999737, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_wait_timestamp": 0.25056877100041675, + "tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py::TestBaseScenarios::test_wait_timestamp_path": 1.1983717749999414, + "tests/aws/services/stepfunctions/v2/scenarios/test_sfn_scenarios.py::TestFundamental::test_path_based_on_data": 5.345517918000496, + "tests/aws/services/stepfunctions/v2/scenarios/test_sfn_scenarios.py::TestFundamental::test_step_functions_calling_api_gateway": 11.221401865000189, + "tests/aws/services/stepfunctions/v2/scenarios/test_sfn_scenarios.py::TestFundamental::test_wait_for_callback": 10.494894752000619, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_base": 2.529082130999541, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_error": 2.5326288530000056, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_body_post[\"HelloWorld\"]": 0.0006430119992728578, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_body_post[0]": 0.0006473109997386928, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_body_post[HelloWorld]": 2.489161168999999, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_body_post[None]": 2.4995500990003165, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_body_post[]": 2.4982931920003466, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_body_post[request_body3]": 2.5003672859998005, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_body_post[true]": 0.0008650159998069284, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_body_post[{\"message\": \"HelloWorld!\"}]": 0.000655711000035808, + "tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py::TestTaskApiGateway::test_invoke_with_query_parameters": 2.552554788999714, + "tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py::TestTaskServiceAwsSdk::test_dynamodb_put_delete_item": 1.2287566149998383, + "tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py::TestTaskServiceAwsSdk::test_dynamodb_put_get_item": 1.307452504000139, + "tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py::TestTaskServiceAwsSdk::test_dynamodb_put_update_get_item": 1.2631877649996568, + "tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py::TestTaskServiceAwsSdk::test_list_secrets": 1.2569300689997362, + "tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py::TestTaskServiceAwsSdk::test_sfn_send_task_outcome_with_no_such_token[state_machine_template0]": 1.2506086110001888, + "tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py::TestTaskServiceAwsSdk::test_sfn_send_task_outcome_with_no_such_token[state_machine_template1]": 1.248827031000019, + "tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py::TestTaskServiceAwsSdk::test_sfn_start_execution": 1.2768445269998665, + "tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py::TestTaskServiceAwsSdk::test_sfn_start_execution_implicit_json_serialisation": 1.2769134889999805, + "tests/aws/services/stepfunctions/v2/services/test_dynamodb_task_service.py::TestTaskServiceDynamoDB::test_put_delete_item": 2.5044815129995186, + "tests/aws/services/stepfunctions/v2/services/test_dynamodb_task_service.py::TestTaskServiceDynamoDB::test_put_get_item": 1.2801598540004306, + "tests/aws/services/stepfunctions/v2/services/test_dynamodb_task_service.py::TestTaskServiceDynamoDB::test_put_update_get_item": 1.3071870730000228, + "tests/aws/services/stepfunctions/v2/services/test_ecs_task_service.py::TestTaskServiceECS::test_run_task": 0.0007873849999668892, + "tests/aws/services/stepfunctions/v2/services/test_ecs_task_service.py::TestTaskServiceECS::test_run_task_raise_failure": 0.0007305580002139322, + "tests/aws/services/stepfunctions/v2/services/test_ecs_task_service.py::TestTaskServiceECS::test_run_task_sync": 0.0007393949999823235, + "tests/aws/services/stepfunctions/v2/services/test_ecs_task_service.py::TestTaskServiceECS::test_run_task_sync_raise_failure": 0.0007556850000582926, + "tests/aws/services/stepfunctions/v2/services/test_events_task_service.py::TestTaskServiceEvents::test_put_events_base": 2.286106137999468, + "tests/aws/services/stepfunctions/v2/services/test_events_task_service.py::TestTaskServiceEvents::test_put_events_malformed_detail": 0.0007228950003081991, + "tests/aws/services/stepfunctions/v2/services/test_events_task_service.py::TestTaskServiceEvents::test_put_events_no_source": 0.0007273520000126155, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task.py::TestTaskLambda::test_invoke_bytes_payload": 2.2224772410004334, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task.py::TestTaskLambda::test_invoke_json_values[0.0]": 2.217133954000019, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task.py::TestTaskLambda::test_invoke_json_values[00]": 0.0007518130005337298, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task.py::TestTaskLambda::test_invoke_json_values[01]": 0.0007272140001077787, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task.py::TestTaskLambda::test_invoke_json_values[0_0]": 2.235189616000298, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task.py::TestTaskLambda::test_invoke_json_values[0_1]": 2.242439020999882, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task.py::TestTaskLambda::test_invoke_json_values[HelloWorld]": 2.214334553999379, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task.py::TestTaskLambda::test_invoke_json_values[True]": 2.2138918889995693, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task.py::TestTaskLambda::test_invoke_json_values[json_value5]": 2.2364281660002234, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task.py::TestTaskLambda::test_invoke_json_values[json_value6]": 2.2186389410003358, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task.py::TestTaskLambda::test_invoke_pipe": 3.281665389999489, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task.py::TestTaskLambda::test_invoke_string_payload": 2.233850230999451, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task.py::TestTaskLambda::test_lambda_task_filter_parameters_input": 2.252343918999486, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py::TestTaskServiceLambda::test_invoke": 2.2924130219998915, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py::TestTaskServiceLambda::test_invoke_bytes_payload": 2.294613636000122, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py::TestTaskServiceLambda::test_invoke_json_values[0.0]": 2.3082614420000027, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py::TestTaskServiceLambda::test_invoke_json_values[00]": 0.0007316140008697403, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py::TestTaskServiceLambda::test_invoke_json_values[01]": 0.0007280119998540613, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py::TestTaskServiceLambda::test_invoke_json_values[0_0]": 2.286935566000011, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py::TestTaskServiceLambda::test_invoke_json_values[0_1]": 2.30423875400038, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py::TestTaskServiceLambda::test_invoke_json_values[HelloWorld]": 2.302306954999949, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py::TestTaskServiceLambda::test_invoke_json_values[True]": 2.305901375000758, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py::TestTaskServiceLambda::test_invoke_json_values[json_value5]": 2.315292279999994, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py::TestTaskServiceLambda::test_invoke_json_values[json_value6]": 2.3188689789999444, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py::TestTaskServiceLambda::test_invoke_unsupported_param": 2.3119171859993912, + "tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py::TestTaskServiceLambda::test_list_functions": 0.000920493999728933, + "tests/aws/services/stepfunctions/v2/services/test_sfn_task_service.py::TestTaskServiceSfn::test_start_execution": 1.3545300020000468, + "tests/aws/services/stepfunctions/v2/services/test_sfn_task_service.py::TestTaskServiceSfn::test_start_execution_input_json": 1.2860477770000216, + "tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py::TestTaskServiceSns::test_fifo_message_attribute[input_params0-True]": 1.2665998560000844, + "tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py::TestTaskServiceSns::test_fifo_message_attribute[input_params1-False]": 1.2713119270001698, + "tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py::TestTaskServiceSns::test_publish_base[1]": 2.4420812070002285, + "tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py::TestTaskServiceSns::test_publish_base[HelloWorld]": 1.264649363999979, + "tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py::TestTaskServiceSns::test_publish_base[None]": 1.2631447910002862, + "tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py::TestTaskServiceSns::test_publish_base[True]": 1.249298417999853, + "tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py::TestTaskServiceSns::test_publish_base[]": 1.255765078999957, + "tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py::TestTaskServiceSns::test_publish_base[message1]": 1.2507929389994388, + "tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py::TestTaskServiceSns::test_publish_base_error_topic_arn": 1.252046691000487, + "tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py::TestTaskServiceSns::test_publish_message_attributes[\"HelloWorld\"]": 1.2710523400000966, + "tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py::TestTaskServiceSns::test_publish_message_attributes[HelloWorld]": 1.277354103000107, + "tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py::TestTaskServiceSns::test_publish_message_attributes[message_value3]": 1.2685206489995835, + "tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py::TestTaskServiceSns::test_publish_message_attributes[{}]": 1.284928055999444, + "tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.py::TestTaskServiceSqs::test_send_message": 1.3148450910002794, + "tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.py::TestTaskServiceSqs::test_send_message_unsupported_parameters": 1.3103900170003726, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_cloudformation_definition_create_describe[dump]": 1.1489715739999156, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_cloudformation_definition_create_describe[dumps]": 1.1495569929998055, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_cloudformation_definition_string_create_describe[dump]": 1.1551584969997748, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_cloudformation_definition_string_create_describe[dumps]": 1.1463066129999788, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_create_delete_invalid_sm": 0.1737205460003679, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_create_delete_valid_sm": 1.174095335000402, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_create_duplicate_definition_format_sm": 0.13847309499988114, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_create_duplicate_sm_name": 0.1369334009996237, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_create_exact_duplicate_sm": 0.14478718299960747, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_create_update_state_machine_base_definition": 0.14758433099996182, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_create_update_state_machine_base_definition_and_role": 0.1937246090001281, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_create_update_state_machine_base_role_arn": 0.18689917899973807, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_create_update_state_machine_base_update_none": 0.14129733399931865, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_create_update_state_machine_same_parameters": 0.1688852369993583, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_delete_nonexistent_sm": 0.13440983899954517, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_describe_execution": 0.23314825500028746, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_describe_execution_invalid_arn": 0.12169805300027292, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_describe_execution_no_such_state_machine": 0.1610926179996568, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_describe_invalid_arn_sm": 0.12383655899975565, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_describe_nonexistent_sm": 0.13406862600004388, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_describe_state_machine_for_execution": 1.1681541059997471, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_get_execution_history_invalid_arn": 0.12116574600031527, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_get_execution_history_no_such_execution": 0.15787125599945284, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_get_execution_history_reversed": 0.17071638100060227, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_invalid_start_execution_arn": 0.13350749100027315, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_invalid_start_execution_input": 0.27717484899994815, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_list_execution_invalid_arn": 0.12298606400008794, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_list_execution_no_such_state_machine": 0.1344913500001894, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_list_sms": 0.16510124399928827, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_start_execution": 1.160315981000167, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_state_machine_status_filter": 0.1992330969997056, + "tests/aws/services/stepfunctions/v2/test_sfn_api.py::TestSnfApi::test_stop_execution": 0.15385945800062473, + "tests/aws/services/stepfunctions/v2/test_sfn_api_activities.py::TestSnfApiActivities::test_create_activity_invalid_name": 0.09877255399987916, + "tests/aws/services/stepfunctions/v2/test_sfn_api_activities.py::TestSnfApiActivities::test_create_describe_delete_activity": 0.1229737119992933, + "tests/aws/services/stepfunctions/v2/test_sfn_api_activities.py::TestSnfApiActivities::test_describe_activity_invalid_arn": 0.12090411200006201, + "tests/aws/services/stepfunctions/v2/test_sfn_api_activities.py::TestSnfApiActivities::test_describe_deleted_activity": 0.10367855899994538, + "tests/aws/services/stepfunctions/v2/test_sfn_api_activities.py::TestSnfApiActivities::test_get_activity_task_deleted": 0.10328002500000366, + "tests/aws/services/stepfunctions/v2/test_sfn_api_activities.py::TestSnfApiActivities::test_get_activity_task_invalid_arn": 1.3548929029998362, + "tests/aws/services/stepfunctions/v2/test_sfn_api_activities.py::TestSnfApiActivities::test_list_activities": 0.11363760500034914, + "tests/aws/services/stepfunctions/v2/test_sfn_api_map_run.py::TestSnfApiMapRun::test_list_map_runs_and_describe_map_run": 1.3886129300003631, + "tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py::TestSnfApiTagging::test_create_state_machine": 0.13071788699971876, + "tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py::TestSnfApiTagging::test_tag_invalid_state_machine[None]": 0.12678601300012815, + "tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py::TestSnfApiTagging::test_tag_invalid_state_machine[tag_list1]": 0.13184645700039255, + "tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py::TestSnfApiTagging::test_tag_invalid_state_machine[tag_list2]": 0.12609355699987645, + "tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py::TestSnfApiTagging::test_tag_invalid_state_machine[tag_list3]": 0.12600879000001441, + "tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py::TestSnfApiTagging::test_tag_state_machine[tag_list0]": 0.1342963270008113, + "tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py::TestSnfApiTagging::test_tag_state_machine[tag_list1]": 0.13275713099983477, + "tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py::TestSnfApiTagging::test_tag_state_machine[tag_list2]": 0.1319442960007109, + "tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py::TestSnfApiTagging::test_tag_state_machine[tag_list3]": 0.13112201200010531, + "tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py::TestSnfApiTagging::test_tag_state_machine[tag_list4]": 0.1381854989999738, + "tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py::TestSnfApiTagging::test_tag_state_machine_version": 0.13272652099976767, + "tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py::TestSnfApiTagging::test_untag_state_machine[tag_keys0]": 0.13547980200019083, + "tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py::TestSnfApiTagging::test_untag_state_machine[tag_keys1]": 0.134829362000346, + "tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py::TestSnfApiTagging::test_untag_state_machine[tag_keys2]": 0.138637887999721, + "tests/aws/services/stepfunctions/v2/test_sfn_api_tagging.py::TestSnfApiTagging::test_untag_state_machine[tag_keys3]": 0.1348460500003057, + "tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py::TestSnfApiVersioning::test_create_publish_describe_no_version_description": 0.13501852200033682, + "tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py::TestSnfApiVersioning::test_create_publish_describe_with_version_description": 0.13440018500023143, + "tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py::TestSnfApiVersioning::test_create_with_publish": 0.12792277800008378, + "tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py::TestSnfApiVersioning::test_create_with_version_description_no_publish": 0.12159145400028137, + "tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py::TestSnfApiVersioning::test_describe_state_machine_for_execution_of_version": 0.16288587399958487, + "tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py::TestSnfApiVersioning::test_describe_state_machine_for_execution_of_version_with_revision": 1.1746031120001135, + "tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py::TestSnfApiVersioning::test_empty_revision_with_publish_and_no_publish_on_creation": 0.1352180789999693, + "tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py::TestSnfApiVersioning::test_empty_revision_with_publish_and_publish_on_creation": 0.13586523200001466, + "tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py::TestSnfApiVersioning::test_idempotent_publish": 0.1388997469998685, + "tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py::TestSnfApiVersioning::test_list_delete_version": 0.15276989299991328, + "tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py::TestSnfApiVersioning::test_publish_state_machine_version": 0.16757372400024906, + "tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py::TestSnfApiVersioning::test_publish_state_machine_version_invalid_arn": 0.1285801239991997, + "tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py::TestSnfApiVersioning::test_publish_state_machine_version_no_such_machine": 0.1294195390000823, + "tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py::TestSnfApiVersioning::test_start_version_execution": 1.2042144550000558, + "tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py::TestSnfApiVersioning::test_update_state_machine": 0.15233678200002032, + "tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py::TestSnfApiVersioning::test_version_ids_between_deletions": 0.14306419300055495, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[BASE_CHOICE_STATE]": 1.5576138190003803, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[BASE_FAIL_STATE]": 0.2658461870000792, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[BASE_PASS_STATE]": 0.28290374400012297, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[BASE_RESULT_PASS_STATE]": 0.28379453500019736, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[BASE_SUCCEED_STATE]": 0.27497936300005676, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[IO_PASS_STATE]": 0.30957154299994727, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_debug[IO_RESULT_PASS_STATE]": 0.3185298710004645, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[BASE_CHOICE_STATE]": 0.2548167459999604, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[BASE_FAIL_STATE]": 0.17448305200014147, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[BASE_PASS_STATE]": 0.1927377259999048, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[BASE_RESULT_PASS_STATE]": 0.19705708399942523, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[BASE_SUCCEED_STATE]": 0.18526743499978693, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[IO_PASS_STATE]": 0.2224739119997139, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_info[IO_RESULT_PASS_STATE]": 0.2232659049996073, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[BASE_CHOICE_STATE]": 0.3338604900000064, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[BASE_FAIL_STATE]": 0.26853150499982803, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[BASE_PASS_STATE]": 0.27940251399968474, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[BASE_RESULT_PASS_STATE]": 0.2888873399992917, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[BASE_SUCCEED_STATE]": 0.2881338749994029, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[IO_PASS_STATE]": 0.31847319400048946, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_inspection_level_trace[IO_RESULT_PASS_STATE]": 0.3103289769996991, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_service_task_state[DEBUG]": 1.7803103940000256, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_service_task_state[INFO]": 1.776396436000141, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_service_task_state[TRACE]": 1.7674205179996534, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_task_state[DEBUG]": 1.738810179000211, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_task_state[INFO]": 1.7683831389999796, + "tests/aws/services/stepfunctions/v2/test_state/test_test_state_scenarios.py::TestStateCaseScenarios::test_base_lambda_task_state[TRACE]": 1.7913402169997426, + "tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py::TestStateMachine::test_create_choice_state_machine": 3.40673896699991, + "tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py::TestStateMachine::test_create_run_map_state_machine": 2.0663090319999355, + "tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py::TestStateMachine::test_create_run_state_machine": 1.455961691000084, + "tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py::TestStateMachine::test_create_state_machines_in_parallel": 0.3011932370000068, + "tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py::TestStateMachine::test_events_state_machine": 0.0009118980001403543, + "tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py::TestStateMachine::test_intrinsic_functions": 1.0767073089996302, + "tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py::TestStateMachine::test_try_catch_state_machine": 10.046795902999747, + "tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py::test_aws_sdk_task": 1.068198334000499, + "tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py::test_default_logging_configuration": 0.024006556999665918, + "tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py::test_multiregion_nested[statemachine_definition0-eu-central-1]": 0.0007504749996769533, + "tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py::test_multiregion_nested[statemachine_definition0-eu-west-1]": 0.0007653030002074956, + "tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py::test_multiregion_nested[statemachine_definition0-us-east-1]": 0.0007920129996819014, + "tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py::test_multiregion_nested[statemachine_definition0-us-east-2]": 0.0007503150000047754, + "tests/aws/services/stepfunctions/v2/test_stepfunctions_v2.py::test_run_aws_sdk_secrets_manager": 3.0819305079994592, + "tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py::TestHeartbeats::test_heartbeat_no_timeout": 6.280253770999934, + "tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py::TestHeartbeats::test_heartbeat_path_timeout": 6.297726803999922, + "tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py::TestHeartbeats::test_heartbeat_timeout": 6.37324635899995, + "tests/aws/services/stepfunctions/v2/timeouts/test_timeouts.py::TestTimeouts::test_fixed_timeout_lambda": 6.30536302199971, + "tests/aws/services/stepfunctions/v2/timeouts/test_timeouts.py::TestTimeouts::test_fixed_timeout_service_lambda": 6.297024496000176, + "tests/aws/services/stepfunctions/v2/timeouts/test_timeouts.py::TestTimeouts::test_fixed_timeout_service_lambda_with_path": 6.331386586999997, + "tests/aws/services/stepfunctions/v2/timeouts/test_timeouts.py::TestTimeouts::test_global_timeout": 5.209999657000026, + "tests/aws/services/stepfunctions/v2/timeouts/test_timeouts.py::TestTimeouts::test_service_lambda_map_timeout": 0.0009122380001826969, + "tests/aws/services/sts/test_sts.py::TestSTSIntegrations::test_assume_role": 0.0069940160001351614, + "tests/aws/services/sts/test_sts.py::TestSTSIntegrations::test_assume_role_with_saml": 0.010635859000103665, + "tests/aws/services/sts/test_sts.py::TestSTSIntegrations::test_assume_role_with_web_identity": 0.011590931999762688, + "tests/aws/services/sts/test_sts.py::TestSTSIntegrations::test_expiration_date_format": 0.012360113000340789, + "tests/aws/services/sts/test_sts.py::TestSTSIntegrations::test_get_caller_identity_role_access_key[False]": 0.04024162600035197, + "tests/aws/services/sts/test_sts.py::TestSTSIntegrations::test_get_caller_identity_role_access_key[True]": 0.03559968200033836, + "tests/aws/services/sts/test_sts.py::TestSTSIntegrations::test_get_caller_identity_root": 0.004306295999867871, + "tests/aws/services/sts/test_sts.py::TestSTSIntegrations::test_get_caller_identity_user_access_key[False]": 0.028769208000539948, + "tests/aws/services/sts/test_sts.py::TestSTSIntegrations::test_get_caller_identity_user_access_key[True]": 0.12627164900004573, + "tests/aws/services/sts/test_sts.py::TestSTSIntegrations::test_get_federation_token": 0.008408912999584572, + "tests/aws/services/support/test_support.py::TestConfigService::test_create_support_case": 0.024754039000072225, + "tests/aws/services/support/test_support.py::TestConfigService::test_resolve_case": 0.006862492999971437, + "tests/aws/services/swf/test_swf.py::TestSwf::test_run_workflow": 0.07576388200004658, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_failing_deletion": 0.06457204699972863, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_failing_start_transcription_job": 0.2116187589999754, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_get_transcription_job": 0.13648712000031082, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_list_transcription_jobs": 0.17568949499991504, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_happy_path": 18.581507314999726, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_start_job[None-None]": 2.105431485000281, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_start_job[test-output-bucket-2-None]": 4.29992977000029, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_start_job[test-output-bucket-3-test-output]": 2.1220187469998564, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_start_job[test-output-bucket-4-test-output.json]": 2.133142702000441, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_start_job[test-output-bucket-5-test-files/test-output.json]": 2.121545863999472, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_start_job[test-output-bucket-6-test-files/test-output]": 2.1199919260006936, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_start_job_same_name": 2.082218980000107, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_supported_media_formats[../../files/en-gb.amr-hello my name is]": 2.038946243999817, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_supported_media_formats[../../files/en-gb.amr]": 4.16744228799962, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_supported_media_formats[../../files/en-gb.flac-hello my name is]": 2.0469406010001876, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_supported_media_formats[../../files/en-gb.flac]": 4.393071784000313, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_supported_media_formats[../../files/en-gb.mp3-hello my name is]": 2.0412869390002015, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_supported_media_formats[../../files/en-gb.mp3]": 4.13508770000044, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_supported_media_formats[../../files/en-gb.mp4-hello my name is]": 2.041900448999513, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_supported_media_formats[../../files/en-gb.mp4]": 4.171764185000029, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_supported_media_formats[../../files/en-gb.ogg-hello my name is]": 2.0432189579996702, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_supported_media_formats[../../files/en-gb.ogg]": 4.132274517000042, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_supported_media_formats[../../files/en-gb.webm-hello my name is]": 2.043348246999358, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_supported_media_formats[../../files/en-gb.webm]": 4.132285629999387, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_supported_media_formats[../../files/en-us_video.mkv-one of the most vital]": 2.0441828039997745, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_supported_media_formats[../../files/en-us_video.mp4-one of the most vital]": 2.044615457999953, + "tests/aws/services/transcribe/test_transcribe.py::TestTranscribe::test_transcribe_unsupported_media_format_failure": 3.0754569550003907, + "tests/aws/test_error_injection.py::TestErrorInjection::test_dynamodb_error_injection": 0.038375841999368276, + "tests/aws/test_error_injection.py::TestErrorInjection::test_dynamodb_read_error_injection": 0.04417379899950902, + "tests/aws/test_error_injection.py::TestErrorInjection::test_dynamodb_write_error_injection": 0.04718215799994141, + "tests/aws/test_error_injection.py::TestErrorInjection::test_kinesis_error_injection": 0.6236201229994549, + "tests/aws/test_integration.py::TestIntegration::test_firehose_extended_s3": 0.09072639399983018, + "tests/aws/test_integration.py::TestIntegration::test_firehose_kinesis_to_s3": 25.636096764000285, + "tests/aws/test_integration.py::TestIntegration::test_firehose_s3": 0.08333330699997532, + "tests/aws/test_integration.py::TestIntegration::test_lambda_streams_batch_and_transactions": 30.29284756900006, + "tests/aws/test_integration.py::TestIntegration::test_scheduled_lambda": 61.145021748999625, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_put_item_to_dynamodb[python3.10]": 1.7993120539999836, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_put_item_to_dynamodb[python3.11]": 1.752714415000355, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_put_item_to_dynamodb[python3.12]": 1.7904242869999507, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_put_item_to_dynamodb[python3.7]": 2.2431136210000204, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_put_item_to_dynamodb[python3.8]": 1.764076552000006, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_put_item_to_dynamodb[python3.9]": 1.7355119969997759, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_send_message_to_sqs[python3.10]": 1.681803987999956, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_send_message_to_sqs[python3.11]": 1.741374678000284, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_send_message_to_sqs[python3.12]": 1.9487257010000576, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_send_message_to_sqs[python3.7]": 32.26960711200002, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_send_message_to_sqs[python3.8]": 1.7192408689998047, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_send_message_to_sqs[python3.9]": 1.6954834379998829, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_start_stepfunctions_execution[python3.10]": 3.7331660800000463, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_start_stepfunctions_execution[python3.11]": 3.7148249490001035, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_start_stepfunctions_execution[python3.12]": 3.7383467940003356, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_start_stepfunctions_execution[python3.7]": 14.813730624999948, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_start_stepfunctions_execution[python3.8]": 3.7433726810004373, + "tests/aws/test_integration.py::TestLambdaOutgoingSdkCalls::test_lambda_start_stepfunctions_execution[python3.9]": 3.755074887000319, + "tests/aws/test_integration.py::test_kinesis_lambda_forward_chain": 5.47219550499949, + "tests/aws/test_moto.py::test_call_include_response_metadata": 0.0023050580002745846, + "tests/aws/test_moto.py::test_call_multi_region_backends": 0.00912074500001836, + "tests/aws/test_moto.py::test_call_non_implemented_operation": 0.032224953999957506, + "tests/aws/test_moto.py::test_call_s3_with_streaming_trait[IO[bytes]]": 0.005873351999525767, + "tests/aws/test_moto.py::test_call_s3_with_streaming_trait[bytes]": 0.014866044000427792, + "tests/aws/test_moto.py::test_call_s3_with_streaming_trait[str]": 0.015799696000158292, + "tests/aws/test_moto.py::test_call_sqs_invalid_call_raises_http_exception": 0.002378686000156449, + "tests/aws/test_moto.py::test_call_with_es_creates_state_correctly": 0.09336334800036639, + "tests/aws/test_moto.py::test_call_with_modified_request": 0.004354417000286048, + "tests/aws/test_moto.py::test_call_with_sqs_creates_state_correctly": 0.1175531450003291, + "tests/aws/test_moto.py::test_call_with_sqs_invalid_call_raises_exception": 0.0026765230004457408, + "tests/aws/test_moto.py::test_call_with_sqs_modifies_state_in_moto_backend": 0.0028113769999436045, + "tests/aws/test_moto.py::test_call_with_sqs_returns_service_response": 0.0021269540002322174, + "tests/aws/test_moto.py::test_moto_fallback_dispatcher": 0.002907465999669512, + "tests/aws/test_moto.py::test_moto_fallback_dispatcher_error_handling": 0.012517149000359495, + "tests/aws/test_moto.py::test_request_with_response_header_location_fields": 0.02609437999990405, + "tests/aws/test_multi_accounts.py::TestMultiAccounts::test_account_id_namespacing_for_localstack_backends": 0.24181552700019893, + "tests/aws/test_multi_accounts.py::TestMultiAccounts::test_account_id_namespacing_for_moto_backends": 0.4503635429996393, + "tests/aws/test_multi_accounts.py::TestMultiAccounts::test_multi_accounts_dynamodb": 1.8875837479999973, + "tests/aws/test_multi_accounts.py::TestMultiAccounts::test_multi_accounts_kinesis": 1.4587138959996082, + "tests/aws/test_multiregion.py::TestMultiRegion::test_multi_region_api_gateway": 0.21427776200016524, + "tests/aws/test_multiregion.py::TestMultiRegion::test_multi_region_sns": 0.029319417999886355, + "tests/aws/test_network_configuration.py::TestLambda::test_function_url": 1.0333384740001748, + "tests/aws/test_network_configuration.py::TestLambda::test_http_api_for_function_url": 0.0007545309999841265, + "tests/aws/test_network_configuration.py::TestOpenSearch::test_default_strategy": 16.786412501000086, + "tests/aws/test_network_configuration.py::TestOpenSearch::test_path_strategy": 16.208905281999705, + "tests/aws/test_network_configuration.py::TestOpenSearch::test_port_strategy": 0.0009717510001792107, + "tests/aws/test_network_configuration.py::TestS3::test_201_response": 0.02301164000027711, + "tests/aws/test_network_configuration.py::TestS3::test_multipart_upload": 0.02636601299946051, + "tests/aws/test_network_configuration.py::TestS3::test_non_us_east_1_location": 0.032523577000119985, + "tests/aws/test_network_configuration.py::TestSQS::test_domain_based_strategies[domain]": 0.006127999000000273, + "tests/aws/test_network_configuration.py::TestSQS::test_domain_based_strategies[standard]": 0.006337572000120417, + "tests/aws/test_network_configuration.py::TestSQS::test_domain_strategy": 0.015223975000026257, + "tests/aws/test_network_configuration.py::TestSQS::test_off_strategy_with_external_port": 0.006836210000074061, + "tests/aws/test_network_configuration.py::TestSQS::test_off_strategy_without_external_port": 0.006618642999910662, + "tests/aws/test_network_configuration.py::TestSQS::test_path_strategy": 0.006149558999823057, + "tests/aws/test_notifications.py::TestNotifications::test_sns_to_sqs": 0.04808764099971086, + "tests/aws/test_notifications.py::TestNotifications::test_sqs_queue_names": 0.006976897000186, + "tests/aws/test_serverless.py::TestServerless::test_apigateway_deployed": 0.019258438000179012, + "tests/aws/test_serverless.py::TestServerless::test_dynamodb_stream_handler_deployed": 0.025152290999812976, + "tests/aws/test_serverless.py::TestServerless::test_event_rules_deployed": 132.49614647299995, + "tests/aws/test_serverless.py::TestServerless::test_kinesis_stream_handler_deployed": 3.0378900909995536, + "tests/aws/test_serverless.py::TestServerless::test_lambda_with_configs_deployed": 0.006681817999833584, + "tests/aws/test_serverless.py::TestServerless::test_queue_handler_deployed": 0.009442168999612477, + "tests/aws/test_serverless.py::TestServerless::test_s3_bucket_deployed": 7.846145035999598, + "tests/aws/test_terraform.py::TestTerraform::test_acm": 0.0007609559993397852, + "tests/aws/test_terraform.py::TestTerraform::test_apigateway": 0.0007659150001018133, + "tests/aws/test_terraform.py::TestTerraform::test_apigateway_escaped_policy": 0.0011245470004723757, + "tests/aws/test_terraform.py::TestTerraform::test_bucket_exists": 0.0015642610001123103, + "tests/aws/test_terraform.py::TestTerraform::test_dynamodb": 0.0007978650000950438, + "tests/aws/test_terraform.py::TestTerraform::test_event_source_mapping": 0.0008875429998624895, + "tests/aws/test_terraform.py::TestTerraform::test_lambda": 0.0007607440002175281, + "tests/aws/test_terraform.py::TestTerraform::test_route53": 0.0007587400000375055, + "tests/aws/test_terraform.py::TestTerraform::test_security_groups": 0.0007626589999745192, + "tests/aws/test_terraform.py::TestTerraform::test_sqs": 0.0007753829995635897, + "tests/aws/test_validate.py::TestMissingParameter::test_elasticache": 0.1613454130001628, + "tests/aws/test_validate.py::TestMissingParameter::test_opensearch": 0.007248651999361755, + "tests/aws/test_validate.py::TestMissingParameter::test_sns": 0.005048546999660175, + "tests/aws/test_validate.py::TestMissingParameter::test_sqs_create_queue": 0.012705278999874281, + "tests/aws/test_validate.py::TestMissingParameter::test_sqs_send_message": 0.34448126199959006, + "tests/aws/scenario/bookstore/test_bookstore.py::TestBookstoreApplication::test_lambda_dynamodb": 3.477916371000049, + "tests/aws/scenario/bookstore/test_bookstore.py::TestBookstoreApplication::test_opensearch_crud": 2.3307244279999964, + "tests/aws/scenario/bookstore/test_bookstore.py::TestBookstoreApplication::test_search_books": 58.88997849600008, + "tests/aws/scenario/bookstore/test_bookstore.py::TestBookstoreApplication::test_setup": 154.294501222, + "tests/aws/scenario/kinesis_firehose/test_kinesis_firehose.py::TestKinesisFirehoseScenario::test_kinesis_firehose_s3": 51.82636695400004, + "tests/aws/scenario/lambda_destination/test_lambda_destination_scenario.py::TestLambdaDestinationScenario::test_destination_sns": 5.2036974130001, + "tests/aws/scenario/lambda_destination/test_lambda_destination_scenario.py::TestLambdaDestinationScenario::test_infra": 11.414998202999982, + "tests/aws/scenario/loan_broker/test_loan_broker.py::TestLoanBrokerScenario::test_prefill_dynamodb_table": 28.786333350000064, + "tests/aws/scenario/loan_broker/test_loan_broker.py::TestLoanBrokerScenario::test_stepfunctions_input_recipient_list[step_function_input0-SUCCEEDED]": 4.020295293000004, + "tests/aws/scenario/loan_broker/test_loan_broker.py::TestLoanBrokerScenario::test_stepfunctions_input_recipient_list[step_function_input1-SUCCEEDED]": 1.161103550000007, + "tests/aws/scenario/loan_broker/test_loan_broker.py::TestLoanBrokerScenario::test_stepfunctions_input_recipient_list[step_function_input2-FAILED]": 1.1504265579999355, + "tests/aws/scenario/loan_broker/test_loan_broker.py::TestLoanBrokerScenario::test_stepfunctions_input_recipient_list[step_function_input3-FAILED]": 1.1461320819998946, + "tests/aws/scenario/loan_broker/test_loan_broker.py::TestLoanBrokerScenario::test_stepfunctions_input_recipient_list[step_function_input4-FAILED]": 0.321517705000133, + "tests/aws/scenario/mythical_mysfits/test_mythical_misfits.py::TestMythicalMisfitsScenario::test_deployed_infra_state": 0.002108180999925935, + "tests/aws/scenario/mythical_mysfits/test_mythical_misfits.py::TestMythicalMisfitsScenario::test_populate_data": 0.0013189249999641106, + "tests/aws/scenario/mythical_mysfits/test_mythical_misfits.py::TestMythicalMisfitsScenario::test_user_clicks_are_stored": 0.0010095290000435853, + "tests/aws/scenario/note_taking/test_note_taking.py::TestNoteTakingScenario::test_notes_rest_api": 4.735018889999992, + "tests/aws/scenario/note_taking/test_note_taking.py::TestNoteTakingScenario::test_validate_infra_setup": 30.857381476, + "tests/aws/scenario/reference/test_lambda_scenario.py::TestBasicLambda::test_scenario_validate_infra": 2.9452711329997783, + "tests/aws/scenario/reference/test_lambda_scenario.py::TestBasicLambdaInS3::test_scenario_validate_infra": 3.01210771700039, + "tests/aws/scenario/reference/test_s3_cleanup.py::TestS3CleanupScenario::test_scenario_validate_infra": 1.4403234839999186, + "tests/aws/scenario/test_apigateway_scenario.py::TestApigatewayLambdaIntegrationScenario::test_scenario_validate_infra": 5.2552084339999965, + "tests/aws/scenario/test_ecs_scenario.py::TestEcsScenario::test_scenario_call_service": 0.0007761130000289995, + "tests/aws/scenario/test_ecs_scenario.py::TestEcsScenario::test_scenario_validate_infra": 0.001073818999998366, + "tests/aws/scenario/test_lambda_destination_scenario.py::TestLambdaDestinationScenario::test_destination_sns": 5.130692522000004, + "tests/aws/scenario/test_lambda_destination_scenario.py::TestLambdaDestinationScenario::test_infra": 3.432292427999869, + "tests/aws/scenario/test_loan_broker.py::TestLoanBrokerScenario::test_prefill_dynamodb_table": 18.10304303600003, + "tests/aws/scenario/test_loan_broker.py::TestLoanBrokerScenario::test_stepfunctions_input_recipient_list[step_function_input0-SUCCEEDED]": 4.419216917000085, + "tests/aws/scenario/test_loan_broker.py::TestLoanBrokerScenario::test_stepfunctions_input_recipient_list[step_function_input1-SUCCEEDED]": 2.327644861000067, + "tests/aws/scenario/test_loan_broker.py::TestLoanBrokerScenario::test_stepfunctions_input_recipient_list[step_function_input2-FAILED]": 2.324511502000064, + "tests/aws/scenario/test_loan_broker.py::TestLoanBrokerScenario::test_stepfunctions_input_recipient_list[step_function_input3-FAILED]": 0.2755351769999379, + "tests/aws/scenario/test_loan_broker.py::TestLoanBrokerScenario::test_stepfunctions_input_recipient_list[step_function_input4-FAILED]": 23.278776194999978, + "tests/aws/scenario/test_note_taking.py::TestNoteTakingScenario::test_another_scenario": 1.8476102649999575, + "tests/aws/scenario/test_note_taking.py::TestNoteTakingScenario::test_notes_rest_api": 14.547484846000202, + "tests/aws/services/acm/test_acm.py::TestACM::test_boto_wait_for_certificate_validation": 1.0882955969999557, + "tests/aws/services/acm/test_acm.py::TestACM::test_certificate_for_subdomain_wildcard": 2.1542295859999285, + "tests/aws/services/acm/test_acm.py::TestACM::test_create_certificate_for_multiple_alternative_domains": 10.444221939000045, + "tests/aws/services/acm/test_acm.py::TestACM::test_domain_validation": 0.15973235199987812, + "tests/aws/services/acm/test_acm.py::TestACM::test_import_certificate": 1.4062014740001132, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_authorizer_crud_no_api": 0.017839015999925323, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_create_proxy_resource": 0.09292995099986001, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_create_proxy_resource_validation": 0.06349232799971105, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_create_resource_parent_invalid": 0.024362833999930444, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_create_rest_api_with_custom_id_tag": 0.0164405920002082, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_create_rest_api_with_optional_params": 0.05727091800008566, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_create_rest_api_with_tags": 0.03283188299997164, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_delete_resource": 0.055564686999559854, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_doc_arts_crud_no_api": 0.01896513699989555, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_get_api_case_insensitive": 0.02727518400001827, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_invoke_test_method": 0.19699409999998352, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_list_and_delete_apis": 0.05293283999958476, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_method_lifecycle": 0.0599874639999598, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_method_request_parameters": 0.04571901200006323, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_model_lifecycle": 0.051385814000241226, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_model_validation": 0.08115424200013877, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_put_method_model": 0.21398750299999847, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_put_method_validation": 0.07289629599995351, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_resource_lifecycle": 0.07946371200000613, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_method": 0.06820091200006573, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_method_validation": 0.11371022000002995, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_model": 0.06057377600018299, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_resource_behaviour": 0.11856860699981553, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_rest_api_behaviour": 0.04213464899999053, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_rest_api_compression": 0.05709281399981592, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_rest_api_invalid_api_id": 0.009617071000093347, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_update_rest_api_operation_add_remove": 0.0453762059996734, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApi::test_validators_crud_no_api": 0.01844572799996058, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiAuthorizer::test_authorizer_crud_no_api": 0.015552984999999353, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_doc_parts_crud_no_api": 0.015011910999987776, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_documentation_part_lifecycle": 0.032626671999878454, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_import_documentation_parts": 0.060850981999919895, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_create_documentation_part_operations": 0.022513140999990355, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_delete_documentation_part": 0.03058625900007428, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_get_documentation_part": 0.024563482999951702, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_get_documentation_parts": 0.005248416000085854, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_invalid_update_documentation_part": 0.027922511000042505, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_method_lifecycle": 0.05372603999990133, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_method_request_parameters": 0.03180001500004437, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_put_method_model": 0.1210676749999493, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_put_method_validation": 0.03346751500009759, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_update_method": 0.03715287099987563, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiMethod::test_update_method_validation": 0.059581382000033045, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiModels::test_model_lifecycle": 0.037310096999931375, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiModels::test_model_validation": 0.04221153499986485, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiModels::test_update_model": 0.032643934999896373, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_create_request_validator_invalid_api_id": 0.005269394000038119, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_invalid_delete_request_validator": 0.025161635000017668, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_invalid_get_request_validator": 0.024329168000008394, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_invalid_get_request_validators": 0.00546706800002994, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_invalid_update_request_validator_operations": 0.031093233999968106, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_request_validator_lifecycle": 0.043515403000014885, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRequestValidator::test_validators_crud_no_api": 0.014511081000136983, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_create_proxy_resource": 0.09102838500007238, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_create_proxy_resource_validation": 0.05447047499990276, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_create_resource_parent_invalid": 0.01927647800005161, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_delete_resource": 0.04819079299988971, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_resource_lifecycle": 0.0816067869999415, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiResource::test_update_resource_behaviour": 0.08795525200002885, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_create_rest_api_with_custom_id_tag": 0.015405479999799354, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_create_rest_api_with_optional_params": 0.05021143400006167, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_create_rest_api_with_tags": 0.029080995999947845, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_get_api_case_insensitive": 0.021435898000049747, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_list_and_delete_apis": 0.06403264000005038, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_update_rest_api_behaviour": 0.029366944000116746, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_update_rest_api_compression": 0.052802527000039845, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_update_rest_api_invalid_api_id": 0.005112986999961322, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayApiRestApi::test_update_rest_api_operation_add_remove": 0.024917063000089, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayGatewayResponse::test_gateway_response_crud": 0.04856824499995582, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayGatewayResponse::test_gateway_response_validation": 0.04530543399994258, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApiGatewayGatewayResponse::test_update_gateway_response": 0.051333197000076325, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApigatewayIntegration::test_put_integration_wrong_type": 0.02790455899992139, + "tests/aws/services/apigateway/test_apigateway_api.py::TestApigatewayTestInvoke::test_invoke_test_method": 0.13886573699994642, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_account": 0.028746029999979328, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_gateway_authorizer_crud": 0.01057647600009659, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_gateway_handle_domain_name": 0.16477081199991517, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_gateway_http_integration_with_path_request_parameter": 0.18831725900020047, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_gateway_lambda_asynchronous_invocation": 1.1387644660001115, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_gateway_lambda_integration": 1.7653641709998737, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_gateway_lambda_integration_aws_type": 7.789280462999955, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_gateway_lambda_proxy_integration": 2.20605153099973, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_gateway_lambda_proxy_integration[/lambda/foo1]": 1.9288370470001155, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_gateway_lambda_proxy_integration[/lambda/{test_param1}]": 1.9242172250000067, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_gateway_lambda_proxy_integration_any_method": 1.911459448000187, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_gateway_lambda_proxy_integration_any_method_with_path_param": 1.9456654570000183, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_gateway_lambda_proxy_integration_with_is_base_64_encoded": 1.8451842360000228, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_gateway_lambda_proxy_integration_with_path_param": 2.1981000530001893, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_gateway_mock_integration": 0.09164269099994726, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_gateway_update_resource_path_part": 0.04832171800001106, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_api_mock_integration_response_params": 0.06921206599997731, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_apigateway_with_custom_authorization_method": 1.1330843449999293, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_apigateway_with_step_function_integration[DeleteStateMachine]": 1.2194697760000963, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_apigateway_with_step_function_integration[StartExecution]": 1.2653954130000784, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_apigw_stage_variables[dev]": 1.6078426270000818, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_apigw_stage_variables[local]": 1.6192630530000542, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_apigw_test_invoke_method_api": 2.6495798149999246, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_base_path_mapping": 0.1514292240000259, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_base_path_mapping_root": 0.14274339599978703, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_create_rest_api_with_custom_id[host_based_url]": 0.2847100210000235, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_create_rest_api_with_custom_id[path_based_url]": 0.05505662400003075, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_delete_rest_api_with_invalid_id": 0.007826248000014857, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_invoke_endpoint_cors_headers[http://allowed-False-False]": 0.07580905099985102, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_invoke_endpoint_cors_headers[http://allowed-False-True]": 0.14216403499995067, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_invoke_endpoint_cors_headers[http://allowed-False-UrlType.HOST_BASED]": 0.1054383370000096, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_invoke_endpoint_cors_headers[http://allowed-False-UrlType.PATH_BASED]": 0.06623891200001708, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_invoke_endpoint_cors_headers[http://allowed-True-False]": 0.20054907600024308, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_invoke_endpoint_cors_headers[http://allowed-True-True]": 0.15805921899982422, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_invoke_endpoint_cors_headers[http://allowed-True-UrlType.HOST_BASED]": 0.12724949599987667, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_invoke_endpoint_cors_headers[http://allowed-True-UrlType.PATH_BASED]": 0.08312600900012512, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_invoke_endpoint_cors_headers[http://denied-False-False]": 0.07295160000012402, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_invoke_endpoint_cors_headers[http://denied-False-True]": 0.08347618900006637, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_invoke_endpoint_cors_headers[http://denied-False-UrlType.HOST_BASED]": 0.1674566030000051, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_invoke_endpoint_cors_headers[http://denied-False-UrlType.PATH_BASED]": 0.06673912899998413, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_invoke_endpoint_cors_headers[http://denied-True-False]": 0.06763440600002468, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_invoke_endpoint_cors_headers[http://denied-True-True]": 0.0707036619999144, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_invoke_endpoint_cors_headers[http://denied-True-UrlType.HOST_BASED]": 0.10175011100011488, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_invoke_endpoint_cors_headers[http://denied-True-UrlType.PATH_BASED]": 0.05285534800009373, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_malformed_response_apigw_invocation": 1.7051776420000806, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_multiple_api_keys_validate": 0.4676744719999988, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_put_integration_dynamodb_proxy_validation_with_request_template": 0.19004143600011503, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_put_integration_dynamodb_proxy_validation_without_request_template": 0.1390020909999521, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_response_headers_invocation_with_apigw": 1.7015254070000765, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestAPIGateway::test_update_rest_api_deployment": 0.05425919900005738, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_api_gateway_http_integrations[custom]": 0.26726688400003695, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_api_gateway_http_integrations[proxy]": 0.27454196800010777, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_api_gateway_kinesis_integration": 0.8966703690000486, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_api_gateway_s3_get_integration": 0.2596500540001898, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_api_gateway_sqs_integration_with_event_source": 1.4259167239999897, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_mock_integration_response[NEVER-host_based_url-GET]": 0.1290358629998991, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_mock_integration_response[NEVER-host_based_url-POST]": 0.1097737430000052, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_mock_integration_response[NEVER-path_based_url-GET]": 0.07618333400000665, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_mock_integration_response[NEVER-path_based_url-POST]": 0.07625801300002877, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_mock_integration_response[WHEN_NO_MATCH-host_based_url-GET]": 0.12478555600011987, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_mock_integration_response[WHEN_NO_MATCH-host_based_url-POST]": 0.1284490769999138, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_mock_integration_response[WHEN_NO_MATCH-path_based_url-GET]": 0.07485073400005149, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_mock_integration_response[WHEN_NO_MATCH-path_based_url-POST]": 0.07311385900004552, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_mock_integration_response[WHEN_NO_TEMPLATES-host_based_url-GET]": 0.12042057100006787, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_mock_integration_response[WHEN_NO_TEMPLATES-host_based_url-POST]": 0.11225486699993326, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_mock_integration_response[WHEN_NO_TEMPLATES-path_based_url-GET]": 0.08358714699988923, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestIntegrations::test_mock_integration_response[WHEN_NO_TEMPLATES-path_based_url-POST]": 0.08263438799997402, + "tests/aws/services/apigateway/test_apigateway_basic.py::TestTagging::test_tag_api": 0.04764917299996796, + "tests/aws/services/apigateway/test_apigateway_basic.py::test_apigateway_rust_lambda": 3.6540429550000226, + "tests/aws/services/apigateway/test_apigateway_basic.py::test_apigw_call_api_with_aws_endpoint_url": 0.010107716999982586, + "tests/aws/services/apigateway/test_apigateway_basic.py::test_rest_api_multi_region[host_based_url-ANY]": 2.3970648310001934, + "tests/aws/services/apigateway/test_apigateway_basic.py::test_rest_api_multi_region[host_based_url-GET]": 2.372463762000052, + "tests/aws/services/apigateway/test_apigateway_basic.py::test_rest_api_multi_region[path_based_url-ANY]": 2.350869143999944, + "tests/aws/services/apigateway/test_apigateway_basic.py::test_rest_api_multi_region[path_based_url-GET]": 2.3987281700001404, + "tests/aws/services/apigateway/test_apigateway_common.py::TestApiGatewayCommon::test_api_gateway_request_validator": 2.064513439000052, + "tests/aws/services/apigateway/test_apigateway_common.py::TestApiGatewayCommon::test_integration_request_parameters_mapping": 0.21117918399988866, + "tests/aws/services/apigateway/test_apigateway_common.py::TestApigatewayRouting::test_proxy_routing_with_hardcoded_resource_sibling": 0.2601159229999439, + "tests/aws/services/apigateway/test_apigateway_common.py::TestApigatewayRouting::test_routing_with_hardcoded_resource_sibling_order": 0.2451097809998828, + "tests/aws/services/apigateway/test_apigateway_common.py::TestDeployments::test_create_delete_deployments[False]": 0.22861270600003536, + "tests/aws/services/apigateway/test_apigateway_common.py::TestDeployments::test_create_delete_deployments[True]": 0.2733888490000709, + "tests/aws/services/apigateway/test_apigateway_common.py::TestDeployments::test_create_update_deployments": 0.17365979300018353, + "tests/aws/services/apigateway/test_apigateway_common.py::TestDocumentations::test_documentation_parts_and_versions": 0.050296527000000424, + "tests/aws/services/apigateway/test_apigateway_common.py::TestStages::test_create_update_stages": 0.1734378780000725, + "tests/aws/services/apigateway/test_apigateway_common.py::TestStages::test_update_stage_remove_wildcard": 0.1561528240000598, + "tests/aws/services/apigateway/test_apigateway_common.py::TestUsagePlans::test_api_key_required_for_methods": 0.28274646299985307, + "tests/aws/services/apigateway/test_apigateway_common.py::TestUsagePlans::test_usage_plan_crud": 0.13096129500002007, + "tests/aws/services/apigateway/test_apigateway_dynamodb.py::test_error_aws_proxy_not_supported": 0.10981122600003346, + "tests/aws/services/apigateway/test_apigateway_dynamodb.py::test_rest_api_to_dynamodb_integration[PutItem]": 0.31946585999992294, + "tests/aws/services/apigateway/test_apigateway_dynamodb.py::test_rest_api_to_dynamodb_integration[Query]": 0.4405786030000627, + "tests/aws/services/apigateway/test_apigateway_dynamodb.py::test_rest_api_to_dynamodb_integration[Scan]": 0.335137895999992, + "tests/aws/services/apigateway/test_apigateway_eventbridge.py::test_apigateway_to_eventbridge": 1.1331456859999207, + "tests/aws/services/apigateway/test_apigateway_extended.py::test_create_domain_names": 0.015976731999785443, + "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_oas30_openapi": 0.05029930199998489, + "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_oas30_openapi[TEST_IMPORT_PETSTORE_SWAGGER]": 0.1444799549999516, + "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_oas30_openapi[TEST_IMPORT_PETS]": 0.11109835499996734, + "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_swagger_openapi": 0.051547723999874506, + "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_swagger_openapi[TEST_IMPORT_PETSTORE_SWAGGER]": 0.1413389830000824, + "tests/aws/services/apigateway/test_apigateway_extended.py::test_export_swagger_openapi[TEST_IMPORT_PETS]": 0.11011755999982142, + "tests/aws/services/apigateway/test_apigateway_extended.py::test_get_api_keys": 0.10714882399997805, + "tests/aws/services/apigateway/test_apigateway_extended.py::test_get_domain_name": 0.015215743999988263, + "tests/aws/services/apigateway/test_apigateway_extended.py::test_get_domain_names": 0.017139621999831434, + "tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_invoke_status_code_passthrough[HTTP]": 1.7585836150000205, + "tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_invoke_status_code_passthrough[HTTP_PROXY]": 1.6826421820001087, + "tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_with_lambda[HTTP]": 1.9856118930000548, + "tests/aws/services/apigateway/test_apigateway_http.py::test_http_integration_with_lambda[HTTP_PROXY]": 2.299757697000132, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_and_validate_rest_api[openapi.spec.tf.json]": 0.16078215700008514, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_and_validate_rest_api[swagger-mock-cors.json]": 0.20848865800007843, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_api": 0.05208866699990722, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_api_with_base_path_oas30[ignore]": 0.44865060100005394, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_api_with_base_path_oas30[prepend]": 0.45062892900000406, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_api_with_base_path_oas30[split]": 0.45744468200007304, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_apis_with_base_path_swagger[ignore]": 0.24714642699996148, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_apis_with_base_path_swagger[prepend]": 0.2617690929998844, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_rest_apis_with_base_path_swagger[split]": 0.25305435799998577, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_swagger_api": 0.40785024600006636, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_circular_models": 0.13269966899997598, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_circular_models_and_request_validation": 0.26267862899987904, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_global_api_key_authorizer": 0.13369590000002063, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_http_method_integration": 0.13878552400001354, + "tests/aws/services/apigateway/test_apigateway_import.py::TestApiGatewayImportRestApi::test_import_with_stage_variables": 0.11509622799997032, + "tests/aws/services/apigateway/test_apigateway_integrations.py::test_create_execute_api_vpc_endpoint": 5.28207968799984, + "tests/aws/services/apigateway/test_apigateway_integrations.py::test_http_integration": 0.08096929799989994, + "tests/aws/services/apigateway/test_apigateway_integrations.py::test_http_integration_status_code_selection": 0.1790502149998474, + "tests/aws/services/apigateway/test_apigateway_integrations.py::test_put_integration_response_with_response_template": 0.05690440700016097, + "tests/aws/services/apigateway/test_apigateway_integrations.py::test_put_integration_responses": 0.19574820399998316, + "tests/aws/services/apigateway/test_apigateway_integrations.py::test_put_integration_validation": 0.13044808200004354, + "tests/aws/services/apigateway/test_apigateway_kinesis.py::test_apigateway_to_kinesis": 0.9094357159999618, + "tests/aws/services/apigateway/test_apigateway_lambda.py::test_lambda_aws_integration": 1.6488009559999455, + "tests/aws/services/apigateway/test_apigateway_lambda.py::test_lambda_aws_integration_response_with_mapping_templates": 1.7460941509999657, + "tests/aws/services/apigateway/test_apigateway_lambda.py::test_lambda_aws_integration_with_request_template": 1.7123387820000744, + "tests/aws/services/apigateway/test_apigateway_lambda.py::test_lambda_aws_proxy_integration": 3.7413921449999634, + "tests/aws/services/apigateway/test_apigateway_lambda.py::test_lambda_aws_proxy_integration_non_post_method": 1.2119762100001026, + "tests/aws/services/apigateway/test_apigateway_lambda.py::test_lambda_aws_proxy_response_format": 1.8136356610000348, + "tests/aws/services/apigateway/test_apigateway_lambda.py::test_lambda_selection_patterns": 1.7913833320000094, + "tests/aws/services/apigateway/test_apigateway_lambda_cfn.py::TestApigatewayLambdaIntegration::test_scenario_validate_infra": 7.277244286999917, + "tests/aws/services/apigateway/test_apigateway_sqs.py::test_api_gateway_sqs_integration": 0.15575299100009943, + "tests/aws/services/apigateway/test_apigateway_sqs.py::test_sqs_aws_integration": 0.19086660899995422, + "tests/aws/services/apigateway/test_apigateway_sqs.py::test_sqs_request_and_response_xml_templates_integration": 0.2621440129998973, + "tests/aws/services/cloudcontrol/test_cloudcontrol_api.py::TestCloudControlResourceApi::test_api_exceptions": 0.0008589179999489716, + "tests/aws/services/cloudcontrol/test_cloudcontrol_api.py::TestCloudControlResourceApi::test_create_exceptions": 0.0008542990000250938, + "tests/aws/services/cloudcontrol/test_cloudcontrol_api.py::TestCloudControlResourceApi::test_create_invalid_desiredstate": 0.0008663019999630706, + "tests/aws/services/cloudcontrol/test_cloudcontrol_api.py::TestCloudControlResourceApi::test_double_create_with_client_token": 0.0008408040000631445, + "tests/aws/services/cloudcontrol/test_cloudcontrol_api.py::TestCloudControlResourceApi::test_lifecycle": 0.0009519420000287937, + "tests/aws/services/cloudcontrol/test_cloudcontrol_api.py::TestCloudControlResourceApi::test_list_resources": 0.0008551509999961127, + "tests/aws/services/cloudcontrol/test_cloudcontrol_api.py::TestCloudControlResourceApi::test_list_resources_with_resource_model": 0.0008406160001186436, + "tests/aws/services/cloudcontrol/test_cloudcontrol_api.py::TestCloudControlResourceApi::test_update": 0.0010535709999430765, + "tests/aws/services/cloudcontrol/test_cloudcontrol_api.py::TestCloudControlResourceRequestApi::test_cancel_edge_cases[FAIL]": 0.0008463039999924149, + "tests/aws/services/cloudcontrol/test_cloudcontrol_api.py::TestCloudControlResourceRequestApi::test_cancel_edge_cases[SUCCESS]": 0.0008724740000616293, + "tests/aws/services/cloudcontrol/test_cloudcontrol_api.py::TestCloudControlResourceRequestApi::test_cancel_request": 0.0008815500000309839, + "tests/aws/services/cloudcontrol/test_cloudcontrol_api.py::TestCloudControlResourceRequestApi::test_get_request_status": 0.0008541890000515195, + "tests/aws/services/cloudcontrol/test_cloudcontrol_api.py::TestCloudControlResourceRequestApi::test_invalid_request_token_exc": 0.0008518460000459527, + "tests/aws/services/cloudcontrol/test_cloudcontrol_api.py::TestCloudControlResourceRequestApi::test_list_request_status": 0.0009096030000819155, + "tests/aws/services/cloudformation/api/test_changesets.py::test_autoexpand_capability_requirement": 0.022886836000111543, + "tests/aws/services/cloudformation/api/test_changesets.py::test_create_and_then_remove_non_supported_resource_change_set": 4.095288434000054, + "tests/aws/services/cloudformation/api/test_changesets.py::test_create_and_then_remove_supported_resource_change_set": 4.071890856999971, + "tests/aws/services/cloudformation/api/test_changesets.py::test_create_and_then_update_refreshes_template_metadata": 2.055669013999932, + "tests/aws/services/cloudformation/api/test_changesets.py::test_create_change_set_create_existing": 0.0008109290000675173, + "tests/aws/services/cloudformation/api/test_changesets.py::test_create_change_set_import": 0.0008479159998842078, + "tests/aws/services/cloudformation/api/test_changesets.py::test_create_change_set_invalid_params": 0.004883116999849335, + "tests/aws/services/cloudformation/api/test_changesets.py::test_create_change_set_missing_stackname": 0.0014486370001804971, + "tests/aws/services/cloudformation/api/test_changesets.py::test_create_change_set_update_nonexisting": 0.00487939699985418, + "tests/aws/services/cloudformation/api/test_changesets.py::test_create_change_set_update_without_parameters": 1.0519029739999723, + "tests/aws/services/cloudformation/api/test_changesets.py::test_create_change_set_with_ssm_parameter": 1.060777413999972, + "tests/aws/services/cloudformation/api/test_changesets.py::test_create_change_set_with_template_url": 0.000886216000026252, + "tests/aws/services/cloudformation/api/test_changesets.py::test_create_change_set_without_parameters": 0.03118065099999967, + "tests/aws/services/cloudformation/api/test_changesets.py::test_create_changeset_with_stack_id": 0.08483331599995836, + "tests/aws/services/cloudformation/api/test_changesets.py::test_create_while_in_review": 0.0008625049999864132, + "tests/aws/services/cloudformation/api/test_changesets.py::test_delete_change_set_exception": 0.007791688999986945, + "tests/aws/services/cloudformation/api/test_changesets.py::test_deleted_changeset": 0.017420399999991787, + "tests/aws/services/cloudformation/api/test_changesets.py::test_describe_change_set_nonexisting": 0.0045824309999034085, + "tests/aws/services/cloudformation/api/test_changesets.py::test_describe_change_set_with_similarly_named_stacks": 0.018268898999963312, + "tests/aws/services/cloudformation/api/test_changesets.py::test_empty_changeset": 1.116914672999883, + "tests/aws/services/cloudformation/api/test_changesets.py::test_execute_change_set": 0.0008563640000147643, + "tests/aws/services/cloudformation/api/test_changesets.py::test_multiple_create_changeset": 1.0087758399999984, + "tests/aws/services/cloudformation/api/test_changesets.py::test_name_conflicts": 1.3043874419998929, + "tests/aws/services/cloudformation/api/test_drift_detection.py::test_drift_detection_on_lambda": 0.0008690280000109851, + "tests/aws/services/cloudformation/api/test_extensions_api.py::TestExtensionsApi::test_crud_extension[HOOK-LocalStack::Testing::TestHook-hooks/localstack-testing-testhook.zip]": 0.0008870909999814103, + "tests/aws/services/cloudformation/api/test_extensions_api.py::TestExtensionsApi::test_crud_extension[MODULE-LocalStack::Testing::TestModule::MODULE-modules/localstack-testing-testmodule-module.zip]": 0.0008950460000960447, + "tests/aws/services/cloudformation/api/test_extensions_api.py::TestExtensionsApi::test_crud_extension[RESOURCE-LocalStack::Testing::TestResource-resourcetypes/localstack-testing-testresource.zip]": 0.0008578060001127596, + "tests/aws/services/cloudformation/api/test_extensions_api.py::TestExtensionsApi::test_extension_not_complete": 0.0008640579999337206, + "tests/aws/services/cloudformation/api/test_extensions_api.py::TestExtensionsApi::test_extension_type_configuration": 0.0009892499999750726, + "tests/aws/services/cloudformation/api/test_extensions_api.py::TestExtensionsApi::test_extension_versioning": 0.0008790150000095309, + "tests/aws/services/cloudformation/api/test_extensions_hooks.py::TestExtensionsHooks::test_hook_deployment[FAIL]": 0.0009878379999008757, + "tests/aws/services/cloudformation/api/test_extensions_hooks.py::TestExtensionsHooks::test_hook_deployment[WARN]": 0.0009966550001081487, + "tests/aws/services/cloudformation/api/test_extensions_modules.py::TestExtensionsModules::test_module_usage": 0.001038892999986274, + "tests/aws/services/cloudformation/api/test_extensions_resourcetypes.py::TestExtensionsResourceTypes::test_deploy_resource_type": 0.0009939690000919654, + "tests/aws/services/cloudformation/api/test_get_template_summary.py::test_get_template_summary": 1.1569098149998354, + "tests/aws/services/cloudformation/api/test_nested_stacks.py::test_lifecycle_nested_stack": 0.0008000889999948413, + "tests/aws/services/cloudformation/api/test_nested_stacks.py::test_nested_output_in_params": 12.201400118000038, + "tests/aws/services/cloudformation/api/test_nested_stacks.py::test_nested_stack": 6.0785334440000724, + "tests/aws/services/cloudformation/api/test_nested_stacks.py::test_nested_stack_output_refs": 6.0766301610000255, + "tests/aws/services/cloudformation/api/test_nested_stacks.py::test_nested_stacks_conditions": 6.080782344000113, + "tests/aws/services/cloudformation/api/test_nested_stacks.py::test_nested_with_nested_stack": 0.0008736450000697005, + "tests/aws/services/cloudformation/api/test_reference_resolving.py::test_nested_getatt_ref[TopicArn]": 2.038456786000097, + "tests/aws/services/cloudformation/api/test_reference_resolving.py::test_nested_getatt_ref[TopicName]": 2.0393732190000264, + "tests/aws/services/cloudformation/api/test_reference_resolving.py::test_sub_resolving": 2.0379964479999444, + "tests/aws/services/cloudformation/api/test_reference_resolving.py::test_unexisting_resource_dependency": 2.038418270999955, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_create_stack_with_policy": 0.0008216889999630439, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_different_action_attribute": 0.0008291230000168071, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_different_principal_attribute": 0.000987670999961665, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_empty_policy": 0.0008799879999514815, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_not_json_policy": 0.001191086000062569, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_policy_during_update": 0.0008205069999576153, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_policy_lifecycle": 0.0009260940000785922, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_prevent_deletion[resource0]": 0.0008304450000196084, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_prevent_deletion[resource1]": 0.0008535589998928117, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_prevent_modifying_with_policy_specifying_resource_id": 0.0008359049999171475, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_prevent_replacement": 0.0008233610000161207, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_prevent_resource_deletion": 0.0008329099999855316, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_prevent_stack_update": 0.0008448520000001736, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_prevent_update[AWS::S3::Bucket]": 0.0008327289999670029, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_prevent_update[AWS::SNS::Topic]": 0.0008516139999983352, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_set_empty_policy_with_url": 0.0008819610001182809, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_set_invalid_policy_with_url": 0.0008708809999689038, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_set_policy_both_policy_and_url": 0.0008885449999525008, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_set_policy_with_update_operation": 0.0008266780000667495, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_set_policy_with_url": 0.0008726029999479579, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_update_with_empty_policy": 0.0008254059999899255, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_update_with_overlapping_policies[False]": 0.0008583169999383244, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_update_with_overlapping_policies[True]": 0.0008186329999944064, + "tests/aws/services/cloudformation/api/test_stack_policies.py::TestStackPolicy::test_update_with_policy": 0.0010286529999348204, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_failure_options_for_stack_creation[False-0]": 0.0008924299999080176, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_failure_options_for_stack_creation[True-1]": 0.000886639999862382, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_failure_options_for_stack_update[False-2]": 0.0007775770000080229, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_failure_options_for_stack_update[True-1]": 0.0008162789999914821, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_get_template[json]": 2.0470047640000075, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_get_template[yaml]": 2.052301395000086, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_list_events_after_deployment": 2.0632266860000072, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_list_stack_resources_for_removed_resource": 4.076876359999915, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_stack_description_special_chars": 2.0514886629999864, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_stack_lifecycle": 4.137881199000049, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_stack_name_creation": 0.02836366100007126, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_stack_update_resources": 4.179978384000151, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_update_stack_actual_update": 4.070941078000033, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_update_stack_with_same_template_withoutchange": 2.0385548200000585, + "tests/aws/services/cloudformation/api/test_stacks.py::TestStacksApi::test_update_stack_with_same_template_withoutchange_transformation": 2.111747391999984, + "tests/aws/services/cloudformation/api/test_stacks.py::test_blocked_stack_deletion": 0.0009056250000867294, + "tests/aws/services/cloudformation/api/test_stacks.py::test_describe_stack_events_errors": 0.007795556000019133, + "tests/aws/services/cloudformation/api/test_stacks.py::test_events_resource_types": 2.07527051999989, + "tests/aws/services/cloudformation/api/test_stacks.py::test_linting_error_during_creation": 0.0008339109999724315, + "tests/aws/services/cloudformation/api/test_stacks.py::test_list_parameter_type": 2.039570433999984, + "tests/aws/services/cloudformation/api/test_stacks.py::test_name_conflicts": 2.1395686490000116, + "tests/aws/services/cloudformation/api/test_stacks.py::test_notifications": 0.0008177709999017679, + "tests/aws/services/cloudformation/api/test_stacks.py::test_prevent_resource_deletion": 0.0007445139999617822, + "tests/aws/services/cloudformation/api/test_stacks.py::test_prevent_stack_update": 0.0007677139997213089, + "tests/aws/services/cloudformation/api/test_stacks.py::test_update_termination_protection": 2.0534433939999417, + "tests/aws/services/cloudformation/api/test_stacks.py::test_updating_an_updated_stack_sets_status": 6.139521631999855, + "tests/aws/services/cloudformation/api/test_templates.py::test_create_stack_from_s3_template_url[http_host]": 1.0578399470000477, + "tests/aws/services/cloudformation/api/test_templates.py::test_create_stack_from_s3_template_url[http_invalid]": 0.03766624199988655, + "tests/aws/services/cloudformation/api/test_templates.py::test_create_stack_from_s3_template_url[http_path]": 1.04746612800011, + "tests/aws/services/cloudformation/api/test_templates.py::test_create_stack_from_s3_template_url[s3_url]": 0.026073138000015206, + "tests/aws/services/cloudformation/api/test_templates.py::test_get_template_summary": 2.062673459999928, + "tests/aws/services/cloudformation/api/test_transformers.py::test_duplicate_resources": 2.2076581360000773, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_basic_update": 3.0504331810001304, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_diff_after_update": 3.0619468159999315, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_no_parameters_update": 3.048812785999985, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_no_template_error": 0.000841195000020889, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_set_notification_arn_with_update": 0.0008370080000759117, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_update_tags": 0.0008366969999542562, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_update_using_template_url": 3.0725789060001034, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_update_with_capabilities[capability0]": 0.0008490989999927478, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_update_with_capabilities[capability1]": 0.0008403840000710261, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_update_with_invalid_rollback_configuration_errors": 0.0008847769998965305, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_update_with_previous_parameter_value": 3.0485700059998635, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_update_with_previous_template": 0.0009071370000128809, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_update_with_resource_types": 0.0008729540001013447, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_update_with_role_without_permissions": 0.000918217999924309, + "tests/aws/services/cloudformation/api/test_update_stack.py::test_update_with_rollback_configuration": 0.0008666219999895475, + "tests/aws/services/cloudformation/engine/test_attributes.py::TestResourceAttributes::test_dependency_on_attribute_with_dot_notation": 2.0426174279999714, + "tests/aws/services/cloudformation/engine/test_attributes.py::TestResourceAttributes::test_invalid_getatt_fails": 0.0007962320000842737, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_condition_on_outputs": 2.0403541810001116, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_conditional_att_to_conditional_resources[create]": 2.0492531950000057, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_conditional_att_to_conditional_resources[no-create]": 2.045514876999846, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_conditional_in_conditional[dev-us-west-2]": 2.0373811450000403, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_conditional_in_conditional[production-us-east-1]": 2.038264306999963, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_conditional_with_select": 2.0424386709999, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_dependency_in_non_evaluated_if_branch[None-FallbackParamValue]": 2.0522272939999766, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_dependency_in_non_evaluated_if_branch[false-DefaultParamValue]": 2.0471297850000383, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_dependency_in_non_evaluated_if_branch[true-FallbackParamValue]": 2.0469157419998965, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_dependent_ref": 0.0009178679998740336, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_dependent_ref_intrinsic_fn_condition": 0.0008406840000816374, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_dependent_ref_with_macro": 0.0008521250000512737, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_nested_conditions[prod-bucket-policy]": 0.0008696480000480733, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_nested_conditions[prod-nobucket-nopolicy]": 0.001775813999984166, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_nested_conditions[test-bucket-nopolicy]": 0.0008309660000804797, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_nested_conditions[test-nobucket-nopolicy]": 0.0008305249999693842, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_output_reference_to_skipped_resource": 0.0008888840001191056, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_simple_condition_evaluation_deploys_resource": 2.0396888930000614, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_simple_condition_evaluation_doesnt_deploy_resource": 0.030826933999833273, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_simple_intrinsic_fn_condition_evaluation[nope]": 2.0354308170000195, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_simple_intrinsic_fn_condition_evaluation[yep]": 2.0340910529999974, + "tests/aws/services/cloudformation/engine/test_conditions.py::TestCloudFormationConditions::test_sub_in_conditions": 2.0461107610000226, + "tests/aws/services/cloudformation/engine/test_mappings.py::TestCloudFormationMappings::test_mapping_maximum_nesting_depth": 0.001083856000036576, + "tests/aws/services/cloudformation/engine/test_mappings.py::TestCloudFormationMappings::test_mapping_minimum_nesting_depth": 0.0008428779999576363, + "tests/aws/services/cloudformation/engine/test_mappings.py::TestCloudFormationMappings::test_mapping_with_invalid_refs": 0.0008421169999337508, + "tests/aws/services/cloudformation/engine/test_mappings.py::TestCloudFormationMappings::test_mapping_with_nonexisting_key": 0.0009558919999790305, + "tests/aws/services/cloudformation/engine/test_mappings.py::TestCloudFormationMappings::test_simple_mapping_working": 2.042054396000026, + "tests/aws/services/cloudformation/engine/test_references.py::TestDependsOn::test_depends_on_with_missing_reference": 0.0008533390000593499, + "tests/aws/services/cloudformation/engine/test_references.py::TestFnSub::test_fn_sub_cases": 2.0442270969999754, + "tests/aws/services/cloudformation/engine/test_references.py::test_useful_error_when_invalid_ref": 2.0336845179999727, + "tests/aws/services/cloudformation/resource_providers/ec2/aws_ec2_networkacl/test_basic.py::TestBasicCRD::test_black_box": 4.1945004010000275, + "tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.py::test_deploy_instance_with_key_pair": 4.135233243000016, + "tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.py::test_deploy_prefix_list": 7.076050901999906, + "tests/aws/services/cloudformation/resource_providers/ec2/test_ec2.py::test_deploy_vpc_endpoint": 2.1916250049999917, + "tests/aws/services/cloudformation/resource_providers/iam/aws_iam_user/test_basic.py::TestBasicCRD::test_autogenerated_values": 2.0429863599999862, + "tests/aws/services/cloudformation/resource_providers/iam/aws_iam_user/test_basic.py::TestBasicCRD::test_black_box": 4.05683628099996, + "tests/aws/services/cloudformation/resource_providers/iam/aws_iam_user/test_basic.py::TestBasicCRD::test_getatt": 4.061428042000102, + "tests/aws/services/cloudformation/resource_providers/iam/aws_iam_user/test_basic.py::TestUpdates::test_update_without_replacement": 0.0008376790000284018, + "tests/aws/services/cloudformation/resource_providers/iam/aws_iam_user/test_exploration.py::TestAttributeAccess::test_getatt[Arn]": 0.0008641080000870716, + "tests/aws/services/cloudformation/resource_providers/iam/aws_iam_user/test_exploration.py::TestAttributeAccess::test_getatt[Id]": 0.0008857679999891843, + "tests/aws/services/cloudformation/resource_providers/iam/aws_iam_user/test_exploration.py::TestAttributeAccess::test_getatt[Path]": 0.000894595999966441, + "tests/aws/services/cloudformation/resource_providers/iam/aws_iam_user/test_exploration.py::TestAttributeAccess::test_getatt[PermissionsBoundary]": 0.0008584769999515629, + "tests/aws/services/cloudformation/resource_providers/iam/aws_iam_user/test_exploration.py::TestAttributeAccess::test_getatt[UserName]": 0.0008986719999484194, + "tests/aws/services/cloudformation/resource_providers/iam/aws_iam_user/test_parity.py::TestParity::test_create_with_full_properties": 4.079032794, + "tests/aws/services/cloudformation/resource_providers/iam/test_iam.py::test_delete_role_detaches_role_policy": 4.085911298999804, + "tests/aws/services/cloudformation/resource_providers/iam/test_iam.py::test_iam_user_access_key": 4.08294174599996, + "tests/aws/services/cloudformation/resource_providers/iam/test_iam.py::test_iam_username_defaultname": 2.0668841460000067, + "tests/aws/services/cloudformation/resource_providers/iam/test_iam.py::test_managed_policy_with_empty_resource": 2.1687617200000204, + "tests/aws/services/cloudformation/resource_providers/iam/test_iam.py::test_policy_attachments": 2.1384754310000744, + "tests/aws/services/cloudformation/resource_providers/iam/test_iam.py::test_server_certificate": 4.101147690000062, + "tests/aws/services/cloudformation/resource_providers/iam/test_iam.py::test_update_inline_policy": 4.1189090319999195, + "tests/aws/services/cloudformation/resource_providers/opensearch/test_domain.py::TestAttributeAccess::test_getattr[Arn]": 0.0008292930000379783, + "tests/aws/services/cloudformation/resource_providers/opensearch/test_domain.py::TestAttributeAccess::test_getattr[DomainArn]": 0.0011233310000307029, + "tests/aws/services/cloudformation/resource_providers/opensearch/test_domain.py::TestAttributeAccess::test_getattr[DomainEndpoint]": 0.0008386110000628832, + "tests/aws/services/cloudformation/resource_providers/opensearch/test_domain.py::TestAttributeAccess::test_getattr[DomainName]": 0.0008056789999955072, + "tests/aws/services/cloudformation/resource_providers/opensearch/test_domain.py::TestAttributeAccess::test_getattr[EngineVersion]": 0.0008374779999940074, + "tests/aws/services/cloudformation/resource_providers/opensearch/test_domain.py::TestAttributeAccess::test_getattr[Id]": 0.0008014210001192623, + "tests/aws/services/cloudformation/resource_providers/scheduler/test_scheduler.py::test_schedule_and_group": 2.1927610039998626, + "tests/aws/services/cloudformation/resource_providers/ssm/test_parameter.py::TestBasicCRD::test_black_box": 0.0009289680000392764, + "tests/aws/services/cloudformation/resource_providers/ssm/test_parameter.py::TestUpdates::test_update_without_replacement": 0.0009296199999653254, + "tests/aws/services/cloudformation/resource_providers/ssm/test_parameter_getatt_exploration.py::TestAttributeAccess::test_getattr[AllowedPattern]": 0.0008597400000098787, + "tests/aws/services/cloudformation/resource_providers/ssm/test_parameter_getatt_exploration.py::TestAttributeAccess::test_getattr[DataType]": 0.0008859580000262213, + "tests/aws/services/cloudformation/resource_providers/ssm/test_parameter_getatt_exploration.py::TestAttributeAccess::test_getattr[Description]": 0.0008665530001508159, + "tests/aws/services/cloudformation/resource_providers/ssm/test_parameter_getatt_exploration.py::TestAttributeAccess::test_getattr[Id]": 0.0008476469998868197, + "tests/aws/services/cloudformation/resource_providers/ssm/test_parameter_getatt_exploration.py::TestAttributeAccess::test_getattr[Name]": 0.0008782950000068013, + "tests/aws/services/cloudformation/resource_providers/ssm/test_parameter_getatt_exploration.py::TestAttributeAccess::test_getattr[Policies]": 0.000871601999961058, + "tests/aws/services/cloudformation/resource_providers/ssm/test_parameter_getatt_exploration.py::TestAttributeAccess::test_getattr[Tier]": 0.0009143620001168529, + "tests/aws/services/cloudformation/resource_providers/ssm/test_parameter_getatt_exploration.py::TestAttributeAccess::test_getattr[Type]": 0.001084729000012885, + "tests/aws/services/cloudformation/resource_providers/ssm/test_parameter_getatt_exploration.py::TestAttributeAccess::test_getattr[Value]": 0.0011690549999912037, + "tests/aws/services/cloudformation/resources/test_acm.py::test_cfn_acm_certificate": 2.043282887000032, + "tests/aws/services/cloudformation/resources/test_apigateway.py::TestServerlessApigwLambda::test_serverless_like_deployment_with_update": 23.23299297099993, + "tests/aws/services/cloudformation/resources/test_apigateway.py::test_account": 4.0606275849999065, + "tests/aws/services/cloudformation/resources/test_apigateway.py::test_api_gateway_with_policy_as_dict": 2.04615392300002, + "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_apigateway_aws_integration": 2.130981512999938, + "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_apigateway_rest_api": 6.106160724000006, + "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_apigateway_swagger_import": 2.278363438000042, + "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_deploy_apigateway_from_s3_swagger": 2.144817229000182, + "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_deploy_apigateway_integration": 2.2674637840000287, + "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_deploy_apigateway_models": 2.339063706000047, + "tests/aws/services/cloudformation/resources/test_apigateway.py::test_cfn_with_apigateway_resources": 4.144108368999923, + "tests/aws/services/cloudformation/resources/test_apigateway.py::test_rest_api_serverless_ref_resolving": 9.356581727999924, + "tests/aws/services/cloudformation/resources/test_apigateway.py::test_update_usage_plan": 4.108621546999984, + "tests/aws/services/cloudformation/resources/test_apigateway.py::test_url_output": 2.072566396999946, + "tests/aws/services/cloudformation/resources/test_cdk.py::TestCdkInit::test_cdk_bootstrap[10]": 8.298278021000101, + "tests/aws/services/cloudformation/resources/test_cdk.py::TestCdkInit::test_cdk_bootstrap[11]": 8.298103130999948, + "tests/aws/services/cloudformation/resources/test_cdk.py::TestCdkInit::test_cdk_bootstrap[12]": 8.267776138000045, + "tests/aws/services/cloudformation/resources/test_cdk.py::TestCdkInit::test_cdk_bootstrap_redeploy": 16.374735407000117, + "tests/aws/services/cloudformation/resources/test_cdk.py::TestCdkInit::test_cdk_template": 18.689219933000004, + "tests/aws/services/cloudformation/resources/test_cdk.py::TestCdkSampleApp::test_cdk_sample": 2.168747770999971, + "tests/aws/services/cloudformation/resources/test_cloudformation.py::test_create_macro": 3.0851997419997588, + "tests/aws/services/cloudformation/resources/test_cloudformation.py::test_waitcondition": 2.071343509000144, + "tests/aws/services/cloudformation/resources/test_cloudwatch.py::test_alarm_creation": 2.040429009000036, + "tests/aws/services/cloudformation/resources/test_cloudwatch.py::test_alarm_ext_statistic": 4.07955783000034, + "tests/aws/services/cloudformation/resources/test_cloudwatch.py::test_composite_alarm_creation": 4.337754918999963, + "tests/aws/services/cloudformation/resources/test_dynamodb.py::test_billing_mode_as_conditional[PAY_PER_REQUEST]": 2.1842558620003274, + "tests/aws/services/cloudformation/resources/test_dynamodb.py::test_billing_mode_as_conditional[PROVISIONED]": 2.179177883000193, + "tests/aws/services/cloudformation/resources/test_dynamodb.py::test_default_name_for_table": 2.17226381699993, + "tests/aws/services/cloudformation/resources/test_dynamodb.py::test_deploy_stack_with_dynamodb_table": 4.091663974000085, + "tests/aws/services/cloudformation/resources/test_dynamodb.py::test_global_table": 4.201684791999924, + "tests/aws/services/cloudformation/resources/test_dynamodb.py::test_global_table_with_ttl_and_sse": 2.0728305030002048, + "tests/aws/services/cloudformation/resources/test_dynamodb.py::test_globalindex_read_write_provisioned_throughput_dynamodb_table": 2.0709549969999443, + "tests/aws/services/cloudformation/resources/test_dynamodb.py::test_table_with_ttl_and_sse": 2.0545863469999404, + "tests/aws/services/cloudformation/resources/test_dynamodb.py::test_ttl_cdk": 1.1046267249996617, + "tests/aws/services/cloudformation/resources/test_ec2.py::test_cfn_with_multiple_route_table_associations": 2.0802130480003598, + "tests/aws/services/cloudformation/resources/test_ec2.py::test_cfn_with_multiple_route_tables": 2.0973495659998207, + "tests/aws/services/cloudformation/resources/test_ec2.py::test_dhcp_options": 2.0941615029998957, + "tests/aws/services/cloudformation/resources/test_ec2.py::test_internet_gateway_ref_and_attr": 2.106323280000197, + "tests/aws/services/cloudformation/resources/test_ec2.py::test_simple_route_table_creation": 4.069790817000239, + "tests/aws/services/cloudformation/resources/test_ec2.py::test_simple_route_table_creation_without_vpc": 4.072942124999599, + "tests/aws/services/cloudformation/resources/test_ec2.py::test_transit_gateway_attachment": 4.208585577000122, + "tests/aws/services/cloudformation/resources/test_ec2.py::test_vpc_creates_default_sg": 2.1667227249999996, + "tests/aws/services/cloudformation/resources/test_elasticsearch.py::test_cfn_handle_elasticsearch_domain": 2.1667439789998753, + "tests/aws/services/cloudformation/resources/test_events.py::test_cfn_event_api_destination_resource": 14.148844732000043, + "tests/aws/services/cloudformation/resources/test_events.py::test_cfn_event_bus_resource": 4.055369927000129, + "tests/aws/services/cloudformation/resources/test_events.py::test_cfn_handle_events_rule": 4.065435637999826, + "tests/aws/services/cloudformation/resources/test_events.py::test_cfn_handle_events_rule_without_name": 4.058330060999651, + "tests/aws/services/cloudformation/resources/test_events.py::test_event_rule_creation_without_target": 2.0398805620000076, + "tests/aws/services/cloudformation/resources/test_events.py::test_event_rule_to_logs": 2.0813969189998716, + "tests/aws/services/cloudformation/resources/test_events.py::test_eventbus_policies": 4.079276295999989, + "tests/aws/services/cloudformation/resources/test_events.py::test_eventbus_policy_statement": 2.0404439299998103, + "tests/aws/services/cloudformation/resources/test_events.py::test_rule_properties": 2.0487073840001813, + "tests/aws/services/cloudformation/resources/test_firehose.py::test_firehose_stack_with_kinesis_as_source": 20.200513211000043, + "tests/aws/services/cloudformation/resources/test_integration.py::test_events_sqs_sns_lambda": 67.1667460120002, + "tests/aws/services/cloudformation/resources/test_kinesis.py::test_cfn_handle_kinesis_firehose_resources": 8.183276059000036, + "tests/aws/services/cloudformation/resources/test_kinesis.py::test_default_parameters_kinesis": 6.092997437000122, + "tests/aws/services/cloudformation/resources/test_kinesis.py::test_describe_template": 0.05032553899991399, + "tests/aws/services/cloudformation/resources/test_kinesis.py::test_dynamodb_stream_response_with_cf": 6.088011818999803, + "tests/aws/services/cloudformation/resources/test_kinesis.py::test_kinesis_stream_consumer_creations": 12.113410462000274, + "tests/aws/services/cloudformation/resources/test_kinesis.py::test_stream_creation": 6.12053161200015, + "tests/aws/services/cloudformation/resources/test_kms.py::test_cfn_with_kms_resources": 4.056452067999999, + "tests/aws/services/cloudformation/resources/test_kms.py::test_deploy_stack_with_kms": 4.048110594000036, + "tests/aws/services/cloudformation/resources/test_kms.py::test_kms_key_disabled": 2.0531200599998556, + "tests/aws/services/cloudformation/resources/test_lambda.py::TestCfnLambdaDestinations::test_generic_destination_routing[sqs-sqs]": 0.0008878810001533566, + "tests/aws/services/cloudformation/resources/test_lambda.py::TestCfnLambdaIntegrations::test_cfn_lambda_dynamodb_source": 11.804462380999894, + "tests/aws/services/cloudformation/resources/test_lambda.py::TestCfnLambdaIntegrations::test_cfn_lambda_kinesis_source": 15.666210306999801, + "tests/aws/services/cloudformation/resources/test_lambda.py::TestCfnLambdaIntegrations::test_cfn_lambda_permissions": 7.296413626999993, + "tests/aws/services/cloudformation/resources/test_lambda.py::TestCfnLambdaIntegrations::test_cfn_lambda_sqs_source": 10.746989835000022, + "tests/aws/services/cloudformation/resources/test_lambda.py::TestCfnLambdaIntegrations::test_lambda_dynamodb_event_filter": 9.176384392000045, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_cfn_function_url": 7.023365932000161, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_event_invoke_config": 6.105662381999991, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_alias": 12.198844445000077, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_cfn_dead_letter_config_async_invocation": 8.714698986999792, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_cfn_run": 6.526521894000325, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_code_signing_config": 2.1041911570000593, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_version": 6.599474241000053, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_vpc": 0.0010688080001273192, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_w_dynamodb_event_filter": 0.0009169100001145125, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_lambda_w_dynamodb_event_filter_update": 16.365778621000118, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_multiple_lambda_permissions_for_singlefn": 6.074341433999962, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_python_lambda_code_deployed_via_s3": 6.58813539699986, + "tests/aws/services/cloudformation/resources/test_lambda.py::test_update_lambda_permissions": 12.149241206999704, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_cfn_conditional_deployment": 2.041945154000132, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_cfn_handle_iam_role_resource": 3.1563289279999935, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_cfn_handle_iam_role_resource_no_role_name": 4.0542439509999895, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_cfn_handle_log_group_resource": 4.105201651000016, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_cfn_handle_s3_notification_configuration[False-us-east-1]": 4.058456043000206, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_cfn_handle_s3_notification_configuration[True-eu-west-1]": 4.101855455000077, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_cfn_handle_serverless_api_resource": 42.27118932500002, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_cfn_template_with_short_form_fn_sub": 6.105303240000012, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_cfn_update_ec2_instance_type": 0.0010265380001328595, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_cfn_with_exports": 2.243627031999722, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_cfn_with_route_table": 4.13399088999995, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_deploy_stack_with_sub_select_and_sub_getaz": 60.93473070999971, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_functions_in_output_export_name": 4.081535875999862, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_resolve_transitive_placeholders_in_strings": 2.0492933699999867, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_sub_in_lambda_function_name": 13.14322270300022, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_update_conditions": 3.065227663999849, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_update_lambda_function": 0.0009119050000663265, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_updating_stack_with_iam_role": 18.128999513000053, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_validate_invalid_json_template_should_fail": 0.05729208100001415, + "tests/aws/services/cloudformation/resources/test_legacy.py::TestCloudFormation::test_validate_template": 0.009663457999977254, + "tests/aws/services/cloudformation/resources/test_logs.py::test_logstream": 2.0441439950002405, + "tests/aws/services/cloudformation/resources/test_opensearch.py::test_domain": 22.503680798999994, + "tests/aws/services/cloudformation/resources/test_opensearch.py::test_domain_with_alternative_types": 26.530102408000175, + "tests/aws/services/cloudformation/resources/test_redshift.py::test_redshift_cluster": 23.121097803999874, + "tests/aws/services/cloudformation/resources/test_route53.py::test_create_health_check": 2.0974813600000743, + "tests/aws/services/cloudformation/resources/test_route53.py::test_create_record_set_via_id": 2.072676214000012, + "tests/aws/services/cloudformation/resources/test_route53.py::test_create_record_set_via_name": 2.065708043999848, + "tests/aws/services/cloudformation/resources/test_route53.py::test_create_record_set_without_resource_record": 2.0629599769999913, + "tests/aws/services/cloudformation/resources/test_s3.py::test_bucket_autoname": 2.0430397349998657, + "tests/aws/services/cloudformation/resources/test_s3.py::test_bucket_versioning": 2.0481287819998215, + "tests/aws/services/cloudformation/resources/test_s3.py::test_bucketpolicy": 4.085996777000219, + "tests/aws/services/cloudformation/resources/test_s3.py::test_cors_configuration": 2.180137378999916, + "tests/aws/services/cloudformation/resources/test_s3.py::test_object_lock_configuration": 2.172703400000046, + "tests/aws/services/cloudformation/resources/test_s3.py::test_website_configuration": 2.1796572050002396, + "tests/aws/services/cloudformation/resources/test_sam.py::test_sam_policies": 6.159034999000141, + "tests/aws/services/cloudformation/resources/test_sam.py::test_sam_sqs_event": 21.2987784259999, + "tests/aws/services/cloudformation/resources/test_sam.py::test_sam_template": 6.564580453999952, + "tests/aws/services/cloudformation/resources/test_secretsmanager.py::test_cfn_handle_secretsmanager_secret": 2.050237464000247, + "tests/aws/services/cloudformation/resources/test_secretsmanager.py::test_cfn_secret_policy": 2.0466734560000077, + "tests/aws/services/cloudformation/resources/test_secretsmanager.py::test_cfn_secretsmanager_gen_secret": 2.0641350880000573, + "tests/aws/services/cloudformation/resources/test_sns.py::test_deploy_stack_with_sns_topic": 4.057012951000161, + "tests/aws/services/cloudformation/resources/test_sns.py::test_sns_subscription": 2.044726677000199, + "tests/aws/services/cloudformation/resources/test_sns.py::test_sns_topic_fifo_with_deduplication": 2.131571765999979, + "tests/aws/services/cloudformation/resources/test_sns.py::test_sns_topic_fifo_without_suffix_fails": 2.044225977999986, + "tests/aws/services/cloudformation/resources/test_sns.py::test_update_subscription": 4.101858637000078, + "tests/aws/services/cloudformation/resources/test_sqs.py::test_cfn_handle_sqs_resource": 4.070059840000113, + "tests/aws/services/cloudformation/resources/test_sqs.py::test_sqs_fifo_queue_generates_valid_name": 2.0414686060000804, + "tests/aws/services/cloudformation/resources/test_sqs.py::test_sqs_non_fifo_queue_generates_valid_name": 2.0389704090000578, + "tests/aws/services/cloudformation/resources/test_sqs.py::test_sqs_queue_policy": 2.0684032600001956, + "tests/aws/services/cloudformation/resources/test_sqs.py::test_update_queue_no_change": 4.0748805610001, + "tests/aws/services/cloudformation/resources/test_sqs.py::test_update_sqs_queuepolicy": 4.102689664000081, + "tests/aws/services/cloudformation/resources/test_ssm.py::test_deploy_patch_baseline": 2.088828094999826, + "tests/aws/services/cloudformation/resources/test_ssm.py::test_maintenance_window": 2.0669437240001116, + "tests/aws/services/cloudformation/resources/test_ssm.py::test_parameter_defaults": 4.058919205000166, + "tests/aws/services/cloudformation/resources/test_ssm.py::test_update_ssm_parameter_tag": 4.071812947000126, + "tests/aws/services/cloudformation/resources/test_ssm.py::test_update_ssm_parameters": 4.066897374999826, + "tests/aws/services/cloudformation/resources/test_stack_sets.py::test_create_stack_set_with_stack_instances": 1.047110051000118, + "tests/aws/services/cloudformation/resources/test_stepfunctions.py::test_apigateway_invoke": 9.270461513000328, + "tests/aws/services/cloudformation/resources/test_stepfunctions.py::test_apigateway_invoke_localhost": 9.297168553000347, + "tests/aws/services/cloudformation/resources/test_stepfunctions.py::test_apigateway_invoke_localhost_with_path": 13.282651892000104, + "tests/aws/services/cloudformation/resources/test_stepfunctions.py::test_apigateway_invoke_with_path": 13.33260191300019, + "tests/aws/services/cloudformation/resources/test_stepfunctions.py::test_cfn_statemachine_with_dependencies": 0.005938310000146885, + "tests/aws/services/cloudformation/resources/test_stepfunctions.py::test_nested_statemachine_with_sync2": 15.253439995000008, + "tests/aws/services/cloudformation/resources/test_stepfunctions.py::test_retry_and_catch": 0.0009037610000177665, + "tests/aws/services/cloudformation/resources/test_stepfunctions.py::test_statemachine_definitionsubstitution": 7.107742370999858, + "tests/aws/services/cloudformation/test_cloudformation_ui.py::TestCloudFormationUi::test_get_cloudformation_ui": 1.4121936550002374, + "tests/aws/services/cloudformation/test_cloudtrail_trace.py::test_cloudtrail_trace_example": 0.0008779610000146931, + "tests/aws/services/cloudformation/test_template_engine.py::TestImportValues::test_import_values_across_stacks": 4.073072275000186, + "tests/aws/services/cloudformation/test_template_engine.py::TestImports::test_stack_imports": 0.0008876029999100865, + "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_and_or_functions[Fn::And-0-0-False]": 0.031315957999822785, + "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_and_or_functions[Fn::And-0-1-False]": 0.02807635199997094, + "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_and_or_functions[Fn::And-1-0-False]": 0.02766771400001744, + "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_and_or_functions[Fn::And-1-1-True]": 2.0388169719999496, + "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_and_or_functions[Fn::Or-0-0-False]": 0.028219824999951015, + "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_and_or_functions[Fn::Or-0-1-True]": 2.0397359500002494, + "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_and_or_functions[Fn::Or-1-0-True]": 2.0420045959997424, + "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_and_or_functions[Fn::Or-1-1-True]": 2.0396585969999705, + "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_base64_sub_and_getatt_functions": 2.038621601000159, + "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_cidr_function": 0.0008666730000186362, + "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_find_map_function": 2.037318692000099, + "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_get_azs_function": 2.037569243000007, + "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_split_length_and_join_functions": 2.0513349479995213, + "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_sub_not_ready": 2.045096147999857, + "tests/aws/services/cloudformation/test_template_engine.py::TestIntrinsicFunctions::test_to_json_functions": 0.0009171970000352303, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_attribute_uses_macro": 5.613923444999955, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_capabilities_requirements": 4.79755037100017, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_error_pass_macro_as_reference": 0.012241911000046457, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_failed_state[raise_error.py]": 3.6120795340000313, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_failed_state[return_invalid_template.py]": 3.581166643000188, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_failed_state[return_unsuccessful_with_message.py]": 3.57434767299992, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_failed_state[return_unsuccessful_without_message.py]": 3.6118325209999966, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_functions_and_references_during_transformation": 4.587801245000037, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_global_scope": 4.7490714230002595, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_macro_deployment": 3.073047640999903, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_scope_order_and_parameters": 0.0011262059999808116, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_snipped_scope[transformation_snippet_topic.json]": 5.6007912340000985, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_snipped_scope[transformation_snippet_topic.yml]": 5.624018116999878, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_to_validate_template_limit_for_macro": 3.2868769719998454, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_validate_lambda_internals": 4.77159508800014, + "tests/aws/services/cloudformation/test_template_engine.py::TestPreviousValues::test_parameter_usepreviousvalue_behavior": 2.040570674999799, + "tests/aws/services/cloudformation/test_template_engine.py::TestSecretsManagerParameters::test_resolve_secretsmanager[resolve_secretsmanager.yaml]": 2.0400322220002636, + "tests/aws/services/cloudformation/test_template_engine.py::TestSecretsManagerParameters::test_resolve_secretsmanager[resolve_secretsmanager_full.yaml]": 2.0426604440001483, + "tests/aws/services/cloudformation/test_template_engine.py::TestSecretsManagerParameters::test_resolve_secretsmanager[resolve_secretsmanager_partial.yaml]": 2.0395155870000963, + "tests/aws/services/cloudformation/test_template_engine.py::TestSsmParameters::test_create_stack_with_ssm_parameters": 2.0617806130003373, + "tests/aws/services/cloudformation/test_template_engine.py::TestSsmParameters::test_resolve_ssm": 2.0447707359999185, + "tests/aws/services/cloudformation/test_template_engine.py::TestSsmParameters::test_resolve_ssm_secure": 2.045308281999951, + "tests/aws/services/cloudformation/test_template_engine.py::TestSsmParameters::test_resolve_ssm_with_version": 2.0559690789998513, + "tests/aws/services/cloudformation/test_template_engine.py::TestStackEvents::test_invalid_stack_deploy": 2.1646231920001355, + "tests/aws/services/cloudformation/test_template_engine.py::TestTypes::test_implicit_type_conversion": 2.0573611709999113, + "tests/aws/services/cloudformation/test_unsupported.py::test_unsupported": 2.038901815999907, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_alarm_lambda_target": 1.5543641719998504, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_anomaly_detector_lifecycle": 0.0008639880002192513, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_aws_sqs_metrics_created": 2.5154344030001994, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_breaching_alarm_actions": 5.1342187259997445, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_create_metric_stream": 0.001046387000087634, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_dashboard_lifecycle": 0.08594650800023373, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_default_ordering": 0.04935939899996811, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_delete_alarm": 0.15873994600019614, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_describe_alarms_converts_date_format_correctly": 0.033495645000130025, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_describe_minimal_metric_alarm": 0.0008217789998070657, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_enable_disable_alarm_actions": 10.143311728000072, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_data": 2.026526080000167, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_data_different_units_no_unit_in_query[metric_data0]": 0.0008445420000953163, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_data_different_units_no_unit_in_query[metric_data1]": 0.000969694000104937, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_data_different_units_no_unit_in_query[metric_data2]": 0.0009229580000464921, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_data_for_multiple_metrics": 1.0210071139999854, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_data_pagination": 0.0009276530001898209, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_data_stats[Average]": 0.014721853000082774, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_data_stats[Maximum]": 0.014869266000005155, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_data_stats[Minimum]": 0.014366444000188494, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_data_stats[SampleCount]": 0.014690198000153032, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_data_stats[Sum]": 0.01669125500006885, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_data_with_different_units": 0.011535217999835368, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_data_with_dimensions": 0.01945822700008648, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_data_with_zero_and_labels": 0.0008192850000341423, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_statistics": 0.06605519300023843, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_with_no_results": 0.024844693999966694, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_get_metric_with_null_dimensions": 0.07133599800022239, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_handle_different_units": 0.000906133999933445, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_insight_rule": 0.001586721999956353, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_label_generation[input_pairs0]": 0.001036097999985941, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_label_generation[input_pairs1]": 0.0020113660000333766, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_label_generation[input_pairs2]": 0.0008295640002415894, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_label_generation[input_pairs3]": 0.0008278090001567762, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_label_generation[input_pairs4]": 0.0008255069999449915, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_label_generation[input_pairs5]": 0.0008419260000209761, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_label_generation[input_pairs6]": 0.0008242330000030051, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_list_metrics_pagination": 2.5111425810000583, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_list_metrics_uniqueness": 2.0251304210000853, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_list_metrics_with_filters": 4.0344325039995965, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_metric_widget": 0.0008424069997090555, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_multiple_dimensions": 2.0458368399997653, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_parallel_put_metric_data_list_metrics": 0.0010643200000686193, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_put_composite_alarm_describe_alarms": 0.04290012900014517, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_put_composite_alarm_describe_alarms_converts_date_format_correctly": 0.02368472299986024, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_put_metric_alarm": 0.0007943780001369305, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_put_metric_alarm_escape_character": 0.03109137300043585, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_put_metric_data": 0.04432999200071208, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_put_metric_data_gzip": 0.015345314999876791, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_put_metric_data_validation": 0.0008453820000795531, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_put_metric_data_values_list": 0.019906578999780322, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_put_metric_uses_utc": 0.01248836200011283, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_raw_metric_data": 0.014021472999729667, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_set_alarm": 2.1513013290000345, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_set_alarm_invalid_input": 0.0006914160001088021, + "tests/aws/services/cloudwatch/test_cloudwatch.py::TestCloudwatch::test_store_tags": 0.10592323699984263, + "tests/aws/services/cloudwatch/test_cloudwatch_metrics.py::TestCloudWatchLambdaMetrics::test_lambda_invoke_error": 2.568172144999835, + "tests/aws/services/cloudwatch/test_cloudwatch_metrics.py::TestCloudWatchLambdaMetrics::test_lambda_invoke_successful": 2.4907766500000434, + "tests/aws/services/cloudwatch/test_cloudwatch_metrics.py::TestSQSMetrics::test_alarm_number_of_messages_sent": 60.5466990609998, + "tests/aws/services/cloudwatch/test_cloudwatch_metrics.py::TestSqsApproximateMetrics::test_sqs_approximate_metrics": 34.28811240799996, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_batch_write_binary": 0.058457972000269365, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_batch_write_items": 0.04539564900005644, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_batch_write_items_streaming": 0.8186017050002192, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_batch_write_not_existing_table": 0.06196175799959747, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_batch_write_not_matching_schema": 0.04502164999985325, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_binary_data_with_stream": 0.6695192299994233, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_continuous_backup_update": 0.1216096920002201, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_create_duplicate_table": 0.04944516600016868, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_data_encoding_consistency": 0.7120149049997053, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_delete_table": 0.053943251999953645, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_dynamodb_batch_execute_statement": 0.07884430799958864, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_dynamodb_create_table_with_class": 0.1017365100005918, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_dynamodb_create_table_with_partial_sse_specification": 0.062201502999414515, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_dynamodb_create_table_with_sse_specification": 0.030486718000247492, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_dynamodb_execute_transaction": 0.13053319799973906, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_dynamodb_get_batch_items": 0.05216818200051421, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_dynamodb_idempotent_writing": 0.07071689499980494, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_dynamodb_partiql_missing": 0.08590830500043012, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_dynamodb_pay_per_request": 0.02169656100022621, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_dynamodb_stream_records_with_update_item": 0.6184014589998696, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_dynamodb_stream_shard_iterator": 0.6962355349996869, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_dynamodb_stream_stream_view_type": 0.9722240829996736, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_dynamodb_streams_describe_with_exclusive_start_shard_id": 0.6637856140005169, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_dynamodb_streams_shard_iterator_format": 2.672864094999568, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_dynamodb_with_kinesis_stream": 1.2423450079995746, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_empty_and_binary_values": 0.04062757400015471, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_global_tables": 0.041946168999857036, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_global_tables_version_2019": 2.2219274150002093, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_invalid_query_index": 0.03294104599990533, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_large_data_download": 0.23413537000033102, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_list_tags_of_resource": 0.03733071800024845, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_more_than_20_global_secondary_indexes": 0.1422407430000021, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_multiple_update_expressions": 0.07745713399981469, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_non_ascii_chars": 0.07948317699947438, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_nosql_workbench_localhost_region": 0.03328827000041201, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_query_on_deleted_resource": 0.07727206500021566, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_return_values_in_put_item": 0.06658911199974682, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_return_values_on_conditions_check_failure": 0.10232375299983687, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_stream_destination_records": 13.776073273998918, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_stream_spec_and_region_replacement": 1.1913716040003237, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_time_to_live": 0.11647257899949182, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_time_to_live_deletion": 0.21254729699967356, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_transact_get_items": 0.050528229999599716, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_transact_write_items_streaming": 0.9105194449998635, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_transact_write_items_streaming_for_different_tables": 0.8700495010007216, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_transaction_write_binary_data": 0.04259166100018774, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_transaction_write_canceled": 0.06176785000070595, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_transaction_write_items": 0.06315911899991988, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_valid_local_secondary_index": 0.06044732400050634, + "tests/aws/services/dynamodb/test_dynamodb.py::TestDynamoDB::test_valid_query_index": 0.0440658339998663, + "tests/aws/services/dynamodbstreams/test_dynamodb_streams.py::TestDynamoDBStreams::test_enable_kinesis_streaming_destination": 0.0009143239999502839, + "tests/aws/services/dynamodbstreams/test_dynamodb_streams.py::TestDynamoDBStreams::test_stream_spec_and_region_replacement": 1.8224712920005004, + "tests/aws/services/ec2/test_ec2.py::TestEc2Integrations::test_create_route_table_association": 0.057712879000519024, + "tests/aws/services/ec2/test_ec2.py::TestEc2Integrations::test_create_vpc_end_point": 0.04834617599954072, + "tests/aws/services/ec2/test_ec2.py::TestEc2Integrations::test_describe_vpc_endpoints_with_filter": 0.08195298699956766, + "tests/aws/services/ec2/test_ec2.py::TestEc2Integrations::test_describe_vpn_gateways_filter_by_vpc": 0.035476833999837254, + "tests/aws/services/ec2/test_ec2.py::TestEc2Integrations::test_modify_launch_template[id]": 0.08947242700014613, + "tests/aws/services/ec2/test_ec2.py::TestEc2Integrations::test_modify_launch_template[name]": 0.021027852999850438, + "tests/aws/services/ec2/test_ec2.py::TestEc2Integrations::test_reserved_instance_api": 0.012466765000226587, + "tests/aws/services/ec2/test_ec2.py::TestEc2Integrations::test_vcp_peering_difference_regions": 0.0527308109999467, + "tests/aws/services/ec2/test_ec2.py::test_pickle_ec2_backend": 0.3781174120003925, + "tests/aws/services/ec2/test_ec2.py::test_raise_duplicate_launch_template_name": 0.012725781999961328, + "tests/aws/services/ec2/test_ec2.py::test_raise_invalid_launch_template_name": 0.0038310750001073757, + "tests/aws/services/ec2/test_ec2.py::test_raise_modify_to_invalid_default_version": 0.01193862399986756, + "tests/aws/services/ec2/test_ec2.py::test_raise_when_launch_template_data_missing": 0.004138935999890236, + "tests/aws/services/es/test_es.py::TestElasticsearchProvider::test_create_domain": 16.736776522999662, + "tests/aws/services/es/test_es.py::TestElasticsearchProvider::test_create_existing_domain_causes_exception": 16.274150960000497, + "tests/aws/services/es/test_es.py::TestElasticsearchProvider::test_describe_domains": 15.773939529000018, + "tests/aws/services/es/test_es.py::TestElasticsearchProvider::test_domain_version": 34.62791466800036, + "tests/aws/services/es/test_es.py::TestElasticsearchProvider::test_get_compatible_version_for_domain": 18.786615187000734, + "tests/aws/services/es/test_es.py::TestElasticsearchProvider::test_get_compatible_versions": 0.008318247999795858, + "tests/aws/services/es/test_es.py::TestElasticsearchProvider::test_list_versions": 0.00997339799960173, + "tests/aws/services/es/test_es.py::TestElasticsearchProvider::test_path_endpoint_strategy": 16.749200803999884, + "tests/aws/services/es/test_es.py::TestElasticsearchProvider::test_update_domain_config": 16.85690607200013, + "tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.py::test_scheduled_rule_logs": 1.14328350400001, + "tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.py::test_scheduled_rule_sqs": 0.16720040200016228, + "tests/aws/services/events/test_event_patterns.py::test_event_pattern_source": 0.006643609000548167, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays]": 0.003973725000378181, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays_NEG]": 0.004025758000352653, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays_empty_EXC]": 0.002301161000104912, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[arrays_empty_null_NEG]": 0.005058396000549692, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[boolean]": 0.004188563000298018, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[boolean_NEG]": 0.003952591999677679, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_many_rules]": 0.004618763999587827, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_multi_match]": 0.005252934000054665, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_multi_match_NEG]": 0.004104027000721544, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_or]": 0.002529928000058135, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[complex_or_NEG]": 0.004000821999852633, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase]": 0.001716202999887173, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase_NEG]": 0.0051849870001206, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase_list]": 0.002287707000050432, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_ignorecase_list_NEG]": 0.0039663610004936345, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number]": 0.004344584999671497, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number_NEG]": 0.003997294999862788, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number_list]": 0.0039325299994743546, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_number_list_NEG]": 0.004086268000264681, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string]": 0.003963070999361662, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string_NEG]": 0.003941390999898431, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string_list]": 0.004198995000479044, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_but_string_list_NEG]": 0.00551063399962004, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_prefix]": 0.005257663000520552, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_prefix_NEG]": 0.0039797479998924246, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_suffix]": 0.0019394189998820366, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_anything_suffix_NEG]": 0.005194646999825636, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists]": 0.004064021999965917, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists_NEG]": 0.004040920000079495, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists_false]": 0.0028360780002003594, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_exists_false_NEG]": 0.003964721000102145, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ignorecase]": 0.00173671100037609, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ignorecase_NEG]": 0.0017213629994330404, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ip_address]": 0.0023859889997766004, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_ip_address_NEG]": 0.005443135000405164, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_EXC]": 0.00179863999983354, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_and]": 0.001796893000118871, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_and_NEG]": 0.005890114000067115, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_operatorcasing_EXC]": 0.001763350999681279, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_numeric_syntax_EXC]": 0.0022936180007491203, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_prefix]": 0.004018526999971073, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_prefix_NEG]": 0.003956091999498312, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_prefix_ignorecase]": 0.0018842360004782677, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix]": 0.0025996769995799696, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix_NEG]": 0.004145971999150788, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix_ignorecase]": 0.002315669000381604, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_suffix_ignorecase_NEG]": 0.003942807999919751, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_complex_EXC]": 0.0017898209998747916, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_nonrepeating]": 0.002297464000093896, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_nonrepeating_NEG]": 0.00400436100017032, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_repeating]": 0.0017008329996315297, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_repeating_NEG]": 0.004039445999751479, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[content_wildcard_simplified]": 0.0017705439991004823, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_event]": 0.0018139560002055077, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_event_NEG]": 0.004008237999642006, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_pattern]": 0.0017444359996261483, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dot_joining_pattern_NEG]": 0.004168544000549446, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[dynamodb]": 0.004011507999621244, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[exists_dynamodb]": 0.005275537999750668, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[exists_dynamodb_NEG]": 0.002349019999655866, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[int_nolist_EXC]": 0.0017611570001463406, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[key_case_sensitive_NEG]": 0.003963494999425166, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[list_within_dict]": 0.0046108879996609176, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[minimal]": 0.0040156019999813, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[nested_json_NEG]": 0.002253542999824276, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[null_value]": 0.003953542000090238, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[null_value_NEG]": 0.0042827599995689525, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[number_comparison_float]": 0.004009211000720825, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[operator_case_sensitive_EXC]": 0.0019231600003877247, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[operator_multiple_list]": 0.0040162129994314455, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[or-anything-but]": 0.004110347999812802, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[or-exists-parent]": 0.002493209000022034, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[or-exists]": 0.0017962730003091565, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[prefix]": 0.004005855999821506, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[sample1]": 0.0062469069998769555, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[string]": 0.004055313999742793, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[string_empty]": 0.0040180659998441115, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern[string_nolist_EXC]": 0.0017308420001427294, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern_with_escape_characters": 0.0034642519999579235, + "tests/aws/services/events/test_event_patterns.py::test_test_event_pattern_with_multi_key": 0.004149358000177017, + "tests/aws/services/events/test_events.py::TestEventBus::test_create_list_describe_delete_custom_event_buses[regions0]": 0.0007367509997493471, + "tests/aws/services/events/test_events.py::TestEventBus::test_create_list_describe_delete_custom_event_buses[regions1]": 0.0012050689997522568, + "tests/aws/services/events/test_events.py::TestEventBus::test_create_multiple_event_buses_same_name": 0.012579930000811146, + "tests/aws/services/events/test_events.py::TestEventBus::test_delete_default_event_bus": 0.004558007000468933, + "tests/aws/services/events/test_events.py::TestEventBus::test_describe_delete_not_existing_event_bus": 0.006907012999818107, + "tests/aws/services/events/test_events.py::TestEventBus::test_list_event_buses_with_limit": 0.0007561970001006557, + "tests/aws/services/events/test_events.py::TestEventBus::test_list_event_buses_with_prefix": 0.022732713000095828, + "tests/aws/services/events/test_events.py::TestEventBus::test_put_events_into_event_bus[domain]": 0.060694239999520505, + "tests/aws/services/events/test_events.py::TestEventBus::test_put_events_into_event_bus[path]": 0.06031193400031043, + "tests/aws/services/events/test_events.py::TestEventBus::test_put_events_into_event_bus[standard]": 0.06050450999964596, + "tests/aws/services/events/test_events.py::TestEventBus::test_put_events_nonexistent_event_bus": 0.05335261399977753, + "tests/aws/services/events/test_events.py::TestEventBus::test_put_events_to_default_eventbus_for_custom_eventbus": 0.3471783069999219, + "tests/aws/services/events/test_events.py::TestEventPattern::test_put_events_pattern_nested": 10.081432977000077, + "tests/aws/services/events/test_events.py::TestEventPattern::test_put_events_pattern_with_values_in_array": 5.098570006000045, + "tests/aws/services/events/test_events.py::TestEventRule::test_delete_rule_with_targets": 0.024927014000240888, + "tests/aws/services/events/test_events.py::TestEventRule::test_describe_nonexistent_rule": 0.0008705500003998168, + "tests/aws/services/events/test_events.py::TestEventRule::test_disable_re_enable_rule[custom]": 0.030548287999863533, + "tests/aws/services/events/test_events.py::TestEventRule::test_disable_re_enable_rule[default]": 0.02158660399982182, + "tests/aws/services/events/test_events.py::TestEventRule::test_list_rule_with_limit": 0.08005313599960573, + "tests/aws/services/events/test_events.py::TestEventRule::test_put_list_with_prefix_describe_delete_rule[custom]": 0.02767081199999666, + "tests/aws/services/events/test_events.py::TestEventRule::test_put_list_with_prefix_describe_delete_rule[default]": 0.019771490000039194, + "tests/aws/services/events/test_events.py::TestEventRule::test_put_multiple_rules_with_same_name": 0.026854377000290697, + "tests/aws/services/events/test_events.py::TestEventRule::test_update_rule_with_targets": 0.03280507700083035, + "tests/aws/services/events/test_events.py::TestEventTarget::test_add_exceed_fife_targets_per_rule": 0.0008347439998033224, + "tests/aws/services/events/test_events.py::TestEventTarget::test_list_target_by_rule_limit": 0.0007556060004390019, + "tests/aws/services/events/test_events.py::TestEventTarget::test_put_list_remove_target[custom]": 0.03818029700050829, + "tests/aws/services/events/test_events.py::TestEventTarget::test_put_list_remove_target[default]": 0.03381099999978687, + "tests/aws/services/events/test_events.py::TestEventTarget::test_put_target_id_validation": 0.028447403999962262, + "tests/aws/services/events/test_events.py::TestEvents::test_api_destinations[auth0]": 0.04281618399954823, + "tests/aws/services/events/test_events.py::TestEvents::test_api_destinations[auth1]": 0.03807928200012611, + "tests/aws/services/events/test_events.py::TestEvents::test_api_destinations[auth2]": 0.03668372800029829, + "tests/aws/services/events/test_events.py::TestEvents::test_create_connection_validations": 0.004363156000181334, + "tests/aws/services/events/test_events.py::TestEvents::test_create_rule_with_one_unit_in_plural_should_fail[rate(1 days)]": 0.008749861000069359, + "tests/aws/services/events/test_events.py::TestEvents::test_create_rule_with_one_unit_in_plural_should_fail[rate(1 hours)]": 0.009157166999102628, + "tests/aws/services/events/test_events.py::TestEvents::test_create_rule_with_one_unit_in_plural_should_fail[rate(1 minutes)]": 0.011133704999792826, + "tests/aws/services/events/test_events.py::TestEvents::test_create_rule_with_one_unit_in_singular_should_succeed[rate(1 day)]": 0.013730550999753177, + "tests/aws/services/events/test_events.py::TestEvents::test_create_rule_with_one_unit_in_singular_should_succeed[rate(1 hour)]": 0.013914654000473092, + "tests/aws/services/events/test_events.py::TestEvents::test_create_rule_with_one_unit_in_singular_should_succeed[rate(1 minute)]": 0.014196360999449098, + "tests/aws/services/events/test_events.py::TestEvents::test_events_written_to_disk_are_timestamp_prefixed_for_chronological_ordering": 0.051563425000495045, + "tests/aws/services/events/test_events.py::TestEvents::test_list_tags_for_resource": 0.01888138499998604, + "tests/aws/services/events/test_events.py::TestEvents::test_put_event_with_content_base_rule_in_pattern": 0.12393227400025353, + "tests/aws/services/events/test_events.py::TestEvents::test_put_event_without_detail": 0.0008039949998419615, + "tests/aws/services/events/test_events.py::TestEvents::test_put_event_without_source": 0.007847643000332027, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_into_event_bus": 0.1309731019996434, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_nonexistent_event_bus": 0.344152104999921, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_time": 0.10381933500048035, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_to_default_eventbus_for_custom_eventbus": 0.9275336260002405, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_input_path": 0.12653382100006638, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_input_path_multiple": 0.1631062930000553, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_nested_event_pattern": 0.1573128669997459, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_target_firehose": 0.19549808600004326, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_target_kinesis": 1.0276327510000556, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_target_lambda": 4.2238141100001485, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_target_sns": 1.1394145369995385, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_target_sqs": 0.1192510979999497, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_target_sqs_event_detail_match": 0.1550437270002476, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_target_sqs_new_region": 0.04448281599979964, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_with_values_in_array": 0.18060967500014158, + "tests/aws/services/events/test_events.py::TestEvents::test_put_events_without_source": 0.0010136360001524736, + "tests/aws/services/events/test_events.py::TestEvents::test_put_rule": 0.026382164000096964, + "tests/aws/services/events/test_events.py::TestEvents::test_put_target_id_validation": 0.06400809099977778, + "tests/aws/services/events/test_events.py::TestEvents::test_rule_disable": 0.107648209999752, + "tests/aws/services/events/test_events.py::TestEvents::test_scheduled_expression_events": 60.590954170999794, + "tests/aws/services/events/test_events.py::TestEvents::test_should_ignore_schedules_for_put_event": 56.40675291799971, + "tests/aws/services/events/test_events.py::TestEvents::test_test_event_pattern": 0.01617187300007572, + "tests/aws/services/events/test_events.py::TestEvents::test_trigger_event_on_ssm_change": 0.09596906100023261, + "tests/aws/services/events/test_events.py::TestEvents::test_verify_rule_event_content": 55.27749305999987, + "tests/aws/services/events/test_events_inputs.py::TestInputPath::test_put_events_with_input_path": 0.05888234399981229, + "tests/aws/services/events/test_events_inputs.py::TestInputPath::test_put_events_with_input_path_max_level_depth": 0.05886068800009525, + "tests/aws/services/events/test_events_inputs.py::TestInputPath::test_put_events_with_input_path_multiple_targets": 0.09534159499980888, + "tests/aws/services/events/test_events_inputs.py::TestInputPath::test_put_events_with_input_path_nested[event_detail0]": 0.058043990999522066, + "tests/aws/services/events/test_events_inputs.py::TestInputPath::test_put_events_with_input_path_nested[event_detail1]": 0.06028112099966165, + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_input_transformer_predefined_variables[\"Message containing all pre defined variables <aws.events.rule-arn> <aws.events.rule-name> <aws.events.event.ingestion-time>\"]": 0.0008324090003952733, + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_input_transformer_predefined_variables[{\"originalEvent\": <aws.events.event>, \"originalEventJson\": <aws.events.event.json>}]": 0.0007704449999437202, + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_put_events_with_input_transformer_input_template_json": 0.0009744339995449991, + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_put_events_with_input_transformer_input_template_string[\"Event of <detail-type> type, at time <timestamp>, info extracted from detail <command>\"]": 0.1346594799997547, + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_put_events_with_input_transformer_input_template_string[\"{[/Check with special starting characters for event of <detail-type> type\"]": 0.1277010320000045, + "tests/aws/services/events/test_events_inputs.py::TestInputTransformer::test_put_events_with_input_transformer_missing_keys": 0.0008730360004847171, + "tests/aws/services/events/test_events_inputs.py::test_put_event_input_path_and_input_transfomer": 0.0008078830001068127, + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_firehose": 0.09235822000027838, + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_kinesis": 0.99285613200027, + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_lambda": 4.079361673000221, + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_lambda_list_entries_partial_match": 4.091096621999895, + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_lambda_list_entry": 4.08400764299995, + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_sns[domain]": 0.060943511000004946, + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_sns[path]": 0.061815817000024254, + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_sns[standard]": 0.06098407799981942, + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_sqs": 0.05361615600031655, + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_sqs_event_detail_match": 5.068614671999967, + "tests/aws/services/events/test_events_integrations.py::test_put_events_with_target_sqs_new_region": 0.02369493699961822, + "tests/aws/services/events/test_events_integrations.py::test_should_ignore_schedules_for_put_event": 26.120857641000384, + "tests/aws/services/events/test_events_integrations.py::test_trigger_event_on_ssm_change[domain]": 0.04284906100019725, + "tests/aws/services/events/test_events_integrations.py::test_trigger_event_on_ssm_change[path]": 0.04211745700013125, + "tests/aws/services/events/test_events_integrations.py::test_trigger_event_on_ssm_change[standard]": 0.04919465700004366, + "tests/aws/services/events/test_events_rules.py::test_create_rule_with_one_unit_in_singular_should_succeed[rate(1 day)]": 0.007226723000258062, + "tests/aws/services/events/test_events_rules.py::test_create_rule_with_one_unit_in_singular_should_succeed[rate(1 hour)]": 0.007140518999676715, + "tests/aws/services/events/test_events_rules.py::test_create_rule_with_one_unit_in_singular_should_succeed[rate(1 minute)]": 0.007354334000410745, + "tests/aws/services/events/test_events_rules.py::test_put_event_with_content_base_rule_in_pattern": 3.0630882049999855, + "tests/aws/services/events/test_events_rules.py::test_put_events_with_rule_anything_but_to_sqs": 5.1064781759992, + "tests/aws/services/events/test_events_rules.py::test_put_events_with_rule_exists_false_to_sqs": 5.081474376000642, + "tests/aws/services/events/test_events_rules.py::test_put_events_with_rule_exists_true_to_sqs": 5.080222726000102, + "tests/aws/services/events/test_events_rules.py::test_put_rule": 0.013903215999562235, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[ rate(10 minutes)]": 0.005620940000426344, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[rate( 10 minutes )]": 0.004031273999771656, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[rate()]": 0.004195355000319978, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[rate(-10 minutes)]": 0.004196208999474038, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[rate(0 minutes)]": 0.0041960249996009225, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[rate(1 days)]": 0.0050962069999513915, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[rate(1 hours)]": 0.005706234000172117, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[rate(1 minutes)]": 0.005845119000241539, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[rate(10 MINUTES)]": 0.003971447999902011, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[rate(10 day)]": 0.004406924000250001, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[rate(10 hour)]": 0.004310996000185696, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[rate(10 minute)]": 0.0048694560000512865, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[rate(10 minutess)]": 0.004452198999842949, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[rate(10 seconds)]": 0.004371961999822815, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[rate(10 years)]": 0.0039603940003871685, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[rate(10)]": 0.004052199999932782, + "tests/aws/services/events/test_events_rules.py::test_put_rule_invalid_rate_schedule_expression[rate(foo minutes)]": 0.004042486999878747, + "tests/aws/services/events/test_events_rules.py::test_rule_disable": 0.015406769000037457, + "tests/aws/services/events/test_events_rules.py::test_verify_rule_event_content": 40.08291648000022, + "tests/aws/services/firehose/test_firehose.py::TestFirehoseIntegration::test_delivery_stream_with_kinesis_as_source": 26.386103965000075, + "tests/aws/services/firehose/test_firehose.py::TestFirehoseIntegration::test_kinesis_firehose_elasticsearch_s3_backup": 25.213196993999645, + "tests/aws/services/firehose/test_firehose.py::TestFirehoseIntegration::test_kinesis_firehose_kinesis_as_source": 38.14876707900066, + "tests/aws/services/firehose/test_firehose.py::TestFirehoseIntegration::test_kinesis_firehose_kinesis_as_source_multiple_delivery_streams": 41.7700724489996, + "tests/aws/services/firehose/test_firehose.py::TestFirehoseIntegration::test_kinesis_firehose_opensearch_s3_backup[domain]": 27.4597393490003, + "tests/aws/services/firehose/test_firehose.py::TestFirehoseIntegration::test_kinesis_firehose_opensearch_s3_backup[path]": 24.194070300999556, + "tests/aws/services/firehose/test_firehose.py::TestFirehoseIntegration::test_kinesis_firehose_opensearch_s3_backup[port]": 23.344568455999706, + "tests/aws/services/firehose/test_firehose.py::test_firehose_http[False]": 0.05765511900017373, + "tests/aws/services/firehose/test_firehose.py::test_firehose_http[True]": 1.84923299600041, + "tests/aws/services/firehose/test_firehose.py::test_kinesis_firehose_http[False]": 0.03911529699962557, + "tests/aws/services/firehose/test_firehose.py::test_kinesis_firehose_http[True]": 1.485420026000611, + "tests/aws/services/iam/test_iam.py::TestIAMExtensions::test_create_role_with_malformed_assume_role_policy_document": 0.005252710000149818, + "tests/aws/services/iam/test_iam.py::TestIAMExtensions::test_create_user_add_permission_boundary_afterwards": 0.03602346800016676, + "tests/aws/services/iam/test_iam.py::TestIAMExtensions::test_create_user_with_permission_boundary": 0.03099995200000194, + "tests/aws/services/iam/test_iam.py::TestIAMExtensions::test_get_user_without_username_as_role": 0.04542086899937203, + "tests/aws/services/iam/test_iam.py::TestIAMExtensions::test_get_user_without_username_as_root": 0.017389899999670888, + "tests/aws/services/iam/test_iam.py::TestIAMExtensions::test_get_user_without_username_as_user": 0.06645723400015413, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_attach_detach_role_policy": 0.032785012000204006, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_attach_iam_role_to_new_iam_user": 0.03466959699971994, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_create_describe_role": 0.0314909839999018, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_create_role_with_assume_role_policy": 0.05348500199988848, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_create_user_with_tags": 0.011096889999407722, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_delete_non_existent_policy_returns_no_such_entity": 0.0038582320003115456, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_instance_profile_tags": 0.05314553199968941, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_list_roles_with_permission_boundary": 0.03715826199959338, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_recreate_iam_role": 0.02416144600010739, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_role_attach_policy": 0.11393636399998286, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_service_linked_role_name_should_match_aws[ecs.amazonaws.com-AWSServiceRoleForECS]": 0.008331382000051235, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_service_linked_role_name_should_match_aws[eks.amazonaws.com-AWSServiceRoleForAmazonEKS]": 0.007967493999331055, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_simulate_principle_policy": 0.008367229999748815, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_update_assume_role_policy": 0.018374218000190012, + "tests/aws/services/iam/test_iam.py::TestIAMIntegrations::test_user_attach_policy": 0.11591100499981621, + "tests/aws/services/kinesis/test_kinesis.py::TestKinesis::test_add_tags_to_stream": 0.6098278440003924, + "tests/aws/services/kinesis/test_kinesis.py::TestKinesis::test_create_stream_without_shard_count": 0.5932691550001437, + "tests/aws/services/kinesis/test_kinesis.py::TestKinesis::test_create_stream_without_stream_name_raises": 0.020914307000111876, + "tests/aws/services/kinesis/test_kinesis.py::TestKinesis::test_get_records": 0.6898630439995941, + "tests/aws/services/kinesis/test_kinesis.py::TestKinesis::test_get_records_empty_stream": 0.6139344130001518, + "tests/aws/services/kinesis/test_kinesis.py::TestKinesis::test_get_records_next_shard_iterator": 0.6160011730003134, + "tests/aws/services/kinesis/test_kinesis.py::TestKinesis::test_get_records_shard_iterator_with_surrounding_quotes": 0.6140143930001614, + "tests/aws/services/kinesis/test_kinesis.py::TestKinesis::test_record_lifecycle_data_integrity": 0.7168080880001071, + "tests/aws/services/kinesis/test_kinesis.py::TestKinesis::test_stream_consumers": 1.2239697380000507, + "tests/aws/services/kinesis/test_kinesis.py::TestKinesis::test_subscribe_to_shard": 4.413150507999944, + "tests/aws/services/kinesis/test_kinesis.py::TestKinesis::test_subscribe_to_shard_timeout": 6.200140599999941, + "tests/aws/services/kinesis/test_kinesis.py::TestKinesis::test_subscribe_to_shard_with_sequence_number_as_iterator": 4.344802550000622, + "tests/aws/services/kinesis/test_kinesis.py::TestKinesisPythonClient::test_run_kcl": 30.262808036000024, + "tests/aws/services/kms/test_kms.py::TestKMS::test_all_types_of_key_id_can_be_used_for_encryption": 0.024757378000231256, + "tests/aws/services/kms/test_kms.py::TestKMS::test_cant_delete_deleted_key": 0.011500745999910578, + "tests/aws/services/kms/test_kms.py::TestKMS::test_cant_use_disabled_or_deleted_keys": 0.019218005000311678, + "tests/aws/services/kms/test_kms.py::TestKMS::test_create_alias": 0.046199681999496534, + "tests/aws/services/kms/test_kms.py::TestKMS::test_create_grant_with_invalid_key": 0.008567868000227463, + "tests/aws/services/kms/test_kms.py::TestKMS::test_create_grant_with_same_name_two_keys": 0.03117312199992739, + "tests/aws/services/kms/test_kms.py::TestKMS::test_create_grant_with_valid_key": 0.016016828999909194, + "tests/aws/services/kms/test_kms.py::TestKMS::test_create_key": 0.04202067400001397, + "tests/aws/services/kms/test_kms.py::TestKMS::test_create_key_custom_id": 0.00959658000010677, + "tests/aws/services/kms/test_kms.py::TestKMS::test_create_key_custom_key_material_hmac": 0.013694444999600819, + "tests/aws/services/kms/test_kms.py::TestKMS::test_create_key_custom_key_material_symmetric_decrypt": 0.010652954999841313, + "tests/aws/services/kms/test_kms.py::TestKMS::test_create_list_delete_alias": 0.021936500999800046, + "tests/aws/services/kms/test_kms.py::TestKMS::test_create_multi_region_key": 0.06470501999956468, + "tests/aws/services/kms/test_kms.py::TestKMS::test_describe_and_list_sign_key": 0.012278018999495544, + "tests/aws/services/kms/test_kms.py::TestKMS::test_disable_and_enable_key": 0.020305007999922964, + "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_decrypt[RSA_2048-RSAES_OAEP_SHA_256]": 0.023440301999926305, + "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_decrypt[SYMMETRIC_DEFAULT-SYMMETRIC_DEFAULT]": 0.01170983600013642, + "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_decrypt_encryption_context": 0.06704463200003374, + "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_validate_plaintext_size_per_key_type[RSA_2048-RSAES_OAEP_SHA_1]": 0.06391170500000953, + "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_validate_plaintext_size_per_key_type[RSA_2048-RSAES_OAEP_SHA_256]": 0.08602870699951382, + "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_validate_plaintext_size_per_key_type[RSA_3072-RSAES_OAEP_SHA_1]": 0.2715490820000923, + "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_validate_plaintext_size_per_key_type[RSA_3072-RSAES_OAEP_SHA_256]": 0.19376190599996335, + "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_validate_plaintext_size_per_key_type[RSA_4096-RSAES_OAEP_SHA_1]": 0.5638283700000102, + "tests/aws/services/kms/test_kms.py::TestKMS::test_encrypt_validate_plaintext_size_per_key_type[RSA_4096-RSAES_OAEP_SHA_256]": 0.48916123800017886, + "tests/aws/services/kms/test_kms.py::TestKMS::test_error_messaging_for_invalid_keys": 0.10109555999952136, + "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_and_verify_mac[HMAC_224-HMAC_SHA_224]": 0.043671123000422085, + "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_and_verify_mac[HMAC_256-HMAC_SHA_256]": 1.4143355789997258, + "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_and_verify_mac[HMAC_384-HMAC_SHA_384]": 0.04661744499935594, + "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_and_verify_mac[HMAC_512-HMAC_SHA_512]": 0.04474350200007393, + "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_random[1024]": 0.030516924000494328, + "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_random[12]": 0.030074396999680175, + "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_random[1]": 0.03046969100023489, + "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_random[44]": 0.03048295500002496, + "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_random[91]": 0.029780656000184536, + "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_random_invalid_number_of_bytes[0]": 0.029970525999942765, + "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_random_invalid_number_of_bytes[1025]": 0.033459171000231436, + "tests/aws/services/kms/test_kms.py::TestKMS::test_generate_random_invalid_number_of_bytes[None]": 0.035748409000007086, + "tests/aws/services/kms/test_kms.py::TestKMS::test_get_key_does_not_exist": 0.04351390600004379, + "tests/aws/services/kms/test_kms.py::TestKMS::test_get_key_in_different_region": 0.05115493099992818, + "tests/aws/services/kms/test_kms.py::TestKMS::test_get_key_invalid_uuid": 0.03743459699990126, + "tests/aws/services/kms/test_kms.py::TestKMS::test_get_parameters_for_import": 0.3561277630001314, + "tests/aws/services/kms/test_kms.py::TestKMS::test_get_public_key": 0.044266418000006524, + "tests/aws/services/kms/test_kms.py::TestKMS::test_get_put_list_key_policies": 0.018160949000048277, + "tests/aws/services/kms/test_kms.py::TestKMS::test_hmac_create_key": 0.04180584200048543, + "tests/aws/services/kms/test_kms.py::TestKMS::test_hmac_create_key_invalid_operations": 0.03659522200041465, + "tests/aws/services/kms/test_kms.py::TestKMS::test_import_key": 0.332781807000174, + "tests/aws/services/kms/test_kms.py::TestKMS::test_import_key_asymmetric": 0.14871291400004338, + "tests/aws/services/kms/test_kms.py::TestKMS::test_import_key_symmetric": 0.17120169600002555, + "tests/aws/services/kms/test_kms.py::TestKMS::test_invalid_generate_mac[HMAC_224-HMAC_SHA_256]": 0.054694616999768186, + "tests/aws/services/kms/test_kms.py::TestKMS::test_invalid_generate_mac[HMAC_256-INVALID]": 0.0371839400004319, + "tests/aws/services/kms/test_kms.py::TestKMS::test_invalid_key_usage": 1.3026825890001419, + "tests/aws/services/kms/test_kms.py::TestKMS::test_invalid_verify_mac[HMAC_256-HMAC_SHA_256-some different important message]": 0.07916943500049456, + "tests/aws/services/kms/test_kms.py::TestKMS::test_invalid_verify_mac[HMAC_256-HMAC_SHA_512-some important message]": 0.0663144629997987, + "tests/aws/services/kms/test_kms.py::TestKMS::test_invalid_verify_mac[HMAC_256-INVALID-some important message]": 0.06567004099997575, + "tests/aws/services/kms/test_kms.py::TestKMS::test_key_rotation_status": 0.020345214999451855, + "tests/aws/services/kms/test_kms.py::TestKMS::test_list_aliases_of_key": 0.027389467999910266, + "tests/aws/services/kms/test_kms.py::TestKMS::test_list_grants_with_invalid_key": 0.005527390999759518, + "tests/aws/services/kms/test_kms.py::TestKMS::test_list_keys": 0.0101671779998469, + "tests/aws/services/kms/test_kms.py::TestKMS::test_list_retirable_grants": 0.027150383999924088, + "tests/aws/services/kms/test_kms.py::TestKMS::test_non_multi_region_keys_should_not_have_multi_region_properties": 0.056968583000070794, + "tests/aws/services/kms/test_kms.py::TestKMS::test_plaintext_size_for_encrypt": 0.037234924000131286, + "tests/aws/services/kms/test_kms.py::TestKMS::test_replicate_key": 0.17330967899988536, + "tests/aws/services/kms/test_kms.py::TestKMS::test_retire_grant_with_grant_id_and_key_id": 0.022254864999467827, + "tests/aws/services/kms/test_kms.py::TestKMS::test_retire_grant_with_grant_token": 0.022653162000096927, + "tests/aws/services/kms/test_kms.py::TestKMS::test_revoke_grant": 0.02263850999997885, + "tests/aws/services/kms/test_kms.py::TestKMS::test_schedule_and_cancel_key_deletion": 0.01685115400050563, + "tests/aws/services/kms/test_kms.py::TestKMS::test_sign_verify[ECC_NIST_P256-ECDSA_SHA_256]": 0.10488485299993044, + "tests/aws/services/kms/test_kms.py::TestKMS::test_sign_verify[ECC_NIST_P384-ECDSA_SHA_384]": 0.10774222099962572, + "tests/aws/services/kms/test_kms.py::TestKMS::test_sign_verify[ECC_SECG_P256K1-ECDSA_SHA_256]": 0.10796016900030736, + "tests/aws/services/kms/test_kms.py::TestKMS::test_sign_verify[RSA_2048-RSASSA_PSS_SHA_256]": 0.5303769010001815, + "tests/aws/services/kms/test_kms.py::TestKMS::test_sign_verify[RSA_2048-RSASSA_PSS_SHA_384]": 0.5039198829995257, + "tests/aws/services/kms/test_kms.py::TestKMS::test_sign_verify[RSA_2048-RSASSA_PSS_SHA_512]": 0.5302625749995968, + "tests/aws/services/kms/test_kms.py::TestKMS::test_sign_verify[RSA_4096-RSASSA_PKCS1_V1_5_SHA_256]": 2.942191553000157, + "tests/aws/services/kms/test_kms.py::TestKMS::test_sign_verify[RSA_4096-RSASSA_PKCS1_V1_5_SHA_512]": 3.241802091000409, + "tests/aws/services/kms/test_kms.py::TestKMS::test_tag_untag_list_tags": 0.024445914999887464, + "tests/aws/services/kms/test_kms.py::TestKMS::test_update_alias": 0.024862129999746685, + "tests/aws/services/kms/test_kms.py::TestKMS::test_update_key_description": 0.014338387999487168, + "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_encryption_context_generate_data_key": 0.0616781959997752, + "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_encryption_context_generate_data_key_pair": 0.05730113800018444, + "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_encryption_context_generate_data_key_pair_without_plaintext": 0.08812087999967844, + "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_encryption_context_generate_data_key_without_plaintext": 0.06476724699996339, + "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_generate_data_key": 0.013904109000122844, + "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_generate_data_key_pair": 0.06897613199998887, + "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_generate_data_key_pair_without_plaintext": 0.030683571000736265, + "tests/aws/services/kms/test_kms.py::TestKMSGenerateKeys::test_generate_data_key_without_plaintext": 0.010205460999713978, + "tests/aws/services/kms/test_kms.py::TestKMSMultiAccounts::test_cross_accounts_access": 1.4963271399997211, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaAliases::test_alias_routingconfig": 3.0136081919999924, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaAliases::test_lambda_alias_moving": 3.1205351939997854, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_assume_role[1]": 1.8269876600002135, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_assume_role[2]": 1.7289078419998987, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_function_state": 1.0743930490007187, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_lambda_different_iam_keys_environment": 3.825432293999711, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_lambda_large_response": 1.6384225279994098, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_lambda_too_large_response": 1.7626147330001913, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_lambda_too_large_response_but_with_custom_limit": 1.6690593340003943, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBaseFeatures::test_large_payloads": 1.854786544000035, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_ignore_architecture": 1.5467969449996417, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_cache_local[nodejs]": 1.656418467000094, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_cache_local[python]": 1.5471836390001954, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_init_environment": 2.217623070999707, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_invoke_no_timeout": 3.570784807000109, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_invoke_timed_out_environment_reuse": 3.7341221130009217, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_lambda_invoke_with_timeout": 3.566384669999934, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_mixed_architecture": 0.001097622000543197, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_runtime_introspection_arm": 0.0010084129999086144, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_runtime_introspection_x86": 1.6018547919998127, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaBehavior::test_runtime_ulimits": 1.555886012999963, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaCleanup::test_delete_lambda_during_sync_invoke": 0.0008003689999895869, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_lambda_concurrency_block": 2.5495790990003115, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_lambda_concurrency_crud": 1.0890948420001223, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_lambda_provisioned_concurrency_moves_with_alias": 0.0010674259997358604, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_lambda_provisioned_concurrency_scheduling": 8.189281024999673, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_provisioned_concurrency": 4.633704119999948, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_reserved_concurrency": 4.169220324999969, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_reserved_concurrency_async_queue": 19.14029518899997, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaConcurrency::test_reserved_provisioned_overlap": 3.165988672000367, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_handler_error": 1.4855193749999671, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_handler_exit": 0.0009853330002442817, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_invoke_payload_encoding_error[body-n\\x87r\\x9e\\xe9\\xb5\\xd7I\\xee\\x9bmt]": 1.1149771420000434, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_invoke_payload_encoding_error[message-\\x99\\xeb,j\\x07\\xa1zYh]": 1.113637703999757, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_runtime_error": 1.6869561220000833, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_runtime_exit": 0.0008245950002674363, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_runtime_exit_segfault": 0.0007770349998281745, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_runtime_startup_error": 1.4861032949997934, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_runtime_startup_timeout": 21.11425623100058, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaErrors::test_lambda_runtime_wrapper_not_found": 0.0009651660002418794, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_dry_run[nodejs16.x]": 0.0009746109999468899, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_dry_run[python3.10]": 0.0009216930002367008, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_event[nodejs16.x]": 2.1071475539997664, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_event[python3.10]": 2.1073558360003517, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_event_error": 0.0015327589999287738, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_request_response[nodejs16.x]": 1.5671771280003668, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_type_request_response[python3.10]": 1.5471004670002912, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_with_logs[nodejs16.x]": 1.665314483000202, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_with_logs[python3.10]": 1.5805960290003895, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invocation_with_qualifier": 1.7148426030003066, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_invoke_exceptions": 0.04611378399977184, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_lambda_with_context": 0.0009853229998952884, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaFeatures::test_upload_lambda_from_s3": 1.8310103119993073, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaMultiAccounts::test_cross_account_access": 3.1509439840001505, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaPermissions::test_lambda_permission_url_invocation": 0.0010181729999203526, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_update_function_url_config": 1.1859974169997258, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_http_fixture_default": 1.7474183580002318, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_http_fixture_trim_x_headers": 1.6882037899999887, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_invoke": 1.1667230950001795, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_invoke[BUFFERED]": 1.6534030509997137, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_invoke[None]": 1.8632305450000786, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_echo_invoke[RESPONSE_STREAM]": 0.004370184000435984, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_headers_and_status": 1.5172227749999365, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invalid_invoke_mode": 1.1576342240000486, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[boolean]": 1.6315115350002998, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[dict]": 1.7450560580000456, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[float]": 1.6184870260003663, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[http-response-json]": 1.7572113309997803, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[http-response]": 1.6418214300001637, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[integer]": 1.6382678610002586, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[list-mixed]": 1.6981820959999823, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation[string]": 1.627201650999723, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_invocation_exception": 1.6577881379998871, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaURL::test_lambda_url_non_existing_url": 0.0141777430003458, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaVersions::test_lambda_handler_update": 2.1011096389997874, + "tests/aws/services/lambda_/test_lambda.py::TestLambdaVersions::test_lambda_versions_with_code_changes": 5.445134016000338, + "tests/aws/services/lambda_/test_lambda.py::TestRequestIdHandling::test_request_id_async_invoke_with_retry": 11.09204713600002, + "tests/aws/services/lambda_/test_lambda.py::TestRequestIdHandling::test_request_id_format": 0.011055338000005577, + "tests/aws/services/lambda_/test_lambda.py::TestRequestIdHandling::test_request_id_invoke": 3.551337884000077, + "tests/aws/services/lambda_/test_lambda.py::TestRequestIdHandling::test_request_id_invoke_url": 3.5757050050001453, + "tests/aws/services/lambda_/test_lambda_api.py::TestCodeSigningConfig::test_code_signing_not_found_excs": 1.1119115700003022, + "tests/aws/services/lambda_/test_lambda_api.py::TestCodeSigningConfig::test_function_code_signing_config": 1.1158772479998333, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAccountSettings::test_account_settings": 0.03921893100005036, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAccountSettings::test_account_settings_total_code_size": 1.1550952470001903, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAccountSettings::test_account_settings_total_code_size_config_update": 1.114706058000138, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAlias::test_alias_lifecycle": 1.204180670999449, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAlias::test_alias_naming": 1.5607231300000421, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaAlias::test_notfound_and_invalid_routingconfigs": 2.1678496339995945, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventInvokeConfig::test_lambda_eventinvokeconfig_exceptions": 2.267789251999602, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventInvokeConfig::test_lambda_eventinvokeconfig_lifecycle": 2.548250762000407, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventSourceMappings::test_create_event_source_validation": 3.1502962459994706, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventSourceMappings::test_event_source_mapping_exceptions": 0.05655696200028615, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventSourceMappings::test_event_source_mapping_lifecycle": 3.447348085999238, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaEventSourceMappings::test_function_name_variations": 15.268256421000387, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_create_lambda_exceptions": 0.05937461400026223, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_delete_on_nonexisting_version": 1.0890171529999861, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_function_arns": 2.185245990000112, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_function_lifecycle": 2.158740977999514, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[delete_function]": 1.0704339099997924, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function]": 1.0728652149996378, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function_code_signing_config]": 1.0693792779998148, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function_concurrency]": 1.085993655999573, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function_configuration]": 1.0777374330004932, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function_event_invoke_config]": 1.0763786589996016, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[get_function_url_config]": 1.0759586039998794, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_get_function_wrong_region[invoke]": 1.0756664549999186, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_invalid_invoke": 0.03530466999973214, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_code_location_s3": 1.288397654000164, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_code_location_zipfile": 1.1762426100003722, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_concurrent_code_updates": 2.1106833879998703, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_lambda_concurrent_config_updates": 2.1095157359995937, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_list_functions": 2.2028509110000414, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[delete_function]": 0.03614957099989624, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function]": 0.035907537000184675, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function_code_signing_config]": 0.04126884700008304, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function_concurrency]": 0.035521417999916594, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function_configuration]": 0.03631637899979978, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function_event_invoke_config]": 0.03766943699974945, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_fn[get_function_url_config]": 0.037411303000226326, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_version[get_function]": 1.0696540960002494, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_version[get_function_configuration]": 1.0714918659996329, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_on_nonexisting_version[get_function_event_invoke_config]": 1.0696314650003842, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_with_arn_qualifier_mismatch[delete_function]": 0.044321543000478414, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_with_arn_qualifier_mismatch[get_function]": 0.03774709199979043, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_ops_with_arn_qualifier_mismatch[get_function_configuration]": 0.03682659900005092, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_redundant_updates": 2.118458174999887, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_update_lambda_exceptions": 1.0721345879996989, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaFunction::test_vpc_config": 2.2121030690000225, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaImages::test_lambda_image_and_image_config_crud": 3.928998978000436, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaImages::test_lambda_image_crud": 8.375515361999987, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaImages::test_lambda_image_versions": 4.841139360000398, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaImages::test_lambda_zip_file_to_image": 1.4690828829998281, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_compatibilities[runtimes0]": 0.04579837899973427, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_compatibilities[runtimes1]": 0.04773662299976422, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_exceptions": 0.1046400790005464, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_function_exceptions": 17.160172558999875, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_function_quota_exception": 16.12074918299959, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_lifecycle": 2.1553643920001377, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_policy_exceptions": 0.07886915700009922, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_policy_lifecycle": 0.06106029199963814, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaLayer::test_layer_s3_content": 0.3012666299996454, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_add_lambda_permission_aws": 1.080864981999639, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_add_lambda_permission_fields": 1.1135773940000036, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_create_multiple_lambda_permissions": 1.0886676040004204, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_lambda_permission_fn_versioning": 1.1431614960001752, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_permission_exceptions": 1.186450571000023, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaPermissions::test_remove_multi_permissions": 1.0974148309996963, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaProvisionedConcurrency::test_lambda_provisioned_lifecycle": 2.289023255000302, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaProvisionedConcurrency::test_provisioned_concurrency_exceptions": 1.143705831999796, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaProvisionedConcurrency::test_provisioned_concurrency_limits": 1.0956666649999534, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaReservedConcurrency::test_function_concurrency": 1.1028478060002271, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaReservedConcurrency::test_function_concurrency_exceptions": 1.0836544280000453, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaReservedConcurrency::test_function_concurrency_limits": 1.0755841739996868, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaRevisions::test_function_revisions_basic": 3.2176875750001273, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaRevisions::test_function_revisions_permissions": 1.096526551000352, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaRevisions::test_function_revisions_version_and_alias": 1.1328962769994178, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_lambda_envvars_near_limit_succeeds": 1.1057132410001032, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_large_environment_fails_multiple_keys": 16.08274534200018, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_large_environment_variables_fails": 16.092935292000675, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_large_lambda": 11.962990838999303, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_oversized_request_create_lambda": 1.3062144750006155, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_oversized_unzipped_lambda": 5.969584674000089, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSizeLimits::test_oversized_zipped_create_lambda": 1.3703560520002611, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_exceptions": 0.04561908999994557, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java11]": 7.104708442999254, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java17]": 7.117654287000278, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_lifecycle[java21]": 7.1141764529998, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java11]": 1.1935880720002388, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java17]": 1.0834430220002105, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaSnapStart::test_snapstart_update_function_configuration[java21]": 1.0867318530004013, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTag::test_create_tag_on_fn_create": 1.086086339999838, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTag::test_tag_lifecycle": 1.1147009539995452, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTag::test_tag_nonexisting_resource": 1.0967512350002835, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTags::test_tag_exceptions": 1.1457000089994835, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTags::test_tag_lifecycle": 1.128356187999998, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTags::test_tag_limits": 1.1113983930003997, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaTags::test_tag_versions": 1.0944987909997508, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaUrl::test_url_config_exceptions": 1.2067866269999286, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaUrl::test_url_config_lifecycle": 1.1110591950000526, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaUrl::test_url_config_list_paging": 1.1636455970001407, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaVersions::test_publish_version_on_create": 1.121282820000033, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaVersions::test_publish_with_update": 1.1330524159998276, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaVersions::test_publish_with_wrong_sha256": 1.125072490999628, + "tests/aws/services/lambda_/test_lambda_api.py::TestLambdaVersions::test_version_lifecycle": 2.1705743210004584, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_advanced_logging_configuration_format_switch": 1.1189293719994566, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_function_advanced_logging_configuration": 1.122483977000229, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_function_partial_advanced_logging_configuration_update[partial_config0]": 1.1015625420000106, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_function_partial_advanced_logging_configuration_update[partial_config1]": 1.1004996309998205, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_function_partial_advanced_logging_configuration_update[partial_config2]": 1.0972724610001023, + "tests/aws/services/lambda_/test_lambda_api.py::TestLoggingConfig::test_function_partial_advanced_logging_configuration_update[partial_config3]": 1.0984219930001018, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_calling_localstack_from_lambda[dotnet6]": 2.26765118000003, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_calling_localstack_from_lambda[go1.x]": 2.184898118000092, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_calling_localstack_from_lambda[java11]": 3.8858248880001156, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_calling_localstack_from_lambda[java8.al2]": 3.8139762789996894, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_calling_localstack_from_lambda[nodejs14.x]": 2.1623551780003254, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_calling_localstack_from_lambda[nodejs16.x]": 1.328568768000423, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_calling_localstack_from_lambda[nodejs18.x]": 2.1533279730001595, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_calling_localstack_from_lambda[python3.10]": 2.125166618000094, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_calling_localstack_from_lambda[python3.11]": 2.183334459999969, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_calling_localstack_from_lambda[python3.7]": 2.3004883610001343, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_calling_localstack_from_lambda[python3.8]": 2.1812802599997667, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_calling_localstack_from_lambda[python3.9]": 2.1334158660001776, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_calling_localstack_from_lambda[ruby2.7]": 2.529864799000279, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_calling_localstack_from_lambda[ruby3.2]": 1.717195716000333, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[dotnet6]": 1.8801766070000667, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[dotnet8]": 1.8348592380002628, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[java11]": 4.628829770000266, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[java17]": 3.466601069000262, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[java21]": 3.5975333439996575, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[java8.al2]": 5.416220033000627, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[nodejs16.x]": 2.015136672000608, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[nodejs18.x]": 1.7189846520004721, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[nodejs20.x]": 1.7137619559998711, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.10]": 1.7727077039994583, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.11]": 1.9151684709995607, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.12]": 1.752162999000575, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.8]": 1.8866827859997102, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[python3.9]": 1.7797347069999887, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[ruby3.2]": 2.641906003999793, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaCallingLocalstack::test_manual_endpoint_injection[ruby3.3]": 2.1991673670004275, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[dotnet6]": 4.009110503000102, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[dotnet8]": 8.043166448999727, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[go1.x]": 2.861501092999788, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[java11]": 1.9459150299999237, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[java17]": 1.9317060250000395, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[java21]": 1.89305970300029, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[java8.al2]": 4.962275508999937, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[java8]": 3.427036121000583, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[nodejs14.x]": 2.656573449000007, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[nodejs16.x]": 1.930867434999982, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[nodejs18.x]": 1.9196643350001068, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[nodejs20.x]": 1.983470698000474, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[provided.al2023]": 3.901965659999405, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[provided.al2]": 3.8118438050000805, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[provided]": 5.140870491999976, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.10]": 1.961895234000167, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.11]": 2.0006972669998504, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.12]": 2.076224959000683, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.7]": 2.758442094000202, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.8]": 2.1726769969995985, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[python3.9]": 1.9821970020002482, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[ruby2.7]": 7.529357513999912, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[ruby3.2]": 11.008413206999649, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_echo_invoke[ruby3.3]": 11.036359124000228, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[dotnet6]": 2.2628364830002283, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[dotnet8]": 2.242898785000307, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[go1.x]": 2.9491659200002687, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[java11]": 2.463655427000049, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[java17]": 2.2676663900001586, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[java21]": 2.346105055999942, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[java8.al2]": 2.535549637999793, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[java8]": 3.865341088999685, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[nodejs14.x]": 1.6837163030008924, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[nodejs16.x]": 3.4188527149999572, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[nodejs18.x]": 2.2643161049995797, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[nodejs20.x]": 2.256735130999914, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[provided.al2023]": 2.1486758950004514, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[provided.al2]": 2.1568912950001504, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[provided]": 2.8246409459998176, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.10]": 2.0841572009999254, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.11]": 2.12661745000014, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.12]": 2.1557660859998578, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.7]": 2.8496893530000307, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.8]": 2.188326099000278, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[python3.9]": 2.1615455490000386, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[ruby2.7]": 1.7697625890000381, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[ruby3.2]": 2.2113221049999083, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_introspection_invoke[ruby3.3]": 3.427151830000639, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[dotnet6]": 1.7060418960004426, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[dotnet8]": 1.7061332710004535, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[java11]": 1.8370262580001508, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[java17]": 1.7067100239996762, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[java21]": 1.7452421970001524, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[java8.al2]": 1.9890925170002447, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[nodejs14.x]": 0.9566411029991286, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[nodejs16.x]": 1.5469671740002013, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[nodejs18.x]": 1.5730732329998318, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[nodejs20.x]": 1.5401581590003843, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.10]": 1.5285084120000647, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.11]": 1.5452323729996351, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.12]": 1.5136136749997604, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.8]": 1.5700055639999846, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[python3.9]": 1.5495447130001594, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[ruby3.2]": 1.6265611419999004, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_runtime_wrapper_invoke[ruby3.3]": 1.6326672010004586, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[dotnet6]": 1.6891303269999298, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[dotnet8]": 1.6949536249994708, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[go1.x]": 2.1613094270001056, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[java11]": 1.8810508269998536, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[java17]": 1.736745558000166, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[java21]": 1.7282320119993528, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[java8.al2]": 2.005961630999991, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[java8]": 3.1225874159999876, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[nodejs14.x]": 1.8524841249995916, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[nodejs16.x]": 1.6142605529994398, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[nodejs18.x]": 1.5838172869998743, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[nodejs20.x]": 1.580819087999771, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[provided.al2023]": 1.5516861819996848, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[provided.al2]": 1.5685702279997713, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[provided]": 1.9608117909997418, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.10]": 1.566482881000411, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.11]": 1.5270164749999822, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.12]": 1.5756484139997156, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.7]": 0.973754973999803, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.8]": 1.5650490650000393, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[python3.9]": 1.5309913609999057, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[ruby2.7]": 0.9466098849993614, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[ruby3.2]": 1.6441481789997852, + "tests/aws/services/lambda_/test_lambda_common.py::TestLambdaRuntimesCommon::test_uncaught_exception_invoke[ruby3.3]": 1.6234038679999685, + "tests/aws/services/lambda_/test_lambda_destinations.py::TestLambdaDLQ::test_dead_letter_queue": 22.095704919999662, + "tests/aws/services/lambda_/test_lambda_destinations.py::TestLambdaDestinationEventbridge::test_invoke_lambda_eventbridge": 14.686085979999916, + "tests/aws/services/lambda_/test_lambda_destinations.py::TestLambdaDestinationSqs::test_assess_lambda_destination_invocation[payload0]": 1.6954127259996312, + "tests/aws/services/lambda_/test_lambda_destinations.py::TestLambdaDestinationSqs::test_assess_lambda_destination_invocation[payload1]": 1.7033893190000526, + "tests/aws/services/lambda_/test_lambda_destinations.py::TestLambdaDestinationSqs::test_lambda_destination_default_retries": 21.138937602999704, + "tests/aws/services/lambda_/test_lambda_destinations.py::TestLambdaDestinationSqs::test_maxeventage": 63.401248436999595, + "tests/aws/services/lambda_/test_lambda_destinations.py::TestLambdaDestinationSqs::test_retries": 22.194171198000276, + "tests/aws/services/lambda_/test_lambda_developer_tools.py::TestDockerFlags::test_additional_docker_flags": 1.5069035440001244, + "tests/aws/services/lambda_/test_lambda_developer_tools.py::TestDockerFlags::test_lambda_docker_networks": 8.083727937000276, + "tests/aws/services/lambda_/test_lambda_developer_tools.py::TestHotReloading::test_hot_reloading[nodejs18.x]": 3.06303496099963, + "tests/aws/services/lambda_/test_lambda_developer_tools.py::TestHotReloading::test_hot_reloading[nodejs20.x]": 4.197784885999681, + "tests/aws/services/lambda_/test_lambda_developer_tools.py::TestHotReloading::test_hot_reloading[python3.12]": 6.424590939999689, + "tests/aws/services/lambda_/test_lambda_developer_tools.py::TestHotReloading::test_hot_reloading[python3.9]": 2.900148914000056, + "tests/aws/services/lambda_/test_lambda_developer_tools.py::TestHotReloading::test_hot_reloading_publish_version": 1.0489957139993749, + "tests/aws/services/lambda_/test_lambda_developer_tools.py::TestLambdaDNS::test_lambda_localhost_localstack_cloud_connectivity": 0.0008847130002322956, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_deletion_event_source_mapping_with_dynamodb": 6.989548434999961, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_disabled_dynamodb_event_source_mapping": 11.398254667000401, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_duplicate_event_source_mappings": 4.640086005000285, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put10-None-filter0-1]": 13.56093944200029, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put11-item_to_put21-filter1-2]": 13.692982211000071, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put12-item_to_put22-filter2-1]": 13.578544705999775, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put13-item_to_put23-filter3-1]": 13.72367292600029, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put14-item_to_put24-filter4-0]": 13.460648560000209, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put15-item_to_put25-filter5-0]": 13.429647027999636, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_filter[item_to_put16-item_to_put26-filter6-1]": 13.461413060000268, + "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_source_mapping": 13.954579511999782, "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_event_source_mapping_with_on_failure_destination_config": 12.372332623000148, "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_invalid_event_filter[[{\"eventName\": [\"INSERT\"=123}]]": 5.250110176000362, "tests/aws/services/lambda_/test_lambda_integration_dynamodbstreams.py::TestDynamoDBEventSourceMapping::test_dynamodb_invalid_event_filter[single-string]": 5.447889583000233, From 7b2474f8f100878b71cdc45768e87eb837f2077b Mon Sep 17 00:00:00 2001 From: Daniel Fangl <daniel.fangl@localstack.cloud> Date: Thu, 16 May 2024 11:26:30 +0200 Subject: [PATCH 148/169] Revert "APIGW: Default to an empty dict when the provided body for an API Gateway step function is an empty string" (#10835) --- localstack/services/apigateway/templates.py | 2 +- tests/unit/test_templating.py | 23 --------------------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/localstack/services/apigateway/templates.py b/localstack/services/apigateway/templates.py index 90ae240d32941..9dc1422b9afdd 100644 --- a/localstack/services/apigateway/templates.py +++ b/localstack/services/apigateway/templates.py @@ -190,7 +190,7 @@ def prepare_namespace(self, variables) -> Dict[str, Any]: namespace["stageVariables"] = stage_var input_var = variables.get("input") or {} variables = { - "input": VelocityInput(input_var.get("body") or {}, input_var.get("params")), + "input": VelocityInput(input_var.get("body"), input_var.get("params")), "util": VelocityUtilApiGateway(), } namespace.update(variables) diff --git a/tests/unit/test_templating.py b/tests/unit/test_templating.py index 67872019162e7..f93ff996aed34 100644 --- a/tests/unit/test_templating.py +++ b/tests/unit/test_templating.py @@ -80,21 +80,6 @@ } """ -APIGW_TEMPLATE_BODY_FORWARDING_ONLY = """ -## Template that attempts to forward the request body to the execution input of -## the state machine. - -#set($inputString = '') -#set($allParams = $input.params()) -{ - #set($inputString = "$inputString,@@body@@: $input.body") - #set($inputString = "$inputString}") - #set($inputString = $inputString.replaceAll("@@",'"')) - #set($len = $inputString.length() - 1) - "input": "{$util.escapeJavaScript($inputString.substring(1,$len)).replaceAll("\\'","'")}" -} -""" - class TestMessageTransformationBasic: def test_return_macro(self): @@ -197,14 +182,6 @@ def test_array_size(self): result = ApiGatewayVtlTemplate().render_vtl(template, variables) assert result == " 2" - def test_template_rendering_with_empty_string_body(self): - template = APIGW_TEMPLATE_BODY_FORWARDING_ONLY - variables = {"input": {"body": ""}} - result = ApiGatewayVtlTemplate().render_vtl(template, variables) - result = re.sub(r"\s+", " ", result).strip() - result = json.loads(result) - assert result == {"input": '{"body": {}}'} - def test_message_transformation(self): template = APIGW_TEMPLATE_TRANSFORM_KINESIS records = [ From 2bce22cb874e44d9c2725c6bdd16a8339927f2b0 Mon Sep 17 00:00:00 2001 From: Dominik Schubert <dominik.schubert91@gmail.com> Date: Thu, 16 May 2024 11:29:37 +0200 Subject: [PATCH 149/169] Add image sha to startup information and more details to bug report template (#9761) --- .github/ISSUE_TEMPLATE/bug-report.yml | 13 ++++++++++++- localstack/services/infra.py | 17 +++++++++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 8c7922f005c22..16e4bab6fbb37 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -57,10 +57,21 @@ body: description: | examples: - **OS**: Ubuntu 20.04 - - **LocalStack**: latest + - **LocalStack**: + You can find this information in the logs when starting localstack + + LocalStack version: 3.4.1.dev + LocalStack Docker image sha: sha256:f02ab8ef73f66b0ab26bb3d24a165e1066a714355f79a42bf8aa1a336d5722e7 + LocalStack build date: 2024-05-14 + LocalStack build git hash: ecd7dc879 + value: | - OS: - LocalStack: + LocalStack version: + LocalStack Docker image sha: + LocalStack build date: + LocalStack build git hash: render: markdown validations: required: false diff --git a/localstack/services/infra.py b/localstack/services/infra.py index d54eb64ea0cd5..917b8b41ab8c9 100644 --- a/localstack/services/infra.py +++ b/localstack/services/infra.py @@ -20,7 +20,9 @@ setup_logging, should_eager_load_api, ) -from localstack.utils.container_networking import get_main_container_id +from localstack.utils.container_networking import get_main_container_name +from localstack.utils.container_utils.container_client import ContainerException +from localstack.utils.docker_utils import DOCKER_CLIENT from localstack.utils.files import cleanup_tmp_files from localstack.utils.net import is_port_open from localstack.utils.patch import patch @@ -200,9 +202,16 @@ def print_runtime_information(in_docker=False): print() print(f"LocalStack version: {VERSION}") if in_docker: - id = get_main_container_id() - if id: - print("LocalStack Docker container id: %s" % id[:12]) + try: + container_name = get_main_container_name() + print("LocalStack Docker container name: %s" % container_name) + inspect_result = DOCKER_CLIENT.inspect_container(container_name) + container_id = inspect_result["Id"] + print("LocalStack Docker container id: %s" % container_id[:12]) + image_sha = inspect_result["Image"] + print("LocalStack Docker image sha: %s" % image_sha) + except ContainerException as e: + print("Failed to inspect docker container: %s %s" % (e, traceback.format_exc())) if config.LOCALSTACK_BUILD_DATE: print("LocalStack build date: %s" % config.LOCALSTACK_BUILD_DATE) From b59f520584cebaefe3d5a9f036e23add60c82297 Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Thu, 16 May 2024 12:05:27 +0200 Subject: [PATCH 150/169] skip flaky test_lambda::test_reserved_concurrency_async_queue (#10834) --- tests/aws/services/lambda_/test_lambda.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/aws/services/lambda_/test_lambda.py b/tests/aws/services/lambda_/test_lambda.py index 668f99032ab53..6d90a4c84acba 100644 --- a/tests/aws/services/lambda_/test_lambda.py +++ b/tests/aws/services/lambda_/test_lambda.py @@ -2278,6 +2278,7 @@ def _invoke_lambda(): assert not errored @markers.aws.validated + @pytest.mark.skip(reason="flaky") def test_reserved_concurrency_async_queue(self, create_lambda_function, snapshot, aws_client): min_concurrent_executions = 10 + 3 check_concurrency_quota(aws_client, min_concurrent_executions) From 780b6cb5b77c7e035a95880a78484dd1f8f7e083 Mon Sep 17 00:00:00 2001 From: Alexander Rashed <2796604+alexrashed@users.noreply.github.com> Date: Thu, 16 May 2024 13:46:24 +0200 Subject: [PATCH 151/169] mitigate CI timeouts by increasing parallelism (#10837) --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index cde3d127c506f..aa8ca73c4931c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -366,7 +366,7 @@ jobs: image: << parameters.machine_image >> resource_class: << parameters.resource_class >> working_directory: /tmp/workspace/repo - parallelism: 4 + parallelism: 5 environment: PYTEST_LOGLEVEL: << pipeline.parameters.PYTEST_LOGLEVEL >> steps: From 59afae36536c2f0fd40919632336d2a018ea0003 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Thu, 16 May 2024 13:58:53 +0200 Subject: [PATCH 152/169] fix SNS test with wrong base64 assumption (#10838) --- tests/aws/services/sns/test_sns.py | 2 +- tests/aws/services/sns/test_sns.snapshot.json | 2 +- tests/aws/services/sns/test_sns.validation.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/aws/services/sns/test_sns.py b/tests/aws/services/sns/test_sns.py index df0516928c9e8..0f240aa054c70 100644 --- a/tests/aws/services/sns/test_sns.py +++ b/tests/aws/services/sns/test_sns.py @@ -843,7 +843,7 @@ def test_list_subscriptions_by_topic_pagination( # not snapshotting the results, it contains 100 entries assert "NextToken" in response # seems to be b64 encoded - assert response["NextToken"].endswith("==") + assert base64.b64decode(response["NextToken"]) assert len(response["Subscriptions"]) == 100 # keep the page 1 subscriptions ARNs page_1_subs = {sub["SubscriptionArn"] for sub in response["Subscriptions"]} diff --git a/tests/aws/services/sns/test_sns.snapshot.json b/tests/aws/services/sns/test_sns.snapshot.json index 7eb1125759218..20c9ca68f9e9d 100644 --- a/tests/aws/services/sns/test_sns.snapshot.json +++ b/tests/aws/services/sns/test_sns.snapshot.json @@ -4074,7 +4074,7 @@ } }, "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_list_subscriptions_by_topic_pagination": { - "recorded-date": "13-05-2024, 12:50:25", + "recorded-date": "16-05-2024, 10:32:16", "recorded-content": { "list-sub-per-topic-page-2": { "Subscriptions": [ diff --git a/tests/aws/services/sns/test_sns.validation.json b/tests/aws/services/sns/test_sns.validation.json index 3851b9122493f..521608b21ec89 100644 --- a/tests/aws/services/sns/test_sns.validation.json +++ b/tests/aws/services/sns/test_sns.validation.json @@ -57,7 +57,7 @@ "last_validated_date": "2023-08-25T14:23:53+00:00" }, "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_list_subscriptions_by_topic_pagination": { - "last_validated_date": "2024-05-13T12:50:03+00:00" + "last_validated_date": "2024-05-16T10:31:56+00:00" }, "tests/aws/services/sns/test_sns.py::TestSNSSubscriptionCrud::test_not_found_error_on_set_subscription_attributes": { "last_validated_date": "2023-08-24T21:27:55+00:00" From 50b2064ceaf80ca13162dd214e1675505665fcf3 Mon Sep 17 00:00:00 2001 From: Thomas Rausch <thomas@thrau.at> Date: Thu, 16 May 2024 15:31:28 +0200 Subject: [PATCH 153/169] update plux to 1.10.0 (#10830) --- pyproject.toml | 5 ++--- requirements-base-runtime.txt | 8 +------- requirements-basic.txt | 8 +------- requirements-dev.txt | 8 +------- requirements-runtime.txt | 8 +------- requirements-test.txt | 8 +------- requirements-typehint.txt | 8 +------- 7 files changed, 8 insertions(+), 45 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2b9af5cc8a599..78a89a50e437f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ # LocalStack project configuration [build-system] -requires = ['setuptools', 'wheel', 'plux>=1.7'] +requires = ['setuptools', 'wheel', 'plux>=1.10'] build-backend = "setuptools.build_meta" [project] @@ -18,14 +18,13 @@ dependencies = [ "dill==0.3.6", "dnslib>=0.9.10", "dnspython>=1.16.0", - "plux>=1.7", + "plux>=1.10", "psutil>=5.4.8", "python-dotenv>=0.19.1", "pyyaml>=5.1", "rich>=12.3.0", "requests>=2.20.0", "semver>=2.10", - "stevedore>=3.4.0", "tailer>=0.4.1", ] dynamic = ["version"] diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index c1fd05e40c5be..ff7b27baf98d5 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -112,9 +112,7 @@ packaging==24.0 # via # build # docker -pbr==6.0.0 - # via stevedore -plux==1.9.0 +plux==1.10.0 # via localstack-core (pyproject.toml) priority==1.3.0 # via @@ -162,10 +160,6 @@ six==1.16.0 # via # python-dateutil # requests-aws4auth -stevedore==5.2.0 - # via - # localstack-core (pyproject.toml) - # plux tailer==0.4.1 # via localstack-core (pyproject.toml) typing-extensions==4.11.0 diff --git a/requirements-basic.txt b/requirements-basic.txt index 9e6d84eb302f7..345ce23095ded 100644 --- a/requirements-basic.txt +++ b/requirements-basic.txt @@ -32,9 +32,7 @@ mdurl==0.1.2 # via markdown-it-py packaging==24.0 # via build -pbr==6.0.0 - # via stevedore -plux==1.9.0 +plux==1.10.0 # via localstack-core (pyproject.toml) psutil==5.9.8 # via localstack-core (pyproject.toml) @@ -54,10 +52,6 @@ rich==13.7.1 # via localstack-core (pyproject.toml) semver==3.0.2 # via localstack-core (pyproject.toml) -stevedore==5.2.0 - # via - # localstack-core (pyproject.toml) - # plux tailer==0.4.1 # via localstack-core (pyproject.toml) urllib3==2.2.1 diff --git a/requirements-dev.txt b/requirements-dev.txt index ed612615091d8..11711334cd068 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -311,7 +311,6 @@ pbr==6.0.0 # via # jschema-to-python # sarif-om - # stevedore platformdirs==4.2.1 # via virtualenv pluggy==1.5.0 @@ -320,7 +319,7 @@ pluggy==1.5.0 # pytest plumbum==1.8.3 # via pandoc -plux==1.9.0 +plux==1.10.0 # via # localstack-core # localstack-core (pyproject.toml) @@ -474,11 +473,6 @@ sniffio==1.3.1 # via # anyio # httpx -stevedore==5.2.0 - # via - # localstack-core - # localstack-core (pyproject.toml) - # plux sympy==1.12 # via cfn-lint tailer==0.4.1 diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 3657526f80515..01239d68ea18b 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -238,8 +238,7 @@ pbr==6.0.0 # via # jschema-to-python # sarif-om - # stevedore -plux==1.9.0 +plux==1.10.0 # via # localstack-core # localstack-core (pyproject.toml) @@ -355,11 +354,6 @@ six==1.16.0 # python-dateutil # requests-aws4auth # rfc3339-validator -stevedore==5.2.0 - # via - # localstack-core - # localstack-core (pyproject.toml) - # plux sympy==1.12 # via cfn-lint tailer==0.4.1 diff --git a/requirements-test.txt b/requirements-test.txt index 3ac1f7c2bf030..31cba95a33f09 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -289,12 +289,11 @@ pbr==6.0.0 # via # jschema-to-python # sarif-om - # stevedore pluggy==1.5.0 # via # localstack-core (pyproject.toml) # pytest -plux==1.9.0 +plux==1.10.0 # via # localstack-core # localstack-core (pyproject.toml) @@ -437,11 +436,6 @@ sniffio==1.3.1 # via # anyio # httpx -stevedore==5.2.0 - # via - # localstack-core - # localstack-core (pyproject.toml) - # plux sympy==1.12 # via cfn-lint tailer==0.4.1 diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 4aaf9a535379b..47c8f5f031b4a 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -507,7 +507,6 @@ pbr==6.0.0 # via # jschema-to-python # sarif-om - # stevedore platformdirs==4.2.1 # via virtualenv pluggy==1.5.0 @@ -516,7 +515,7 @@ pluggy==1.5.0 # pytest plumbum==1.8.3 # via pandoc -plux==1.9.0 +plux==1.10.0 # via # localstack-core # localstack-core (pyproject.toml) @@ -670,11 +669,6 @@ sniffio==1.3.1 # via # anyio # httpx -stevedore==5.2.0 - # via - # localstack-core - # localstack-core (pyproject.toml) - # plux sympy==1.12 # via cfn-lint tailer==0.4.1 From 98dbcbc7deed8708afdc3672925dc21b19f2863d Mon Sep 17 00:00:00 2001 From: Mathieu Cloutier <79954947+cloutierMat@users.noreply.github.com> Date: Thu, 16 May 2024 10:32:05 -0600 Subject: [PATCH 154/169] validate attributes when creating SQS queues (#10820) --- localstack/services/sqs/constants.py | 6 + localstack/services/sqs/models.py | 29 ++- .../cloudformation/resources/test_sqs.py | 8 +- .../resources/test_sqs.validation.json | 6 + tests/aws/services/sqs/test_sqs.py | 42 +++- tests/aws/services/sqs/test_sqs.snapshot.json | 234 +++++++++++++++++- .../aws/services/sqs/test_sqs.validation.json | 18 ++ .../templates/sqs_fifo_autogenerate_name.yaml | 11 +- 8 files changed, 332 insertions(+), 22 deletions(-) diff --git a/localstack/services/sqs/constants.py b/localstack/services/sqs/constants.py index 98e77cbc7b26e..1d21945f04ead 100644 --- a/localstack/services/sqs/constants.py +++ b/localstack/services/sqs/constants.py @@ -31,6 +31,12 @@ QueueAttributeName.QueueArn, ] +INVALID_STANDARD_QUEUE_ATTRIBUTES = [ + QueueAttributeName.FifoQueue, + QueueAttributeName.ContentBasedDeduplication, + *INTERNAL_QUEUE_ATTRIBUTES, +] + # URL regexes for various endpoint strategies STANDARD_STRATEGY_URL_REGEX = r"sqs.(?P<region_name>[a-z0-9-]{1,})\.[^:]+:\d{4,5}\/(?P<account_id>\d{12})\/(?P<queue_name>[a-zA-Z0-9_-]+(.fifo)?)$" DOMAIN_STRATEGY_URL_REGEX = r"((?P<region_name>[a-z0-9-]{1,})\.)?queue\.[^:]+:\d{4,5}\/(?P<account_id>\d{12})\/(?P<queue_name>[a-zA-Z0-9_-]+(.fifo)?)$" diff --git a/localstack/services/sqs/models.py b/localstack/services/sqs/models.py index c4269b582b33b..2a8d77023138b 100644 --- a/localstack/services/sqs/models.py +++ b/localstack/services/sqs/models.py @@ -290,6 +290,7 @@ def __init__(self, name: str, region: str, account_id: str, attributes=None, tag self.attributes = self.default_attributes() if attributes: + self.validate_queue_attributes(attributes) self.attributes.update(attributes) self.purge_in_progress = False @@ -588,16 +589,7 @@ def _assert_queue_name(self, name): ) def validate_queue_attributes(self, attributes): - valid = [ - k[1] - for k in inspect.getmembers(QueueAttributeName) - if k not in sqs_constants.INTERNAL_QUEUE_ATTRIBUTES - ] - del valid[valid.index(QueueAttributeName.FifoQueue)] - - for k in attributes.keys(): - if k not in valid: - raise InvalidAttributeName(f"Unknown Attribute {k}.") + pass def add_permission(self, label: str, actions: list[str], account_ids: list[str]) -> None: """ @@ -881,6 +873,23 @@ def _on_remove_message(self, message: SqsMessage): # this may happen if the message no longer exists because it was removed earlier pass + def validate_queue_attributes(self, attributes): + valid = [ + k[1] + for k in inspect.getmembers( + QueueAttributeName, lambda x: isinstance(x, str) and not x.startswith("__") + ) + if k[1] not in sqs_constants.INVALID_STANDARD_QUEUE_ATTRIBUTES + ] + + for k in attributes.keys(): + if k in [QueueAttributeName.FifoThroughputLimit, QueueAttributeName.DeduplicationScope]: + raise InvalidAttributeName( + f"You can specify the {k} only when FifoQueue is set to true." + ) + if k not in valid: + raise InvalidAttributeName(f"Unknown Attribute {k}.") + class MessageGroup: message_group_id: str diff --git a/tests/aws/services/cloudformation/resources/test_sqs.py b/tests/aws/services/cloudformation/resources/test_sqs.py index ddb17ac989c69..68eabf0f2cfd7 100644 --- a/tests/aws/services/cloudformation/resources/test_sqs.py +++ b/tests/aws/services/cloudformation/resources/test_sqs.py @@ -28,19 +28,19 @@ def test_sqs_fifo_queue_generates_valid_name(deploy_cfn_template): template_path=os.path.join( os.path.dirname(__file__), "../../../templates/sqs_fifo_autogenerate_name.yaml" ), - template_mapping={"is_fifo": "true"}, + parameters={"IsFifo": "true"}, + max_wait=240, ) assert ".fifo" in result.outputs["FooQueueName"] -# FIXME: doesn't work on AWS. (known bug in cloudformation: https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/165) -@markers.aws.unknown +@markers.aws.validated def test_sqs_non_fifo_queue_generates_valid_name(deploy_cfn_template): result = deploy_cfn_template( template_path=os.path.join( os.path.dirname(__file__), "../../../templates/sqs_fifo_autogenerate_name.yaml" ), - template_mapping={"is_fifo": "false"}, + parameters={"IsFifo": "false"}, max_wait=240, ) assert ".fifo" not in result.outputs["FooQueueName"] diff --git a/tests/aws/services/cloudformation/resources/test_sqs.validation.json b/tests/aws/services/cloudformation/resources/test_sqs.validation.json index 2d70db6c86b4a..f15ceda39289b 100644 --- a/tests/aws/services/cloudformation/resources/test_sqs.validation.json +++ b/tests/aws/services/cloudformation/resources/test_sqs.validation.json @@ -1,4 +1,10 @@ { + "tests/aws/services/cloudformation/resources/test_sqs.py::test_sqs_fifo_queue_generates_valid_name": { + "last_validated_date": "2024-05-15T02:01:00+00:00" + }, + "tests/aws/services/cloudformation/resources/test_sqs.py::test_sqs_non_fifo_queue_generates_valid_name": { + "last_validated_date": "2024-05-15T01:59:34+00:00" + }, "tests/aws/services/cloudformation/resources/test_sqs.py::test_update_queue_no_change": { "last_validated_date": "2023-12-08T20:11:26+00:00" }, diff --git a/tests/aws/services/sqs/test_sqs.py b/tests/aws/services/sqs/test_sqs.py index 8a1fbe0cc586b..a075cbf9db7ab 100644 --- a/tests/aws/services/sqs/test_sqs.py +++ b/tests/aws/services/sqs/test_sqs.py @@ -627,6 +627,29 @@ def test_create_fifo_queue_with_different_attributes_raises_error( ) snapshot.match("queue-already-exists", e.value.response) + @markers.aws.validated + def test_create_standard_queue_with_fifo_attribute_raises_error( + self, sqs_create_queue, aws_sqs_client, snapshot + ): + queue_name = f"queue-{short_uid()}" + with pytest.raises(ClientError) as e: + sqs_create_queue(QueueName=queue_name, Attributes={"FifoQueue": "false"}) + snapshot.match("invalid-attribute-fifo-queue", e.value.response) + + with pytest.raises(ClientError) as e: + sqs_create_queue( + QueueName=queue_name, Attributes={"ContentBasedDeduplication": "false"} + ) + snapshot.match("invalid-attribute-content-based-deduplication", e.value.response) + + with pytest.raises(ClientError) as e: + sqs_create_queue(QueueName=queue_name, Attributes={"DeduplicationScope": "queue"}) + snapshot.match("invalid-attribute-deduplication-scope", e.value.response) + + with pytest.raises(ClientError) as e: + sqs_create_queue(QueueName=queue_name, Attributes={"FifoThroughputLimit": "perQueue"}) + snapshot.match("invalid-attribute-throughput-limit", e.value.response) + @markers.aws.validated def test_send_message_with_delay_0_works_for_fifo(self, sqs_create_queue, aws_sqs_client): # see issue https://github.com/localstack/localstack/issues/6612 @@ -3034,10 +3057,25 @@ def test_get_specific_queue_attribute_response( assert constructed_arn == get_single_attribute.get("Attributes").get("QueueArn") assert max_receive_count == redrive_policy.get("maxReceiveCount") - @pytest.mark.xfail + @markers.snapshot.skip_snapshot_verify(paths=["$..Error.Detail", "$..Error.Type"]) + @markers.aws.validated + def test_set_unsupported_attribute_standard(self, sqs_create_queue, aws_sqs_client, snapshot): + queue_name = f"queue-{short_uid()}" + queue_url = sqs_create_queue(QueueName=queue_name) + with pytest.raises(ClientError) as e: + aws_sqs_client.set_queue_attributes( + QueueUrl=queue_url, Attributes={"FifoQueue": "true"} + ) + snapshot.match("invalid-attr-name-1", e.value.response) + with pytest.raises(ClientError) as e: + aws_sqs_client.set_queue_attributes( + QueueUrl=queue_url, Attributes={"FifoQueue": "false"} + ) + snapshot.match("invalid-attr-name-2", e.value.response) + + @markers.snapshot.skip_snapshot_verify(paths=["$..Error.Detail", "$..Error.Type"]) @markers.aws.validated def test_set_unsupported_attribute_fifo(self, sqs_create_queue, aws_sqs_client, snapshot): - # TODO: behaviour diverges from AWS queue_name = f"queue-{short_uid()}" queue_url = sqs_create_queue(QueueName=queue_name) with pytest.raises(ClientError) as e: diff --git a/tests/aws/services/sqs/test_sqs.snapshot.json b/tests/aws/services/sqs/test_sqs.snapshot.json index 3f357082ec4fe..eedb9434286ff 100644 --- a/tests/aws/services/sqs/test_sqs.snapshot.json +++ b/tests/aws/services/sqs/test_sqs.snapshot.json @@ -1320,12 +1320,64 @@ } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_fifo[sqs]": { - "recorded-date": "30-04-2024, 13:33:56", - "recorded-content": {} + "recorded-date": "14-05-2024, 22:23:46", + "recorded-content": { + "invalid-attr-name-1": { + "Error": { + "Code": "InvalidAttributeName", + "Message": "Unknown Attribute FifoQueue.", + "QueryErrorCode": "InvalidAttributeName", + "Type": "Sender" + }, + "message": "Unknown Attribute FifoQueue.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "invalid-attr-name-2": { + "Error": { + "Code": "InvalidAttributeValue", + "Message": "Invalid value for the parameter FifoQueue. Reason: Modifying queue type is not supported.", + "QueryErrorCode": "InvalidAttributeValue", + "Type": "Sender" + }, + "message": "Invalid value for the parameter FifoQueue. Reason: Modifying queue type is not supported.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_fifo[sqs_query]": { - "recorded-date": "30-04-2024, 13:33:57", - "recorded-content": {} + "recorded-date": "14-05-2024, 22:23:47", + "recorded-content": { + "invalid-attr-name-1": { + "Error": { + "Code": "InvalidAttributeName", + "Detail": null, + "Message": "Unknown Attribute FifoQueue.", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "invalid-attr-name-2": { + "Error": { + "Code": "InvalidAttributeValue", + "Detail": null, + "Message": "Invalid value for the parameter FifoQueue. Reason: Modifying queue type is not supported.", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_fifo_deduplication_arrives_once_after_delete[sqs-True]": { "recorded-date": "30-04-2024, 13:34:01", @@ -3022,5 +3074,179 @@ } } } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_standard[sqs]": { + "recorded-date": "14-05-2024, 22:34:35", + "recorded-content": { + "invalid-attr-name-1": { + "Error": { + "Code": "InvalidAttributeName", + "Message": "Unknown Attribute FifoQueue.", + "QueryErrorCode": "InvalidAttributeName", + "Type": "Sender" + }, + "message": "Unknown Attribute FifoQueue.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "invalid-attr-name-2": { + "Error": { + "Code": "InvalidAttributeName", + "Message": "Unknown Attribute FifoQueue.", + "QueryErrorCode": "InvalidAttributeName", + "Type": "Sender" + }, + "message": "Unknown Attribute FifoQueue.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_standard[sqs_query]": { + "recorded-date": "14-05-2024, 22:34:35", + "recorded-content": { + "invalid-attr-name-1": { + "Error": { + "Code": "InvalidAttributeName", + "Detail": null, + "Message": "Unknown Attribute FifoQueue.", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "invalid-attr-name-2": { + "Error": { + "Code": "InvalidAttributeName", + "Detail": null, + "Message": "Unknown Attribute FifoQueue.", + "Type": "Sender" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_standard_queue_with_fifo_attribute_raises_error[sqs]": { + "recorded-date": "15-05-2024, 02:30:47", + "recorded-content": { + "invalid-attribute-fifo-queue": { + "Error": { + "Code": "InvalidAttributeName", + "Message": "Unknown Attribute FifoQueue.", + "QueryErrorCode": "InvalidAttributeName", + "Type": "Sender" + }, + "message": "Unknown Attribute FifoQueue.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "invalid-attribute-content-based-deduplication": { + "Error": { + "Code": "InvalidAttributeName", + "Message": "Unknown Attribute ContentBasedDeduplication.", + "QueryErrorCode": "InvalidAttributeName", + "Type": "Sender" + }, + "message": "Unknown Attribute ContentBasedDeduplication.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "invalid-attribute-deduplication-scope": { + "Error": { + "Code": "InvalidAttributeName", + "Message": "You can specify the DeduplicationScope only when FifoQueue is set to true.", + "QueryErrorCode": "InvalidAttributeName", + "Type": "Sender" + }, + "message": "You can specify the DeduplicationScope only when FifoQueue is set to true.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "invalid-attribute-throughput-limit": { + "Error": { + "Code": "InvalidAttributeName", + "Message": "You can specify the FifoThroughputLimit only when FifoQueue is set to true.", + "QueryErrorCode": "InvalidAttributeName", + "Type": "Sender" + }, + "message": "You can specify the FifoThroughputLimit only when FifoQueue is set to true.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_standard_queue_with_fifo_attribute_raises_error[sqs_query]": { + "recorded-date": "15-05-2024, 02:30:48", + "recorded-content": { + "invalid-attribute-fifo-queue": { + "Error": { + "Code": "InvalidAttributeName", + "Message": "Unknown Attribute FifoQueue.", + "QueryErrorCode": "InvalidAttributeName", + "Type": "Sender" + }, + "message": "Unknown Attribute FifoQueue.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "invalid-attribute-content-based-deduplication": { + "Error": { + "Code": "InvalidAttributeName", + "Message": "Unknown Attribute ContentBasedDeduplication.", + "QueryErrorCode": "InvalidAttributeName", + "Type": "Sender" + }, + "message": "Unknown Attribute ContentBasedDeduplication.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "invalid-attribute-deduplication-scope": { + "Error": { + "Code": "InvalidAttributeName", + "Message": "You can specify the DeduplicationScope only when FifoQueue is set to true.", + "QueryErrorCode": "InvalidAttributeName", + "Type": "Sender" + }, + "message": "You can specify the DeduplicationScope only when FifoQueue is set to true.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "invalid-attribute-throughput-limit": { + "Error": { + "Code": "InvalidAttributeName", + "Message": "You can specify the FifoThroughputLimit only when FifoQueue is set to true.", + "QueryErrorCode": "InvalidAttributeName", + "Type": "Sender" + }, + "message": "You can specify the FifoThroughputLimit only when FifoQueue is set to true.", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } } } diff --git a/tests/aws/services/sqs/test_sqs.validation.json b/tests/aws/services/sqs/test_sqs.validation.json index 0b7e911c76c25..1827a1bac0d4c 100644 --- a/tests/aws/services/sqs/test_sqs.validation.json +++ b/tests/aws/services/sqs/test_sqs.validation.json @@ -41,6 +41,12 @@ "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_queue_with_different_attributes_raises_exception[sqs_query]": { "last_validated_date": "2024-04-30T13:33:20+00:00" }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_standard_queue_with_fifo_attribute_raises_error[sqs]": { + "last_validated_date": "2024-05-15T02:30:47+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_create_standard_queue_with_fifo_attribute_raises_error[sqs_query]": { + "last_validated_date": "2024-05-15T02:30:48+00:00" + }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_dead_letter_queue_message_attributes": { "last_validated_date": "2024-04-30T13:33:55+00:00" }, @@ -272,6 +278,18 @@ "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_send_receive_message_multiple_queues": { "last_validated_date": "2024-04-30T13:40:05+00:00" }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_fifo[sqs]": { + "last_validated_date": "2024-05-14T22:23:46+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_fifo[sqs_query]": { + "last_validated_date": "2024-05-14T22:23:47+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_standard[sqs]": { + "last_validated_date": "2024-05-14T22:34:35+00:00" + }, + "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_set_unsupported_attribute_standard[sqs_query]": { + "last_validated_date": "2024-05-14T22:34:35+00:00" + }, "tests/aws/services/sqs/test_sqs.py::TestSqsProvider::test_sqs_fifo_same_dedup_id_different_message_groups[sqs]": { "last_validated_date": "2024-04-30T13:51:17+00:00" }, diff --git a/tests/aws/templates/sqs_fifo_autogenerate_name.yaml b/tests/aws/templates/sqs_fifo_autogenerate_name.yaml index dcc4f86643759..62e85cf2aa551 100644 --- a/tests/aws/templates/sqs_fifo_autogenerate_name.yaml +++ b/tests/aws/templates/sqs_fifo_autogenerate_name.yaml @@ -1,9 +1,16 @@ +Parameters: + IsFifo: + Type: String + +Conditions: + IsFifo: !Equals [ !Ref IsFifo, "true"] + Resources: FooQueueA2A23E59: Type: AWS::SQS::Queue Properties: - ContentBasedDeduplication: true - FifoQueue: {{ is_fifo }} + ContentBasedDeduplication: !If [ IsFifo, "true", !Ref AWS::NoValue ] + FifoQueue: !If [ IsFifo, "true", !Ref AWS::NoValue ] VisibilityTimeout: 300 UpdateReplacePolicy: Delete DeletionPolicy: Delete From 21bdd430f292504081df483be47cd786cec1467d Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Thu, 16 May 2024 20:18:35 +0200 Subject: [PATCH 155/169] implement SNS Filter/operators $or, suffix, equals-ignore-case, anything-but (#10691) Co-authored-by: Mathieu Cloutier <cloutier.mat0@gmail.com> --- localstack/services/sns/filter.py | 176 ++++++++++------ .../services/sns/test_sns_filter_policy.py | 81 ++++++- .../sns/test_sns_filter_policy.snapshot.json | 79 ++++++- .../test_sns_filter_policy.validation.json | 5 +- tests/unit/test_sns.py | 198 +++++++++++++++++- 5 files changed, 467 insertions(+), 72 deletions(-) diff --git a/localstack/services/sns/filter.py b/localstack/services/sns/filter.py index 799ea91f38f36..71bae7527a84f 100644 --- a/localstack/services/sns/filter.py +++ b/localstack/services/sns/filter.py @@ -8,15 +8,22 @@ class SubscriptionFilter: def check_filter_policy_on_message_attributes( self, filter_policy: dict, message_attributes: dict ): - for criteria, conditions in filter_policy.items(): - if not self._evaluate_filter_policy_conditions_on_attribute( - conditions, - message_attributes.get(criteria), - field_exists=criteria in message_attributes, - ): - return False + if not filter_policy: + return True - return True + flat_policy_conditions = self.flatten_policy(filter_policy) + + return any( + all( + self._evaluate_filter_policy_conditions_on_attribute( + conditions, + message_attributes.get(criteria), + field_exists=criteria in message_attributes, + ) + for criteria, conditions in flat_policy.items() + ) + for flat_policy in flat_policy_conditions + ) def check_filter_policy_on_message_body(self, filter_policy: dict, message_body: str): try: @@ -45,18 +52,26 @@ def _evaluate_nested_filter_policy_on_dict(self, filter_policy, payload: dict) - :param payload: a dict, starting at the MessageBody :return: True if the payload respect the filter policy, otherwise False """ - flat_policy = self._flatten_dict(filter_policy) - flat_payloads = self._flatten_dict_with_list(payload) - for key, values in flat_policy.items(): - if not any( - self._evaluate_condition( - flat_payload.get(key), condition, field_exists=key in flat_payload + if not filter_policy: + return True + + # TODO: maybe save/cache the flattened/expanded policy? + flat_policy_conditions = self.flatten_policy(filter_policy) + flat_payloads = self.flatten_payload(payload) + + return any( + all( + any( + self._evaluate_condition( + flat_payload.get(key), condition, field_exists=key in flat_payload + ) + for condition in values + for flat_payload in flat_payloads ) - for condition in values - for flat_payload in flat_payloads - ): - return False - return True + for key, values in flat_policy.items() + ) + for flat_policy in flat_policy_conditions + ) def _evaluate_filter_policy_conditions_on_attribute( self, conditions, attribute, field_exists: bool @@ -94,11 +109,19 @@ def _evaluate_condition(self, value, condition, field_exists: bool): # the remaining conditions require the value to not be None return False elif anything_but := condition.get("anything-but"): - # TODO: support with `prefix` - # https://docs.aws.amazon.com/sns/latest/dg/string-value-matching.html#string-anything-but-matching-prefix - return value not in anything_but - elif prefix := (condition.get("prefix")): + if isinstance(anything_but, dict): + not_prefix = anything_but.get("prefix") + return not value.startswith(not_prefix) + elif isinstance(anything_but, list): + return value not in anything_but + else: + return value != anything_but + elif prefix := condition.get("prefix"): return value.startswith(prefix) + elif suffix := condition.get("suffix"): + return value.endswith(suffix) + elif equal_ignore_case := condition.get("equals-ignore-case"): + return equal_ignore_case.lower() == value.lower() elif numeric_condition := condition.get("numeric"): return self._evaluate_numeric_condition(numeric_condition, value) return False @@ -135,35 +158,59 @@ def _evaluate_numeric_condition(conditions, value): return True @staticmethod - def _flatten_dict(nested_dict: dict): + def flatten_policy(nested_dict: dict) -> list[dict]: """ Takes a dictionary as input and will output the dictionary on a single level. Input: - `{"field1": {"field2: {"field3: "val1", "field4": "val2"}}}` + `{"field1": {"field2": {"field3": "val1", "field4": "val2"}}}` Output: - `{ - "field1.field2.field3": "val1", - "field1.field2.field4": "val1" - }` + `[ + { + "field1.field2.field3": "val1", + "field1.field2.field4": "val2" + } + ]` + Input with $or will create multiple outputs: + `{"$or": [{"field1": "val1"}, {"field2": "val2"}], "field3": "val3"}` + Output: + `[ + {"field1": "val1", "field3": "val3"}, + {"field2": "val2", "field3": "val3"} + ]` :param nested_dict: a (nested) dictionary :return: a list of flattened dictionaries with no nested dict or list inside, flattened to a single level, one list item for every list item encountered """ - flatten = {} - def _traverse(_policy: dict, parent_key=None): - for key, values in _policy.items(): - flattened_parent_key = key if not parent_key else f"{parent_key}.{key}" - if not isinstance(values, dict): - flatten[flattened_parent_key] = values + def _traverse_policy(obj, array=None, parent_key=None) -> list: + if array is None: + array = [{}] + + for key, values in obj.items(): + if key == "$or" and isinstance(values, list) and len(values) > 1: + # $or will create multiple new branches in the array. + # Each current branch will traverse with each choice in $or + array = [ + i for value in values for i in _traverse_policy(value, array, parent_key) + ] else: - _traverse(values, parent_key=flattened_parent_key) + # We update the parent key do that {"key1": {"key2": ""}} becomes "key1.key2" + _parent_key = f"{parent_key}.{key}" if parent_key else key + if isinstance(values, dict): + # If the current key has child dict -- key: "key1", child: {"key2": ["val1", val2"]} + # We only update the parent_key and traverse its children with the current branches + array = _traverse_policy(values, array, _parent_key) + else: + # If the current key has no child, this means we found the values to match -- child: ["val1", val2"] + # we update the branches with the parent chain and the values -- {"key1.key2": ["val1, val2"]} + array = [{**item, _parent_key: values} for item in array] - _traverse(nested_dict) - return flatten + return array + + return _traverse_policy(nested_dict) @staticmethod - def _flatten_dict_with_list(nested_dict: dict) -> list[dict]: + def flatten_payload(nested_dict: dict) -> list[dict]: """ Takes a dictionary as input and will output the dictionary on a single level. The dictionary can have lists containing other dictionaries, and one root level entry will be created for every @@ -189,37 +236,22 @@ def _flatten_dict_with_list(nested_dict: dict) -> list[dict]: :param nested_dict: a (nested) dictionary :return: flatten_dict: a dictionary with no nested dict inside, flattened to a single level """ - flattened = [] - current_object = {} - def _traverse(_object, parent_key=None): + def _traverse(_object: dict, array=None, parent_key=None) -> list: if isinstance(_object, dict): for key, values in _object.items(): - flattened_parent_key = key if not parent_key else f"{parent_key}.{key}" - _traverse(values, flattened_parent_key) + # We update the parent key do that {"key1": {"key2": ""}} becomes "key1.key2" + _parent_key = f"{parent_key}.{key}" if parent_key else key + array = _traverse(values, array, _parent_key) - # we don't have to worry about `parent_key` being None for list or any other type, because we have a check - # that the first object is always a dict, thus setting a parent key on first iteration elif isinstance(_object, list): - for value in _object: - if isinstance(value, (dict, list)): - _traverse(value, parent_key=parent_key) - else: - current_object[parent_key] = value - - if current_object: - flattened.append({**current_object}) - current_object.clear() + array = [i for value in _object for i in _traverse(value, array, parent_key)] else: - current_object[parent_key] = _object - - _traverse(nested_dict) + array = [{**item, parent_key: _object} for item in array] - # if the payload did not have any list, we manually append the current object - if not flattened: - flattened.append(current_object) + return array - return flattened + return _traverse(nested_dict, array=[{}], parent_key=None) class FilterPolicyValidator: @@ -340,7 +372,6 @@ def _validate_rule(self, rule: t.Any) -> None: operator, value = k, v if operator in ( - "anything-but", "equals-ignore-case", "prefix", "suffix", @@ -351,6 +382,25 @@ def _validate_rule(self, rule: t.Any) -> None: ) return + elif operator == "anything-but": + # anything-but can actually contain any kind of simple rule (str, number, and list) + if isinstance(value, list): + for v in value: + self._validate_rule(v) + + return + + # or have a nested `prefix` pattern + elif isinstance(value, dict): + for inner_operator in value.keys(): + if inner_operator != "prefix": + raise InvalidParameterException( + f"{self.error_prefix}FilterPolicy: Unsupported anything-but pattern: {inner_operator}" + ) + + self._validate_rule(value) + return + elif operator == "exists": if not isinstance(value, bool): raise InvalidParameterException( diff --git a/tests/aws/services/sns/test_sns_filter_policy.py b/tests/aws/services/sns/test_sns_filter_policy.py index f2ace9a283a60..ae5e6fce240dd 100644 --- a/tests/aws/services/sns/test_sns_filter_policy.py +++ b/tests/aws/services/sns/test_sns_filter_policy.py @@ -1067,7 +1067,6 @@ def get_messages(_queue_url: str, _received_messages: list): snapshot.match("messages", {"Messages": received_messages}) @markers.aws.validated - @pytest.mark.skip("Not yet supported by LocalStack") def test_filter_policy_on_message_body_or_attribute( self, sqs_create_queue, @@ -1237,7 +1236,7 @@ def test_validate_policy_string_operators( topic_arn = sns_create_topic()["TopicArn"] def _subscribe(policy: dict): - sns_subscription( + return sns_subscription( TopicArn=topic_arn, Protocol="sms", Endpoint=phone_number, @@ -1262,6 +1261,18 @@ def _subscribe(policy: dict): self._add_normalized_field_to_snapshot(e.value.response) snapshot.match("error-condition-is-not-list-and-operator", e.value.response) + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"suffix": []}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-condition-empty-list", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"suffix": ["test", "test2"]}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-condition-list-wrong-type", e.value.response) + with pytest.raises(ClientError) as e: filter_policy = {"key": {"suffix": "value", "prefix": "value"}} _subscribe(filter_policy) @@ -1413,6 +1424,72 @@ def _subscribe(policy: dict): self._add_normalized_field_to_snapshot(e.value.response) snapshot.match("error-condition-string", e.value.response) + @markers.aws.validated + @markers.snapshot.skip_snapshot_verify(paths=["$..Error.Message"]) + def test_validate_policy_nested_anything_but_operator( + self, + sns_create_topic, + sns_subscription, + snapshot, + aws_client, + ): + phone_number = "+123123123" + topic_arn = sns_create_topic()["TopicArn"] + + def _subscribe(policy: dict): + return sns_subscription( + TopicArn=topic_arn, + Protocol="sms", + Endpoint=phone_number, + Attributes={"FilterPolicy": json.dumps(policy)}, + ) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"anything-but": {"wrong-operator": None}}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-condition-wrong-operator", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"anything-but": {"suffix": "test"}}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-condition-anything-but-suffix", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"anything-but": {"exists": False}}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-condition-anything-but-exists", e.value.response) + + with pytest.raises(ClientError) as e: + filter_policy = {"key": [{"anything-but": {"prefix": False}}]} + _subscribe(filter_policy) + self._add_normalized_field_to_snapshot(e.value.response) + snapshot.match("error-condition-anything-but-prefix-wrong-type", e.value.response) + + # positive testing + filter_policy = {"key": [{"anything-but": {"prefix": "test-"}}]} + response = _subscribe(filter_policy) + assert "SubscriptionArn" in response + subscription_arn = response["SubscriptionArn"] + + filter_policy = {"key": [{"anything-but": ["test", "test2"]}]} + response = aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=json.dumps(filter_policy), + ) + assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 + + filter_policy = {"key": [{"anything-but": "test"}]} + response = aws_client.sns.set_subscription_attributes( + SubscriptionArn=subscription_arn, + AttributeName="FilterPolicy", + AttributeValue=json.dumps(filter_policy), + ) + assert response["ResponseMetadata"]["HTTPStatusCode"] == 200 + @markers.aws.validated def test_policy_complexity( self, diff --git a/tests/aws/services/sns/test_sns_filter_policy.snapshot.json b/tests/aws/services/sns/test_sns_filter_policy.snapshot.json index 63b9df5dbede5..7c917b09bdf62 100644 --- a/tests/aws/services/sns/test_sns_filter_policy.snapshot.json +++ b/tests/aws/services/sns/test_sns_filter_policy.snapshot.json @@ -41,7 +41,7 @@ } }, "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_string_operators": { - "recorded-date": "14-05-2024, 16:51:03", + "recorded-date": "15-05-2024, 14:39:23", "recorded-content": { "error-condition-is-numeric": { "Error": { @@ -79,6 +79,30 @@ "HTTPStatusCode": 400 } }, + "error-condition-empty-list": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: suffix match pattern must be a string\n at [Source: (String)\"{\"key\":[{\"suffix\":[]}]}\"; line: 1, column: 20]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: suffix match pattern must be a string" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-condition-list-wrong-type": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: suffix match pattern must be a string\n at [Source: (String)\"{\"key\":[{\"suffix\":[\"test\",\"test2\"]}]}\"; line: 1, column: 20]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: suffix match pattern must be a string" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, "error-condition-is-not-list-two-ops": { "Error": { "Code": "InvalidParameter", @@ -1529,5 +1553,58 @@ "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyBody::test_filter_policy_on_message_body_or_attribute": { "recorded-date": "14-05-2024, 16:51:02", "recorded-content": {} + }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_nested_anything_but_operator": { + "recorded-date": "15-05-2024, 14:39:32", + "recorded-content": { + "error-condition-wrong-operator": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Unsupported anything-but pattern: wrong-operator\n at [Source: (String)\"{\"key\":[{\"anything-but\":{\"wrong-operator\":null}}]}\"; line: 1, column: 47]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Unsupported anything-but pattern: wrong-operator" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-condition-anything-but-suffix": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Unsupported anything-but pattern: suffix\n at [Source: (String)\"{\"key\":[{\"anything-but\":{\"suffix\":\"test\"}}]}\"; line: 1, column: 36]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Unsupported anything-but pattern: suffix" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-condition-anything-but-exists": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: Unsupported anything-but pattern: exists\n at [Source: (String)\"{\"key\":[{\"anything-but\":{\"exists\":false}}]}\"; line: 1, column: 40]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: Unsupported anything-but pattern: exists" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + }, + "error-condition-anything-but-prefix-wrong-type": { + "Error": { + "Code": "InvalidParameter", + "Message": "Invalid parameter: Attributes Reason: FilterPolicy: prefix match pattern must be a string\n at [Source: (String)\"{\"key\":[{\"anything-but\":{\"prefix\":false}}]}\"; line: 1, column: 40]", + "Type": "Sender", + "_normalized": "Invalid parameter: Attributes Reason: FilterPolicy: prefix match pattern must be a string" + }, + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } } } diff --git a/tests/aws/services/sns/test_sns_filter_policy.validation.json b/tests/aws/services/sns/test_sns_filter_policy.validation.json index 67fd226d3fb71..080e798e90491 100644 --- a/tests/aws/services/sns/test_sns_filter_policy.validation.json +++ b/tests/aws/services/sns/test_sns_filter_policy.validation.json @@ -41,11 +41,14 @@ "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_exists_operator": { "last_validated_date": "2024-05-14T16:51:06+00:00" }, + "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_nested_anything_but_operator": { + "last_validated_date": "2024-05-15T14:39:32+00:00" + }, "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_numeric_operator": { "last_validated_date": "2024-05-14T16:51:06+00:00" }, "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyConditions::test_validate_policy_string_operators": { - "last_validated_date": "2024-05-14T16:51:03+00:00" + "last_validated_date": "2024-05-15T14:39:23+00:00" }, "tests/aws/services/sns/test_sns_filter_policy.py::TestSNSFilterPolicyCrud::test_set_subscription_filter_policy_scope": { "last_validated_date": "2024-05-14T16:49:11+00:00" diff --git a/tests/unit/test_sns.py b/tests/unit/test_sns.py index 9de1298f1be34..f09850fd33b8a 100644 --- a/tests/unit/test_sns.py +++ b/tests/unit/test_sns.py @@ -279,6 +279,30 @@ def test_filter_policy(self): {"filter": {"Type": "String", "Value": "type2"}}, True, ), + ( + "anything-but list filter with match", + {"filter": [{"anything-but": ["type1", "type2"]}]}, + {"filter": {"Type": "String", "Value": "type1"}}, + False, + ), + ( + "anything-but list filter with no match", + {"filter": [{"anything-but": ["type1", "type3"]}]}, + {"filter": {"Type": "String", "Value": "type2"}}, + True, + ), + ( + "anything-but string filter with prefix match", + {"filter": [{"anything-but": {"prefix": "type"}}]}, + {"filter": {"Type": "String", "Value": "type1"}}, + False, + ), + ( + "anything-but string filter with no prefix match", + {"filter": [{"anything-but": {"prefix": "type-"}}]}, + {"filter": {"Type": "String", "Value": "type1"}}, + True, + ), ( "prefix string filter with match", {"filter": [{"prefix": "typ"}]}, @@ -302,6 +326,52 @@ def test_filter_policy(self): {"filter": {"Type": "String", "Value": "type2"}}, False, ), + ( + "suffix string filter with match", + {"filter": [{"suffix": "pe1"}]}, + {"filter": {"Type": "String", "Value": "type1"}}, + True, + ), + ( + "suffix string filter match with an array", + {"filter": [{"suffix": "gby"}]}, + { + "filter": { + "Type": "String.Array", + "Value": '["soccer", "rugby", "hockey"]', + } + }, + True, + ), + ( + "suffix string filter with no match", + {"filter": [{"suffix": "test"}]}, + {"filter": {"Type": "String", "Value": "type2"}}, + False, + ), + ( + "equals-ignore-case string filter with match", + {"filter": [{"equals-ignore-case": "TYPE1"}]}, + {"filter": {"Type": "String", "Value": "type1"}}, + True, + ), + ( + "equals-ignore-case string filter match with an array", + {"filter": [{"equals-ignore-case": "RuGbY"}]}, + { + "filter": { + "Type": "String.Array", + "Value": '["soccer", "rugby", "hockey"]', + } + }, + True, + ), + ( + "equals-ignore-case string filter with no match", + {"filter": [{"equals-ignore-case": "test"}]}, + {"filter": {"Type": "String", "Value": "type2"}}, + False, + ), ( "numeric = filter with match", {"filter": [{"numeric": ["=", 300]}]}, @@ -528,15 +598,54 @@ def test_filter_policy(self): {"field": {"Type": "String.Array", "Value": "['anything']"}}, False, ), + ( + "$or ", + {"f1": ["v1"], "$or": [{"f2": ["v2"]}, {"f3": ["v3"]}]}, + {"f1": {"Type": "String", "Value": "v1"}, "f3": {"Type": "String", "Value": "v3"}}, + True, + ), + ( + "$or ", + {"f1": ["v1"], "$or": [{"f2": ["v2"]}, {"f3": ["v3"]}]}, + {"f1": {"Type": "String", "Value": "v2"}, "f3": {"Type": "String", "Value": "v3"}}, + False, + ), + ( + "$or2", + { + "f1": ["v1"], + "$or": [ + {"f2": ["v2", "v3"]}, + {"f3": ["v4"], "$or": [{"f4": ["v5", "v6"]}, {"f5": ["v7", "v8"]}]}, + ], + }, + {"f1": {"Type": "String", "Value": "v1"}, "f2": {"Type": "String", "Value": "v2"}}, + True, + ), + ( + "$or3", + { + "f1": ["v1"], + "$or": [ + {"f2": ["v2", "v3"]}, + {"f3": ["v4"], "$or": [{"f4": ["v5", "v6"]}, {"f5": ["v7", "v8"]}]}, + ], + }, + { + "f1": {"Type": "String", "Value": "v1"}, + "f3": {"Type": "String", "Value": "v4"}, + "f4": {"Type": "String", "Value": "v6"}, + }, + True, + ), ] sub_filter = SubscriptionFilter() for test in test_data: - filter_policy = test[1] - attributes = test[2] - expected = test[3] - assert expected == sub_filter.check_filter_policy_on_message_attributes( - filter_policy, attributes + _, filter_policy, attributes, expected = test + assert ( + sub_filter.check_filter_policy_on_message_attributes(filter_policy, attributes) + == expected ) def test_is_raw_message_delivery(self, subscriber): @@ -609,6 +718,16 @@ def test_filter_policy_on_message_body(self): ({"f1": ["v3", "v4"], "f2": "v5"}, False), ), ), + ( + {"f1": {"f2": ["v1"]}}, # f1.f2 must be v1 + ( + ({"f1": {"f2": "v1"}, "f3": "v4"}, True), + ({"f1": {"f2": ["v1"]}, "f3": "v4"}, True), + ({"f1": {"f4": "v1"}, "f3": "v4"}, False), + ({"f1": ["v1", "v3"], "f3": "v5"}, False), + ({"f1": "v1", "f3": "v5"}, False), + ), + ), ( {"f1": {"f2": {"f3": {"f4": ["v1"]}}}}, ( @@ -626,6 +745,37 @@ def test_filter_policy_on_message_body(self): ({"f1": [{"f2": [[{"f3": {"f4": "v2"}}, {"f3": {"f4": "v3"}}]]}]}, False), ), ), + ( + {"f1": {"f2": ["v2"]}}, + [ + ({"f3": ["v3"], "f1": {"f2": "v2"}}, True), + ], + ), + ( + { + "$or": [{"f1": ["v1", "v2"]}, {"f2": ["v3", "v4"]}], + "f3": { + "f4": ["v5"], + "$or": [ + {"f5": ["v6"]}, + {"f6": ["v7"]}, + ], + }, + }, + ( + ({"f1": "v1", "f3": {"f4": "v5", "f5": "v6"}}, True), + ({"f1": "v2", "f3": {"f4": "v5", "f5": "v6"}}, True), + ({"f2": "v3", "f3": {"f4": "v5", "f5": "v6"}}, True), + ({"f2": "v4", "f3": {"f4": "v5", "f5": "v6"}}, True), + ({"f1": "v1", "f3": {"f4": "v5", "f6": "v7"}}, True), + ({"f1": "v3", "f3": {"f4": "v5", "f6": "v7"}}, False), + ({"f2": "v1", "f3": {"f4": "v5", "f6": "v7"}}, False), + ({"f1": "v1", "f3": {"f4": "v6", "f6": "v7"}}, False), + ({"f1": "v1", "f3": {"f4": "v5", "f6": "v1"}}, False), + ({"f1": "v1", "f3": {"f6": "v7"}}, False), + ({"f1": "v1", "f3": {"f4": "v5"}}, False), + ), + ), ] sub_filter = SubscriptionFilter() @@ -763,3 +913,41 @@ def test_filter_policy_complexity(self): } rules, combinations = validator_flat.aggregate_rules(filter_policy) assert combinations == 150 + + @pytest.mark.parametrize( + "payload,expected", + [ + ( + {"f3": ["v3"], "f1": {"f2": "v2"}}, + [{"f3": "v3", "f1.f2": "v2"}], + ), + ( + {"f3": ["v3", "v4"], "f1": {"f2": "v2"}}, + [{"f3": "v3", "f1.f2": "v2"}, {"f3": "v4", "f1.f2": "v2"}], + ), + ], + ) + def test_filter_flatten_payload(self, payload, expected): + sub_filter = SubscriptionFilter() + assert sub_filter.flatten_payload(payload) == expected + + @pytest.mark.parametrize( + "policy,expected", + [ + ( + {"filter": [{"anything-but": {"prefix": "type"}}]}, + [{"filter": [{"anything-but": {"prefix": "type"}}]}], + ), + ( + {"field1": {"field2": {"field3": "val1", "field4": "val2"}}}, + [{"field1.field2.field3": "val1", "field1.field2.field4": "val2"}], + ), + ( + {"$or": [{"field1": "val1"}, {"field2": "val2"}], "field3": "val3"}, + [{"field1": "val1", "field3": "val3"}, {"field2": "val2", "field3": "val3"}], + ), + ], + ) + def test_filter_flatten_policy(self, policy, expected): + sub_filter = SubscriptionFilter() + assert sub_filter.flatten_policy(policy) == expected From 8d51830492d699e6611d8da215eac9a6c3682852 Mon Sep 17 00:00:00 2001 From: Max <max.hoheiser@gmail.com> Date: Fri, 17 May 2024 09:22:18 +0200 Subject: [PATCH 156/169] Feature: Eventbridge v2: add schedule executor (#10817) --- localstack/services/events/provider.py | 48 +- localstack/services/events/rule.py | 19 +- localstack/services/events/scheduler.py | 56 ++- tests/aws/services/events/conftest.py | 56 +++ tests/aws/services/events/helper_functions.py | 29 ++ .../events/scheduled_rules/__init__.py | 0 .../test_events_scheduled_rules_logs.py | 146 ------ ..._events_scheduled_rules_logs.snapshot.json | 69 --- ...vents_scheduled_rules_logs.validation.json | 5 - .../test_events_scheduled_rules_sqs.py | 69 --- ...t_events_scheduled_rules_sqs.snapshot.json | 35 -- ...events_scheduled_rules_sqs.validation.json | 5 - tests/aws/services/events/test_events.py | 6 +- .../aws/services/events/test_events_inputs.py | 8 +- .../aws/services/events/test_events_rules.py | 52 --- .../services/events/test_events_schedule.py | 347 +++++++++++++++ .../events/test_events_schedule.snapshot.json | 421 ++++++++++++++++++ .../test_events_schedule.validation.json | 98 ++++ 18 files changed, 1068 insertions(+), 401 deletions(-) delete mode 100644 tests/aws/services/events/scheduled_rules/__init__.py delete mode 100644 tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.py delete mode 100644 tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.snapshot.json delete mode 100644 tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.validation.json delete mode 100644 tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.py delete mode 100644 tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.snapshot.json delete mode 100644 tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.validation.json create mode 100644 tests/aws/services/events/test_events_schedule.py create mode 100644 tests/aws/services/events/test_events_schedule.snapshot.json create mode 100644 tests/aws/services/events/test_events_schedule.validation.json diff --git a/localstack/services/events/provider.py b/localstack/services/events/provider.py index b2f71bbdb6646..ddfcdd8754a8d 100644 --- a/localstack/services/events/provider.py +++ b/localstack/services/events/provider.py @@ -2,7 +2,7 @@ import json import logging from datetime import datetime, timezone -from typing import Optional +from typing import Callable, Optional from localstack.aws.api import RequestContext, handler from localstack.aws.api.events import ( @@ -70,9 +70,12 @@ InvalidEventPatternException as InternalInvalidEventPatternException, ) from localstack.services.events.rule import RuleService, RuleServiceDict +from localstack.services.events.scheduler import JobScheduler from localstack.services.events.target import TargetSender, TargetSenderDict, TargetSenderFactory from localstack.services.plugins import ServiceLifecycleHook +from localstack.utils.common import truncate from localstack.utils.strings import long_uid +from localstack.utils.time import TIMESTAMP_FORMAT_TZ, timestamp LOG = logging.getLogger(__name__) @@ -151,6 +154,12 @@ def __init__(self): self._rule_services_store: RuleServiceDict = {} self._target_sender_store: TargetSenderDict = {} + def on_before_start(self): + JobScheduler.start() + + def on_before_stop(self): + JobScheduler.shutdown() + ########## # EventBus ########## @@ -427,6 +436,11 @@ def put_targets( for target in targets: # TODO only add successful targets self.create_target_sender(target, region, account_id, rule_arn, rule_name) + if rule_service.schedule_cron: + schedule_job_function = self._get_scheduled_rule_job_function( + account_id, region, rule_service.rule + ) + rule_service.create_schedule_job(schedule_job_function) response = PutTargetsResponse( FailedEntryCount=len(failed_entries), FailedEntries=failed_entries ) @@ -698,3 +712,35 @@ def _process_entries( ) failed_entry_count += 1 return processed_entries, failed_entry_count + + def _get_scheduled_rule_job_function(self, account_id, region, rule: Rule) -> Callable: + def func(*args, **kwargs): + """Create custom scheduled event and send it to all targets specified by associated rule using respective TargetSender""" + for target in rule.targets.values(): + if custom_input := target.get("Input"): + event = json.loads(custom_input) + else: + event = { + "version": "0", + "id": long_uid(), + "detail-type": "Scheduled Event", + "source": "aws.events", + "account": account_id, + "time": timestamp(format=TIMESTAMP_FORMAT_TZ), + "region": region, + "resources": [rule.arn], + "detail": {}, + } + + target_sender = self._target_sender_store[target["Arn"]] + try: + target_sender.process_event(event) + except Exception as e: + LOG.info( + "Unable to send event notification %s to target %s: %s", + truncate(event), + target, + e, + ) + + return func diff --git a/localstack/services/events/rule.py b/localstack/services/events/rule.py index 4298f9c4622e8..a57d9b8ca88d0 100644 --- a/localstack/services/events/rule.py +++ b/localstack/services/events/rule.py @@ -1,5 +1,5 @@ import re -from typing import Optional +from typing import Callable, Optional from localstack.aws.api.events import ( Arn, @@ -20,6 +20,7 @@ TargetList, ) from localstack.services.events.models import Rule, TargetDict, ValidationException +from localstack.services.events.scheduler import JobScheduler, convert_schedule_to_cron TARGET_ID_REGEX = re.compile(r"^[\.\-_A-Za-z0-9]+$") TARGET_ARN_REGEX = re.compile(r"arn:[\d\w:\-/]*") @@ -44,6 +45,10 @@ def __init__( managed_by: Optional[ManagedBy] = None, ): self._validate_input(event_pattern, schedule_expression, event_bus_name) + if schedule_expression: + self.schedule_cron = self._get_schedule_cron(schedule_expression) + else: + self.schedule_cron = None # required to keep data and functionality separate for persistence self.rule = Rule( name, @@ -112,6 +117,11 @@ def remove_targets( ) return delete_errors + def create_schedule_job(self, schedule_job_sender_func: Callable) -> None: + cron = self.schedule_cron + state = self.rule.state != "DISABLED" + self.job_id = JobScheduler.instance().add_job(schedule_job_sender_func, cron, state) + def validate_targets_input(self, targets: TargetList) -> PutTargetsResultEntryList: validation_errors = [] for index, target in enumerate(targets): @@ -182,5 +192,12 @@ def _check_target_limit_reached(self) -> bool: return True return False + def _get_schedule_cron(self, schedule_expression: ScheduleExpression) -> str: + try: + cron = convert_schedule_to_cron(schedule_expression) + return cron + except ValueError as e: + raise ValidationException("Parameter ScheduleExpression is not valid.") from e + RuleServiceDict = dict[Arn, RuleService] diff --git a/localstack/services/events/scheduler.py b/localstack/services/events/scheduler.py index d6dfa66c06ec9..067b61d3ed96f 100644 --- a/localstack/services/events/scheduler.py +++ b/localstack/services/events/scheduler.py @@ -1,4 +1,5 @@ import logging +import re import threading from crontab import CronTab @@ -8,6 +9,41 @@ LOG = logging.getLogger(__name__) +CRON_REGEX = re.compile(r"\s*cron\s*\(([^\)]*)\)\s*") +RATE_REGEX = re.compile(r"\s*rate\s*\(([^\)]*)\)\s*") + + +def convert_schedule_to_cron(schedule): + """Convert Events schedule like "cron(0 20 * * ? *)" or "rate(5 minutes)" """ + cron_match = CRON_REGEX.match(schedule) + if cron_match: + return cron_match.group(1) + + rate_match = RATE_REGEX.match(schedule) + if rate_match: + rate = rate_match.group(1) + rate_value, rate_unit = re.split(r"\s+", rate.strip()) + rate_value = int(rate_value) + + if rate_value < 1: + raise ValueError("Rate value must be larger than 0") + # see https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rate-expressions.html + if rate_value == 1 and rate_unit.endswith("s"): + raise ValueError("If the value is equal to 1, then the unit must be singular") + if rate_value > 1 and not rate_unit.endswith("s"): + raise ValueError("If the value is greater than 1, the unit must be plural") + + if "minute" in rate_unit: + return f"*/{rate_value} * * * *" + if "hour" in rate_unit: + return f"0 */{rate_value} * * *" + if "day" in rate_unit: + return f"0 0 */{rate_value} * *" + + raise ValueError(f"Unable to parse events schedule expression: {schedule}") + + return schedule + class Job: def __init__(self, job_func, schedule, enabled): @@ -25,7 +61,10 @@ def run(self): def should_run_now(self): schedule = CronTab(self.schedule) - delay_secs = schedule.next() + delay_secs = schedule.next( + default_utc=True + ) # utc default time format for rule schedule cron + # TODO fix execute on exact cron time return delay_secs is not None and delay_secs < 60 def do_run(self): @@ -50,6 +89,7 @@ def get_job(self, job_id) -> Job | None: for job in self.jobs: if job.job_id == job_id: return job + return None def disable_job(self, job_id): for job in self.jobs: @@ -58,12 +98,7 @@ def disable_job(self, job_id): break def cancel_job(self, job_id): - i = 0 - while i < len(self.jobs): - if self.jobs[i].job_id == job_id: - del self.jobs[i] - else: - i += 1 + self.jobs = [job for job in self.jobs if job.job_id != job_id] def loop(self, *args): while not self._stop_event.is_set(): @@ -72,7 +107,7 @@ def loop(self, *args): job.run() except Exception: pass - # This is a simple heuristic to cause the loop to run apprx every minute + # This is a simple heuristic to cause the loop to run approximately every minute # TODO: we should keep track of jobs execution times, to avoid duplicate executions self._stop_event.wait(timeout=59.9) @@ -96,6 +131,5 @@ def start(cls): @classmethod def shutdown(cls): instance = cls.instance() - if not instance.thread: - return - instance._stop_event.set() + if instance.thread: + instance._stop_event.set() diff --git a/tests/aws/services/events/conftest.py b/tests/aws/services/events/conftest.py index ec0ea6bcba1db..97abaa1ca57d4 100644 --- a/tests/aws/services/events/conftest.py +++ b/tests/aws/services/events/conftest.py @@ -392,3 +392,59 @@ def collect_events() -> None: retry(collect_events, retries=retries, sleep=0.01) return events + + +@pytest.fixture +def logs_create_log_group(aws_client): + log_group_names = [] + + def _create_log_group(name: str = None) -> str: + if not name: + name = f"test-log-group-{short_uid()}" + + aws_client.logs.create_log_group(logGroupName=name) + log_group_names.append(name) + + return name + + yield _create_log_group + + for name in log_group_names: + try: + aws_client.logs.delete_log_group(logGroupName=name) + except Exception as e: + LOG.debug("error cleaning up log group %s: %s", name, e) + + +@pytest.fixture +def add_resource_policy_logs_events_access(aws_client): + policies = [] + + def _add_resource_policy_logs_events_access(log_group_arn: str): + policy_name = f"test-policy-{short_uid()}" + + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowPutEvents", + "Effect": "Allow", + "Principal": {"Service": "events.amazonaws.com"}, + "Action": ["logs:PutLogEvents", "logs:CreateLogStream"], + "Resource": log_group_arn, + }, + ], + } + policy = aws_client.logs.put_resource_policy( + policyName=policy_name, + policyDocument=json.dumps(policy_document), + ) + + policies.append(policy_name) + + return policy + + yield _add_resource_policy_logs_events_access + + for policy_name in policies: + aws_client.logs.delete_resource_policy(policyName=policy_name) diff --git a/tests/aws/services/events/helper_functions.py b/tests/aws/services/events/helper_functions.py index ecf6238bf7ed8..7ed29640b0d5d 100644 --- a/tests/aws/services/events/helper_functions.py +++ b/tests/aws/services/events/helper_functions.py @@ -1,7 +1,36 @@ import os +from datetime import datetime, timedelta, timezone from localstack.testing.aws.util import is_aws_cloud def is_v2_provider(): return os.environ.get("PROVIDER_OVERRIDE_EVENTS") == "v2" and not is_aws_cloud() + + +def is_old_provider(): + return ( + "PROVIDER_OVERRIDE_EVENTS" not in os.environ + or os.environ.get("PROVIDER_OVERRIDE_EVENTS") != "v2" + ) + + +def events_time_string_to_timestamp(time_string: str) -> datetime: + time_string_format = "%Y-%m-%dT%H:%M:%SZ" + return datetime.strptime(time_string, time_string_format) + + +def get_cron_expression(delta_minutes: int) -> tuple[str, datetime]: + """Get a exact cron expression for a future time in UTC from now rounded to the next full minute + delta_minutes.""" + now = datetime.now(timezone.utc) + future_time = now + timedelta(minutes=delta_minutes) + + # Round to the next full minute + future_time += timedelta(minutes=1) + future_time = future_time.replace(second=0, microsecond=0) + + cron_string = ( + f"cron({future_time.minute} {future_time.hour} {future_time.day} {future_time.month} ? *)" + ) + + return cron_string, future_time diff --git a/tests/aws/services/events/scheduled_rules/__init__.py b/tests/aws/services/events/scheduled_rules/__init__.py deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.py b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.py deleted file mode 100644 index 76d8989c32420..0000000000000 --- a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.py +++ /dev/null @@ -1,146 +0,0 @@ -import json -import logging - -import pytest - -from localstack.testing.aws.eventbus_utils import trigger_scheduled_rule -from localstack.testing.pytest import markers -from localstack.testing.snapshots.transformer_utility import TransformerUtility -from localstack.utils.strings import short_uid -from localstack.utils.sync import retry -from tests.aws.services.events.helper_functions import is_v2_provider - -LOG = logging.getLogger(__name__) - - -@pytest.fixture -def logs_log_group(aws_client): - name = f"test-log-group-{short_uid()}" - aws_client.logs.create_log_group(logGroupName=name) - yield name - aws_client.logs.delete_log_group(logGroupName=name) - - -@pytest.fixture -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") -def add_logs_resource_policy_for_rule(aws_client): - policies = [] - - def _provide_access(rule_arn: str, log_group_arn: str): - policy_name = f"test-policy-{short_uid()}" - - policy = aws_client.logs.put_resource_policy( - policyName=policy_name, - policyDocument=json.dumps( - { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "AllowPutEvents", - "Effect": "Allow", - "Principal": {"Service": "events.amazonaws.com"}, - "Action": ["logs:PutLogEvents", "logs:CreateLogStream"], - "Resource": log_group_arn, - }, - ], - } - ), - ) - - policies.append(policy_name) - - return policy - - yield _provide_access - - for policy_name in policies: - aws_client.logs.delete_resource_policy(policyName=policy_name) - - -@markers.aws.validated -@markers.snapshot.skip_snapshot_verify( - paths=[ - # tokens and IDs cannot be properly transformed - "$..eventId", - "$..uploadSequenceToken", - # FIXME: storedBytes should be implemented - "$..storedBytes", - ] -) -@pytest.mark.xfail( - reason="This test is flaky is CI, might be race conditions" # FIXME: investigate and fix -) -def test_scheduled_rule_logs( - logs_log_group, - events_put_rule, - add_logs_resource_policy_for_rule, - aws_client, - snapshot, -): - schedule_expression = "rate(1 minute)" - rule_name = f"rule-{short_uid()}" - snapshot.add_transformers_list( - [ - snapshot.transform.regex(rule_name, "<rule-name>"), - snapshot.transform.regex(logs_log_group, "<log-group-name>"), - ] - ) - snapshot.add_transformer(TransformerUtility.logs_api()) - - response = aws_client.logs.describe_log_groups(logGroupNamePrefix=logs_log_group) - log_group_arn = response["logGroups"][0]["arn"] - - rule_arn = events_put_rule(Name=rule_name, ScheduleExpression=schedule_expression)["RuleArn"] - add_logs_resource_policy_for_rule(rule_arn, log_group_arn) - - # TODO: add target to test InputTransformer - aws_client.events.put_targets( - Rule=rule_name, - Targets=[ - {"Id": "1", "Arn": log_group_arn}, - {"Id": "2", "Arn": log_group_arn}, - ], - ) - - trigger_scheduled_rule(rule_arn) - - # wait for log stream to be created - def _get_log_stream(): - result = ( - aws_client.logs.get_paginator("describe_log_streams") - .paginate(logGroupName=logs_log_group) - .build_full_result() - ) - assert len(result["logStreams"]) >= 2 - # FIXME: this is a check against a flake in LocalStack - # sometimes the logStreams are created but not yet populated with events, so the snapshot fails - # assert that the stream has the events before returning - assert result["logStreams"][0]["firstEventTimestamp"] - return result["logStreams"] - - log_streams = retry(_get_log_stream, 60) - log_streams.sort(key=lambda stream: stream["creationTime"]) - snapshot.match("log-streams", log_streams) - - # collect events from log streams in group - def _get_events(): - _events = [] - - _response = ( - aws_client.logs.get_paginator("filter_log_events") - .paginate(logGroupName=logs_log_group) - .build_full_result() - ) - _events.extend(_response["events"]) - - if len(_events) < 2: - raise AssertionError( - f"Expected at least two events in log group streams, was {_events}" - ) - return _events - - events = retry(_get_events, retries=5) - - events.sort(key=lambda event: event["timestamp"]) - - snapshot.match("log-events", events) diff --git a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.snapshot.json b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.snapshot.json deleted file mode 100644 index 734e7a772bf5a..0000000000000 --- a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.snapshot.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.py::test_scheduled_rule_logs": { - "recorded-date": "05-10-2023, 19:09:18", - "recorded-content": { - "log-streams": [ - { - "logStreamName": "<log-stream-name:1>", - "creationTime": "timestamp", - "firstEventTimestamp": "timestamp", - "lastEventTimestamp": "timestamp", - "lastIngestionTime": "<time>", - "uploadSequenceToken": "49039859562777973601797067008229305122883632487254944921", - "arn": "arn:aws:logs:<region>:111111111111:log-group:<log-group-name>:log-stream:<log-stream-name:1>", - "storedBytes": 0 - }, - { - "logStreamName": "<log-stream-name:2>", - "creationTime": "timestamp", - "firstEventTimestamp": "timestamp", - "lastEventTimestamp": "timestamp", - "lastIngestionTime": "<time>", - "uploadSequenceToken": "49039859562777973593821699033519811020151723440824085720", - "arn": "arn:aws:logs:<region>:111111111111:log-group:<log-group-name>:log-stream:<log-stream-name:2>", - "storedBytes": 0 - } - ], - "log-events": [ - { - "logStreamName": "<log-stream-name:1>", - "timestamp": "timestamp", - "message": { - "version": "0", - "id": "<uuid:1>", - "detail-type": "Scheduled Event", - "source": "aws.events", - "account": "111111111111", - "time": "date", - "region": "<region>", - "resources": [ - "arn:aws:events:<region>:111111111111:rule/<rule-name>" - ], - "detail": {} - }, - "ingestionTime": "timestamp", - "eventId": "37833787759872217972232273854937524678578329357763018752" - }, - { - "logStreamName": "<log-stream-name:2>", - "timestamp": "timestamp", - "message": { - "version": "0", - "id": "<uuid:1>", - "detail-type": "Scheduled Event", - "source": "aws.events", - "account": "111111111111", - "time": "date", - "region": "<region>", - "resources": [ - "arn:aws:events:<region>:111111111111:rule/<rule-name>" - ], - "detail": {} - }, - "ingestionTime": "timestamp", - "eventId": "37833787759872217972232273854930096880382512496684171264" - } - ] - } - } -} diff --git a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.validation.json b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.validation.json deleted file mode 100644 index 6a1a270bc5722..0000000000000 --- a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.validation.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_logs.py::test_scheduled_rule_logs": { - "last_validated_date": "2023-10-05T17:09:18+00:00" - } -} diff --git a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.py b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.py deleted file mode 100644 index e38f691a480da..0000000000000 --- a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.py +++ /dev/null @@ -1,69 +0,0 @@ -import json -import logging - -import pytest - -from localstack.testing.aws.eventbus_utils import ( - allow_event_rule_to_sqs_queue, - trigger_scheduled_rule, -) -from localstack.testing.pytest import markers -from localstack.testing.snapshots.transformer_utility import TransformerUtility -from localstack.utils.strings import short_uid -from localstack.utils.sync import retry -from tests.aws.services.events.helper_functions import is_v2_provider - -LOG = logging.getLogger(__name__) - - -@markers.aws.validated -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") -def test_scheduled_rule_sqs( - sqs_create_queue, - events_put_rule, - aws_client, - snapshot, -): - schedule_expression = "rate(1 minute)" - rule_name = f"rule-{short_uid()}" - - snapshot.add_transformer(TransformerUtility.sqs_api()) - # the generated message has a date, so the MD5 will be different every time - snapshot.add_transformer(snapshot.transform.key_value("MD5OfBody")) - snapshot.add_transformer(snapshot.transform.regex(rule_name, "<rule-name>")) - - queue_url = sqs_create_queue() - queue_arn = aws_client.sqs.get_queue_attributes( - QueueUrl=queue_url, AttributeNames=["QueueArn"] - )["Attributes"]["QueueArn"] - - rule_arn = events_put_rule(Name=rule_name, ScheduleExpression=schedule_expression)["RuleArn"] - - allow_event_rule_to_sqs_queue(aws_client, queue_url, queue_arn, rule_arn) - aws_client.events.put_targets( - Rule=rule_name, - Targets=[ - {"Id": "1", "Arn": queue_arn}, - {"Id": "2", "Arn": queue_arn, "Input": json.dumps({"custom-value": "somecustominput"})}, - ], - ) - - messages = [] - - trigger_scheduled_rule(rule_arn) - - def _collect_sqs_messages(): - _response = aws_client.sqs.receive_message( - QueueUrl=queue_url, WaitTimeSeconds=20, MaxNumberOfMessages=10 - ) - messages.extend(_response.get("Messages", [])) - - if len(messages) < 2: - raise AssertionError(f"Expected at least 2 messages in {messages}") - - retry(_collect_sqs_messages, retries=6, sleep=0.1) - - # hacky sorting of messages - messages.sort(key=lambda m: 1 if "custom-value" in m["Body"] else 0) - - snapshot.match("sqs-messages", messages) diff --git a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.snapshot.json b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.snapshot.json deleted file mode 100644 index 75e96850e6451..0000000000000 --- a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.snapshot.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.py::test_scheduled_rule_sqs": { - "recorded-date": "05-10-2023, 19:18:50", - "recorded-content": { - "sqs-messages": [ - { - "MessageId": "<uuid:1>", - "ReceiptHandle": "<receipt-handle:1>", - "MD5OfBody": "<m-d5-of-body:1>", - "Body": { - "version": "0", - "id": "<uuid:2>", - "detail-type": "Scheduled Event", - "source": "aws.events", - "account": "111111111111", - "time": "date", - "region": "<region>", - "resources": [ - "arn:aws:events:<region>:111111111111:rule/<rule-name>" - ], - "detail": {} - } - }, - { - "MessageId": "<uuid:3>", - "ReceiptHandle": "<receipt-handle:2>", - "MD5OfBody": "<m-d5-of-body:2>", - "Body": { - "custom-value": "somecustominput" - } - } - ] - } - } -} diff --git a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.validation.json b/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.validation.json deleted file mode 100644 index cec37431c0d81..0000000000000 --- a/tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.validation.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "tests/aws/services/events/scheduled_rules/test_events_scheduled_rules_sqs.py::test_scheduled_rule_sqs": { - "last_validated_date": "2023-10-05T17:18:50+00:00" - } -} diff --git a/tests/aws/services/events/test_events.py b/tests/aws/services/events/test_events.py index 5f3efddada7ab..7e6365c339462 100644 --- a/tests/aws/services/events/test_events.py +++ b/tests/aws/services/events/test_events.py @@ -23,7 +23,7 @@ from localstack.utils.strings import long_uid, short_uid, to_str from localstack.utils.sync import poll_condition, retry from tests.aws.services.events.conftest import assert_valid_event, sqs_collect_messages -from tests.aws.services.events.helper_functions import is_v2_provider +from tests.aws.services.events.helper_functions import is_old_provider, is_v2_provider EVENT_DETAIL = {"command": "update-account", "payload": {"acc_id": "0a787ecb-4015", "sf_id": "baz"}} @@ -83,7 +83,7 @@ class TestEvents: @markers.aws.validated @pytest.mark.skipif( - not is_v2_provider(), + is_old_provider(), reason="V1 provider does not support this feature", ) def test_put_events_without_source(self, snapshot, aws_client): @@ -98,7 +98,7 @@ def test_put_events_without_source(self, snapshot, aws_client): @markers.aws.unknown @pytest.mark.skipif( - not is_v2_provider(), + is_old_provider(), reason="V1 provider does not support this feature", ) def test_put_event_without_detail(self, snapshot, aws_client): diff --git a/tests/aws/services/events/test_events_inputs.py b/tests/aws/services/events/test_events_inputs.py index 9888073053dce..3c8910d2817c9 100644 --- a/tests/aws/services/events/test_events_inputs.py +++ b/tests/aws/services/events/test_events_inputs.py @@ -8,7 +8,7 @@ from localstack.testing.pytest import markers from localstack.utils.strings import short_uid from tests.aws.services.events.conftest import sqs_collect_messages -from tests.aws.services.events.helper_functions import is_v2_provider +from tests.aws.services.events.helper_functions import is_old_provider, is_v2_provider from tests.aws.services.events.test_events import EVENT_DETAIL, TEST_EVENT_PATTERN EVENT_DETAIL_DUPLICATED_KEY = { @@ -270,7 +270,7 @@ def test_put_events_with_input_transformer_input_template_string( @markers.aws.validated @pytest.mark.skipif( - not is_v2_provider(), + is_old_provider(), reason="V1 provider does not support this feature", ) def test_put_events_with_input_transformer_input_template_json( @@ -329,7 +329,7 @@ def test_put_events_with_input_transformer_input_template_json( @markers.aws.validated @pytest.mark.skipif( - not is_v2_provider(), + is_old_provider(), reason="V1 provider does not support this feature", ) def test_put_events_with_input_transformer_missing_keys( @@ -381,7 +381,7 @@ def test_put_events_with_input_transformer_missing_keys( @markers.aws.validated @pytest.mark.skipif( - not is_v2_provider(), + is_old_provider(), reason="V1 provider does not support this feature", ) @pytest.mark.parametrize( diff --git a/tests/aws/services/events/test_events_rules.py b/tests/aws/services/events/test_events_rules.py index 1926910cc8261..566bf00ee2d49 100644 --- a/tests/aws/services/events/test_events_rules.py +++ b/tests/aws/services/events/test_events_rules.py @@ -5,7 +5,6 @@ import json import pytest -from botocore.exceptions import ClientError from localstack.constants import TEST_AWS_ACCOUNT_ID, TEST_AWS_REGION_NAME from localstack.testing.pytest import markers @@ -51,41 +50,6 @@ def test_rule_disable(aws_client, clean_up): clean_up(rule_name=rule_name) -@markers.aws.validated -# TODO move to test_events_schedules.py -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") -@pytest.mark.parametrize( - "expression", - [ - "rate(10 seconds)", - "rate(10 years)", - "rate(1 minutes)", - "rate(1 hours)", - "rate(1 days)", - "rate(10 minute)", - "rate(10 hour)", - "rate(10 day)", - "rate()", - "rate(10)", - "rate(10 minutess)", - "rate(foo minutes)", - "rate(0 minutes)", - "rate(-10 minutes)", - "rate(10 MINUTES)", - "rate( 10 minutes )", - " rate(10 minutes)", - ], -) -def test_put_rule_invalid_rate_schedule_expression(expression, aws_client): - with pytest.raises(ClientError) as e: - aws_client.events.put_rule(Name=f"rule-{short_uid()}", ScheduleExpression=expression) - - assert e.value.response["Error"] == { - "Code": "ValidationException", - "Message": "Parameter ScheduleExpression is not valid.", - } - - @markers.aws.validated def test_put_events_with_rule_anything_but_to_sqs(put_events_with_filter_to_sqs, snapshot): snapshot.add_transformer( @@ -335,22 +299,6 @@ def test_put_event_with_content_base_rule_in_pattern(aws_client, clean_up): ) -@markers.aws.validated -# TODO move to test_events_schedules.py -@pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") -@pytest.mark.parametrize("schedule_expression", ["rate(1 minute)", "rate(1 day)", "rate(1 hour)"]) -def test_create_rule_with_one_unit_in_singular_should_succeed( - schedule_expression, aws_client, clean_up -): - rule_name = f"rule-{short_uid()}" - - # rule should be creatable with given expression - try: - aws_client.events.put_rule(Name=rule_name, ScheduleExpression=schedule_expression) - finally: - clean_up(rule_name=rule_name) - - @markers.aws.validated @pytest.mark.xfail def test_verify_rule_event_content(aws_client, clean_up): diff --git a/tests/aws/services/events/test_events_schedule.py b/tests/aws/services/events/test_events_schedule.py new file mode 100644 index 0000000000000..16c65ebf311c3 --- /dev/null +++ b/tests/aws/services/events/test_events_schedule.py @@ -0,0 +1,347 @@ +import json +import time +from datetime import timedelta, timezone + +import pytest +from botocore.exceptions import ClientError + +from localstack.testing.aws.eventbus_utils import trigger_scheduled_rule +from localstack.testing.pytest import markers +from localstack.testing.snapshots.transformer_utility import TransformerUtility +from localstack.utils.strings import short_uid +from localstack.utils.sync import retry +from tests.aws.services.events.conftest import sqs_collect_messages +from tests.aws.services.events.helper_functions import ( + events_time_string_to_timestamp, + get_cron_expression, +) + + +class TestScheduleRate: + @markers.aws.validated + def test_put_rule_with_schedule_rate(self, events_put_rule, aws_client, snapshot): + rule_name = f"rule-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(rule_name, "<rule-name>")) + + response = events_put_rule(Name=rule_name, ScheduleExpression="rate(1 minute)") + snapshot.match("put-rule", response) + + response = aws_client.events.list_rules(NamePrefix=rule_name) + snapshot.match("list-rules", response) + + @markers.aws.validated + def tests_put_rule_with_schedule_custom_event_bus( + self, + events_create_event_bus, + aws_client, + snapshot, + ): + bus_name = f"test-bus-{short_uid()}" + events_create_event_bus(Name=bus_name) + + rule_name = f"test-rule-{short_uid()}" + with pytest.raises(ClientError) as e: + aws_client.events.put_rule( + Name=rule_name, EventBusName=bus_name, ScheduleExpression="rate(1 minute)" + ) + snapshot.match("put-rule-with-custom-event-bus-error", e) + + @markers.aws.validated + @pytest.mark.parametrize( + "schedule_expression", + [ + "rate(10 seconds)", + "rate(10 years)", + "rate(1 minutes)", + "rate(1 hours)", + "rate(1 days)", + "rate(10 minute)", + "rate(10 hour)", + "rate(10 day)", + "rate()", + "rate(10)", + "rate(10 minutess)", + "rate(foo minutes)", + "rate(0 minutes)", + "rate(-10 minutes)", + "rate(10 MINUTES)", + "rate( 10 minutes )", + " rate(10 minutes)", + ], + ) + def test_put_rule_with_invalid_schedule_rate(self, schedule_expression, aws_client): + with pytest.raises(ClientError) as e: + aws_client.events.put_rule( + Name=f"rule-{short_uid()}", ScheduleExpression=schedule_expression + ) + + assert e.value.response["Error"] == { + "Code": "ValidationException", + "Message": "Parameter ScheduleExpression is not valid.", + } + + @markers.aws.validated + def tests_schedule_rate_target_sqs( + self, + create_sqs_events_target, + events_put_rule, + aws_client, + snapshot, + ): + queue_name = f"test-queue-{short_uid()}" + queue_url, queue_arn = create_sqs_events_target(queue_name) + + bus_name = "default" + rule_name = f"test-rule-{short_uid()}" + events_put_rule(Name=rule_name, EventBusName=bus_name, ScheduleExpression="rate(1 minute)") + + target_id = f"test-target-{short_uid()}" + aws_client.events.put_targets( + Rule=rule_name, + EventBusName=bus_name, + Targets=[ + {"Id": target_id, "Arn": queue_arn}, + ], + ) # cleanup is handled by rule fixture + + response = aws_client.events.list_targets_by_rule(Rule=rule_name) + snapshot.match("list-targets", response) + + time.sleep(60) + messages_first = sqs_collect_messages(aws_client, queue_url, min_events=1, retries=3) + + snapshot.add_transformers_list( + [ + snapshot.transform.key_value("MD5OfBody"), + snapshot.transform.key_value("ReceiptHandle"), + snapshot.transform.regex(target_id, "<target-id>"), + snapshot.transform.regex(rule_name, "<rule-name>"), + snapshot.transform.regex(queue_name, "<queue-name"), + ] + ) + snapshot.match("messages-first", messages_first) + + time.sleep(60) + messages_second = sqs_collect_messages(aws_client, queue_url, min_events=1, retries=3) + snapshot.match("messages-second", messages_second) + + # check if the messages are 60 seconds apart + time_messages_first = events_time_string_to_timestamp( + json.loads(messages_first[0]["Body"])["time"] + ) + time_messages_second = events_time_string_to_timestamp( + json.loads(messages_second[0]["Body"])["time"] + ) + time_delta = time_messages_second - time_messages_first + assert time_delta == timedelta(seconds=60) + + @markers.aws.validated + def tests_schedule_rate_custom_input_target_sqs( + self, create_sqs_events_target, events_put_rule, aws_client, snapshot + ): + queue_url, queue_arn = create_sqs_events_target() + + bus_name = "default" + rule_name = f"test-rule-{short_uid()}" + events_put_rule(Name=rule_name, EventBusName=bus_name, ScheduleExpression="rate(1 minute)") + + target_id = f"target-{short_uid()}" + aws_client.events.put_targets( + Rule=rule_name, + EventBusName=bus_name, + Targets=[ + { + "Id": target_id, + "Arn": queue_arn, + "Input": json.dumps({"custom-value": "somecustominput"}), + }, + ], + ) # cleanup is handled by rule fixture + + response = aws_client.events.list_targets_by_rule(Rule=rule_name) + snapshot.match("list-targets", response) + + time.sleep(60) + messages_first = sqs_collect_messages(aws_client, queue_url, min_events=1, retries=3) + + snapshot.add_transformers_list( + [ + snapshot.transform.key_value("MD5OfBody"), + snapshot.transform.key_value("ReceiptHandle"), + snapshot.transform.regex(target_id, "<target-id>"), + snapshot.transform.regex(queue_arn, "<queue-arn>"), + ] + ) + snapshot.match("messages", messages_first) + + @markers.aws.needs_fixing + @markers.snapshot.skip_snapshot_verify( + paths=[ + # tokens and IDs cannot be properly transformed + "$..eventId", + "$..uploadSequenceToken", + # FIXME: storedBytes should be implemented + "$..storedBytes", + ] + ) + @pytest.mark.xfail( + reason="This test is flaky is CI, might be race conditions" # FIXME: investigate and fix + ) + def test_scheduled_rule_logs( + self, + logs_create_log_group, + events_put_rule, + add_resource_policy_logs_events_access, + aws_client, + snapshot, + ): + schedule_expression = "rate(1 minute)" + rule_name = f"rule-{short_uid()}" + snapshot.add_transformers_list( + [ + snapshot.transform.regex(rule_name, "<rule-name>"), + snapshot.transform.regex(logs_create_log_group, "<log-group-name>"), + ] + ) + snapshot.add_transformer(TransformerUtility.logs_api()) + + response = aws_client.logs.describe_log_groups(logGroupNamePrefix=logs_create_log_group) + log_group_arn = response["logGroups"][0]["arn"] + + rule_arn = events_put_rule(Name=rule_name, ScheduleExpression=schedule_expression)[ + "RuleArn" + ] + add_resource_policy_logs_events_access(rule_arn, log_group_arn) + + # TODO: add target to test InputTransformer + aws_client.events.put_targets( + Rule=rule_name, + Targets=[ + {"Id": "1", "Arn": log_group_arn}, + {"Id": "2", "Arn": log_group_arn}, + ], + ) + + trigger_scheduled_rule(rule_arn) + + # wait for log stream to be created + def _get_log_stream(): + result = ( + aws_client.logs.get_paginator("describe_log_streams") + .paginate(logGroupName=logs_create_log_group) + .build_full_result() + ) + assert len(result["logStreams"]) >= 2 + # FIXME: this is a check against a flake in LocalStack + # sometimes the logStreams are created but not yet populated with events, so the snapshot fails + # assert that the stream has the events before returning + assert result["logStreams"][0]["firstEventTimestamp"] + return result["logStreams"] + + log_streams = retry(_get_log_stream, 60) + log_streams.sort(key=lambda stream: stream["creationTime"]) + snapshot.match("log-streams", log_streams) + + # collect events from log streams in group + def _get_events(): + _events = [] + + _response = ( + aws_client.logs.get_paginator("filter_log_events") + .paginate(logGroupName=logs_create_log_group) + .build_full_result() + ) + _events.extend(_response["events"]) + + if len(_events) < 2: + raise AssertionError( + f"Expected at least two events in log group streams, was {_events}" + ) + return _events + + events = retry(_get_events, retries=5) + + events.sort(key=lambda event: event["timestamp"]) + + snapshot.match("log-events", events) + + +class TestScheduleCron: + @markers.aws.validated + @pytest.mark.parametrize( + "schedule_cron", + [ + "cron(0 10 * * ? *)", # Run at 10:00 am every day + "cron(15 12 * * ? *)", # Run at 12:15 pm every day + "cron(0 18 ? * MON-FRI *)", # Run at 6:00 pm every Monday through Friday + "cron(0 8 1 * ? *)", # Run at 8:00 am on the 1st day of every month + "cron(0/15 * * * ? *)", # Run every 15 minutes + "cron(0/10 * ? * MON-FRI *)", # Run every 10 minutes Monday through Friday + "cron(0/5 8-17 ? * MON-FRI *)", # Run every 5 minutes Monday through Friday between 8:00 am and 5:55 pm + "cron(0/30 20-23 ? * MON-FRI *)", # Run every 30 minutes between 8:00 pm and 11:59 pm Monday through Friday + "cron(0/30 0-2 ? * MON-FRI *)", # Run every 30 minutes between 12:00 am and 2:00 am Monday through Friday + ], + ) + def tests_put_rule_with_schedule_cron( + self, schedule_cron, events_put_rule, aws_client, snapshot + ): + rule_name = f"rule-{short_uid()}" + snapshot.add_transformer(snapshot.transform.regex(rule_name, "<rule-name>")) + + response = events_put_rule(Name=rule_name, ScheduleExpression=schedule_cron) + snapshot.match("put-rule", response) + + response = aws_client.events.list_rules(NamePrefix=rule_name) + snapshot.match("list-rules", response) + + @markers.aws.validated + def test_schedule_cron_target_sqs( + self, + create_sqs_events_target, + events_put_rule, + aws_client, + snapshot, + ): + queue_url, queue_arn = create_sqs_events_target() + + schedule_cron, target_datetime = get_cron_expression( + 1 + ) # only next full minut might be to fast for setup must be UTC time zone + + bus_name = "default" + rule_name = f"test-rule-{short_uid()}" + events_put_rule(Name=rule_name, EventBusName=bus_name, ScheduleExpression=schedule_cron) + + target_id = f"target-{short_uid()}" + aws_client.events.put_targets( + Rule=rule_name, + EventBusName=bus_name, + Targets=[ + {"Id": target_id, "Arn": queue_arn}, + ], + ) + + time.sleep(120) # required to wait for time delta 1 minute starting from next full minute + messages = sqs_collect_messages(aws_client, queue_url, min_events=1, retries=5) + + snapshot.add_transformers_list( + [ + snapshot.transform.key_value("MD5OfBody"), + snapshot.transform.key_value("ReceiptHandle"), + snapshot.transform.regex(rule_name, "<rule-name>"), + ] + ) + snapshot.match("message", messages) + + # check if message was delivered at the correct time + time_message = events_time_string_to_timestamp( + json.loads(messages[0]["Body"])["time"] + ).replace(tzinfo=timezone.utc) + + # TODO fix JobScheduler to execute on exact time + # round datetime to nearest minute + if time_message.second > 0 or time_message.microsecond > 0: + time_message += timedelta(minutes=1) + time_message = time_message.replace(second=0, microsecond=0) + + assert time_message == target_datetime diff --git a/tests/aws/services/events/test_events_schedule.snapshot.json b/tests/aws/services/events/test_events_schedule.snapshot.json new file mode 100644 index 0000000000000..d428a95f7f4d2 --- /dev/null +++ b/tests/aws/services/events/test_events_schedule.snapshot.json @@ -0,0 +1,421 @@ +{ + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_schedule_rate": { + "recorded-date": "14-05-2024, 11:23:22", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "EventBusName": "default", + "Name": "<rule-name>", + "ScheduleExpression": "rate(1 minute)", + "State": "ENABLED" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::tests_schedule_sqs_target": { + "recorded-date": "14-05-2024, 11:34:46", + "recorded-content": {} + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::tests_put_rule_with_schedule_custom_event_bus": { + "recorded-date": "14-05-2024, 11:38:21", + "recorded-content": { + "put-rule-with-custom-event-bus-error": "<ExceptionInfo ClientError('An error occurred (ValidationException) when calling the PutRule operation: ScheduleExpression is supported only on the default event bus.') tblen=3>" + } + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::tests_schedule_rate_target_sqs": { + "recorded-date": "15-05-2024, 08:57:51", + "recorded-content": { + "list-targets": { + "Targets": [ + { + "Arn": "arn:aws:sqs:<region>:111111111111:<queue-name", + "Id": "<target-id>" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages-first": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": { + "version": "0", + "id": "<uuid:2>", + "detail-type": "Scheduled Event", + "source": "aws.events", + "account": "111111111111", + "time": "date", + "region": "<region>", + "resources": [ + "arn:aws:events:<region>:111111111111:rule/<rule-name>" + ], + "detail": {} + } + } + ], + "messages-second": [ + { + "MessageId": "<uuid:3>", + "ReceiptHandle": "<receipt-handle:2>", + "MD5OfBody": "<m-d5-of-body:2>", + "Body": { + "version": "0", + "id": "<uuid:4>", + "detail-type": "Scheduled Event", + "source": "aws.events", + "account": "111111111111", + "time": "date", + "region": "<region>", + "resources": [ + "arn:aws:events:<region>:111111111111:rule/<rule-name>" + ], + "detail": {} + } + } + ] + } + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron": { + "recorded-date": "14-05-2024, 14:50:51", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "EventBusName": "default", + "Name": "<rule-name>", + "ScheduleExpression": "cron(0 20 * * ? *)", + "State": "ENABLED" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::test_schedule_cron_target_sqs": { + "recorded-date": "15-05-2024, 10:58:53", + "recorded-content": { + "message": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": { + "version": "0", + "id": "<uuid:2>", + "detail-type": "Scheduled Event", + "source": "aws.events", + "account": "111111111111", + "time": "date", + "region": "<region>", + "resources": [ + "arn:aws:events:<region>:111111111111:rule/<rule-name>" + ], + "detail": {} + } + } + ] + } + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(0 10 * * ? *)]": { + "recorded-date": "14-05-2024, 15:43:09", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "EventBusName": "default", + "Name": "<rule-name>", + "ScheduleExpression": "cron(0 10 * * ? *)", + "State": "ENABLED" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(15 12 * * ? *)]": { + "recorded-date": "14-05-2024, 15:43:09", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "EventBusName": "default", + "Name": "<rule-name>", + "ScheduleExpression": "cron(15 12 * * ? *)", + "State": "ENABLED" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(0 18 ? * MON-FRI *)]": { + "recorded-date": "14-05-2024, 15:43:10", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "EventBusName": "default", + "Name": "<rule-name>", + "ScheduleExpression": "cron(0 18 ? * MON-FRI *)", + "State": "ENABLED" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(0 8 1 * ? *)]": { + "recorded-date": "14-05-2024, 15:43:11", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "EventBusName": "default", + "Name": "<rule-name>", + "ScheduleExpression": "cron(0 8 1 * ? *)", + "State": "ENABLED" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(0/15 * * * ? *)]": { + "recorded-date": "14-05-2024, 15:43:12", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "EventBusName": "default", + "Name": "<rule-name>", + "ScheduleExpression": "cron(0/15 * * * ? *)", + "State": "ENABLED" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(0/10 * ? * MON-FRI *)]": { + "recorded-date": "14-05-2024, 15:43:12", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "EventBusName": "default", + "Name": "<rule-name>", + "ScheduleExpression": "cron(0/10 * ? * MON-FRI *)", + "State": "ENABLED" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(0/5 8-17 ? * MON-FRI *)]": { + "recorded-date": "14-05-2024, 15:43:13", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "EventBusName": "default", + "Name": "<rule-name>", + "ScheduleExpression": "cron(0/5 8-17 ? * MON-FRI *)", + "State": "ENABLED" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(0/30 20-23 ? * MON-FRI *)]": { + "recorded-date": "14-05-2024, 15:43:14", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "EventBusName": "default", + "Name": "<rule-name>", + "ScheduleExpression": "cron(0/30 20-23 ? * MON-FRI *)", + "State": "ENABLED" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(0/30 0-2 ? * MON-FRI *)]": { + "recorded-date": "14-05-2024, 15:43:14", + "recorded-content": { + "put-rule": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list-rules": { + "Rules": [ + { + "Arn": "arn:aws:events:<region>:111111111111:rule/<rule-name>", + "EventBusName": "default", + "Name": "<rule-name>", + "ScheduleExpression": "cron(0/30 0-2 ? * MON-FRI *)", + "State": "ENABLED" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::tests_schedule_rate_custom_input_target_sqs": { + "recorded-date": "15-05-2024, 09:31:53", + "recorded-content": { + "list-targets": { + "Targets": [ + { + "Arn": "<queue-arn>", + "Id": "<target-id>", + "Input": { + "custom-value": "somecustominput" + } + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "messages": [ + { + "MessageId": "<uuid:1>", + "ReceiptHandle": "<receipt-handle:1>", + "MD5OfBody": "<m-d5-of-body:1>", + "Body": { + "custom-value": "somecustominput" + } + } + ] + } + } +} diff --git a/tests/aws/services/events/test_events_schedule.validation.json b/tests/aws/services/events/test_events_schedule.validation.json new file mode 100644 index 0000000000000..a21ca78f556dd --- /dev/null +++ b/tests/aws/services/events/test_events_schedule.validation.json @@ -0,0 +1,98 @@ +{ + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::test_schedule_cron_target_sqs": { + "last_validated_date": "2024-05-15T10:58:53+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron": { + "last_validated_date": "2024-05-14T14:50:51+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(0 10 * * ? *)]": { + "last_validated_date": "2024-05-14T15:43:09+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(0 18 ? * MON-FRI *)]": { + "last_validated_date": "2024-05-14T15:43:10+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(0 8 1 * ? *)]": { + "last_validated_date": "2024-05-14T15:43:11+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(0/10 * ? * MON-FRI *)]": { + "last_validated_date": "2024-05-14T15:43:12+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(0/15 * * * ? *)]": { + "last_validated_date": "2024-05-14T15:43:12+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(0/30 0-2 ? * MON-FRI *)]": { + "last_validated_date": "2024-05-14T15:43:14+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(0/30 20-23 ? * MON-FRI *)]": { + "last_validated_date": "2024-05-14T15:43:14+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(0/5 8-17 ? * MON-FRI *)]": { + "last_validated_date": "2024-05-14T15:43:13+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleCron::tests_put_rule_with_schedule_cron[cron(15 12 * * ? *)]": { + "last_validated_date": "2024-05-14T15:43:09+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[ rate(10 minutes)]": { + "last_validated_date": "2024-05-14T11:27:18+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[rate( 10 minutes )]": { + "last_validated_date": "2024-05-14T11:27:18+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[rate()]": { + "last_validated_date": "2024-05-14T11:27:13+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[rate(-10 minutes)]": { + "last_validated_date": "2024-05-14T11:27:16+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[rate(0 minutes)]": { + "last_validated_date": "2024-05-14T11:27:16+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[rate(1 days)]": { + "last_validated_date": "2024-05-14T11:27:11+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[rate(1 hours)]": { + "last_validated_date": "2024-05-14T11:27:10+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[rate(1 minutes)]": { + "last_validated_date": "2024-05-14T11:27:09+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[rate(10 MINUTES)]": { + "last_validated_date": "2024-05-14T11:27:17+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[rate(10 day)]": { + "last_validated_date": "2024-05-14T11:27:13+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[rate(10 hour)]": { + "last_validated_date": "2024-05-14T11:27:12+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[rate(10 minute)]": { + "last_validated_date": "2024-05-14T11:27:11+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[rate(10 minutess)]": { + "last_validated_date": "2024-05-14T11:27:14+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[rate(10 seconds)]": { + "last_validated_date": "2024-05-14T11:27:08+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[rate(10 years)]": { + "last_validated_date": "2024-05-14T11:27:09+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[rate(10)]": { + "last_validated_date": "2024-05-14T11:27:14+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_invalid_schedule_rate[rate(foo minutes)]": { + "last_validated_date": "2024-05-14T11:27:15+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::test_put_rule_with_schedule_rate": { + "last_validated_date": "2024-05-14T11:23:22+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::tests_put_rule_with_schedule_custom_event_bus": { + "last_validated_date": "2024-05-14T11:38:21+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::tests_schedule_rate_custom_input_target_sqs": { + "last_validated_date": "2024-05-15T09:31:53+00:00" + }, + "tests/aws/services/events/test_events_schedule.py::TestScheduleRate::tests_schedule_rate_target_sqs": { + "last_validated_date": "2024-05-15T08:57:51+00:00" + } +} From c3e9179bdd3540ac6927fc19c436a75ccd4bdcb8 Mon Sep 17 00:00:00 2001 From: Sannya Singal <32308435+sannya-singal@users.noreply.github.com> Date: Fri, 17 May 2024 15:29:36 +0530 Subject: [PATCH 157/169] add support for `CommaDelimitedList` in cloudformation macro (#10836) --- .../cloudformation/engine/transformers.py | 6 +- .../cloudformation/test_template_engine.py | 32 ++++++ .../test_template_engine.snapshot.json | 19 +++ .../test_template_engine.validation.json | 3 + .../aws/templates/pyplate_deploy_template.yml | 108 ++++++++++++++++++ tests/aws/templates/pyplate_example.yml | 26 +++++ 6 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 tests/aws/templates/pyplate_deploy_template.yml create mode 100644 tests/aws/templates/pyplate_example.yml diff --git a/localstack/services/cloudformation/engine/transformers.py b/localstack/services/cloudformation/engine/transformers.py index 4029a428c5b7b..e67a95e7ae50b 100644 --- a/localstack/services/cloudformation/engine/transformers.py +++ b/localstack/services/cloudformation/engine/transformers.py @@ -205,7 +205,11 @@ def execute_macro( formatted_stack_parameters = {} for key, value in stack_parameters.items(): - formatted_stack_parameters[key] = value.get("ParameterValue") + # TODO: we want to support other types of parameters + if value.get("ParameterType") == "CommaDelimitedList": + formatted_stack_parameters[key] = value.get("ParameterValue").split(",") + else: + formatted_stack_parameters[key] = value.get("ParameterValue") transformation_id = f"{account_id}::{macro['Name']}" event = { diff --git a/tests/aws/services/cloudformation/test_template_engine.py b/tests/aws/services/cloudformation/test_template_engine.py index 61c33d39dc769..43f1a8454a2bf 100644 --- a/tests/aws/services/cloudformation/test_template_engine.py +++ b/tests/aws/services/cloudformation/test_template_engine.py @@ -1064,6 +1064,38 @@ def test_failed_state( snapshot.add_transformer(snapshot.transform.cloudformation_api()) snapshot.match("failed_description", failed_events_by_policy[0]) + @markers.aws.validated + def test_pyplate_param_type_list(self, deploy_cfn_template, aws_client, snapshot): + deploy_cfn_template( + template_path=os.path.join( + os.path.dirname(__file__), "../../templates/pyplate_deploy_template.yml" + ), + ) + + tags = "Env=Prod,Application=MyApp,BU=ModernisationTeam" + param_tags = {pair.split("=")[0]: pair.split("=")[1] for pair in tags.split(",")} + + stack_with_macro = deploy_cfn_template( + template_path=os.path.join( + os.path.dirname(__file__), "../../templates/pyplate_example.yml" + ), + parameters={"Tags": tags}, + ) + + bucket_name_output = stack_with_macro.outputs["BucketName"] + assert bucket_name_output + + tagging = aws_client.s3.get_bucket_tagging(Bucket=bucket_name_output) + tags_s3 = [tag for tag in tagging["TagSet"]] + + resp = [] + for tag in tags_s3: + if tag["Key"] in param_tags: + assert tag["Value"] == param_tags[tag["Key"]] + resp.append([tag["Key"], tag["Value"]]) + assert len(tags_s3) >= len(param_tags) + snapshot.match("tags", sorted(resp)) + class TestStackEvents: @markers.aws.validated diff --git a/tests/aws/services/cloudformation/test_template_engine.snapshot.json b/tests/aws/services/cloudformation/test_template_engine.snapshot.json index 04a85b9b88777..1f89720c9c037 100644 --- a/tests/aws/services/cloudformation/test_template_engine.snapshot.json +++ b/tests/aws/services/cloudformation/test_template_engine.snapshot.json @@ -612,5 +612,24 @@ "<region>f" ] } + }, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_pyplate_param_type_list": { + "recorded-date": "17-05-2024, 06:19:03", + "recorded-content": { + "tags": [ + [ + "Application", + "MyApp" + ], + [ + "BU", + "ModernisationTeam" + ], + [ + "Env", + "Prod" + ] + ] + } } } diff --git a/tests/aws/services/cloudformation/test_template_engine.validation.json b/tests/aws/services/cloudformation/test_template_engine.validation.json index 744147553fae6..e60855eb6836b 100644 --- a/tests/aws/services/cloudformation/test_template_engine.validation.json +++ b/tests/aws/services/cloudformation/test_template_engine.validation.json @@ -29,6 +29,9 @@ "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_macro_deployment": { "last_validated_date": "2023-01-30T19:13:58+00:00" }, + "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_pyplate_param_type_list": { + "last_validated_date": "2024-05-17T06:19:03+00:00" + }, "tests/aws/services/cloudformation/test_template_engine.py::TestMacros::test_scope_order_and_parameters": { "last_validated_date": "2022-12-07T08:08:26+00:00" }, diff --git a/tests/aws/templates/pyplate_deploy_template.yml b/tests/aws/templates/pyplate_deploy_template.yml new file mode 100644 index 0000000000000..134d0e2e9a947 --- /dev/null +++ b/tests/aws/templates/pyplate_deploy_template.yml @@ -0,0 +1,108 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Description: Macro allowing you to run arbitrary Python code in your CloudFormation templates + +Parameters: + LambdaTimeout: + Description: "Optional setting of the Lambda's execution timeout (in seconds). \nThe default of 3 seconds won't be enough if you call AWS services; \nthen at least 10 seconds is recommended, more depending on complexity.\n" + Type: Number + Default: 10 + MinValue: 3 + +Resources: + TransformExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: + - lambda.amazonaws.com + Action: + - sts:AssumeRole + Path: / + Policies: + - PolicyName: root + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - logs:* + Resource: arn:aws:logs:*:*:* + ManagedPolicyArns: + - arn:aws:iam::aws:policy/AdministratorAccess + + TransformFunction: + Type: AWS::Lambda::Function + Metadata: + guard: + SuppressedRules: + - LAMBDA_INSIDE_VPC + Properties: + Description: Support for the PyPlate CloudFormation macro + Code: + ZipFile: | + #pylint: disable=exec-used + + import traceback + import json + + def obj_iterate(obj, params): + "Iterate over template resources and execute any PyPlate directives" + if isinstance(obj, dict): + for k in obj: + obj[k] = obj_iterate(obj[k], params) + elif isinstance(obj, list): + for i, v in enumerate(obj): + obj[i] = obj_iterate(v, params) + elif isinstance(obj, str): + if obj.startswith("#!PyPlate"): + params["output"] = None + exec(obj, params) + obj = params["output"] + return obj + + def handler(event, _): + "Lambda handler" + print(json.dumps(event)) + + macro_response = {"requestId": event["requestId"], "status": "success"} + try: + params = { + "params": event["templateParameterValues"], + "template": event["fragment"], + "account_id": event["accountId"], + "region": event["region"], + } + response = event["fragment"] + macro_response["fragment"] = obj_iterate(response, params) + except Exception as e: + traceback.print_exc() + macro_response["status"] = "failure" + macro_response["errorMessage"] = str(e) + return macro_response + Handler: index.handler + Runtime: python3.11 + Role: !GetAtt TransformExecutionRole.Arn + Timeout: !Ref LambdaTimeout + + TransformFunctionPermissions: + Type: AWS::Lambda::Permission + Metadata: + guard: + SuppressedRules: + - LAMBDA_FUNCTION_PUBLIC_ACCESS_PROHIBITED + Properties: + Action: lambda:InvokeFunction + FunctionName: !GetAtt TransformFunction.Arn + Principal: cloudformation.amazonaws.com + + Transform: + Type: AWS::CloudFormation::Macro + Properties: + Name: !Sub PyPlate + Description: Processes inline python in templates + FunctionName: !GetAtt TransformFunction.Arn diff --git a/tests/aws/templates/pyplate_example.yml b/tests/aws/templates/pyplate_example.yml new file mode 100644 index 0000000000000..f35f344bb1a05 --- /dev/null +++ b/tests/aws/templates/pyplate_example.yml @@ -0,0 +1,26 @@ +AWSTemplateFormatVersion: "2010-09-09" + +Description: tests String macro functions + +Parameters: + Tags: + Type: CommaDelimitedList + +Transform: + - PyPlate + +Resources: + S3Bucket: + Type: "AWS::S3::Bucket" + Properties: + Tags: | + #!PyPlate + output = [] + for tag in params['Tags']: + key, value = tag.split('=') + output.append({"Key": key, "Value": value}) + +Outputs: + BucketName: + Value: + Ref: S3Bucket From 179a8e31e015b102db9d04429c8dde87c48b8f84 Mon Sep 17 00:00:00 2001 From: Viren Nadkarni <viren.nadkarni@localstack.cloud> Date: Fri, 17 May 2024 16:02:51 +0530 Subject: [PATCH 158/169] Bump moto-ext to 5.0.7.post1 (#10843) --- pyproject.toml | 2 +- requirements-base-runtime.txt | 2 +- requirements-dev.txt | 18 +++++++++--------- requirements-runtime.txt | 10 +++++----- requirements-test.txt | 12 ++++++------ requirements-typehint.txt | 30 +++++++++++++++--------------- 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 78a89a50e437f..f94a073836ceb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,7 @@ runtime = [ "json5>=0.9.11", "jsonpath-ng>=1.6.1", "jsonpath-rw>=1.4.0", - "moto-ext[all]==5.0.6.post2", + "moto-ext[all]==5.0.7.post1", "opensearch-py>=2.4.1", "pymongo>=4.2.0", "pyopenssl>=23.0.0", diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index ff7b27baf98d5..f3a9a29b575c5 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -184,7 +184,7 @@ wsproto==1.2.0 # via hypercorn xmltodict==0.13.0 # via localstack-core (pyproject.toml) -zope-interface==6.3 +zope-interface==6.4 # via localstack-twisted # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements-dev.txt b/requirements-dev.txt index 11711334cd068..4e0761bda1f19 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -37,9 +37,9 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.141.0 +aws-cdk-lib==2.142.1 # via localstack-core -aws-sam-translator==1.88.0 +aws-sam-translator==1.89.0 # via # cfn-lint # localstack-core @@ -108,11 +108,11 @@ constantly==23.10.4 # via localstack-twisted constructs==10.3.0 # via aws-cdk-lib -coverage==7.4.4 +coverage==7.5.1 # via # coveralls # localstack-core -coveralls==4.0.0 +coveralls==4.0.1 # via localstack-core (pyproject.toml) crontab==1.0.1 # via localstack-core @@ -275,7 +275,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext==5.0.6.post2 +moto-ext==5.0.7.post1 # via localstack-core mpmath==1.3.0 # via sympy @@ -311,7 +311,7 @@ pbr==6.0.0 # via # jschema-to-python # sarif-om -platformdirs==4.2.1 +platformdirs==4.2.2 # via virtualenv pluggy==1.5.0 # via @@ -346,7 +346,7 @@ publication==0.0.3 # aws-cdk-lib # constructs # jsii -py-partiql-parser==0.5.4 +py-partiql-parser==0.5.5 # via moto-ext pyasn1==0.6.0 # via rsa @@ -413,7 +413,7 @@ referencing==0.31.1 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2024.5.10 +regex==2024.5.15 # via cfn-lint requests==2.31.0 # via @@ -525,7 +525,7 @@ xmltodict==0.13.0 # via # localstack-core # moto-ext -zope-interface==6.3 +zope-interface==6.4 # via localstack-twisted # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 01239d68ea18b..2c8a3a8acfc31 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -27,7 +27,7 @@ attrs==23.2.0 # localstack-twisted # referencing # sarif-om -aws-sam-translator==1.88.0 +aws-sam-translator==1.89.0 # via # cfn-lint # localstack-core (pyproject.toml) @@ -212,7 +212,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext==5.0.6.post2 +moto-ext==5.0.7.post1 # via localstack-core (pyproject.toml) mpmath==1.3.0 # via sympy @@ -254,7 +254,7 @@ psutil==5.9.8 # via # localstack-core # localstack-core (pyproject.toml) -py-partiql-parser==0.5.4 +py-partiql-parser==0.5.5 # via moto-ext pyasn1==0.6.0 # via rsa @@ -304,7 +304,7 @@ referencing==0.31.1 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2024.5.10 +regex==2024.5.15 # via cfn-lint requests==2.31.0 # via @@ -392,7 +392,7 @@ xmltodict==0.13.0 # via # localstack-core # moto-ext -zope-interface==6.3 +zope-interface==6.4 # via localstack-twisted # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements-test.txt b/requirements-test.txt index 31cba95a33f09..94d6e5c6eeeae 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -37,9 +37,9 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.141.0 +aws-cdk-lib==2.142.1 # via localstack-core (pyproject.toml) -aws-sam-translator==1.88.0 +aws-sam-translator==1.89.0 # via # cfn-lint # localstack-core @@ -259,7 +259,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext==5.0.6.post2 +moto-ext==5.0.7.post1 # via localstack-core mpmath==1.3.0 # via sympy @@ -317,7 +317,7 @@ publication==0.0.3 # aws-cdk-lib # constructs # jsii -py-partiql-parser==0.5.4 +py-partiql-parser==0.5.5 # via moto-ext pyasn1==0.6.0 # via rsa @@ -381,7 +381,7 @@ referencing==0.31.1 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2024.5.10 +regex==2024.5.15 # via cfn-lint requests==2.31.0 # via @@ -486,7 +486,7 @@ xmltodict==0.13.0 # via # localstack-core # moto-ext -zope-interface==6.3 +zope-interface==6.4 # via localstack-twisted # The following packages are considered to be unsafe in a requirements file: diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 47c8f5f031b4a..401ced0aa3767 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -37,9 +37,9 @@ aws-cdk-asset-kubectl-v20==2.1.2 # via aws-cdk-lib aws-cdk-asset-node-proxy-agent-v6==2.0.3 # via aws-cdk-lib -aws-cdk-lib==2.141.0 +aws-cdk-lib==2.142.1 # via localstack-core -aws-sam-translator==1.88.0 +aws-sam-translator==1.89.0 # via # cfn-lint # localstack-core @@ -112,11 +112,11 @@ constantly==23.10.4 # via localstack-twisted constructs==10.3.0 # via aws-cdk-lib -coverage==7.4.4 +coverage==7.5.1 # via # coveralls # localstack-core -coveralls==4.0.0 +coveralls==4.0.1 # via localstack-core crontab==1.0.1 # via localstack-core @@ -279,7 +279,7 @@ markupsafe==2.1.5 # werkzeug mdurl==0.1.2 # via markdown-it-py -moto-ext==5.0.6.post2 +moto-ext==5.0.7.post1 # via localstack-core mpmath==1.3.0 # via sympy @@ -287,7 +287,7 @@ multipart==0.2.4 # via moto-ext mypy-boto3-acm==1.34.0 # via boto3-stubs -mypy-boto3-acm-pca==1.34.28 +mypy-boto3-acm-pca==1.34.107 # via boto3-stubs mypy-boto3-amplify==1.34.94 # via boto3-stubs @@ -381,7 +381,7 @@ mypy-boto3-iotanalytics==1.34.0 # via boto3-stubs mypy-boto3-iotwireless==1.34.85 # via boto3-stubs -mypy-boto3-kafka==1.34.61 +mypy-boto3-kafka==1.34.107 # via boto3-stubs mypy-boto3-kinesis==1.34.0 # via boto3-stubs @@ -405,7 +405,7 @@ mypy-boto3-mediastore==1.34.0 # via boto3-stubs mypy-boto3-mq==1.34.0 # via boto3-stubs -mypy-boto3-mwaa==1.34.57 +mypy-boto3-mwaa==1.34.107 # via boto3-stubs mypy-boto3-neptune==1.34.0 # via boto3-stubs @@ -437,15 +437,15 @@ mypy-boto3-route53==1.34.31 # via boto3-stubs mypy-boto3-route53resolver==1.34.102 # via boto3-stubs -mypy-boto3-s3==1.34.91 +mypy-boto3-s3==1.34.105 # via boto3-stubs mypy-boto3-s3control==1.34.83 # via boto3-stubs -mypy-boto3-sagemaker==1.34.103 +mypy-boto3-sagemaker==1.34.107 # via boto3-stubs mypy-boto3-sagemaker-runtime==1.34.0 # via boto3-stubs -mypy-boto3-secretsmanager==1.34.72 +mypy-boto3-secretsmanager==1.34.107 # via boto3-stubs mypy-boto3-serverlessrepo==1.34.0 # via boto3-stubs @@ -507,7 +507,7 @@ pbr==6.0.0 # via # jschema-to-python # sarif-om -platformdirs==4.2.1 +platformdirs==4.2.2 # via virtualenv pluggy==1.5.0 # via @@ -542,7 +542,7 @@ publication==0.0.3 # aws-cdk-lib # constructs # jsii -py-partiql-parser==0.5.4 +py-partiql-parser==0.5.5 # via moto-ext pyasn1==0.6.0 # via rsa @@ -609,7 +609,7 @@ referencing==0.31.1 # jsonschema # jsonschema-path # jsonschema-specifications -regex==2024.5.10 +regex==2024.5.15 # via cfn-lint requests==2.31.0 # via @@ -822,7 +822,7 @@ xmltodict==0.13.0 # via # localstack-core # moto-ext -zope-interface==6.3 +zope-interface==6.4 # via localstack-twisted # The following packages are considered to be unsafe in a requirements file: From ed823885ab0c093e8d30698639bb31233efdd37c Mon Sep 17 00:00:00 2001 From: Daniel Fangl <daniel.fangl@localstack.cloud> Date: Fri, 17 May 2024 13:23:45 +0200 Subject: [PATCH 159/169] Fix entrypoint script configurator encoding and line endings in windows (#10814) --- localstack/dev/run/configurators.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/localstack/dev/run/configurators.py b/localstack/dev/run/configurators.py index 1b92661dd42b6..91741ff0c883d 100644 --- a/localstack/dev/run/configurators.py +++ b/localstack/dev/run/configurators.py @@ -97,7 +97,9 @@ def __call__(self, cfg: ContainerConfiguration): file = Path(tempdir, file_name) if not file.exists(): - file.write_text(self.script) + # newline separator should be '\n' independent of the os, since the entrypoint is executed in the container + # encoding needs to be "utf-8" since scripts could include emojis + file.write_text(self.script, newline="\n", encoding="utf-8") file.chmod(0o777) cfg.volumes.add(VolumeBind(str(file), f"/tmp/{file.name}")) cfg.entrypoint = f"/tmp/{file.name}" From 4fddd0d67b4a2ee3647d1e5b13b7246e8d1771c3 Mon Sep 17 00:00:00 2001 From: Simon Walker <simon.walker@localstack.cloud> Date: Fri, 17 May 2024 15:06:38 +0100 Subject: [PATCH 160/169] Cleanup code mounting dev scripts (#10842) --- Makefile | 24 +----------------------- bin/localstack-start-docker-dev.sh | 13 ------------- 2 files changed, 1 insertion(+), 36 deletions(-) delete mode 100755 bin/localstack-start-docker-dev.sh diff --git a/Makefile b/Makefile index 4e1df728147db..bcae11f0e6f80 100644 --- a/Makefile +++ b/Makefile @@ -198,13 +198,6 @@ docker-run-tests-s3-only: ## Initializes the test environment and runs the te bash -c "apt-get update && apt-get install -y g++ && make install-test && apt-get install -y --no-install-recommends gnupg && mkdir -p /etc/apt/keyrings && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main\" > /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install -y --no-install-recommends nodejs && DEBUG=$(DEBUG) PYTEST_LOGLEVEL=$(PYTEST_LOGLEVEL) PYTEST_ARGS='$(PYTEST_ARGS)' TEST_PATH='$(TEST_PATH)' TINYBIRD_PYTEST_ARGS='$(TINYBIRD_PYTEST_ARGS)' TINYBIRD_DATASOURCE='$(TINYBIRD_DATASOURCE)' TINYBIRD_TOKEN='$(TINYBIRD_TOKEN)' TINYBIRD_URL='$(TINYBIRD_URL)' CI_COMMIT_BRANCH='$(CI_COMMIT_BRANCH)' CI_COMMIT_SHA='$(CI_COMMIT_SHA)' CI_JOB_URL='$(CI_JOB_URL)' CI_JOB_NAME='$(CI_JOB_NAME)' CI_JOB_ID='$(CI_JOB_ID)' CI='$(CI)' make test" -docker-run: ## Run Docker image locally - ($(VENV_RUN); bin/localstack start) - -docker-mount-run: - MOTO_DIR=$$(echo $$(pwd)/.venv/lib/python*/site-packages/moto | awk '{print $$NF}'); echo MOTO_DIR $$MOTO_DIR; \ - DOCKER_FLAGS="$(DOCKER_FLAGS) -v `pwd`/localstack/constants.py:/opt/code/localstack/localstack/constants.py -v `pwd`/localstack/config.py:/opt/code/localstack/localstack/config.py -v `pwd`/localstack/plugins.py:/opt/code/localstack/localstack/plugins.py -v `pwd`/localstack/plugin:/opt/code/localstack/localstack/plugin -v `pwd`/localstack/runtime:/opt/code/localstack/localstack/runtime -v `pwd`/localstack/utils:/opt/code/localstack/localstack/utils -v `pwd`/localstack/services:/opt/code/localstack/localstack/services -v `pwd`/localstack/http:/opt/code/localstack/localstack/http -v `pwd`/localstack/contrib:/opt/code/localstack/localstack/contrib -v `pwd`/tests:/opt/code/localstack/tests -v $$MOTO_DIR:/opt/code/localstack/.venv/lib/python3.11/site-packages/moto/" make docker-run - docker-cp-coverage: @echo 'Extracting .coverage file from Docker image'; \ id=$$(docker create localstack/localstack); \ @@ -218,21 +211,6 @@ test-coverage: LOCALSTACK_INTERNAL_TEST_COLLECT_METRIC = 1 test-coverage: TEST_EXEC = python -m coverage run $(COVERAGE_ARGS) -m test-coverage: test ## Run automated tests and create coverage report -test-docker: - DOCKER_FLAGS="--entrypoint= $(DOCKER_FLAGS)" CMD="make test" make docker-run - -test-docker-mount: ## Run automated tests in Docker (mounting local code) - # TODO: find a cleaner way to mount/copy the dependencies into the container... - VENV_DIR=$$(pwd)/.venv/; \ - PKG_DIR=$$(echo $$VENV_DIR/lib/python*/site-packages | awk '{print $$NF}'); \ - PKG_DIR_CON=/opt/code/localstack/.venv/lib/python3.11/site-packages; \ - echo "#!/usr/bin/env python" > /tmp/pytest.ls.bin; cat $$VENV_DIR/bin/pytest >> /tmp/pytest.ls.bin; chmod +x /tmp/pytest.ls.bin; \ - DOCKER_FLAGS="-v `pwd`/tests:/opt/code/localstack/tests -v /tmp/pytest.ls.bin:/opt/code/localstack/.venv/bin/pytest -v $$PKG_DIR/deepdiff:$$PKG_DIR_CON/deepdiff -v $$PKG_DIR/ordered_set:$$PKG_DIR_CON/ordered_set -v $$PKG_DIR/py:$$PKG_DIR_CON/py -v $$PKG_DIR/pluggy:$$PKG_DIR_CON/pluggy -v $$PKG_DIR/iniconfig:$$PKG_DIR_CON/iniconfig -v $$PKG_DIR/jsonpath_ng:$$PKG_DIR_CON/jsonpath_ng -v $$PKG_DIR/packaging:$$PKG_DIR_CON/packaging -v $$PKG_DIR/pytest:$$PKG_DIR_CON/pytest -v $$PKG_DIR/pytest_httpserver:$$PKG_DIR_CON/pytest_httpserver -v $$PKG_DIR/_pytest:$$PKG_DIR_CON/_pytest -v $$PKG_DIR/_pytest:$$PKG_DIR_CON/orjson" make test-docker-mount-code - -test-docker-mount-code: - PACKAGES_DIR=$$(echo $$(pwd)/.venv/lib/python*/site-packages | awk '{print $$NF}'); \ - DOCKER_FLAGS="$(DOCKER_FLAGS) --entrypoint= -v `pwd`/localstack/config.py:/opt/code/localstack/localstack/config.py -v `pwd`/localstack/constants.py:/opt/code/localstack/localstack/constants.py -v `pwd`/localstack/utils:/opt/code/localstack/localstack/utils -v `pwd`/localstack/services:/opt/code/localstack/localstack/services -v `pwd`/localstack/aws:/opt/code/localstack/localstack/aws -v `pwd`/Makefile:/opt/code/localstack/Makefile -v $$PACKAGES_DIR/moto:/opt/code/localstack/.venv/lib/python3.11/site-packages/moto/ -e TEST_PATH=\\'$(TEST_PATH)\\' -e LAMBDA_JAVA_OPTS=$(LAMBDA_JAVA_OPTS) $(ENTRYPOINT)" CMD="make test" make docker-run - lint: ## Run code linter to check code style, check if formatter would make changes and check if dependency pins need to be updated ($(VENV_RUN); python -m ruff check --output-format=full . && python -m ruff format --check .) $(VENV_RUN); pre-commit run check-pinned-deps-for-needed-upgrade --files pyproject.toml # run pre-commit hook manually here to ensure that this check runs in CI as well @@ -265,4 +243,4 @@ clean-dist: ## Clean up python distribution directories rm -rf dist/ build/ rm -rf *.egg-info -.PHONY: usage freeze install-basic install-runtime install-test install-dev install entrypoints dist publish coveralls start docker-save-image docker-build docker-build-multiarch docker-push-master docker-create-push-manifests docker-run-tests docker-run docker-mount-run docker-cp-coverage test test-coverage test-docker test-docker-mount test-docker-mount-code lint lint-modified format format-modified init-precommit clean clean-dist upgrade-pinned-dependencies +.PHONY: usage freeze install-basic install-runtime install-test install-dev install entrypoints dist publish coveralls start docker-save-image docker-build docker-build-multiarch docker-push-master docker-create-push-manifests docker-run-tests docker-cp-coverage test test-coverage lint lint-modified format format-modified init-precommit clean clean-dist upgrade-pinned-dependencies diff --git a/bin/localstack-start-docker-dev.sh b/bin/localstack-start-docker-dev.sh deleted file mode 100755 index e8766d4c56678..0000000000000 --- a/bin/localstack-start-docker-dev.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -source ${VENV_DIR=.venv}/bin/activate - -export LOCALSTACK_VOLUME_DIR=$(pwd)/.filesystem/var/lib/localstack -export DOCKER_FLAGS="${DOCKER_FLAGS} --v $(pwd)/localstack:/opt/code/localstack/localstack --v $(pwd)/localstack_core.egg-info:/opt/code/localstack/localstack_core.egg-info --v $(pwd)/.filesystem/etc/localstack:/etc/localstack --v $(pwd)/bin/localstack-supervisor:/opt/code/localstack/bin/localstack-supervisor --v $(pwd)/bin/docker-entrypoint.sh:/usr/local/bin/docker-entrypoint.sh" - -exec python -m localstack.cli.main start "$@" From 4a3fee67d5b774a4f2009faf13e9414d0d8deae7 Mon Sep 17 00:00:00 2001 From: MEPalma <64580864+MEPalma@users.noreply.github.com> Date: Fri, 17 May 2024 17:07:42 +0200 Subject: [PATCH 161/169] StepFunctions: Support for Glue Optimised Service Integration (#10802) --- .github/workflows/marker-report-issue.yml | 2 +- .github/workflows/marker-report.yml | 2 +- .github/workflows/tests-cli.yml | 2 +- .../service/state_task_service_callback.py | 2 + .../service/state_task_service_factory.py | 5 + .../service/state_task_service_glue.py | 195 +++++++++ .../testing/pytest/stepfunctions/__init__.py | 0 .../testing/pytest/stepfunctions/fixtures.py | 381 ++++++++++++++---- .../testing/pytest}/stepfunctions/utils.py | 58 ++- .../scenario/loan_broker/test_loan_broker.py | 2 +- .../resources/test_stepfunctions.py | 2 +- .../legacy/test_stepfunctions_legacy.py | 2 +- .../v2/activities/test_activities.py | 6 +- .../stepfunctions/v2/base/test_base.py | 8 +- .../stepfunctions/v2/base/test_wait.py | 4 +- .../v2/callback/test_callback.py | 10 +- .../v2/choice_operators/utils.py | 2 +- .../v2/comments/test_comments.py | 2 +- .../v2/error_handling/test_aws_sdk.py | 4 +- .../v2/error_handling/test_states_errors.py | 4 +- .../v2/error_handling/test_task_lambda.py | 4 +- .../test_task_service_dynamodb.py | 4 +- .../test_task_service_lambda.py | 4 +- .../error_handling/test_task_service_sfn.py | 5 +- .../error_handling/test_task_service_sqs.py | 4 +- .../stepfunctions/v2/error_handling/utils.py | 2 +- .../test_math_operations.py | 2 +- .../test_unique_id_generation.py | 2 +- .../v2/intrinsic_functions/utils.py | 2 +- .../v2/scenarios/test_base_scenarios.py | 12 +- .../v2/scenarios/test_sfn_scenarios.py | 2 +- .../services/test_apigetway_task_service.py | 4 +- .../v2/services/test_aws_sdk_task_service.py | 5 +- .../v2/services/test_dynamodb_task_service.py | 4 +- .../v2/services/test_ecs_task_service.py | 2 +- .../v2/services/test_events_task_service.py | 5 +- .../v2/services/test_lambda_task.py | 4 +- .../v2/services/test_lambda_task_service.py | 4 +- .../v2/services/test_sfn_task_service.py | 5 +- .../v2/services/test_sns_task_service.py | 4 +- .../v2/services/test_sqs_task_service.py | 4 +- .../services/stepfunctions/v2/test_sfn_api.py | 8 +- .../stepfunctions/v2/test_sfn_api_map_run.py | 5 +- .../v2/test_sfn_api_versioning.py | 6 +- .../v2/timeouts/test_heartbeats.py | 4 +- .../v2/timeouts/test_timeouts.py | 8 +- tests/conftest.py | 1 + 47 files changed, 662 insertions(+), 147 deletions(-) create mode 100644 localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_glue.py create mode 100644 localstack/testing/pytest/stepfunctions/__init__.py rename tests/aws/services/stepfunctions/conftest.py => localstack/testing/pytest/stepfunctions/fixtures.py (53%) rename {tests/aws/services => localstack/testing/pytest}/stepfunctions/utils.py (87%) diff --git a/.github/workflows/marker-report-issue.yml b/.github/workflows/marker-report-issue.yml index c04b7e9ce5726..f4d99a02ff293 100644 --- a/.github/workflows/marker-report-issue.yml +++ b/.github/workflows/marker-report-issue.yml @@ -41,7 +41,7 @@ jobs: # makes use of the marker report plugin localstack.testing.pytest.marker_report - name: Generate marker report env: - PYTEST_ADDOPTS: "-p no:localstack.testing.pytest.fixtures -p no:localstack_snapshot.pytest.snapshot -p no:localstack.testing.pytest.filters -p no:localstack.testing.pytest.fixture_conflicts -p no:tests.fixtures -s --co --disable-warnings --marker-report --marker-report-path './target'" + PYTEST_ADDOPTS: "-p no:localstack.testing.pytest.fixtures -p no:localstack_snapshot.pytest.snapshot -p no:localstack.testing.pytest.filters -p no:localstack.testing.pytest.fixture_conflicts -p no:tests.fixtures -p no:localstack.testing.pytest.stepfunctions.fixtures -s --co --disable-warnings --marker-report --marker-report-path './target'" MARKER_REPORT_PROJECT_NAME: localstack MARKER_REPORT_COMMIT_SHA: ${{ github.sha }} run: | diff --git a/.github/workflows/marker-report.yml b/.github/workflows/marker-report.yml index aa1036e7e39b2..90ac2e8791781 100644 --- a/.github/workflows/marker-report.yml +++ b/.github/workflows/marker-report.yml @@ -36,7 +36,7 @@ jobs: - name: Collect marker report env: - PYTEST_ADDOPTS: "-p no:localstack.testing.pytest.fixtures -p no:localstack_snapshot.pytest.snapshot -p no:localstack.testing.pytest.filters -p no:localstack.testing.pytest.fixture_conflicts -p no:tests.fixtures -s --co --disable-warnings --marker-report --marker-report-tinybird-upload" + PYTEST_ADDOPTS: "-p no:localstack.testing.pytest.fixtures -p no:localstack_snapshot.pytest.snapshot -p no:localstack.testing.pytest.filters -p no:localstack.testing.pytest.fixture_conflicts -p no:tests.fixtures -p no:localstack.testing.pytest.stepfunctions.fixtures -s --co --disable-warnings --marker-report --marker-report-tinybird-upload" MARKER_REPORT_PROJECT_NAME: localstack MARKER_REPORT_TINYBIRD_TOKEN: ${{ secrets.MARKER_REPORT_TINYBIRD_TOKEN }} MARKER_REPORT_COMMIT_SHA: ${{ github.sha }} diff --git a/.github/workflows/tests-cli.yml b/.github/workflows/tests-cli.yml index 42f9656e54997..001e85ab6d3da 100644 --- a/.github/workflows/tests-cli.yml +++ b/.github/workflows/tests-cli.yml @@ -98,7 +98,7 @@ jobs: pip install pytest pytest-tinybird - name: Run CLI tests env: - PYTEST_ADDOPTS: "${{ env.TINYBIRD_PYTEST_ARGS }}-p no:localstack.testing.pytest.fixtures -p no:localstack_snapshot.pytest.snapshot -p no:localstack.testing.pytest.filters -p no:localstack.testing.pytest.fixture_conflicts -p no:localstack.testing.pytest.validation_tracking -p no:localstack.testing.pytest.path_filter -p no:tests.fixtures -s" + PYTEST_ADDOPTS: "${{ env.TINYBIRD_PYTEST_ARGS }}-p no:localstack.testing.pytest.fixtures -p no:localstack_snapshot.pytest.snapshot -p no:localstack.testing.pytest.filters -p no:localstack.testing.pytest.fixture_conflicts -p no:localstack.testing.pytest.validation_tracking -p no:localstack.testing.pytest.path_filter -p no:tests.fixtures -p no:localstack.testing.pytest.stepfunctions.fixtures -s" TEST_PATH: "tests/cli/" run: make test diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py index ba061d0299458..4447e483aa7e8 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_callback.py @@ -112,6 +112,8 @@ def _sync2( @staticmethod def _throttle_sync_iteration(seconds: float = 0.5): + # TODO: consider implementing a polling pattern similar to that observable from AWS: + # https://repost.aws/questions/QUFFlHcbvIQFe-bS3RAi7TWA/a-glue-job-in-a-step-function-is-taking-so-long-to-continue-the-next-step time.sleep(seconds) def _is_condition(self): diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_factory.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_factory.py index e3bf01200ecba..588c34d8b081e 100644 --- a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_factory.py +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_factory.py @@ -18,6 +18,9 @@ from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.service.state_task_service_events import ( StateTaskServiceEvents, ) +from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.service.state_task_service_glue import ( + StateTaskServiceGlue, +) from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.service.state_task_service_lambda import ( StateTaskServiceLambda, ) @@ -53,5 +56,7 @@ def state_task_service_for(service_name: str) -> StateTaskService: return StateTaskServiceEvents() case "ecs": return StateTaskServiceEcs() + case "glue": + return StateTaskServiceGlue() case unknown: raise NotImplementedError(f"Unsupported service: '{unknown}'.") # noqa diff --git a/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_glue.py b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_glue.py new file mode 100644 index 0000000000000..d058be93c4672 --- /dev/null +++ b/localstack/services/stepfunctions/asl/component/state/state_execution/state_task/service/state_task_service_glue.py @@ -0,0 +1,195 @@ +from typing import Callable, Final, Optional + +import boto3 +from botocore.exceptions import ClientError + +from localstack.aws.api.stepfunctions import HistoryEventType, TaskFailedEventDetails +from localstack.services.stepfunctions.asl.component.common.error_name.custom_error_name import ( + CustomErrorName, +) +from localstack.services.stepfunctions.asl.component.common.error_name.failure_event import ( + FailureEvent, + FailureEventException, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name import ( + StatesErrorName, +) +from localstack.services.stepfunctions.asl.component.common.error_name.states_error_name_type import ( + StatesErrorNameType, +) +from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.service.resource import ( + ResourceRuntimePart, +) +from localstack.services.stepfunctions.asl.component.state.state_execution.state_task.service.state_task_service_callback import ( + StateTaskServiceCallback, +) +from localstack.services.stepfunctions.asl.eval.environment import Environment +from localstack.services.stepfunctions.asl.eval.event.event_detail import EventDetails +from localstack.services.stepfunctions.asl.utils.boto_client import boto_client_for +from localstack.services.stepfunctions.asl.utils.encoding import to_json_str + +# Set of JobRunState value that indicate the JobRun had terminated in an abnormal state. +_JOB_RUN_STATE_ABNORMAL_TERMINAL_VALUE: Final[set[str]] = {"FAILED", "TIMEOUT", "ERROR"} + +# Set of JobRunState values that indicate the JobRun has terminated. +_JOB_RUN_STATE_TERMINAL_VALUES: Final[set[str]] = { + "STOPPED", + "SUCCEEDED", + *_JOB_RUN_STATE_ABNORMAL_TERMINAL_VALUE, +} + +# The handler function name prefix for StateTaskServiceGlue objects. +_HANDLER_REFLECTION_PREFIX: Final[str] = "_handle_" +# The sync handler function name prefix for StateTaskServiceGlue objects. +_SYNC_HANDLER_REFLECTION_PREFIX: Final[str] = "_sync_to_" +# The type of (sync)handler function for StateTaskServiceGlue objects. +_API_ACTION_HANDLER_TYPE = Callable[[Environment, ResourceRuntimePart, dict], None] + + +class StateTaskServiceGlue(StateTaskServiceCallback): + def _get_handler_by_reflection(self, prefix: str) -> _API_ACTION_HANDLER_TYPE: + # Retrieve the request handler based on the service action and prefix value. + api_action = self._get_boto_service_action() + handler_name = prefix + api_action + resolver_handler = getattr(self, handler_name) + if resolver_handler is None: + raise ValueError(f"Unknown or unsupported glue action '{api_action}'.") + return resolver_handler + + def _get_api_action_handler(self) -> _API_ACTION_HANDLER_TYPE: + return self._get_handler_by_reflection(_HANDLER_REFLECTION_PREFIX) + + def _get_api_action_sync_handler(self) -> _API_ACTION_HANDLER_TYPE: + return self._get_handler_by_reflection(_SYNC_HANDLER_REFLECTION_PREFIX) + + @staticmethod + def _get_glue_client(resource_runtime_part: ResourceRuntimePart) -> boto3.client: + return boto_client_for( + region=resource_runtime_part.region, + account=resource_runtime_part.account, + service="glue", + ) + + def _from_error(self, env: Environment, ex: Exception) -> FailureEvent: + if isinstance(ex, ClientError): + error_code = ex.response["Error"]["Code"] + error_name: str = f"Glue.{error_code}" + return FailureEvent( + env=env, + error_name=CustomErrorName(error_name), + event_type=HistoryEventType.TaskFailed, + event_details=EventDetails( + taskFailedEventDetails=TaskFailedEventDetails( + error=error_name, + cause=ex.response["Error"]["Message"], + resource=self._get_sfn_resource(), + resourceType=self._get_sfn_resource_type(), + ) + ), + ) + return super()._from_error(env=env, ex=ex) + + def _wait_for_task_token( + self, + env: Environment, + resource_runtime_part: ResourceRuntimePart, + normalised_parameters: dict, + ) -> None: + raise RuntimeError( + f"Unsupported .waitForTaskToken callback procedure in resource {self.resource.resource_arn}" + ) + + def _handle_start_job_run( + self, + env: Environment, + resource_runtime_part: ResourceRuntimePart, + normalised_parameters: dict, + ): + glue_client = self._get_glue_client(resource_runtime_part=resource_runtime_part) + response = glue_client.start_job_run(**normalised_parameters) + response.pop("ResponseMetadata", None) + # AWS StepFunctions extracts the JobName from the request and inserts it into the response, which + # normally only contains JobRunID; as this is a required field for start_job_run, the access at + # this depth is safe. + response["JobName"] = normalised_parameters.get("JobName") + env.stack.append(response) + + def _eval_service_task( + self, + env: Environment, + resource_runtime_part: ResourceRuntimePart, + normalised_parameters: dict, + ): + # Source the action handler and delegate the evaluation. + api_action_handler = self._get_api_action_handler() + api_action_handler(env, resource_runtime_part, normalised_parameters) + + def _sync_to_start_job_run( + self, + env: Environment, + resource_runtime_part: ResourceRuntimePart, + normalised_parameters: dict, + ): + # Poll the job run state from glue, using GetJobRun until the job has terminated. Hence, append the output + # of GetJobRun to the state. + + # Access the JobName and the JobRunId from the StartJobRun output call that must + # have occurred before this point. + start_job_run_output: dict = env.stack.pop() + job_name: str = start_job_run_output["JobName"] + job_run_id: str = start_job_run_output["JobRunId"] + + glue_client = self._get_glue_client(resource_runtime_part=resource_runtime_part) + + def _has_terminated() -> Optional[dict]: + # Sample GetJobRun until completion. + get_job_run_response: dict = glue_client.get_job_run(JobName=job_name, RunId=job_run_id) + job_run: dict = get_job_run_response["JobRun"] + job_run_state: str = job_run["JobRunState"] + + # If the job run has not terminated, continue and check later. + is_terminated: bool = job_run_state in _JOB_RUN_STATE_TERMINAL_VALUES + if not is_terminated: + return None + + # AWS StepFunctions appears to append attach the JobName to the output both in case of error or success. + job_run["JobName"] = job_name + + # If the job run terminated in a normal state, return the result. + is_abnormal_termination = job_run_state in _JOB_RUN_STATE_ABNORMAL_TERMINAL_VALUE + if not is_abnormal_termination: + return job_run + + # If the job run has terminated with an abnormal state, raise the error in stepfunctions. + raise FailureEventException( + FailureEvent( + env=env, + error_name=StatesErrorName(typ=StatesErrorNameType.StatesTaskFailed), + event_type=HistoryEventType.TaskFailed, + event_details=EventDetails( + taskFailedEventDetails=TaskFailedEventDetails( + resource=self._get_sfn_resource(), + resourceType=self._get_sfn_resource_type(), + error=StatesErrorNameType.StatesTaskFailed.to_name(), + cause=to_json_str(job_run), + ) + ), + ) + ) + + termination_output: Optional[dict] = None + while env.is_running() and not termination_output: + self._throttle_sync_iteration() + termination_output = _has_terminated() + + env.stack.append(termination_output) + + def _sync( + self, + env: Environment, + resource_runtime_part: ResourceRuntimePart, + normalised_parameters: dict, + ) -> None: + # Source the sync action handler and delegate the evaluation. + sync_handler = self._get_api_action_sync_handler() + sync_handler(env, resource_runtime_part, normalised_parameters) diff --git a/localstack/testing/pytest/stepfunctions/__init__.py b/localstack/testing/pytest/stepfunctions/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/tests/aws/services/stepfunctions/conftest.py b/localstack/testing/pytest/stepfunctions/fixtures.py similarity index 53% rename from tests/aws/services/stepfunctions/conftest.py rename to localstack/testing/pytest/stepfunctions/fixtures.py index 3d896beb93c18..7e1eca55f4ee8 100644 --- a/tests/aws/services/stepfunctions/conftest.py +++ b/localstack/testing/pytest/stepfunctions/fixtures.py @@ -4,21 +4,14 @@ import pytest from botocore.config import Config -from jsonpath_ng.ext import parse from localstack_snapshot.snapshots.transformer import ( JsonpathTransformer, RegexTransformer, - TransformContext, ) -from localstack.aws.api.stepfunctions import HistoryEventType -from localstack.services.stepfunctions.asl.utils.encoding import to_json_str from localstack.testing.aws.util import is_aws_cloud +from localstack.testing.pytest.stepfunctions.utils import await_execution_success from localstack.utils.strings import short_uid -from tests.aws.services.stepfunctions.templates.callbacks.callback_templates import ( - CallbackTemplates, -) -from tests.aws.services.stepfunctions.utils import await_execution_success LOG = logging.getLogger(__name__) @@ -97,56 +90,6 @@ def sfn_ecs_snapshot(sfn_snapshot): return sfn_snapshot -class SfnNoneRecursiveParallelTransformer: - """ - Normalises a sublist of events triggered in by a Parallel state to be order-independent. - """ - - def __init__(self, events_jsonpath: str = "$..events"): - self.events_jsonpath: str = events_jsonpath - - @staticmethod - def _normalise_events(events: list[dict]) -> None: - start_idx = None - sublist = list() - in_sublist = False - for i, event in enumerate(events): - event_type = event.get("type") - if event_type is None: - LOG.debug(f"No 'type' in event item '{event}'.") - in_sublist = False - - elif event_type in { - None, - HistoryEventType.ParallelStateSucceeded, - HistoryEventType.ParallelStateAborted, - HistoryEventType.ParallelStateExited, - HistoryEventType.ParallelStateFailed, - }: - events[start_idx:i] = sorted(sublist, key=lambda e: to_json_str(e)) - in_sublist = False - elif event_type == HistoryEventType.ParallelStateStarted: - in_sublist = True - sublist = [] - start_idx = i + 1 - elif in_sublist: - event["id"] = (0,) - event["previousEventId"] = 0 - sublist.append(event) - - def transform(self, input_data: dict, *, ctx: TransformContext) -> dict: - pattern = parse("$..events") - events = pattern.find(input_data) - if not events: - LOG.debug(f"No Stepfunctions 'events' for jsonpath '{self.events_jsonpath}'.") - return input_data - - for events_data in events: - self._normalise_events(events_data.value) - - return input_data - - @pytest.fixture def stepfunctions_client_test_state(aws_client_factory): # For TestState calls, boto will prepend "sync-" to the endpoint string. As we operate on localhost, @@ -296,11 +239,79 @@ def sqs_send_task_success_state_machine(aws_client, create_state_machine, create def _create_state_machine(sqs_queue_url): snf_role_arn = create_iam_role_for_sfn() sm_name: str = f"sqs_send_task_success_state_machine_{short_uid()}" - template = CallbackTemplates.load_sfn_template(CallbackTemplates.SQS_SUCCESS_ON_TASK_TOKEN) - definition = json.dumps(template) + + template = { + "Comment": "sqs_success_on_task_token", + "StartAt": "Iterate", + "States": { + "Iterate": { + "Type": "Pass", + "Parameters": {"Count.$": "States.MathAdd($.Iterator.Count, -1)"}, + "ResultPath": "$.Iterator", + "Next": "IterateStep", + }, + "IterateStep": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.Iterator.Count", + "NumericLessThanEquals": 0, + "Next": "NoMoreCycles", + } + ], + "Default": "WaitAndReceive", + }, + "WaitAndReceive": {"Type": "Wait", "Seconds": 1, "Next": "Receive"}, + "Receive": { + "Type": "Task", + "Parameters": {"QueueUrl.$": "$.QueueUrl"}, + "Resource": "arn:aws:states:::aws-sdk:sqs:receiveMessage", + "ResultPath": "$.SQSOutput", + "Next": "CheckMessages", + }, + "CheckMessages": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.SQSOutput.Messages", + "IsPresent": True, + "Next": "SendSuccesses", + } + ], + "Default": "Iterate", + }, + "SendSuccesses": { + "Type": "Map", + "InputPath": "$.SQSOutput.Messages", + "ItemProcessor": { + "ProcessorConfig": {"Mode": "INLINE"}, + "StartAt": "ParseBody", + "States": { + "ParseBody": { + "Type": "Pass", + "Parameters": {"Body.$": "States.StringToJson($.Body)"}, + "Next": "Send", + }, + "Send": { + "Type": "Task", + "Resource": "arn:aws:states:::aws-sdk:sfn:sendTaskSuccess", + "Parameters": { + "Output.$": "States.JsonToString($.Body.Message)", + "TaskToken.$": "$.Body.TaskToken", + }, + "End": True, + }, + }, + }, + "ResultPath": None, + "Next": "Iterate", + }, + "NoMoreCycles": {"Type": "Pass", "End": True}, + }, + } creation_resp = create_state_machine( - name=sm_name, definition=definition, roleArn=snf_role_arn + name=sm_name, definition=json.dumps(template), roleArn=snf_role_arn ) state_machine_arn = creation_resp["stateMachineArn"] @@ -317,11 +328,80 @@ def sqs_send_task_failure_state_machine(aws_client, create_state_machine, create def _create_state_machine(sqs_queue_url): snf_role_arn = create_iam_role_for_sfn() sm_name: str = f"sqs_send_task_failure_state_machine_{short_uid()}" - template = CallbackTemplates.load_sfn_template(CallbackTemplates.SQS_FAILURE_ON_TASK_TOKEN) - definition = json.dumps(template) + + template = { + "Comment": "sqs_failure_on_task_token", + "StartAt": "Iterate", + "States": { + "Iterate": { + "Type": "Pass", + "Parameters": {"Count.$": "States.MathAdd($.Iterator.Count, -1)"}, + "ResultPath": "$.Iterator", + "Next": "IterateStep", + }, + "IterateStep": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.Iterator.Count", + "NumericLessThanEquals": 0, + "Next": "NoMoreCycles", + } + ], + "Default": "WaitAndReceive", + }, + "WaitAndReceive": {"Type": "Wait", "Seconds": 1, "Next": "Receive"}, + "Receive": { + "Type": "Task", + "Parameters": {"QueueUrl.$": "$.QueueUrl"}, + "Resource": "arn:aws:states:::aws-sdk:sqs:receiveMessage", + "ResultPath": "$.SQSOutput", + "Next": "CheckMessages", + }, + "CheckMessages": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.SQSOutput.Messages", + "IsPresent": True, + "Next": "SendFailure", + } + ], + "Default": "Iterate", + }, + "SendFailure": { + "Type": "Map", + "InputPath": "$.SQSOutput.Messages", + "ItemProcessor": { + "ProcessorConfig": {"Mode": "INLINE"}, + "StartAt": "ParseBody", + "States": { + "ParseBody": { + "Type": "Pass", + "Parameters": {"Body.$": "States.StringToJson($.Body)"}, + "Next": "Send", + }, + "Send": { + "Type": "Task", + "Resource": "arn:aws:states:::aws-sdk:sfn:sendTaskFailure", + "Parameters": { + "Error": "Failure error", + "Cause": "Failure cause", + "TaskToken.$": "$.Body.TaskToken", + }, + "End": True, + }, + }, + }, + "ResultPath": None, + "Next": "Iterate", + }, + "NoMoreCycles": {"Type": "Pass", "End": True}, + }, + } creation_resp = create_state_machine( - name=sm_name, definition=definition, roleArn=snf_role_arn + name=sm_name, definition=json.dumps(template), roleArn=snf_role_arn ) state_machine_arn = creation_resp["stateMachineArn"] @@ -340,13 +420,91 @@ def sqs_send_heartbeat_and_task_success_state_machine( def _create_state_machine(sqs_queue_url): snf_role_arn = create_iam_role_for_sfn() sm_name: str = f"sqs_send_heartbeat_and_task_success_state_machine_{short_uid()}" - template = CallbackTemplates.load_sfn_template( - CallbackTemplates.SQS_HEARTBEAT_SUCCESS_ON_TASK_TOKEN - ) - definition = json.dumps(template) + + template = { + "Comment": "SQS_HEARTBEAT_SUCCESS_ON_TASK_TOKEN", + "StartAt": "Iterate", + "States": { + "Iterate": { + "Type": "Pass", + "Parameters": {"Count.$": "States.MathAdd($.Iterator.Count, -1)"}, + "ResultPath": "$.Iterator", + "Next": "IterateStep", + }, + "IterateStep": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.Iterator.Count", + "NumericLessThanEquals": 0, + "Next": "NoMoreCycles", + } + ], + "Default": "WaitAndReceive", + }, + "WaitAndReceive": {"Type": "Wait", "Seconds": 1, "Next": "Receive"}, + "Receive": { + "Type": "Task", + "Parameters": {"QueueUrl.$": "$.QueueUrl"}, + "Resource": "arn:aws:states:::aws-sdk:sqs:receiveMessage", + "ResultPath": "$.SQSOutput", + "Next": "CheckMessages", + }, + "CheckMessages": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.SQSOutput.Messages", + "IsPresent": True, + "Next": "SendSuccesses", + } + ], + "Default": "Iterate", + }, + "SendSuccesses": { + "Type": "Map", + "InputPath": "$.SQSOutput.Messages", + "ItemProcessor": { + "ProcessorConfig": {"Mode": "INLINE"}, + "StartAt": "ParseBody", + "States": { + "ParseBody": { + "Type": "Pass", + "Parameters": {"Body.$": "States.StringToJson($.Body)"}, + "Next": "WaitBeforeHeartbeat", + }, + "WaitBeforeHeartbeat": { + "Type": "Wait", + "Seconds": 5, + "Next": "SendHeartbeat", + }, + "SendHeartbeat": { + "Type": "Task", + "Resource": "arn:aws:states:::aws-sdk:sfn:sendTaskHeartbeat", + "Parameters": {"TaskToken.$": "$.Body.TaskToken"}, + "ResultPath": None, + "Next": "SendSuccess", + }, + "SendSuccess": { + "Type": "Task", + "Resource": "arn:aws:states:::aws-sdk:sfn:sendTaskSuccess", + "Parameters": { + "Output.$": "States.JsonToString($.Body.Message)", + "TaskToken.$": "$.Body.TaskToken", + }, + "End": True, + }, + }, + }, + "ResultPath": None, + "Next": "Iterate", + }, + "NoMoreCycles": {"Type": "Pass", "End": True}, + }, + } creation_resp = create_state_machine( - name=sm_name, definition=definition, roleArn=snf_role_arn + name=sm_name, definition=json.dumps(template), roleArn=snf_role_arn ) state_machine_arn = creation_resp["stateMachineArn"] @@ -378,20 +536,6 @@ def _create_state_machine(template, activity_arn): return _create_state_machine -@pytest.fixture -def sfn_events_to_sqs_queue(events_to_sqs_queue, aws_client): - def _create(state_machine_arn: str) -> str: - event_pattern = { - "source": ["aws.states"], - "detail": { - "stateMachineArn": [state_machine_arn], - }, - } - return events_to_sqs_queue(event_pattern=event_pattern) - - return _create - - @pytest.fixture def events_to_sqs_queue(events_create_rule, sqs_create_queue, sqs_get_queue_arn, aws_client): def _setup(event_pattern): @@ -427,3 +571,74 @@ def _setup(event_pattern): return queue_url return _setup + + +@pytest.fixture +def sfn_events_to_sqs_queue(events_to_sqs_queue): + def _create(state_machine_arn: str) -> str: + event_pattern = { + "source": ["aws.states"], + "detail": { + "stateMachineArn": [state_machine_arn], + }, + } + return events_to_sqs_queue(event_pattern=event_pattern) + + return _create + + +@pytest.fixture +def sfn_glue_create_job(aws_client, create_role, create_policy, wait_and_assume_role): + job_names = [] + + def _execute(**kwargs): + job_name = f"glue-job-{short_uid()}" + + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": "sts:AssumeRole", + } + ], + } + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["*"], + "Resource": "*", + }, + ], + } + + role = create_role(AssumeRolePolicyDocument=json.dumps(assume_role_policy_document)) + role_name = role["Role"]["RoleName"] + role_arn = role["Role"]["Arn"] + + policy = create_policy(PolicyDocument=json.dumps(policy_document)) + policy_arn = policy["Policy"]["Arn"] + + aws_client.iam.attach_role_policy( + RoleName=role_name, + PolicyArn=policy_arn, + ) + + wait_and_assume_role(role_arn) + + aws_client.glue.create_job(Name=job_name, Role=role_arn, **kwargs) + + job_names.append(job_name) + return job_name + + yield _execute + + for job_name in job_names: + try: + aws_client.glue.delete_job(JobName=job_name) + except Exception as ex: + # TODO: the glue provider should not fail on deletion of deleted job, however this is currently the case. + LOG.warning(f"Could not delete job '{job_name}': {ex}") diff --git a/tests/aws/services/stepfunctions/utils.py b/localstack/testing/pytest/stepfunctions/utils.py similarity index 87% rename from tests/aws/services/stepfunctions/utils.py rename to localstack/testing/pytest/stepfunctions/utils.py index 6999f7e831eee..fb0ea5fa00fb0 100644 --- a/tests/aws/services/stepfunctions/utils.py +++ b/localstack/testing/pytest/stepfunctions/utils.py @@ -4,7 +4,12 @@ from typing import Callable, Final from botocore.exceptions import ClientError -from localstack_snapshot.snapshots.transformer import JsonpathTransformer, RegexTransformer +from jsonpath_ng.ext import parse +from localstack_snapshot.snapshots.transformer import ( + JsonpathTransformer, + RegexTransformer, + TransformContext, +) from localstack.aws.api.stepfunctions import ( CreateStateMachineOutput, @@ -12,6 +17,7 @@ HistoryEventList, HistoryEventType, ) +from localstack.services.stepfunctions.asl.utils.encoding import to_json_str from localstack.services.stepfunctions.asl.utils.json_path import JSONPathUtils from localstack.testing.aws.util import is_aws_cloud from localstack.utils.strings import short_uid @@ -400,3 +406,53 @@ def _get_events(): poll_condition(_get_events, timeout=60) stepfunctions_events.sort(key=lambda e: json.dumps(e.get("detail", dict()))) sfn_snapshot.match("stepfunctions_events", stepfunctions_events) + + +class SfnNoneRecursiveParallelTransformer: + """ + Normalises a sublist of events triggered in by a Parallel state to be order-independent. + """ + + def __init__(self, events_jsonpath: str = "$..events"): + self.events_jsonpath: str = events_jsonpath + + @staticmethod + def _normalise_events(events: list[dict]) -> None: + start_idx = None + sublist = list() + in_sublist = False + for i, event in enumerate(events): + event_type = event.get("type") + if event_type is None: + LOG.debug(f"No 'type' in event item '{event}'.") + in_sublist = False + + elif event_type in { + None, + HistoryEventType.ParallelStateSucceeded, + HistoryEventType.ParallelStateAborted, + HistoryEventType.ParallelStateExited, + HistoryEventType.ParallelStateFailed, + }: + events[start_idx:i] = sorted(sublist, key=lambda e: to_json_str(e)) + in_sublist = False + elif event_type == HistoryEventType.ParallelStateStarted: + in_sublist = True + sublist = [] + start_idx = i + 1 + elif in_sublist: + event["id"] = (0,) + event["previousEventId"] = 0 + sublist.append(event) + + def transform(self, input_data: dict, *, ctx: TransformContext) -> dict: + pattern = parse("$..events") + events = pattern.find(input_data) + if not events: + LOG.debug(f"No Stepfunctions 'events' for jsonpath '{self.events_jsonpath}'.") + return input_data + + for events_data in events: + self._normalise_events(events_data.value) + + return input_data diff --git a/tests/aws/scenario/loan_broker/test_loan_broker.py b/tests/aws/scenario/loan_broker/test_loan_broker.py index aa8ff16973a5f..4eb105c440398 100644 --- a/tests/aws/scenario/loan_broker/test_loan_broker.py +++ b/tests/aws/scenario/loan_broker/test_loan_broker.py @@ -18,9 +18,9 @@ from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import await_execution_terminated from localstack.utils.files import load_file from localstack.utils.strings import short_uid -from tests.aws.services.stepfunctions.utils import await_execution_terminated RECIPIENT_LIST_STACK_NAME = "LoanBroker-RecipientList" PROJECT_NAME = "CDK Loan Broker" diff --git a/tests/aws/services/cloudformation/resources/test_stepfunctions.py b/tests/aws/services/cloudformation/resources/test_stepfunctions.py index e012cda099932..35aa3f41f6783 100644 --- a/tests/aws/services/cloudformation/resources/test_stepfunctions.py +++ b/tests/aws/services/cloudformation/resources/test_stepfunctions.py @@ -6,8 +6,8 @@ from localstack import config from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import await_execution_terminated from localstack.utils.sync import wait_until -from tests.aws.services.stepfunctions.utils import await_execution_terminated @markers.aws.validated diff --git a/tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py b/tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py index 8f2718011f438..00b1a38d571a7 100644 --- a/tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py +++ b/tests/aws/services/stepfunctions/legacy/test_stepfunctions_legacy.py @@ -7,6 +7,7 @@ from localstack.services.events.v1.provider import TEST_EVENTS_CACHE from localstack.services.stepfunctions.stepfunctions_utils import await_sfn_execution_result from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import is_not_legacy_provider from localstack.utils import testutil from localstack.utils.aws import arns from localstack.utils.files import load_file @@ -16,7 +17,6 @@ from localstack.utils.threads import parallelize from tests.aws.services.lambda_.functions import lambda_environment from tests.aws.services.lambda_.test_lambda import TEST_LAMBDA_ENV, TEST_LAMBDA_PYTHON_ECHO -from tests.aws.services.stepfunctions.utils import is_not_legacy_provider THIS_FOLDER = os.path.dirname(os.path.realpath(__file__)) TEST_LAMBDA_NAME_1 = "lambda_sfn_1" diff --git a/tests/aws/services/stepfunctions/v2/activities/test_activities.py b/tests/aws/services/stepfunctions/v2/activities/test_activities.py index 6c55a2ba8d3bd..ce858eb7c97e1 100644 --- a/tests/aws/services/stepfunctions/v2/activities/test_activities.py +++ b/tests/aws/services/stepfunctions/v2/activities/test_activities.py @@ -3,13 +3,13 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, +) from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.activities.activity_templates import ( ActivityTemplate, ) -from tests.aws.services.stepfunctions.utils import ( - create_and_record_execution, -) @markers.snapshot.skip_snapshot_verify(paths=["$..loggingConfiguration", "$..tracingConfiguration"]) diff --git a/tests/aws/services/stepfunctions/v2/base/test_base.py b/tests/aws/services/stepfunctions/v2/base/test_base.py index a5dde1f6cb524..a8c0f69574e73 100644 --- a/tests/aws/services/stepfunctions/v2/base/test_base.py +++ b/tests/aws/services/stepfunctions/v2/base/test_base.py @@ -7,13 +7,13 @@ from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers -from localstack.utils.strings import short_uid -from tests.aws.services.stepfunctions.templates.base.base_templates import BaseTemplate -from tests.aws.services.stepfunctions.utils import ( +from localstack.testing.pytest.stepfunctions.utils import ( await_execution_success, create_and_record_events, create_and_record_execution, ) +from localstack.utils.strings import short_uid +from tests.aws.services.stepfunctions.templates.base.base_templates import BaseTemplate @markers.snapshot.skip_snapshot_verify(paths=["$..loggingConfiguration", "$..tracingConfiguration"]) @@ -165,6 +165,7 @@ def test_event_bridge_events_base( self, create_iam_role_for_sfn, create_state_machine, + events_to_sqs_queue, sfn_events_to_sqs_queue, aws_client, sfn_snapshot, @@ -188,7 +189,6 @@ def test_decl_version_1_0( self, create_iam_role_for_sfn, create_state_machine, - sfn_events_to_sqs_queue, aws_client, sfn_snapshot, ): diff --git a/tests/aws/services/stepfunctions/v2/base/test_wait.py b/tests/aws/services/stepfunctions/v2/base/test_wait.py index 84183ff7c2ad7..7084cb97b5ac5 100644 --- a/tests/aws/services/stepfunctions/v2/base/test_wait.py +++ b/tests/aws/services/stepfunctions/v2/base/test_wait.py @@ -5,10 +5,10 @@ from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers -from tests.aws.services.stepfunctions.templates.base.base_templates import BaseTemplate -from tests.aws.services.stepfunctions.utils import ( +from localstack.testing.pytest.stepfunctions.utils import ( create_and_record_execution, ) +from tests.aws.services.stepfunctions.templates.base.base_templates import BaseTemplate # TODO: add tests for seconds, secondspath, timestamp diff --git a/tests/aws/services/stepfunctions/v2/callback/test_callback.py b/tests/aws/services/stepfunctions/v2/callback/test_callback.py index feb440d99e081..8b9c2f91985c4 100644 --- a/tests/aws/services/stepfunctions/v2/callback/test_callback.py +++ b/tests/aws/services/stepfunctions/v2/callback/test_callback.py @@ -7,6 +7,11 @@ from localstack.services.stepfunctions.asl.eval.count_down_latch import CountDownLatch from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + await_execution_terminated, + create, + create_and_record_execution, +) from localstack.utils.strings import short_uid from localstack.utils.sync import retry from tests.aws.services.stepfunctions.templates.base.base_templates import BaseTemplate as BT @@ -16,11 +21,6 @@ from tests.aws.services.stepfunctions.templates.timeouts.timeout_templates import ( TimeoutTemplates as TT, ) -from tests.aws.services.stepfunctions.utils import ( - await_execution_terminated, - create, - create_and_record_execution, -) from tests.aws.test_notifications import PUBLICATION_RETRIES, PUBLICATION_TIMEOUT diff --git a/tests/aws/services/stepfunctions/v2/choice_operators/utils.py b/tests/aws/services/stepfunctions/v2/choice_operators/utils.py index 6f9a714014c3b..2eb6780bebb06 100644 --- a/tests/aws/services/stepfunctions/v2/choice_operators/utils.py +++ b/tests/aws/services/stepfunctions/v2/choice_operators/utils.py @@ -4,11 +4,11 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer from localstack.services.stepfunctions.asl.utils.json_path import JSONPathUtils +from localstack.testing.pytest.stepfunctions.utils import await_execution_success from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.choiceoperators.choice_operators_templates import ( ChoiceOperatorTemplate as COT, ) -from tests.aws.services.stepfunctions.utils import await_execution_success TYPE_COMPARISONS: Final[list[tuple[Any, bool]]] = [ (None, True), # 0 diff --git a/tests/aws/services/stepfunctions/v2/comments/test_comments.py b/tests/aws/services/stepfunctions/v2/comments/test_comments.py index b69c088d54454..b93c3e9dae629 100644 --- a/tests/aws/services/stepfunctions/v2/comments/test_comments.py +++ b/tests/aws/services/stepfunctions/v2/comments/test_comments.py @@ -3,6 +3,7 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import create_and_record_execution from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.comment.comment_templates import ( CommentTemplates as CT, @@ -10,7 +11,6 @@ from tests.aws.services.stepfunctions.templates.services.services_templates import ( ServicesTemplates as ST, ) -from tests.aws.services.stepfunctions.utils import create_and_record_execution @markers.snapshot.skip_snapshot_verify(paths=["$..loggingConfiguration", "$..tracingConfiguration"]) diff --git a/tests/aws/services/stepfunctions/v2/error_handling/test_aws_sdk.py b/tests/aws/services/stepfunctions/v2/error_handling/test_aws_sdk.py index b2944d5815c8b..801af6b70c8f7 100644 --- a/tests/aws/services/stepfunctions/v2/error_handling/test_aws_sdk.py +++ b/tests/aws/services/stepfunctions/v2/error_handling/test_aws_sdk.py @@ -5,11 +5,13 @@ from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, +) from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.errorhandling.error_handling_templates import ( ErrorHandlingTemplate as EHT, ) -from tests.aws.services.stepfunctions.utils import create_and_record_execution @markers.snapshot.skip_snapshot_verify(paths=["$..loggingConfiguration", "$..tracingConfiguration"]) diff --git a/tests/aws/services/stepfunctions/v2/error_handling/test_states_errors.py b/tests/aws/services/stepfunctions/v2/error_handling/test_states_errors.py index 6626e2e6f8013..078a506e199a9 100644 --- a/tests/aws/services/stepfunctions/v2/error_handling/test_states_errors.py +++ b/tests/aws/services/stepfunctions/v2/error_handling/test_states_errors.py @@ -3,11 +3,13 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, +) from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.errorhandling.error_handling_templates import ( ErrorHandlingTemplate as EHT, ) -from tests.aws.services.stepfunctions.utils import create_and_record_execution @markers.snapshot.skip_snapshot_verify( diff --git a/tests/aws/services/stepfunctions/v2/error_handling/test_task_lambda.py b/tests/aws/services/stepfunctions/v2/error_handling/test_task_lambda.py index ff3eea887213f..7495d26c2592e 100644 --- a/tests/aws/services/stepfunctions/v2/error_handling/test_task_lambda.py +++ b/tests/aws/services/stepfunctions/v2/error_handling/test_task_lambda.py @@ -3,11 +3,13 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, +) from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.errorhandling.error_handling_templates import ( ErrorHandlingTemplate as EHT, ) -from tests.aws.services.stepfunctions.utils import create_and_record_execution @markers.snapshot.skip_snapshot_verify( diff --git a/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_dynamodb.py b/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_dynamodb.py index e3fb7c027295a..42fcb705054f6 100644 --- a/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_dynamodb.py +++ b/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_dynamodb.py @@ -1,11 +1,13 @@ import json from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, +) from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.errorhandling.error_handling_templates import ( ErrorHandlingTemplate as EHT, ) -from tests.aws.services.stepfunctions.utils import create_and_record_execution @markers.snapshot.skip_snapshot_verify( diff --git a/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_lambda.py b/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_lambda.py index 3961df77ed8ba..97d3e1d836c96 100644 --- a/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_lambda.py +++ b/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_lambda.py @@ -3,6 +3,9 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, +) from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.errorhandling.error_handling_templates import ( ErrorHandlingTemplate as EHT, @@ -10,7 +13,6 @@ from tests.aws.services.stepfunctions.templates.timeouts.timeout_templates import ( TimeoutTemplates as TT, ) -from tests.aws.services.stepfunctions.utils import create_and_record_execution @markers.snapshot.skip_snapshot_verify(paths=["$..loggingConfiguration", "$..tracingConfiguration"]) diff --git a/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sfn.py b/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sfn.py index eb57691092a38..df7018854557b 100644 --- a/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sfn.py +++ b/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sfn.py @@ -3,12 +3,15 @@ from localstack_snapshot.snapshots.transformer import JsonpathTransformer, RegexTransformer from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create, + create_and_record_execution, +) from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.base.base_templates import BaseTemplate as BT from tests.aws.services.stepfunctions.templates.services.services_templates import ( ServicesTemplates as ST, ) -from tests.aws.services.stepfunctions.utils import create, create_and_record_execution @markers.snapshot.skip_snapshot_verify( diff --git a/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.py b/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.py index 42f5ee2614eaf..8348e40793cb2 100644 --- a/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.py +++ b/tests/aws/services/stepfunctions/v2/error_handling/test_task_service_sqs.py @@ -5,6 +5,9 @@ from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, +) from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.errorhandling.error_handling_templates import ( ErrorHandlingTemplate as EHT, @@ -12,7 +15,6 @@ from tests.aws.services.stepfunctions.templates.services.services_templates import ( ServicesTemplates as ST, ) -from tests.aws.services.stepfunctions.utils import create_and_record_execution @markers.snapshot.skip_snapshot_verify( diff --git a/tests/aws/services/stepfunctions/v2/error_handling/utils.py b/tests/aws/services/stepfunctions/v2/error_handling/utils.py index 53049e24abce1..1df2463611487 100644 --- a/tests/aws/services/stepfunctions/v2/error_handling/utils.py +++ b/tests/aws/services/stepfunctions/v2/error_handling/utils.py @@ -1,7 +1,7 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer +from localstack.testing.pytest.stepfunctions.utils import await_execution_success from localstack.utils.strings import short_uid -from tests.aws.services.stepfunctions.utils import await_execution_success @staticmethod diff --git a/tests/aws/services/stepfunctions/v2/intrinsic_functions/test_math_operations.py b/tests/aws/services/stepfunctions/v2/intrinsic_functions/test_math_operations.py index c9bc4f0585e58..56d78dc7e5339 100644 --- a/tests/aws/services/stepfunctions/v2/intrinsic_functions/test_math_operations.py +++ b/tests/aws/services/stepfunctions/v2/intrinsic_functions/test_math_operations.py @@ -3,11 +3,11 @@ from localstack_snapshot.snapshots.transformer import JsonpathTransformer, RegexTransformer from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import await_execution_success from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.intrinsicfunctions.intrinsic_functions_templates import ( IntrinsicFunctionTemplate as IFT, ) -from tests.aws.services.stepfunctions.utils import await_execution_success from tests.aws.services.stepfunctions.v2.intrinsic_functions.utils import create_and_test_on_inputs # TODO: test for validation errors, and boundary testing. diff --git a/tests/aws/services/stepfunctions/v2/intrinsic_functions/test_unique_id_generation.py b/tests/aws/services/stepfunctions/v2/intrinsic_functions/test_unique_id_generation.py index b7b8c1d68db27..d09cdecc5c28b 100644 --- a/tests/aws/services/stepfunctions/v2/intrinsic_functions/test_unique_id_generation.py +++ b/tests/aws/services/stepfunctions/v2/intrinsic_functions/test_unique_id_generation.py @@ -4,11 +4,11 @@ from localstack.services.stepfunctions.asl.utils.json_path import JSONPathUtils from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import await_execution_success from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.intrinsicfunctions.intrinsic_functions_templates import ( IntrinsicFunctionTemplate as IFT, ) -from tests.aws.services.stepfunctions.utils import await_execution_success @markers.snapshot.skip_snapshot_verify(paths=["$..loggingConfiguration", "$..tracingConfiguration"]) diff --git a/tests/aws/services/stepfunctions/v2/intrinsic_functions/utils.py b/tests/aws/services/stepfunctions/v2/intrinsic_functions/utils.py index ff75735dede9c..ff523c5f6860c 100644 --- a/tests/aws/services/stepfunctions/v2/intrinsic_functions/utils.py +++ b/tests/aws/services/stepfunctions/v2/intrinsic_functions/utils.py @@ -2,11 +2,11 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer +from localstack.testing.pytest.stepfunctions.utils import await_execution_success from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.intrinsicfunctions.intrinsic_functions_templates import ( IntrinsicFunctionTemplate as IFT, ) -from tests.aws.services.stepfunctions.utils import await_execution_success def create_and_test_on_inputs( diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py index f622b8e9b73aa..40560de42493e 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_base_scenarios.py @@ -7,8 +7,13 @@ from localstack.aws.api.lambda_ import Runtime from localstack.services.stepfunctions.asl.utils.json_path import JSONPathUtils from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + SfnNoneRecursiveParallelTransformer, + await_execution_terminated, + create, + create_and_record_execution, +) from localstack.utils.strings import short_uid -from tests.aws.services.stepfunctions.conftest import SfnNoneRecursiveParallelTransformer from tests.aws.services.stepfunctions.templates.errorhandling.error_handling_templates import ( ErrorHandlingTemplate as EHT, ) @@ -18,11 +23,6 @@ from tests.aws.services.stepfunctions.templates.services.services_templates import ( ServicesTemplates as SerT, ) -from tests.aws.services.stepfunctions.utils import ( - await_execution_terminated, - create, - create_and_record_execution, -) @markers.snapshot.skip_snapshot_verify(paths=["$..loggingConfiguration", "$..tracingConfiguration"]) diff --git a/tests/aws/services/stepfunctions/v2/scenarios/test_sfn_scenarios.py b/tests/aws/services/stepfunctions/v2/scenarios/test_sfn_scenarios.py index 4570264d3ec92..55618c93119b8 100644 --- a/tests/aws/services/stepfunctions/v2/scenarios/test_sfn_scenarios.py +++ b/tests/aws/services/stepfunctions/v2/scenarios/test_sfn_scenarios.py @@ -5,8 +5,8 @@ from localstack.aws.api.stepfunctions import ExecutionStatus from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import is_legacy_provider, is_not_legacy_provider from localstack.utils.sync import wait_until -from tests.aws.services.stepfunctions.utils import is_legacy_provider, is_not_legacy_provider THIS_FOLDER = Path(os.path.dirname(__file__)) diff --git a/tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py b/tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py index cc7f68ce68bd4..50ba4af28cb2e 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py +++ b/tests/aws/services/stepfunctions/v2/services/test_apigetway_task_service.py @@ -7,6 +7,9 @@ from localstack.aws.api.lambda_ import Runtime from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, +) from localstack.utils.aws import arns, aws_stack from localstack.utils.strings import short_uid from tests.aws.services.apigateway.apigateway_fixtures import create_rest_resource @@ -14,7 +17,6 @@ from tests.aws.services.stepfunctions.templates.services.services_templates import ( ServicesTemplates as ST, ) -from tests.aws.services.stepfunctions.utils import create_and_record_execution @markers.snapshot.skip_snapshot_verify( diff --git a/tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py b/tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py index 594e119acee23..2e80262b0c730 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py +++ b/tests/aws/services/stepfunctions/v2/services/test_aws_sdk_task_service.py @@ -4,12 +4,15 @@ from localstack_snapshot.snapshots.transformer import JsonpathTransformer from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create, + create_and_record_execution, +) from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.base.base_templates import BaseTemplate as BT from tests.aws.services.stepfunctions.templates.services.services_templates import ( ServicesTemplates as ST, ) -from tests.aws.services.stepfunctions.utils import create, create_and_record_execution @markers.snapshot.skip_snapshot_verify( diff --git a/tests/aws/services/stepfunctions/v2/services/test_dynamodb_task_service.py b/tests/aws/services/stepfunctions/v2/services/test_dynamodb_task_service.py index eee533ecead82..ecf8b946b985a 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_dynamodb_task_service.py +++ b/tests/aws/services/stepfunctions/v2/services/test_dynamodb_task_service.py @@ -1,11 +1,13 @@ import json from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, +) from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.services.services_templates import ( ServicesTemplates as ST, ) -from tests.aws.services.stepfunctions.utils import create_and_record_execution @markers.snapshot.skip_snapshot_verify( diff --git a/tests/aws/services/stepfunctions/v2/services/test_ecs_task_service.py b/tests/aws/services/stepfunctions/v2/services/test_ecs_task_service.py index ed01626a472a6..1efb9b363ea14 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_ecs_task_service.py +++ b/tests/aws/services/stepfunctions/v2/services/test_ecs_task_service.py @@ -8,8 +8,8 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import launch_and_record_execution from localstack.utils.analytics.metadata import is_license_activated -from tests.aws.services.stepfunctions.utils import launch_and_record_execution _ECS_SNAPSHOT_SKIP_PATHS: [list[str]] = [ "$..Attachments..Details", diff --git a/tests/aws/services/stepfunctions/v2/services/test_events_task_service.py b/tests/aws/services/stepfunctions/v2/services/test_events_task_service.py index 975d8e826ca7b..20a2ee8a7b31d 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_events_task_service.py +++ b/tests/aws/services/stepfunctions/v2/services/test_events_task_service.py @@ -4,11 +4,14 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, + record_sqs_events, +) from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.services.services_templates import ( ServicesTemplates as ST, ) -from tests.aws.services.stepfunctions.utils import create_and_record_execution, record_sqs_events @markers.snapshot.skip_snapshot_verify( diff --git a/tests/aws/services/stepfunctions/v2/services/test_lambda_task.py b/tests/aws/services/stepfunctions/v2/services/test_lambda_task.py index b17766a7d0dcf..ba8cedccdce49 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_lambda_task.py +++ b/tests/aws/services/stepfunctions/v2/services/test_lambda_task.py @@ -4,12 +4,14 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, +) from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.lambda_functions import lambda_functions from tests.aws.services.stepfunctions.templates.services.services_templates import ( ServicesTemplates as ST, ) -from tests.aws.services.stepfunctions.utils import create_and_record_execution @markers.snapshot.skip_snapshot_verify(paths=["$..loggingConfiguration", "$..tracingConfiguration"]) diff --git a/tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py b/tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py index b57eac80df940..5b44cb239a5a7 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py +++ b/tests/aws/services/stepfunctions/v2/services/test_lambda_task_service.py @@ -6,11 +6,13 @@ from localstack.aws.api.lambda_ import LogType from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, +) from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.services.services_templates import ( ServicesTemplates as ST, ) -from tests.aws.services.stepfunctions.utils import create_and_record_execution @markers.snapshot.skip_snapshot_verify( diff --git a/tests/aws/services/stepfunctions/v2/services/test_sfn_task_service.py b/tests/aws/services/stepfunctions/v2/services/test_sfn_task_service.py index 7405d3d99f754..6ee16746ff660 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_sfn_task_service.py +++ b/tests/aws/services/stepfunctions/v2/services/test_sfn_task_service.py @@ -3,11 +3,14 @@ from localstack_snapshot.snapshots.transformer import JsonpathTransformer from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create, + create_and_record_execution, +) from tests.aws.services.stepfunctions.templates.base.base_templates import BaseTemplate as BT from tests.aws.services.stepfunctions.templates.services.services_templates import ( ServicesTemplates as ST, ) -from tests.aws.services.stepfunctions.utils import create, create_and_record_execution @markers.snapshot.skip_snapshot_verify( diff --git a/tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py b/tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py index ff36b95432d29..f633f242628ed 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py +++ b/tests/aws/services/stepfunctions/v2/services/test_sns_task_service.py @@ -4,12 +4,14 @@ import pytest from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, +) from localstack.utils.strings import short_uid from localstack.utils.sync import retry from tests.aws.services.stepfunctions.templates.services.services_templates import ( ServicesTemplates as ST, ) -from tests.aws.services.stepfunctions.utils import create_and_record_execution from tests.aws.test_notifications import PUBLICATION_RETRIES, PUBLICATION_TIMEOUT diff --git a/tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.py b/tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.py index 59c4200a0e2e8..bba6ccbb9aac7 100644 --- a/tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.py +++ b/tests/aws/services/stepfunctions/v2/services/test_sqs_task_service.py @@ -4,11 +4,13 @@ from localstack.aws.api.sqs import MessageSystemAttributeNameForSends from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, +) from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.services.services_templates import ( ServicesTemplates as ST, ) -from tests.aws.services.stepfunctions.utils import create_and_record_execution @markers.snapshot.skip_snapshot_verify( diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api.py b/tests/aws/services/stepfunctions/v2/test_sfn_api.py index fee59b76b4429..0f495c455a5ec 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api.py +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api.py @@ -8,10 +8,7 @@ from localstack.aws.api.lambda_ import Runtime from localstack.aws.api.stepfunctions import HistoryEventList from localstack.testing.pytest import markers -from localstack.utils.strings import short_uid -from tests.aws.services.stepfunctions.lambda_functions import lambda_functions -from tests.aws.services.stepfunctions.templates.base.base_templates import BaseTemplate -from tests.aws.services.stepfunctions.utils import ( +from localstack.testing.pytest.stepfunctions.utils import ( await_execution_aborted, await_execution_success, await_execution_terminated, @@ -20,6 +17,9 @@ await_state_machine_listed, await_state_machine_not_listed, ) +from localstack.utils.strings import short_uid +from tests.aws.services.stepfunctions.lambda_functions import lambda_functions +from tests.aws.services.stepfunctions.templates.base.base_templates import BaseTemplate @markers.snapshot.skip_snapshot_verify(paths=["$..loggingConfiguration", "$..tracingConfiguration"]) diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api_map_run.py b/tests/aws/services/stepfunctions/v2/test_sfn_api_map_run.py index 552916e339c25..48b7d8c5bb49f 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api_map_run.py +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api_map_run.py @@ -3,10 +3,13 @@ from localstack_snapshot.snapshots.transformer import JsonpathTransformer, RegexTransformer from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + await_execution_terminated, + create, +) from tests.aws.services.stepfunctions.templates.scenarios.scenarios_templates import ( ScenariosTemplate as ST, ) -from tests.aws.services.stepfunctions.utils import await_execution_terminated, create @markers.snapshot.skip_snapshot_verify(paths=["$..loggingConfiguration", "$..tracingConfiguration"]) diff --git a/tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py b/tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py index 4a98192dcb282..647a5e836cf9c 100644 --- a/tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py +++ b/tests/aws/services/stepfunctions/v2/test_sfn_api_versioning.py @@ -4,14 +4,14 @@ from localstack_snapshot.snapshots.transformer import RegexTransformer from localstack.testing.pytest import markers -from localstack.utils.strings import short_uid -from tests.aws.services.stepfunctions.templates.base.base_templates import BaseTemplate -from tests.aws.services.stepfunctions.utils import ( +from localstack.testing.pytest.stepfunctions.utils import ( await_execution_lists_terminated, await_execution_terminated, await_state_machine_version_listed, await_state_machine_version_not_listed, ) +from localstack.utils.strings import short_uid +from tests.aws.services.stepfunctions.templates.base.base_templates import BaseTemplate @markers.snapshot.skip_snapshot_verify(paths=["$..loggingConfiguration", "$..tracingConfiguration"]) diff --git a/tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py b/tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py index 8fe0af4fa5f72..334620d4c0557 100644 --- a/tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py +++ b/tests/aws/services/stepfunctions/v2/timeouts/test_heartbeats.py @@ -3,11 +3,13 @@ from localstack_snapshot.snapshots.transformer import JsonpathTransformer, RegexTransformer from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + create_and_record_execution, +) from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.timeouts.timeout_templates import ( TimeoutTemplates as TT, ) -from tests.aws.services.stepfunctions.utils import create_and_record_execution @markers.snapshot.skip_snapshot_verify( diff --git a/tests/aws/services/stepfunctions/v2/timeouts/test_timeouts.py b/tests/aws/services/stepfunctions/v2/timeouts/test_timeouts.py index ddcabf8364d9c..f229424f88f47 100644 --- a/tests/aws/services/stepfunctions/v2/timeouts/test_timeouts.py +++ b/tests/aws/services/stepfunctions/v2/timeouts/test_timeouts.py @@ -5,15 +5,15 @@ from localstack.testing.aws.util import is_aws_cloud from localstack.testing.pytest import markers +from localstack.testing.pytest.stepfunctions.utils import ( + await_execution_terminated, + create_and_record_execution, +) from localstack.utils.strings import short_uid from tests.aws.services.stepfunctions.templates.base.base_templates import BaseTemplate from tests.aws.services.stepfunctions.templates.timeouts.timeout_templates import ( TimeoutTemplates as TT, ) -from tests.aws.services.stepfunctions.utils import ( - await_execution_terminated, - create_and_record_execution, -) @markers.snapshot.skip_snapshot_verify( diff --git a/tests/conftest.py b/tests/conftest.py index 86d0442815a12..eab7afcea8c1b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,7 @@ "localstack.testing.pytest.in_memory_localstack", "localstack.testing.pytest.validation_tracking", "localstack.testing.pytest.path_filter", + "localstack.testing.pytest.stepfunctions.fixtures", ] From 437c24937e764d1599edc047ea037e7142648df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristopher=20Pinz=C3=B3n?= <cristopher.pinzon@gmail.com> Date: Fri, 17 May 2024 10:12:09 -0500 Subject: [PATCH 162/169] add small quirks for new resource providers (#10818) --- localstack/services/cloudformation/engine/quirks.py | 4 ++++ localstack/services/cloudformation/scaffolding/propgen.py | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/localstack/services/cloudformation/engine/quirks.py b/localstack/services/cloudformation/engine/quirks.py index 46ae29e779777..1a94e190180e2 100644 --- a/localstack/services/cloudformation/engine/quirks.py +++ b/localstack/services/cloudformation/engine/quirks.py @@ -31,6 +31,10 @@ "AWS::SSM::Parameter": "/properties/Name", "AWS::RDS::DBProxyTargetGroup": "/properties/TargetGroupName", "AWS::Glue::SchemaVersionMetadata": "</properties/SchemaVersionId>|</properties/Key>|</properties/Value>", # composite + "AWS::WAFv2::WebACL": "</properties/Name>|</properties/Id>|</properties/Scope>", + "AWS::WAFv2::WebACLAssociation": "</properties/ResourceArn>|</properties/WebACLArn>", + "AWS::WAFv2::IPSet": "</properties/Name>|</properties/Id>|</properties/Scope>", + # composite } # You can usually find the available GetAtt targets in the official resource documentation: diff --git a/localstack/services/cloudformation/scaffolding/propgen.py b/localstack/services/cloudformation/scaffolding/propgen.py index 53cfac3107e51..6a7e90166b490 100644 --- a/localstack/services/cloudformation/scaffolding/propgen.py +++ b/localstack/services/cloudformation/scaffolding/propgen.py @@ -178,8 +178,11 @@ def resolve_type_of_property(self, property_def: dict) -> str: case "object": resolved_type = "dict" # TODO: any cases where we need to continue here? case "array": - item_type = self.resolve_type_of_property(property_def["items"]) - resolved_type = f"list[{item_type}]" + try: + item_type = self.resolve_type_of_property(property_def["items"]) + resolved_type = f"list[{item_type}]" + except RecursionError: + resolved_type = "list[Any]" case _: # TODO: allOf, anyOf, patternProperties (?) # AWS::ApiGateway::RestApi passes a ["object", "string"] here for the "Body" property From 6eeb7ccae48fe0bbe6feda897598967fc7ba6b40 Mon Sep 17 00:00:00 2001 From: Max <max.hoheiser@gmail.com> Date: Fri, 17 May 2024 17:12:25 +0200 Subject: [PATCH 163/169] Feature: Eventbridge v2: Add tagging (#10840) --- localstack/services/events/models.py | 11 + localstack/services/events/provider.py | 102 ++++- localstack/utils/tagging.py | 7 + tests/aws/services/events/conftest.py | 19 +- tests/aws/services/events/test_events.py | 29 +- .../services/events/test_events_schedule.py | 4 +- tests/aws/services/events/test_events_tags.py | 289 ++++++++++++ .../events/test_events_tags.snapshot.json | 433 ++++++++++++++++++ .../events/test_events_tags.validation.json | 50 ++ 9 files changed, 904 insertions(+), 40 deletions(-) create mode 100644 tests/aws/services/events/test_events_tags.py create mode 100644 tests/aws/services/events/test_events_tags.snapshot.json create mode 100644 tests/aws/services/events/test_events_tags.validation.json diff --git a/localstack/services/events/models.py b/localstack/services/events/models.py index d64c74bbaa7fd..243accc5110ac 100644 --- a/localstack/services/events/models.py +++ b/localstack/services/events/models.py @@ -1,4 +1,5 @@ from dataclasses import dataclass, field +from enum import Enum from typing import Optional, TypeAlias, TypedDict from localstack.aws.api.core import ServiceException @@ -22,8 +23,10 @@ from localstack.services.stores import ( AccountRegionBundle, BaseStore, + CrossRegionAttribute, LocalAttribute, ) +from localstack.utils.tagging import TaggingService TargetDict = dict[TargetId, Target] @@ -88,6 +91,9 @@ class EventsStore(BaseStore): # Map of eventbus names to eventbus objects. The name MUST be unique per account and region (works with AccountRegionBundle) event_buses: EventBusDict = LocalAttribute(default=dict) + # Maps resource ARN to tags + TAGS: TaggingService = CrossRegionAttribute(default=TaggingService) + events_store = AccountRegionBundle("events", EventsStore) @@ -119,3 +125,8 @@ class FormattedEvent(TypedDict): TransformedEvent: TypeAlias = FormattedEvent | dict | str + + +class ResourceType(Enum): + EVENT_BUS = "event_bus" + RULE = "rule" diff --git a/localstack/services/events/provider.py b/localstack/services/events/provider.py index ddfcdd8754a8d..de7b2ba8a9591 100644 --- a/localstack/services/events/provider.py +++ b/localstack/services/events/provider.py @@ -1,10 +1,12 @@ import base64 import json import logging +import re from datetime import datetime, timezone from typing import Callable, Optional from localstack.aws.api import RequestContext, handler +from localstack.aws.api.config import TagsList from localstack.aws.api.events import ( Arn, Boolean, @@ -23,6 +25,7 @@ ListEventBusesResponse, ListRuleNamesByTargetResponse, ListRulesResponse, + ListTagsForResourceResponse, ListTargetsByRuleResponse, NextToken, PutEventsRequestEntry, @@ -38,18 +41,22 @@ ResourceAlreadyExistsException, ResourceNotFoundException, RoleArn, + RuleArn, RuleDescription, RuleName, RuleResponseList, RuleState, ScheduleExpression, + TagKeyList, TagList, + TagResourceResponse, Target, TargetArn, TargetId, TargetIdList, TargetList, TestEventPatternResponse, + UntagResourceResponse, ) from localstack.aws.api.events import EventBus as ApiTypeEventBus from localstack.aws.api.events import Rule as ApiTypeRule @@ -60,6 +67,7 @@ EventBusDict, EventsStore, FormattedEvent, + ResourceType, Rule, RuleDict, TargetDict, @@ -73,12 +81,17 @@ from localstack.services.events.scheduler import JobScheduler from localstack.services.events.target import TargetSender, TargetSenderDict, TargetSenderFactory from localstack.services.plugins import ServiceLifecycleHook +from localstack.utils.aws.arns import parse_arn from localstack.utils.common import truncate from localstack.utils.strings import long_uid from localstack.utils.time import TIMESTAMP_FORMAT_TZ, timestamp LOG = logging.getLogger(__name__) +RULE_ARN_CUSTOM_EVENT_BUS_PATTERN = re.compile( + r"^arn:aws:events:[a-z0-9-]+:\d{12}:rule/[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+$" +) + def decode_next_token(token: NextToken) -> int: """Decode a pagination token from base64 to integer.""" @@ -146,6 +159,24 @@ def format_event(event: PutEventsRequestEntry, region: str, account_id: str) -> return formatted_event +def get_resource_type(arn: Arn) -> ResourceType: + parsed_arn = parse_arn(arn) + resource_type = parsed_arn["resource"].split("/", 1)[0] + if resource_type == "event-bus": + return ResourceType.EVENT_BUS + if resource_type == "rule": + return ResourceType.RULE + raise ValidationException( + f"Parameter {arn} is not valid. Reason: Provided Arn is not in correct format." + ) + + +def check_unique_tags(tags: TagsList) -> None: + unique_tag_keys = {tag["Key"] for tag in tags} + if len(unique_tag_keys) < len(tags): + raise ValidationException("Invalid parameter: Duplicated keys are not allowed.") + + class EventsProvider(EventsApi, ServiceLifecycleHook): # api methods are grouped by resource type and sorted in hierarchical order # each group is sorted alphabetically @@ -183,6 +214,9 @@ def create_event_bus( ) store.event_buses[event_bus_service.event_bus.name] = event_bus_service.event_bus + if tags: + self.tag_resource(context, event_bus_service.arn, tags) + response = CreateEventBusResponse( EventBusArn=event_bus_service.arn, ) @@ -199,6 +233,7 @@ def delete_event_bus(self, context: RequestContext, name: EventBusName, **kwargs if rules := event_bus.rules: self._delete_rule_services(rules) del store.event_buses[name] + del store.TAGS[event_bus.arn] except ResourceNotFoundException as error: return error @@ -272,6 +307,7 @@ def delete_rule( raise ValidationException("Rule can't be deleted since it has targets.") self._delete_rule_services(rule) del event_bus.rules[name] + del store.TAGS[rule.arn] except ResourceNotFoundException as error: return error @@ -373,6 +409,10 @@ def put_rule( targets, ) event_bus.rules[name] = rule_service.rule + + if tags: + self.tag_resource(context, rule_service.arn, tags) + response = PutRuleResponse(RuleArn=rule_service.arn) return response @@ -491,6 +531,41 @@ def put_partner_events( ) -> PutPartnerEventsResponse: raise NotImplementedError + ###### + # Tags + ###### + + @handler("ListTagsForResource") + def list_tags_for_resource( + self, context: RequestContext, resource_arn: Arn, **kwargs + ) -> ListTagsForResourceResponse: + store = self.get_store(context) + resource_type = get_resource_type(resource_arn) + self._check_resource_exists(resource_arn, resource_type, store) + tags = store.TAGS.list_tags_for_resource(resource_arn) + return ListTagsForResourceResponse(tags) + + @handler("TagResource") + def tag_resource( + self, context: RequestContext, resource_arn: Arn, tags: TagList, **kwargs + ) -> TagResourceResponse: + # each tag key must be unique + # https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html#tag-best-practices + store = self.get_store(context) + resource_type = get_resource_type(resource_arn) + self._check_resource_exists(resource_arn, resource_type, store) + check_unique_tags(tags) + store.TAGS.tag_resource(resource_arn, tags) + + @handler("UntagResource") + def untag_resource( + self, context: RequestContext, resource_arn: Arn, tag_keys: TagKeyList, **kwargs + ) -> UntagResourceResponse: + store = self.get_store(context) + resource_type = get_resource_type(resource_arn) + self._check_resource_exists(resource_arn, resource_type, store) + store.TAGS.untag_resource(resource_arn, tag_keys) + ######### # Methods ######### @@ -610,12 +685,21 @@ def _get_limited_dict_and_next_token( return limited_dict, next_token def _extract_event_bus_name( - self, event_bus_name_or_arn: EventBusNameOrArn | None + self, resource_arn_or_name: EventBusNameOrArn | RuleArn | None ) -> EventBusName: """Return the event bus name. Input can be either an event bus name or ARN.""" - if not event_bus_name_or_arn: + if not resource_arn_or_name: + return "default" + if "arn:aws:events" not in resource_arn_or_name: + return resource_arn_or_name + resource_type = get_resource_type(resource_arn_or_name) + # TODO how to deal with / in event bus name or rule name + if resource_type == ResourceType.EVENT_BUS: + return resource_arn_or_name.split("/")[-1] + if resource_type == ResourceType.RULE: + if bool(RULE_ARN_CUSTOM_EVENT_BUS_PATTERN.match(resource_arn_or_name)): + return resource_arn_or_name.split("rule/", 1)[1].split("/", 1)[0] return "default" - return event_bus_name_or_arn.split("/")[-1] def _event_bust_dict_to_api_type_list(self, event_buses: EventBusDict) -> EventBusList: """Return a converted dict of EventBus model objects as a list of event buses in API type EventBus format.""" @@ -674,6 +758,18 @@ def _delete_target_sender(self, ids: TargetIdList, rule) -> None: except KeyError: LOG.error(f"Error deleting target service {target_arn}.") + def _check_resource_exists( + self, resource_arn: Arn, resource_type: ResourceType, store: EventsStore + ) -> None: + if resource_type == ResourceType.EVENT_BUS: + event_bus_name = self._extract_event_bus_name(resource_arn) + self.get_event_bus(event_bus_name, store) + if resource_type == ResourceType.RULE: + event_bus_name = self._extract_event_bus_name(resource_arn) + event_bus = self.get_event_bus(event_bus_name, store) + rule_name = resource_arn.split("/")[-1] + self.get_rule(rule_name, event_bus) + def _process_entries( self, context: RequestContext, entries: PutEventsRequestEntryList ) -> tuple[PutEventsResultEntryList, int]: diff --git a/localstack/utils/tagging.py b/localstack/utils/tagging.py index dabc1039e3fc5..340b4aa680b4b 100644 --- a/localstack/utils/tagging.py +++ b/localstack/utils/tagging.py @@ -25,3 +25,10 @@ def untag_resource(self, arn: str, tag_names: List[str]): tags = self.tags.get(arn, {}) for name in tag_names: tags.pop(name, None) + + def del_resource(self, arn: str): + if arn in self.tags: + del self.tags[arn] + + def __delitem__(self, arn: str): + self.del_resource(arn) diff --git a/tests/aws/services/events/conftest.py b/tests/aws/services/events/conftest.py index 97abaa1ca57d4..3d811450e5484 100644 --- a/tests/aws/services/events/conftest.py +++ b/tests/aws/services/events/conftest.py @@ -125,15 +125,18 @@ def _factory(**kwargs): yield _factory for rule, event_bus_name in rules: - targets_response = aws_client.events.list_targets_by_rule( - Rule=rule, EventBusName=event_bus_name - ) - if targets := targets_response["Targets"]: - targets_ids = [target["Id"] for target in targets] - aws_client.events.remove_targets( - Rule=rule, EventBusName=event_bus_name, Ids=targets_ids + try: + targets_response = aws_client.events.list_targets_by_rule( + Rule=rule, EventBusName=event_bus_name ) - aws_client.events.delete_rule(Name=rule, EventBusName=event_bus_name) + if targets := targets_response["Targets"]: + targets_ids = [target["Id"] for target in targets] + aws_client.events.remove_targets( + Rule=rule, EventBusName=event_bus_name, Ids=targets_ids + ) + aws_client.events.delete_rule(Name=rule, EventBusName=event_bus_name) + except Exception as e: + LOG.debug("error cleaning up rule %s: %s", rule, e) @pytest.fixture diff --git a/tests/aws/services/events/test_events.py b/tests/aws/services/events/test_events.py index 7e6365c339462..ef61878262873 100644 --- a/tests/aws/services/events/test_events.py +++ b/tests/aws/services/events/test_events.py @@ -190,35 +190,8 @@ def test_events_written_to_disk_are_timestamp_prefixed_for_chronological_orderin assert [json.loads(event["Detail"]) for event in sorted_events] == event_details_to_publish - @markers.aws.validated - @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") - def test_list_tags_for_resource(self, aws_client, clean_up): - rule_name = "rule-{}".format(short_uid()) - - rule = aws_client.events.put_rule( - Name=rule_name, EventPattern=json.dumps(TEST_EVENT_PATTERN) - ) - rule_arn = rule["RuleArn"] - expected = [ - {"Key": "key1", "Value": "value1"}, - {"Key": "key2", "Value": "value2"}, - ] - - # insert two tags, verify both are visible - aws_client.events.tag_resource(ResourceARN=rule_arn, Tags=expected) - actual = aws_client.events.list_tags_for_resource(ResourceARN=rule_arn)["Tags"] - assert actual == expected - - # remove 'key2', verify only 'key1' remains - expected = [{"Key": "key1", "Value": "value1"}] - aws_client.events.untag_resource(ResourceARN=rule_arn, TagKeys=["key2"]) - actual = aws_client.events.list_tags_for_resource(ResourceARN=rule_arn)["Tags"] - assert actual == expected - - # clean up - clean_up(rule_name=rule_name) - @markers.aws.unknown + # TODO move to schedule @pytest.mark.skipif(is_v2_provider(), reason="V2 provider does not support this feature yet") def test_scheduled_expression_events( self, diff --git a/tests/aws/services/events/test_events_schedule.py b/tests/aws/services/events/test_events_schedule.py index 16c65ebf311c3..d34b947a2db6e 100644 --- a/tests/aws/services/events/test_events_schedule.py +++ b/tests/aws/services/events/test_events_schedule.py @@ -133,7 +133,9 @@ def tests_schedule_rate_target_sqs( json.loads(messages_second[0]["Body"])["time"] ) time_delta = time_messages_second - time_messages_first - assert time_delta == timedelta(seconds=60) + expected_time_delta = timedelta(seconds=60) + tolerance = timedelta(seconds=5) + assert expected_time_delta - tolerance <= time_delta <= expected_time_delta + tolerance @markers.aws.validated def tests_schedule_rate_custom_input_target_sqs( diff --git a/tests/aws/services/events/test_events_tags.py b/tests/aws/services/events/test_events_tags.py new file mode 100644 index 0000000000000..dcf9382666361 --- /dev/null +++ b/tests/aws/services/events/test_events_tags.py @@ -0,0 +1,289 @@ +import json + +import pytest + +from localstack.testing.pytest import markers +from localstack.utils.strings import short_uid +from tests.aws.services.events.helper_functions import is_v2_provider +from tests.aws.services.events.test_events import TEST_EVENT_PATTERN + + +@markers.aws.validated +@pytest.mark.parametrize("event_bus_name", ["event_bus_default", "event_bus_custom"]) +@pytest.mark.parametrize("resource_to_tag", ["event_bus", "rule"]) +def tests_tag_untag_resource( + event_bus_name, + resource_to_tag, + region_name, + account_id, + events_create_event_bus, + events_put_rule, + aws_client, + snapshot, +): + if event_bus_name == "event_bus_default": + bus_name = "default" + event_bus_arn = f"arn:aws:events:{region_name}:{account_id}:event-bus/default" + if event_bus_name == "event_bus_custom": + bus_name = f"test_bus-{short_uid()}" + response = events_create_event_bus(Name=bus_name) + event_bus_arn = response["EventBusArn"] + + if resource_to_tag == "event_bus": + resource_arn = event_bus_arn + if resource_to_tag == "rule": + rule_name = f"test_rule-{short_uid()}" + response = events_put_rule( + Name=rule_name, + EventBusName=bus_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + ) + rule_arn = response["RuleArn"] + resource_arn = rule_arn + + tag_key_2 = "tag2" + response_tag_resource = aws_client.events.tag_resource( + ResourceARN=resource_arn, + Tags=[ + { + "Key": "tag1", + "Value": "value1", + }, + { + "Key": tag_key_2, + "Value": "value2", + }, + ], + ) + snapshot.match("tag_resource", response_tag_resource) + + response = aws_client.events.list_tags_for_resource(ResourceARN=resource_arn) + snapshot.match("list_tags_for_resource", response) + + response_untag_resource = aws_client.events.untag_resource( + ResourceARN=resource_arn, + TagKeys=[tag_key_2], + ) + snapshot.match("untag_resource", response_untag_resource) + + response = aws_client.events.list_tags_for_resource(ResourceARN=resource_arn) + snapshot.match("list_tags_for_untagged_resource", response) + + response_untag_resource_not_existing_tag = aws_client.events.untag_resource( + ResourceARN=resource_arn, + TagKeys=[f"not_existing_tag-{short_uid()}"], + ) + snapshot.match("untag_resource_not_existing_tag", response_untag_resource_not_existing_tag) + + +@markers.aws.validated +@pytest.mark.skipif( + not is_v2_provider(), + reason="V1 provider does not support this feature", +) +@pytest.mark.parametrize("resource_to_tag", ["not_existing_rule", "not_existing_event_bus"]) +def tests_tag_list_untag_not_existing_resource( + resource_to_tag, + region_name, + account_id, + aws_client, + snapshot, +): + resource_name = short_uid() + if resource_to_tag == "not_existing_rule": + resource_arn = f"arn:aws:events:{region_name}:{account_id}:rule/{resource_name}" + if resource_to_tag == "not_existing_event_bus": + resource_arn = f"arn:aws:events:{region_name}:{account_id}:event-bus/{resource_name}" + + tag_key_1 = "tag1" + with pytest.raises(aws_client.events.exceptions.ResourceNotFoundException) as error: + aws_client.events.tag_resource( + ResourceARN=resource_arn, + Tags=[ + { + "Key": tag_key_1, + "Value": "value1", + }, + ], + ) + + snapshot.match("tag_not_existing_resource_error", error) + + snapshot.add_transformer( + snapshot.transform.regex(resource_name, "<not-existing-resource-name>") + ) + with pytest.raises(aws_client.events.exceptions.ResourceNotFoundException) as error: + aws_client.events.list_tags_for_resource(ResourceARN=resource_arn) + snapshot.match("list_tags_for_not_existing_resource_error", error) + + with pytest.raises(aws_client.events.exceptions.ResourceNotFoundException) as error: + aws_client.events.untag_resource( + ResourceARN=resource_arn, + TagKeys=[tag_key_1], + ) + snapshot.match("untag_not_existing_resource_error", error) + + +@markers.aws.validated +@pytest.mark.parametrize("event_bus_name", ["event_bus_default", "event_bus_custom"]) +@pytest.mark.parametrize("resource_to_tag", ["event_bus", "rule"]) +def test_recreate_tagged_resource_without_tags( + event_bus_name, + resource_to_tag, + region_name, + account_id, + events_create_event_bus, + events_put_rule, + aws_client, + snapshot, +): + if event_bus_name == "event_bus_default": + bus_name = "default" + event_bus_arn = f"arn:aws:events:{region_name}:{account_id}:event-bus/default" + if event_bus_name == "event_bus_custom": + bus_name = f"test_bus-{short_uid()}" + response = events_create_event_bus(Name=bus_name) + event_bus_arn = response["EventBusArn"] + + if resource_to_tag == "event_bus": + resource_arn = event_bus_arn + if resource_to_tag == "rule": + rule_name = f"test_rule-{short_uid()}" + response = events_put_rule( + Name=rule_name, + EventBusName=bus_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + ) + rule_arn = response["RuleArn"] + resource_arn = rule_arn + + aws_client.events.tag_resource( + ResourceARN=resource_arn, + Tags=[ + { + "Key": "tag1", + "Value": "value1", + } + ], + ) + + if resource_to_tag == "event_bus" and event_bus_name == "event_bus_custom": + aws_client.events.delete_event_bus(Name=bus_name) + events_create_event_bus(Name=bus_name) + + if resource_to_tag == "rule": + aws_client.events.delete_rule(Name=rule_name, EventBusName=bus_name) + events_put_rule( + Name=rule_name, + EventBusName=bus_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + ) + + response = aws_client.events.list_tags_for_resource(ResourceARN=resource_arn) + snapshot.match("list_tags_for_resource", response) + + +class TestRuleTags: + @markers.aws.validated + def test_put_rule_with_tags( + self, events_create_event_bus, events_put_rule, aws_client, snapshot + ): + bus_name = f"test_bus-{short_uid()}" + events_create_event_bus(Name=bus_name) + + rule_name = f"test_rule-{short_uid()}" + response_put_rule = events_put_rule( + Name=rule_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + Tags=[ + { + "Key": "tag1", + "Value": "value1", + }, + { + "Key": "tag2", + "Value": "value2", + }, + ], + ) + rule_arn = response_put_rule["RuleArn"] + snapshot.match("put_rule_with_tags", response_put_rule) + + response_put_rule = aws_client.events.list_tags_for_resource(ResourceARN=rule_arn) + snapshot.add_transformer(snapshot.transform.regex(rule_name, "<rule_name>")) + snapshot.match("list_tags_for_rule", response_put_rule) + + @markers.aws.validated + @pytest.mark.skipif( + not is_v2_provider(), + reason="V1 provider does not support this feature", + ) + def test_list_tags_for_deleted_rule( + self, events_create_event_bus, events_put_rule, aws_client, snapshot + ): + bus_name = f"test_bus-{short_uid()}" + events_create_event_bus(Name=bus_name) + + rule_name = f"test_rule-{short_uid()}" + response_put_rule = events_put_rule( + Name=rule_name, + EventBusName=bus_name, + EventPattern=json.dumps(TEST_EVENT_PATTERN), + ) + rule_arn = response_put_rule["RuleArn"] + + aws_client.events.delete_rule(Name=rule_name, EventBusName=bus_name) + + with pytest.raises(aws_client.events.exceptions.ResourceNotFoundException) as error: + aws_client.events.list_tags_for_resource(ResourceARN=rule_arn) + + snapshot.add_transformer( + [ + snapshot.transform.regex(rule_name, "<rule_name>"), + snapshot.transform.regex(bus_name, "<bus_name>"), + ] + ) + snapshot.match("list_tags_for_deleted_rule_error", error) + + +class TestEventBusTags: + @markers.aws.validated + def test_create_event_bus_with_tags(self, events_create_event_bus, aws_client, snapshot): + bus_name = f"test_bus-{short_uid()}" + response_create_event_bus = events_create_event_bus( + Name=bus_name, + Tags=[ + { + "Key": "tag1", + "Value": "value1", + }, + { + "Key": "tag2", + "Value": "value2", + }, + ], + ) + bus_arn = response_create_event_bus["EventBusArn"] + snapshot.match("create_event_bus_with_tags", response_create_event_bus) + + response_create_event_bus = aws_client.events.list_tags_for_resource(ResourceARN=bus_arn) + snapshot.add_transformer(snapshot.transform.regex(bus_name, "<bus_name>")) + snapshot.match("list_tags_for_event_bus", response_create_event_bus) + + @markers.aws.validated + @pytest.mark.skipif( + not is_v2_provider(), + reason="V1 provider does not support this feature", + ) + def test_list_tags_for_deleted_event_bus(self, events_create_event_bus, aws_client, snapshot): + bus_name = f"test_bus-{short_uid()}" + response = events_create_event_bus(Name=bus_name) + bus_arn = response["EventBusArn"] + + aws_client.events.delete_event_bus(Name=bus_name) + + with pytest.raises(aws_client.events.exceptions.ResourceNotFoundException) as error: + aws_client.events.list_tags_for_resource(ResourceARN=bus_arn) + + snapshot.add_transformer(snapshot.transform.regex(bus_name, "<bus_name>")) + snapshot.match("list_tags_for_deleted_rule_error", error) diff --git a/tests/aws/services/events/test_events_tags.snapshot.json b/tests/aws/services/events/test_events_tags.snapshot.json new file mode 100644 index 0000000000000..cdaa5405393aa --- /dev/null +++ b/tests/aws/services/events/test_events_tags.snapshot.json @@ -0,0 +1,433 @@ +{ + "tests/aws/services/events/test_events_tags.py::tests_tag_untag_resource[event_bus]": { + "recorded-date": "15-05-2024, 14:57:52", + "recorded-content": { + "tag_resource": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_tags_for_resource": { + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + }, + { + "Key": "tag2", + "Value": "value2" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "untag_resource": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_tags_for_untagged_resource": { + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_tags.py::tests_tag_untag_resource[rule]": { + "recorded-date": "15-05-2024, 14:57:54", + "recorded-content": { + "tag_resource": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_tags_for_resource": { + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + }, + { + "Key": "tag2", + "Value": "value2" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "untag_resource": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_tags_for_untagged_resource": { + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_tags.py::tests_tag_list_untag_not_existing_resource[not_existing_rule]": { + "recorded-date": "15-05-2024, 14:57:57", + "recorded-content": { + "tag_not_existing_resource_error": "<ExceptionInfo ResourceNotFoundException('An error occurred (ResourceNotFoundException) when calling the TagResource operation: Rule <not-existing-resource-name> does not exist on EventBus default.') tblen=3>", + "list_tags_for_not_existing_resource_error": "<ExceptionInfo ResourceNotFoundException('An error occurred (ResourceNotFoundException) when calling the ListTagsForResource operation: Rule <not-existing-resource-name> does not exist on EventBus default.') tblen=3>", + "untag_not_existing_resource_error": "<ExceptionInfo ResourceNotFoundException('An error occurred (ResourceNotFoundException) when calling the UntagResource operation: Rule <not-existing-resource-name> does not exist on EventBus default.') tblen=3>" + } + }, + "tests/aws/services/events/test_events_tags.py::tests_tag_list_untag_not_existing_resource[not_existing_event_bus]": { + "recorded-date": "15-05-2024, 14:58:00", + "recorded-content": { + "tag_not_existing_resource_error": "<ExceptionInfo ResourceNotFoundException('An error occurred (ResourceNotFoundException) when calling the TagResource operation: Event bus <not-existing-resource-name> does not exist.') tblen=3>", + "list_tags_for_not_existing_resource_error": "<ExceptionInfo ResourceNotFoundException('An error occurred (ResourceNotFoundException) when calling the ListTagsForResource operation: Event bus <not-existing-resource-name> does not exist.') tblen=3>", + "untag_not_existing_resource_error": "<ExceptionInfo ResourceNotFoundException('An error occurred (ResourceNotFoundException) when calling the UntagResource operation: Event bus <not-existing-resource-name> does not exist.') tblen=3>" + } + }, + "tests/aws/services/events/test_events_tags.py::TestRuleTags::test_put_rule_with_tags": { + "recorded-date": "15-05-2024, 14:58:01", + "recorded-content": { + "put_rule_with_tags": { + "RuleArn": "arn:aws:events:<region>:111111111111:rule/<rule_name>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_tags_for_rule": { + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + }, + { + "Key": "tag2", + "Value": "value2" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_tags.py::TestRuleTags::test_list_tags_for_deleted_rule": { + "recorded-date": "15-05-2024, 14:58:02", + "recorded-content": { + "list_tags_for_deleted_rule_error": "<ExceptionInfo ResourceNotFoundException('An error occurred (ResourceNotFoundException) when calling the ListTagsForResource operation: Rule <rule_name> does not exist on EventBus <bus_name>.') tblen=3>" + } + }, + "tests/aws/services/events/test_events_tags.py::TestEventBusTags::test_create_event_bus_with_tags": { + "recorded-date": "15-05-2024, 14:58:04", + "recorded-content": { + "create_event_bus_with_tags": { + "EventBusArn": "arn:aws:events:<region>:111111111111:event-bus/<bus_name>", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_tags_for_event_bus": { + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + }, + { + "Key": "tag2", + "Value": "value2" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_tags.py::TestEventBusTags::test_list_tags_for_deleted_event_bus": { + "recorded-date": "15-05-2024, 14:58:05", + "recorded-content": { + "list_tags_for_deleted_rule_error": "<ExceptionInfo ResourceNotFoundException('An error occurred (ResourceNotFoundException) when calling the ListTagsForResource operation: Event bus <bus_name> does not exist.') tblen=3>" + } + }, + "tests/aws/services/events/test_events_tags.py::tests_tag_untag_resource[event_bus-event_bus_default]": { + "recorded-date": "16-05-2024, 11:45:30", + "recorded-content": { + "tag_resource": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_tags_for_resource": { + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + }, + { + "Key": "tag2", + "Value": "value2" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "untag_resource": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_tags_for_untagged_resource": { + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "untag_resource_not_existing_tag": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_tags.py::tests_tag_untag_resource[event_bus-event_bus_custom]": { + "recorded-date": "16-05-2024, 11:45:31", + "recorded-content": { + "tag_resource": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_tags_for_resource": { + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + }, + { + "Key": "tag2", + "Value": "value2" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "untag_resource": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_tags_for_untagged_resource": { + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "untag_resource_not_existing_tag": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_tags.py::tests_tag_untag_resource[rule-event_bus_default]": { + "recorded-date": "16-05-2024, 11:45:32", + "recorded-content": { + "tag_resource": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_tags_for_resource": { + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + }, + { + "Key": "tag2", + "Value": "value2" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "untag_resource": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_tags_for_untagged_resource": { + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "untag_resource_not_existing_tag": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_tags.py::tests_tag_untag_resource[rule-event_bus_custom]": { + "recorded-date": "16-05-2024, 11:45:34", + "recorded-content": { + "tag_resource": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_tags_for_resource": { + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + }, + { + "Key": "tag2", + "Value": "value2" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "untag_resource": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "list_tags_for_untagged_resource": { + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + }, + "untag_resource_not_existing_tag": { + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_tags.py::test_recreate_tagged_resource_without_tags[event_bus-event_bus_default]": { + "recorded-date": "16-05-2024, 12:13:16", + "recorded-content": { + "list_tags_for_resource": { + "Tags": [ + { + "Key": "tag1", + "Value": "value1" + } + ], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_tags.py::test_recreate_tagged_resource_without_tags[event_bus-event_bus_custom]": { + "recorded-date": "16-05-2024, 12:13:17", + "recorded-content": { + "list_tags_for_resource": { + "Tags": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_tags.py::test_recreate_tagged_resource_without_tags[rule-event_bus_default]": { + "recorded-date": "16-05-2024, 12:13:18", + "recorded-content": { + "list_tags_for_resource": { + "Tags": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + }, + "tests/aws/services/events/test_events_tags.py::test_recreate_tagged_resource_without_tags[rule-event_bus_custom]": { + "recorded-date": "16-05-2024, 12:13:20", + "recorded-content": { + "list_tags_for_resource": { + "Tags": [], + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 200 + } + } + } + } +} diff --git a/tests/aws/services/events/test_events_tags.validation.json b/tests/aws/services/events/test_events_tags.validation.json new file mode 100644 index 0000000000000..0e71be72de13c --- /dev/null +++ b/tests/aws/services/events/test_events_tags.validation.json @@ -0,0 +1,50 @@ +{ + "tests/aws/services/events/test_events_tags.py::TestEventBusTags::test_create_event_bus_with_tags": { + "last_validated_date": "2024-05-15T14:58:04+00:00" + }, + "tests/aws/services/events/test_events_tags.py::TestEventBusTags::test_list_tags_for_deleted_event_bus": { + "last_validated_date": "2024-05-15T14:58:05+00:00" + }, + "tests/aws/services/events/test_events_tags.py::TestRuleTags::test_list_tags_for_deleted_rule": { + "last_validated_date": "2024-05-15T14:58:02+00:00" + }, + "tests/aws/services/events/test_events_tags.py::TestRuleTags::test_put_rule_with_tags": { + "last_validated_date": "2024-05-15T14:58:01+00:00" + }, + "tests/aws/services/events/test_events_tags.py::test_recreate_tagged_resource_without_tags[event_bus-event_bus_custom]": { + "last_validated_date": "2024-05-16T12:13:17+00:00" + }, + "tests/aws/services/events/test_events_tags.py::test_recreate_tagged_resource_without_tags[event_bus-event_bus_default]": { + "last_validated_date": "2024-05-16T12:13:16+00:00" + }, + "tests/aws/services/events/test_events_tags.py::test_recreate_tagged_resource_without_tags[rule-event_bus_custom]": { + "last_validated_date": "2024-05-16T12:13:20+00:00" + }, + "tests/aws/services/events/test_events_tags.py::test_recreate_tagged_resource_without_tags[rule-event_bus_default]": { + "last_validated_date": "2024-05-16T12:13:18+00:00" + }, + "tests/aws/services/events/test_events_tags.py::tests_tag_list_untag_not_existing_resource[not_existing_event_bus]": { + "last_validated_date": "2024-05-15T14:58:00+00:00" + }, + "tests/aws/services/events/test_events_tags.py::tests_tag_list_untag_not_existing_resource[not_existing_rule]": { + "last_validated_date": "2024-05-15T14:57:57+00:00" + }, + "tests/aws/services/events/test_events_tags.py::tests_tag_untag_resource[event_bus-event_bus_custom]": { + "last_validated_date": "2024-05-16T11:45:31+00:00" + }, + "tests/aws/services/events/test_events_tags.py::tests_tag_untag_resource[event_bus-event_bus_default]": { + "last_validated_date": "2024-05-16T11:45:30+00:00" + }, + "tests/aws/services/events/test_events_tags.py::tests_tag_untag_resource[event_bus]": { + "last_validated_date": "2024-05-15T14:57:52+00:00" + }, + "tests/aws/services/events/test_events_tags.py::tests_tag_untag_resource[rule-event_bus_custom]": { + "last_validated_date": "2024-05-16T11:45:34+00:00" + }, + "tests/aws/services/events/test_events_tags.py::tests_tag_untag_resource[rule-event_bus_default]": { + "last_validated_date": "2024-05-16T11:45:32+00:00" + }, + "tests/aws/services/events/test_events_tags.py::tests_tag_untag_resource[rule]": { + "last_validated_date": "2024-05-15T14:57:54+00:00" + } +} From 78ea777fb7a47ff7afc1d65261f28bb6b7f3c859 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Mon, 20 May 2024 09:54:44 +0200 Subject: [PATCH 164/169] Update CODEOWNERS (#10846) Co-authored-by: LocalStack Bot <localstack-bot@users.noreply.github.com> --- CODEOWNERS | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b30ed8ef1503d..fe9d5fa8f9408 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -231,9 +231,9 @@ /tests/aws/services/ssm/ @dominikschubert # stepfunctions -/localstack/aws/api/stepfunctions/ @dominikschubert @MEPalma -/localstack/services/stepfunctions/ @dominikschubert @MEPalma -/tests/aws/services/stepfunctions/ @dominikschubert @MEPalma +/localstack/aws/api/stepfunctions/ @MEPalma @joe4dev @dominikschubert +/localstack/services/stepfunctions/ @MEPalma @joe4dev @dominikschubert +/tests/aws/services/stepfunctions/ @MEPalma @joe4dev @dominikschubert # sts /localstack/aws/api/sts/ @pinzon From e3db7886053cbd2774fe036287dc9f00a6e0f099 Mon Sep 17 00:00:00 2001 From: LocalStack Bot <88328844+localstack-bot@users.noreply.github.com> Date: Mon, 20 May 2024 11:15:43 +0200 Subject: [PATCH 165/169] Update ASF APIs and provider signatures (#10845) Co-authored-by: LocalStack Bot <localstack-bot@users.noreply.github.com> Co-authored-by: Alexander Rashed <alexander.rashed@localstack.cloud> --- localstack/aws/api/events/__init__.py | 54 ++++++++++++++++++++++++-- localstack/services/events/provider.py | 6 +++ pyproject.toml | 8 ++-- requirements-base-runtime.txt | 4 +- requirements-dev.txt | 6 +-- requirements-runtime.txt | 6 +-- requirements-test.txt | 6 +-- requirements-typehint.txt | 8 ++-- 8 files changed, 75 insertions(+), 23 deletions(-) diff --git a/localstack/aws/api/events/__init__.py b/localstack/aws/api/events/__init__.py index ac552f05e7e58..60a2bbeabcf32 100644 --- a/localstack/aws/api/events/__init__.py +++ b/localstack/aws/api/events/__init__.py @@ -35,6 +35,7 @@ EndpointUrl = str ErrorCode = str ErrorMessage = str +EventBusDescription = str EventBusName = str EventBusNameOrArn = str EventId = str @@ -52,6 +53,7 @@ IamRoleArn = str InputTransformerPathKey = str Integer = int +KmsKeyIdentifier = str LimitMax100 = int LimitMin1 = int ManagedBy = str @@ -571,14 +573,24 @@ class Tag(TypedDict, total=False): TagList = List[Tag] +class DeadLetterConfig(TypedDict, total=False): + Arn: Optional[ResourceArn] + + class CreateEventBusRequest(ServiceRequest): Name: EventBusName EventSourceName: Optional[EventSourceName] + Description: Optional[EventBusDescription] + KmsKeyIdentifier: Optional[KmsKeyIdentifier] + DeadLetterConfig: Optional[DeadLetterConfig] Tags: Optional[TagList] class CreateEventBusResponse(TypedDict, total=False): EventBusArn: Optional[String] + Description: Optional[EventBusDescription] + KmsKeyIdentifier: Optional[KmsKeyIdentifier] + DeadLetterConfig: Optional[DeadLetterConfig] class CreatePartnerEventSourceRequest(ServiceRequest): @@ -594,10 +606,6 @@ class DeactivateEventSourceRequest(ServiceRequest): Name: EventSourceName -class DeadLetterConfig(TypedDict, total=False): - Arn: Optional[ResourceArn] - - class DeauthorizeConnectionRequest(ServiceRequest): Name: ConnectionName @@ -742,7 +750,12 @@ class DescribeEventBusRequest(ServiceRequest): class DescribeEventBusResponse(TypedDict, total=False): Name: Optional[String] Arn: Optional[String] + Description: Optional[EventBusDescription] + KmsKeyIdentifier: Optional[KmsKeyIdentifier] + DeadLetterConfig: Optional[DeadLetterConfig] Policy: Optional[String] + CreationTime: Optional[Timestamp] + LastModifiedTime: Optional[Timestamp] class DescribeEventSourceRequest(ServiceRequest): @@ -885,7 +898,10 @@ class Endpoint(TypedDict, total=False): class EventBus(TypedDict, total=False): Name: Optional[String] Arn: Optional[String] + Description: Optional[EventBusDescription] Policy: Optional[String] + CreationTime: Optional[Timestamp] + LastModifiedTime: Optional[Timestamp] EventBusList = List[EventBus] @@ -1475,6 +1491,21 @@ class UpdateEndpointResponse(TypedDict, total=False): State: Optional[EndpointState] +class UpdateEventBusRequest(ServiceRequest): + Name: Optional[EventBusName] + KmsKeyIdentifier: Optional[KmsKeyIdentifier] + Description: Optional[EventBusDescription] + DeadLetterConfig: Optional[DeadLetterConfig] + + +class UpdateEventBusResponse(TypedDict, total=False): + Arn: Optional[String] + Name: Optional[EventBusName] + KmsKeyIdentifier: Optional[KmsKeyIdentifier] + Description: Optional[EventBusDescription] + DeadLetterConfig: Optional[DeadLetterConfig] + + class EventsApi: service = "events" version = "2015-10-07" @@ -1550,6 +1581,9 @@ def create_event_bus( context: RequestContext, name: EventBusName, event_source_name: EventSourceName = None, + description: EventBusDescription = None, + kms_key_identifier: KmsKeyIdentifier = None, + dead_letter_config: DeadLetterConfig = None, tags: TagList = None, **kwargs, ) -> CreateEventBusResponse: @@ -2007,3 +2041,15 @@ def update_endpoint( **kwargs, ) -> UpdateEndpointResponse: raise NotImplementedError + + @handler("UpdateEventBus") + def update_event_bus( + self, + context: RequestContext, + name: EventBusName = None, + kms_key_identifier: KmsKeyIdentifier = None, + description: EventBusDescription = None, + dead_letter_config: DeadLetterConfig = None, + **kwargs, + ) -> UpdateEventBusResponse: + raise NotImplementedError diff --git a/localstack/services/events/provider.py b/localstack/services/events/provider.py index de7b2ba8a9591..9140a39cd34bf 100644 --- a/localstack/services/events/provider.py +++ b/localstack/services/events/provider.py @@ -11,9 +11,11 @@ Arn, Boolean, CreateEventBusResponse, + DeadLetterConfig, DescribeEventBusResponse, DescribeRuleResponse, EndpointId, + EventBusDescription, EventBusList, EventBusName, EventBusNameOrArn, @@ -21,6 +23,7 @@ EventsApi, EventSourceName, InvalidEventPatternException, + KmsKeyIdentifier, LimitMax100, ListEventBusesResponse, ListRuleNamesByTargetResponse, @@ -201,6 +204,9 @@ def create_event_bus( context: RequestContext, name: EventBusName, event_source_name: EventSourceName = None, + description: EventBusDescription = None, + kms_key_identifier: KmsKeyIdentifier = None, + dead_letter_config: DeadLetterConfig = None, tags: TagList = None, **kwargs, ) -> CreateEventBusResponse: diff --git a/pyproject.toml b/pyproject.toml index f94a073836ceb..84369e0183868 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,9 +47,9 @@ Issues = "https://github.com/localstack/localstack/issues" # minimal required to actually run localstack on the host for services natively implemented in python base-runtime = [ # pinned / updated by ASF update action - "boto3==1.34.103", + "boto3==1.34.108", # pinned / updated by ASF update action - "botocore==1.34.103", + "botocore==1.34.108", "awscrt>=0.13.14", "cbor2>=5.2.0", "dnspython>=1.16.0", @@ -75,7 +75,7 @@ base-runtime = [ runtime = [ "localstack-core[base-runtime]", # pinned / updated by ASF update action - "awscli==1.32.103", + "awscli==1.32.108", "airspeed-ext>=0.6.3", "amazon_kclpy>=2.0.6,!=2.1.0,!=2.1.4", # antlr4-python3-runtime: exact pin because antlr4 runtime is tightly coupled to the generated parser code @@ -132,7 +132,7 @@ typehint = [ # typehint is an optional extension of the dev dependencies "localstack-core[dev]", # pinned / updated by ASF update action - "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.103", + "boto3-stubs[acm,acm-pca,amplify,apigateway,apigatewayv2,appconfig,appconfigdata,application-autoscaling,appsync,athena,autoscaling,backup,batch,ce,cloudcontrol,cloudformation,cloudfront,cloudtrail,cloudwatch,codecommit,cognito-identity,cognito-idp,dms,docdb,dynamodb,dynamodbstreams,ec2,ecr,ecs,efs,eks,elasticache,elasticbeanstalk,elbv2,emr,emr-serverless,es,events,firehose,fis,glacier,glue,iam,identitystore,iot,iot-data,iotanalytics,iotwireless,kafka,kinesis,kinesisanalytics,kinesisanalyticsv2,kms,lakeformation,lambda,logs,managedblockchain,mediaconvert,mediastore,mq,mwaa,neptune,opensearch,organizations,pi,pipes,qldb,qldb-session,rds,rds-data,redshift,redshift-data,resource-groups,resourcegroupstaggingapi,route53,route53resolver,s3,s3control,sagemaker,sagemaker-runtime,secretsmanager,serverlessrepo,servicediscovery,ses,sesv2,sns,sqs,ssm,sso-admin,stepfunctions,sts,timestream-query,timestream-write,transcribe,wafv2,xray]==1.34.108", ] [tool.setuptools] diff --git a/requirements-base-runtime.txt b/requirements-base-runtime.txt index f3a9a29b575c5..3567cbfd8eda6 100644 --- a/requirements-base-runtime.txt +++ b/requirements-base-runtime.txt @@ -14,9 +14,9 @@ blinker==1.8.2 # via # flask # quart -boto3==1.34.103 +boto3==1.34.108 # via localstack-core (pyproject.toml) -botocore==1.34.103 +botocore==1.34.108 # via # boto3 # localstack-core (pyproject.toml) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4e0761bda1f19..70fc7798bbaa0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.89.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.103 +awscli==1.32.108 # via localstack-core awscrt==0.20.9 # via localstack-core @@ -55,12 +55,12 @@ blinker==1.8.2 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.103 +boto3==1.34.108 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.103 +botocore==1.34.108 # via # aws-xray-sdk # awscli diff --git a/requirements-runtime.txt b/requirements-runtime.txt index 2c8a3a8acfc31..c847fbc1e2e3c 100644 --- a/requirements-runtime.txt +++ b/requirements-runtime.txt @@ -33,7 +33,7 @@ aws-sam-translator==1.89.0 # localstack-core (pyproject.toml) aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.103 +awscli==1.32.108 # via localstack-core (pyproject.toml) awscrt==0.20.9 # via localstack-core @@ -43,12 +43,12 @@ blinker==1.8.2 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.103 +boto3==1.34.108 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.103 +botocore==1.34.108 # via # aws-xray-sdk # awscli diff --git a/requirements-test.txt b/requirements-test.txt index 94d6e5c6eeeae..23974dd2975f6 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.89.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.103 +awscli==1.32.108 # via localstack-core awscrt==0.20.9 # via localstack-core @@ -55,12 +55,12 @@ blinker==1.8.2 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.103 +boto3==1.34.108 # via # aws-sam-translator # localstack-core # moto-ext -botocore==1.34.103 +botocore==1.34.108 # via # aws-xray-sdk # awscli diff --git a/requirements-typehint.txt b/requirements-typehint.txt index 401ced0aa3767..6dbc8e509cfaa 100644 --- a/requirements-typehint.txt +++ b/requirements-typehint.txt @@ -45,7 +45,7 @@ aws-sam-translator==1.89.0 # localstack-core aws-xray-sdk==2.13.0 # via moto-ext -awscli==1.32.103 +awscli==1.32.108 # via localstack-core awscrt==0.20.9 # via localstack-core @@ -55,14 +55,14 @@ blinker==1.8.2 # quart boto==2.49.0 # via amazon-kclpy -boto3==1.34.103 +boto3==1.34.108 # via # aws-sam-translator # localstack-core # moto-ext -boto3-stubs==1.34.103 +boto3-stubs==1.34.108 # via localstack-core (pyproject.toml) -botocore==1.34.103 +botocore==1.34.108 # via # aws-xray-sdk # awscli From a462e473c3377ac37bb30c3f4c4401a2c4a43ff8 Mon Sep 17 00:00:00 2001 From: Simon Walker <simon.walker@localstack.cloud> Date: Mon, 20 May 2024 16:27:37 +0100 Subject: [PATCH 166/169] CFn: handle secretsmanager policy BlockPublicPolicy (#10850) --- .../aws_secretsmanager_resourcepolicy.py | 2 +- .../resources/test_secretsmanager.py | 22 ++++++++++++++++--- .../test_secretsmanager.snapshot.json | 13 +++++++++-- .../test_secretsmanager.validation.json | 7 ++++-- 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/localstack/services/secretsmanager/resource_providers/aws_secretsmanager_resourcepolicy.py b/localstack/services/secretsmanager/resource_providers/aws_secretsmanager_resourcepolicy.py index b8f9dc2f1a520..53784023f67f5 100644 --- a/localstack/services/secretsmanager/resource_providers/aws_secretsmanager_resourcepolicy.py +++ b/localstack/services/secretsmanager/resource_providers/aws_secretsmanager_resourcepolicy.py @@ -57,7 +57,7 @@ def create( params = { "SecretId": model["SecretId"], "ResourcePolicy": json.dumps(model["ResourcePolicy"]), - "BlockPublicPolicy": model.get("BlockPublicPolicy"), + "BlockPublicPolicy": model.get("BlockPublicPolicy") or True, } response = secret_manager.put_resource_policy(**params) diff --git a/tests/aws/services/cloudformation/resources/test_secretsmanager.py b/tests/aws/services/cloudformation/resources/test_secretsmanager.py index d3c3a07eb8d46..37f3a9939b920 100644 --- a/tests/aws/services/cloudformation/resources/test_secretsmanager.py +++ b/tests/aws/services/cloudformation/resources/test_secretsmanager.py @@ -1,6 +1,8 @@ import json import re +import pytest + from localstack.testing.pytest import markers from localstack.utils.strings import short_uid from localstack.utils.sync import wait_until @@ -46,6 +48,17 @@ # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-secretsmanager-resourcepolicy.html#aws-resource-secretsmanager-resourcepolicy--examples--Attaching_a_resource-based_policy_to_an_RDS_database_instance_secret_--yaml TEST_TEMPLATE_SECRET_POLICY = """ +Parameters: + BlockPublicPolicy: + Type: String + AllowedValues: + - "true" + - "default" + +Conditions: + ShouldBlockPublicPolicy: + !Equals [!Ref BlockPublicPolicy, "true"] + Resources: MySecret: Type: AWS::SecretsManager::Secret @@ -54,7 +67,7 @@ MySecretResourcePolicy: Type: AWS::SecretsManager::ResourcePolicy Properties: - BlockPublicPolicy: True + BlockPublicPolicy: !If [ShouldBlockPublicPolicy, True, !Ref AWS::NoValue] SecretId: Ref: MySecret ResourcePolicy: @@ -121,8 +134,11 @@ def test_cfn_handle_secretsmanager_secret(deploy_cfn_template, aws_client): @markers.aws.validated -def test_cfn_secret_policy(deploy_cfn_template, aws_client, snapshot): - stack = deploy_cfn_template(template=TEST_TEMPLATE_SECRET_POLICY) +@pytest.mark.parametrize("block_public_policy", ["true", "default"]) +def test_cfn_secret_policy(deploy_cfn_template, block_public_policy, aws_client, snapshot): + stack = deploy_cfn_template( + template=TEST_TEMPLATE_SECRET_POLICY, parameters={"BlockPublicPolicy": block_public_policy} + ) secret_id = stack.outputs["SecretId"] snapshot.match("outputs", stack.outputs) diff --git a/tests/aws/services/cloudformation/resources/test_secretsmanager.snapshot.json b/tests/aws/services/cloudformation/resources/test_secretsmanager.snapshot.json index cbe04bab79505..4570506236aa0 100644 --- a/tests/aws/services/cloudformation/resources/test_secretsmanager.snapshot.json +++ b/tests/aws/services/cloudformation/resources/test_secretsmanager.snapshot.json @@ -1,6 +1,15 @@ { - "tests/aws/services/cloudformation/resources/test_secretsmanager.py::test_cfn_secret_policy": { - "recorded-date": "06-07-2023, 23:05:38", + "tests/aws/services/cloudformation/resources/test_secretsmanager.py::test_cfn_secret_policy[true]": { + "recorded-date": "20-05-2024, 13:00:56", + "recorded-content": { + "outputs": { + "SecretId": "arn:aws:secretsmanager:<region>:111111111111:secret:<secret-name>", + "SecretPolicyArn": "arn:aws:secretsmanager:<region>:111111111111:secret:<secret-name>" + } + } + }, + "tests/aws/services/cloudformation/resources/test_secretsmanager.py::test_cfn_secret_policy[default]": { + "recorded-date": "20-05-2024, 13:01:20", "recorded-content": { "outputs": { "SecretId": "arn:aws:secretsmanager:<region>:111111111111:secret:<secret-name>", diff --git a/tests/aws/services/cloudformation/resources/test_secretsmanager.validation.json b/tests/aws/services/cloudformation/resources/test_secretsmanager.validation.json index e24ce55241817..38f766306bc2d 100644 --- a/tests/aws/services/cloudformation/resources/test_secretsmanager.validation.json +++ b/tests/aws/services/cloudformation/resources/test_secretsmanager.validation.json @@ -1,5 +1,8 @@ { - "tests/aws/services/cloudformation/resources/test_secretsmanager.py::test_cfn_secret_policy": { - "last_validated_date": "2023-07-06T21:05:38+00:00" + "tests/aws/services/cloudformation/resources/test_secretsmanager.py::test_cfn_secret_policy[default]": { + "last_validated_date": "2024-05-20T13:01:20+00:00" + }, + "tests/aws/services/cloudformation/resources/test_secretsmanager.py::test_cfn_secret_policy[true]": { + "last_validated_date": "2024-05-20T13:00:56+00:00" } } From 9311c4b056afd239464ac76ca34e2933f999f232 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 06:51:35 +0200 Subject: [PATCH 167/169] Bump the docker-base-images group with 2 updates (#10852) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 4 ++-- Dockerfile.s3 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 13b85840a7dda..855a145427efb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # java-builder: Stage to build a custom JRE (with jlink) -FROM eclipse-temurin:11@sha256:abccfc31cefa4f3fad66630fceb51e59c7656a2ebfd1a831423dadaf684397fa as java-builder +FROM eclipse-temurin:11@sha256:882268264cb1cc64627bfd6b81efa122c93ea23f6ddf1d894f72db02efb11234 as java-builder # create a custom, minimized JRE via jlink RUN jlink --add-modules \ @@ -29,7 +29,7 @@ jdk.localedata --include-locales en,th \ # base: Stage which installs necessary runtime dependencies (OS packages, java,...) -FROM python:3.11.9-slim-bookworm@sha256:6d2502238109c929569ae99355e28890c438cb11bc88ef02cd189c173b3db07c as base +FROM python:3.11.9-slim-bookworm@sha256:fc39d2e68b554c3f0a5cb8a776280c0b3d73b4c04b83dbade835e2a171ca27ef as base ARG TARGETARCH # Install runtime OS package dependencies diff --git a/Dockerfile.s3 b/Dockerfile.s3 index 359796d93f38a..5bb0aeec1fcd7 100644 --- a/Dockerfile.s3 +++ b/Dockerfile.s3 @@ -1,5 +1,5 @@ # base: Stage which installs necessary runtime dependencies (OS packages, filesystem...) -FROM python:3.11.9-slim-bookworm@sha256:6d2502238109c929569ae99355e28890c438cb11bc88ef02cd189c173b3db07c as base +FROM python:3.11.9-slim-bookworm@sha256:fc39d2e68b554c3f0a5cb8a776280c0b3d73b4c04b83dbade835e2a171ca27ef as base ARG TARGETARCH # set workdir From a374396ebf94e6d8e80e1a07f5a194367f8bcbcf Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Tue, 21 May 2024 16:16:59 +0200 Subject: [PATCH 168/169] add S3 pre-signed credentials validation (#10856) --- localstack/services/s3/presigned_url.py | 14 +++++++++-- tests/aws/services/s3/test_s3.py | 23 +++++++++++++++++++ tests/aws/services/s3/test_s3.snapshot.json | 13 +++++++++++ tests/aws/services/s3/test_s3.validation.json | 3 +++ 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/localstack/services/s3/presigned_url.py b/localstack/services/s3/presigned_url.py index 621b243982f7a..6b5bf33a16869 100644 --- a/localstack/services/s3/presigned_url.py +++ b/localstack/services/s3/presigned_url.py @@ -232,7 +232,7 @@ def get_credentials_from_parameters(parameters: dict) -> PreSignedCredentials: credential_value = parameters.get( "X-Amz-Credential", parameters.get("x-amz-credential", "") ).split("/") - if len(credential_value): + if credential_value: access_key_id = credential_value[0] if not access_key_id: @@ -540,7 +540,7 @@ def __init__(self, context: RequestContext): credentials.security_token, ) self.credentials = credentials - region = self._query_parameters["X-Amz-Credential"].split("/")[2] + region = self._get_region_from_x_amz_credential(self._query_parameters["X-Amz-Credential"]) expires = int(self._query_parameters["X-Amz-Expires"]) self.signature_date = self._query_parameters["X-Amz-Date"] @@ -684,6 +684,16 @@ def _get_aws_request(self) -> AWSRequest: } return create_request_object(request_dict) + @staticmethod + def _get_region_from_x_amz_credential(credential: str) -> str: + if not (split_creds := credential.split("/")) or len(split_creds) != 5: + raise AuthorizationQueryParametersError( + 'Error parsing the X-Amz-Credential parameter; the Credential is mal-formed; expecting "<YOUR-AKID>/YYYYMMDD/REGION/SERVICE/aws4_request".', + HostId=FAKE_HOST_ID, + ) + + return split_creds[2] + def add_headers_to_original_request(context: RequestContext, headers: Mapping[str, str]): for header, value in headers.items(): diff --git a/tests/aws/services/s3/test_s3.py b/tests/aws/services/s3/test_s3.py index 9f7316ae198cd..3d32220e160f4 100644 --- a/tests/aws/services/s3/test_s3.py +++ b/tests/aws/services/s3/test_s3.py @@ -6261,6 +6261,29 @@ def test_get_object_ignores_request_body(self, s3_bucket, aws_client): assert response.status_code == 200 assert response.text == body + @markers.aws.validated + def test_presigned_double_encoded_credentials( + self, s3_bucket, aws_client, snapshot, presigned_snapshot_transformers + ): + key = "foo-key" + body = "foobar" + + aws_client.s3.put_object(Bucket=s3_bucket, Key=key, Body=body) + + presigned_client = _s3_client_pre_signed_client( + Config(signature_version="s3v4"), + endpoint_url=_endpoint_url(), + ) + url = presigned_client.generate_presigned_url( + "get_object", Params={"Bucket": s3_bucket, "Key": key} + ) + url = url.replace("%2F", "%252F") + + response = requests.get(url) + assert response.status_code == 400 + exception = xmltodict.parse(response.content) + snapshot.match("error-malformed", exception) + @markers.aws.validated @pytest.mark.parametrize( "signature_version, verify_signature", diff --git a/tests/aws/services/s3/test_s3.snapshot.json b/tests/aws/services/s3/test_s3.snapshot.json index b34756dbe6990..f41feb7e15e8f 100644 --- a/tests/aws/services/s3/test_s3.snapshot.json +++ b/tests/aws/services/s3/test_s3.snapshot.json @@ -11629,5 +11629,18 @@ } } } + }, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_double_encoded_credentials": { + "recorded-date": "21-05-2024, 10:26:17", + "recorded-content": { + "error-malformed": { + "Error": { + "Code": "AuthorizationQueryParametersError", + "HostId": "host-id", + "Message": "Error parsing the X-Amz-Credential parameter; the Credential is mal-formed; expecting \"<YOUR-AKID>/YYYYMMDD/REGION/SERVICE/aws4_request\".", + "RequestId": "<request-id:1>" + } + } + } } } diff --git a/tests/aws/services/s3/test_s3.validation.json b/tests/aws/services/s3/test_s3.validation.json index 1a6a9bbd8d75e..cbb38c0c0c87c 100644 --- a/tests/aws/services/s3/test_s3.validation.json +++ b/tests/aws/services/s3/test_s3.validation.json @@ -578,6 +578,9 @@ "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_delete_has_empty_content_length_header": { "last_validated_date": "2024-04-24T18:42:46+00:00" }, + "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_double_encoded_credentials": { + "last_validated_date": "2024-05-21T10:26:17+00:00" + }, "tests/aws/services/s3/test_s3.py::TestS3PresignedUrl::test_presigned_url_signature_authentication[s3-False]": { "last_validated_date": "2023-08-04T22:00:25+00:00" }, From 0874f66c574db77bdb3a26ca88c03a46716aa807 Mon Sep 17 00:00:00 2001 From: Ben Simon Hartung <42031100+bentsku@users.noreply.github.com> Date: Wed, 22 May 2024 00:07:29 +0200 Subject: [PATCH 169/169] skip flaky test_schedule_cron_target_sqs (#10863) --- tests/aws/services/events/test_events_schedule.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/aws/services/events/test_events_schedule.py b/tests/aws/services/events/test_events_schedule.py index d34b947a2db6e..49a88b766470f 100644 --- a/tests/aws/services/events/test_events_schedule.py +++ b/tests/aws/services/events/test_events_schedule.py @@ -297,6 +297,7 @@ def tests_put_rule_with_schedule_cron( snapshot.match("list-rules", response) @markers.aws.validated + @pytest.mark.skip("Flaky, target time can be 1min off message time") def test_schedule_cron_target_sqs( self, create_sqs_events_target,