From c508279b6f69a1126e8b58bf6bfc13d246ea2a9e Mon Sep 17 00:00:00 2001 From: biffgaut <78155736+biffgaut@users.noreply.github.com> Date: Mon, 21 Oct 2024 12:40:38 -0400 Subject: [PATCH 1/3] feat(aws-sqs-pipes-stepfunctions): new construct (#1220) * Define construct interface * Udpates, new architecture diagram * Ryan's comments * TYpo * Initial package.json file * Working implementation of pipes-helper.ts * core functionality complete * Implement Construct * Update lint ignore list * Update to match core changes * cfnGuard settings * cfnGuard queue issues * Corrected optional props * Sync docs and props code --- DESIGN_GUIDELINES.md | 6 +- .../aws-fargate-s3/lib/index.ts | 8 +- .../aws-sqs-pipes-stepfunctions/.eslintignore | 7 + .../aws-sqs-pipes-stepfunctions/.gitignore | 16 + .../aws-sqs-pipes-stepfunctions/.npmignore | 21 + .../aws-sqs-pipes-stepfunctions/README.md | 100 ++ .../architecture.png | Bin 0 -> 58677 bytes .../integ.config.json | 0 .../aws-sqs-pipes-stepfunctions/lib/index.ts | 206 +++ .../aws-sqs-pipes-stepfunctions/package.json | 96 ++ .../cdk.out | 1 + .../integ.json | 12 + .../manifest.json | 205 +++ .../sqspstp-custom-log-level.assets.json | 19 + .../sqspstp-custom-log-level.template.json | 654 +++++++++ ...efaultTestDeployAssertD542618D.assets.json | 19 + ...aultTestDeployAssertD542618D.template.json | 36 + .../tree.json | 957 +++++++++++++ .../test/integ.sqspstp-custom-log-level.ts | 35 + .../cdk.out | 1 + .../integ.json | 12 + .../manifest.json | 205 +++ .../sqspstp-existing-queue.assets.json | 19 + .../sqspstp-existing-queue.template.json | 654 +++++++++ ...efaultTestDeployAssert6DA9FDD7.assets.json | 19 + ...aultTestDeployAssert6DA9FDD7.template.json | 36 + .../tree.json | 957 +++++++++++++ .../test/integ.sqspstp-existing-queue.ts | 38 + .../integ.sqspstp-filter.js.snapshot/cdk.out | 1 + .../integ.json | 12 + .../manifest.json | 205 +++ .../sqspstp-filter.assets.json | 19 + .../sqspstp-filter.template.json | 662 +++++++++ ...efaultTestDeployAssert0C52837E.assets.json | 19 + ...aultTestDeployAssert0C52837E.template.json | 36 + .../tree.json | 965 +++++++++++++ .../test/integ.sqspstp-filter.ts | 47 + .../index.js | 9 + .../cdk.out | 1 + .../integ.json | 12 + .../manifest.json | 223 +++ ...stp-lambda-function-enrichment.assets.json | 32 + ...p-lambda-function-enrichment.template.json | 757 ++++++++++ ...efaultTestDeployAssert1F2A8125.assets.json | 19 + ...aultTestDeployAssert1F2A8125.template.json | 36 + .../tree.json | 1127 +++++++++++++++ ...nteg.sqspstp-lambda-function-enrichment.ts | 43 + .../cdk.out | 1 + .../integ.json | 12 + .../manifest.json | 199 +++ .../sqspstp-logs-off.assets.json | 19 + .../sqspstp-logs-off.template.json | 601 ++++++++ ...efaultTestDeployAssert25C29ACD.assets.json | 19 + ...aultTestDeployAssert25C29ACD.template.json | 36 + .../tree.json | 902 ++++++++++++ .../test/integ.sqspstp-logs-off.ts | 35 + .../cdk.out | 1 + .../integ.json | 12 + .../manifest.json | 205 +++ .../sqspstp-no-arguments.assets.json | 19 + .../sqspstp-no-arguments.template.json | 654 +++++++++ ...efaultTestDeployAssert83953622.assets.json | 19 + ...aultTestDeployAssert83953622.template.json | 36 + .../tree.json | 957 +++++++++++++ .../test/integ.sqspstp-no-arguments.ts | 34 + .../cdk.out | 1 + .../integ.json | 12 + .../manifest.json | 205 +++ .../sqspstp-set-queue-batch-size.assets.json | 19 + ...sqspstp-set-queue-batch-size.template.json | 658 +++++++++ ...efaultTestDeployAssertA1752E9D.assets.json | 19 + ...aultTestDeployAssertA1752E9D.template.json | 36 + .../tree.json | 961 +++++++++++++ .../integ.sqspstp-set-queue-batch-size.ts | 41 + .../cdk.out | 1 + .../integ.json | 12 + .../manifest.json | 241 ++++ ...spstp-state-machine-enrichment.assets.json | 19 + ...stp-state-machine-enrichment.template.json | 868 ++++++++++++ ...efaultTestDeployAssert47A5DE6D.assets.json | 19 + ...aultTestDeployAssert47A5DE6D.template.json | 36 + .../tree.json | 1252 +++++++++++++++++ .../integ.sqspstp-state-machine-enrichment.ts | 40 + .../test/lambda/index.js | 9 + .../test/sqs-pipes-stepfunctions.test.ts | 425 ++++++ .../@aws-solutions-constructs/core/index.ts | 2 + .../core/lib/apigateway-defaults.ts | 4 +- .../lib/cloudfront-distribution-helper.ts | 4 +- .../core/lib/kendra-defaults.ts | 4 +- .../core/lib/kendra-helper.ts | 4 +- .../core/lib/pipes-defaults.ts | 31 + .../core/lib/pipes-helper.ts | 257 ++++ .../core/lib/step-function-helper.ts | 6 +- .../core/lib/utils.ts | 36 + .../core/test/pipes-helper.test.ts | 811 +++++++++++ .../core/test/test-helper.ts | 7 +- .../core/test/utils.test.ts | 13 + 97 files changed, 18356 insertions(+), 22 deletions(-) create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/.eslintignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/.gitignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/.npmignore create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/README.md create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/architecture.png create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/integ.config.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/lib/index.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/package.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-custom-log-level.js.snapshot/cdk.out create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-custom-log-level.js.snapshot/integ.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-custom-log-level.js.snapshot/manifest.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-custom-log-level.js.snapshot/sqspstp-custom-log-level.assets.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-custom-log-level.js.snapshot/sqspstp-custom-log-level.template.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-custom-log-level.js.snapshot/sqspstpcustomloglevelIntegDefaultTestDeployAssertD542618D.assets.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-custom-log-level.js.snapshot/sqspstpcustomloglevelIntegDefaultTestDeployAssertD542618D.template.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-custom-log-level.js.snapshot/tree.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-custom-log-level.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-existing-queue.js.snapshot/cdk.out create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-existing-queue.js.snapshot/integ.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-existing-queue.js.snapshot/manifest.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-existing-queue.js.snapshot/sqspstp-existing-queue.assets.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-existing-queue.js.snapshot/sqspstp-existing-queue.template.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-existing-queue.js.snapshot/sqspstpexistingqueueIntegDefaultTestDeployAssert6DA9FDD7.assets.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-existing-queue.js.snapshot/sqspstpexistingqueueIntegDefaultTestDeployAssert6DA9FDD7.template.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-existing-queue.js.snapshot/tree.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-existing-queue.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-filter.js.snapshot/cdk.out create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-filter.js.snapshot/integ.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-filter.js.snapshot/manifest.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-filter.js.snapshot/sqspstp-filter.assets.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-filter.js.snapshot/sqspstp-filter.template.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-filter.js.snapshot/sqspstpfilterIntegDefaultTestDeployAssert0C52837E.assets.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-filter.js.snapshot/sqspstpfilterIntegDefaultTestDeployAssert0C52837E.template.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-filter.js.snapshot/tree.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-filter.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-lambda-function-enrichment.js.snapshot/asset.33adcab38bd8c4e154734b436a40ee81920a89cd6c787ce62302c33df5e1dfcb/index.js create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-lambda-function-enrichment.js.snapshot/cdk.out create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-lambda-function-enrichment.js.snapshot/integ.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-lambda-function-enrichment.js.snapshot/manifest.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-lambda-function-enrichment.js.snapshot/sqspstp-lambda-function-enrichment.assets.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-lambda-function-enrichment.js.snapshot/sqspstp-lambda-function-enrichment.template.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-lambda-function-enrichment.js.snapshot/sqspstplambdafunctionenrichmentIntegDefaultTestDeployAssert1F2A8125.assets.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-lambda-function-enrichment.js.snapshot/sqspstplambdafunctionenrichmentIntegDefaultTestDeployAssert1F2A8125.template.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-lambda-function-enrichment.js.snapshot/tree.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-lambda-function-enrichment.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-logs-off.js.snapshot/cdk.out create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-logs-off.js.snapshot/integ.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-logs-off.js.snapshot/manifest.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-logs-off.js.snapshot/sqspstp-logs-off.assets.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-logs-off.js.snapshot/sqspstp-logs-off.template.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-logs-off.js.snapshot/sqspstplogsoffIntegDefaultTestDeployAssert25C29ACD.assets.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-logs-off.js.snapshot/sqspstplogsoffIntegDefaultTestDeployAssert25C29ACD.template.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-logs-off.js.snapshot/tree.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-logs-off.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-no-arguments.js.snapshot/cdk.out create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-no-arguments.js.snapshot/integ.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-no-arguments.js.snapshot/manifest.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-no-arguments.js.snapshot/sqspstp-no-arguments.assets.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-no-arguments.js.snapshot/sqspstp-no-arguments.template.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-no-arguments.js.snapshot/sqspstpnoargumentsIntegDefaultTestDeployAssert83953622.assets.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-no-arguments.js.snapshot/sqspstpnoargumentsIntegDefaultTestDeployAssert83953622.template.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-no-arguments.js.snapshot/tree.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-no-arguments.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-set-queue-batch-size.js.snapshot/cdk.out create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-set-queue-batch-size.js.snapshot/integ.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-set-queue-batch-size.js.snapshot/manifest.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-set-queue-batch-size.js.snapshot/sqspstp-set-queue-batch-size.assets.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-set-queue-batch-size.js.snapshot/sqspstp-set-queue-batch-size.template.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-set-queue-batch-size.js.snapshot/sqspstpsetqueuebatchsizeIntegDefaultTestDeployAssertA1752E9D.assets.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-set-queue-batch-size.js.snapshot/sqspstpsetqueuebatchsizeIntegDefaultTestDeployAssertA1752E9D.template.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-set-queue-batch-size.js.snapshot/tree.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-set-queue-batch-size.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-state-machine-enrichment.js.snapshot/cdk.out create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-state-machine-enrichment.js.snapshot/integ.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-state-machine-enrichment.js.snapshot/manifest.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-state-machine-enrichment.js.snapshot/sqspstp-state-machine-enrichment.assets.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-state-machine-enrichment.js.snapshot/sqspstp-state-machine-enrichment.template.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-state-machine-enrichment.js.snapshot/sqspstpstatemachineenrichmentIntegDefaultTestDeployAssert47A5DE6D.assets.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-state-machine-enrichment.js.snapshot/sqspstpstatemachineenrichmentIntegDefaultTestDeployAssert47A5DE6D.template.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-state-machine-enrichment.js.snapshot/tree.json create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/integ.sqspstp-state-machine-enrichment.ts create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/lambda/index.js create mode 100644 source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/test/sqs-pipes-stepfunctions.test.ts create mode 100644 source/patterns/@aws-solutions-constructs/core/lib/pipes-defaults.ts create mode 100644 source/patterns/@aws-solutions-constructs/core/lib/pipes-helper.ts create mode 100644 source/patterns/@aws-solutions-constructs/core/test/pipes-helper.test.ts diff --git a/DESIGN_GUIDELINES.md b/DESIGN_GUIDELINES.md index a7cf6f699..4c06c52e3 100644 --- a/DESIGN_GUIDELINES.md +++ b/DESIGN_GUIDELINES.md @@ -332,9 +332,9 @@ Existing Inconsistencies would not be published, that’s for our internal use | deadLetterQueueProps? | [`sqs.QueueProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_sqs.QueueProps.html)|Optional user provided props to override the default props for the SQS queue.| | maxReceiveCount | `int` | The number of times a message can be unsuccessfully dequeued before being moved to the dead letter queue. Defaults to `15`. | | enableQueuePurging | `boolean` | Whether to grant additional permissions to the Lambda function enabling it to purge the SQS queue. Defaults to `false`. | This is only on 2 constructs, docs talk about a Lambda function role.| -|enableEncryptionWithCustomerManagedKey?|`boolean`|If no key is provided, this flag determines whether the queue is encrypted with a new CMK or an AWS managed key. |This flag is ignored if any of the following are defined: queueProps.encryptionMasterKey, encryptionKey or encryptionKeyProps.| -|encryptionKey?|[`kms.Key`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_kms.Key.html)|An optional, imported encryption key to encrypt the SQS Queue with.|Sending messages from an AWS service to an encrypted queue [requires a Customer Master key](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-key-management.html#compatibility-with-aws-services). Those constructs require these properties. | -|encryptionKeyProps?|[`kms.KeyProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_kms.Key.html#construct-props)|Optional user provided properties to override the default properties for the KMS encryption key used to encrypt the SQS queue with.| +|enableEncryptionWithCustomerManagedKey? or encryptQueueWithCmk?|`boolean`|If no key is provided, this flag determines whether the queue is encrypted with a new CMK or an AWS managed key. |In early constructs, the enableEncryptionKeyWithCustomerManagedKey name was used. Later constructs saw name collisions with this name and services like SNS, so the new queue specific name was adopted. This flag is ignored if any of the following are defined: queueProps.encryptionMasterKey, encryptionKey or encryptionKeyProps.| +|encryptionKey? or queueEncryptionKey?|[`kms.Key`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_kms.Key.html)|An optional, imported encryption key to encrypt the SQS Queue with.|Sending messages from an AWS service to an encrypted queue [requires a Customer Master key](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-key-management.html#compatibility-with-aws-services). Those constructs require these properties. In early constructs, the enableEncryptionKeyWithCustomerManagedKey name was used. Later constructs saw name collisions with this name and services like SNS, so the new queue specific name was adopted. | +|encryptionKeyProps? or queueEncryptionKeyProps?|[`kms.KeyProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_kms.Key.html#construct-props)|Optional user provided properties to override the default properties for the KMS encryption key used to encrypt the SQS queue with.|In early constructs, the enableEncryptionKeyWithCustomerManagedKey name was used. Later constructs saw name collisions with this name and services like SNS, so the new queue specific name was adopted. | **Required Construct Properties** diff --git a/source/patterns/@aws-solutions-constructs/aws-fargate-s3/lib/index.ts b/source/patterns/@aws-solutions-constructs/aws-fargate-s3/lib/index.ts index d04dbf50d..1fae0770f 100644 --- a/source/patterns/@aws-solutions-constructs/aws-fargate-s3/lib/index.ts +++ b/source/patterns/@aws-solutions-constructs/aws-fargate-s3/lib/index.ts @@ -240,14 +240,10 @@ export class FargateToS3 extends Construct { } // Add environment variables - const bucketArnEnvironmentVariableName = this.SetStringWithDefault(props.bucketArnEnvironmentVariableName, 'S3_BUCKET_ARN'); + const bucketArnEnvironmentVariableName = defaults.CheckStringWithDefault(props.bucketArnEnvironmentVariableName, 'S3_BUCKET_ARN'); this.container.addEnvironment(bucketArnEnvironmentVariableName, this.s3BucketInterface.bucketArn); - const bucketEnvironmentVariableName = this.SetStringWithDefault(props.bucketEnvironmentVariableName, 'S3_BUCKET_NAME'); + const bucketEnvironmentVariableName = defaults.CheckStringWithDefault(props.bucketEnvironmentVariableName, 'S3_BUCKET_NAME'); this.container.addEnvironment(bucketEnvironmentVariableName, this.s3BucketInterface.bucketName); } - - private SetStringWithDefault(value: string | undefined, defaultValue: string) { - return value || defaultValue; - } } diff --git a/source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/.eslintignore b/source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/.eslintignore new file mode 100644 index 000000000..4866847c3 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/.eslintignore @@ -0,0 +1,7 @@ +lib/*.js +test/*.js +*.d.ts +coverage +test/integ.*.js.snapshot/ +test/cdk-integ.out.integ.*.snapshot +test/lambda \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/.gitignore b/source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/.gitignore new file mode 100644 index 000000000..8626f2274 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/.gitignore @@ -0,0 +1,16 @@ +lib/*.js +test/*.js +!test/lambda/* +*.js.map +*.d.ts +node_modules +*.generated.ts +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +.nycrc +.LAST_PACKAGE +*.snk \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/.npmignore b/source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/.npmignore new file mode 100644 index 000000000..f66791629 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/.npmignore @@ -0,0 +1,21 @@ +# Exclude typescript source and config +*.ts +tsconfig.json +coverage +.nyc_output +*.tgz +*.snk +*.tsbuildinfo + +# Include javascript files and typescript declarations +!*.js +!*.d.ts + +# Exclude jsii outdir +dist + +# Include .jsii +!.jsii + +# Include .jsii +!.jsii \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/README.md b/source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/README.md new file mode 100644 index 000000000..2ce7bff79 --- /dev/null +++ b/source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/README.md @@ -0,0 +1,100 @@ +# aws-sqs-pipes-stepfunctions module + + +--- + +![Stability: Experimental](https://img.shields.io/badge/stability-Experimental-important.svg?style=for-the-badge) + +--- + + +| **Reference Documentation**:| https://docs.aws.amazon.com/solutions/latest/constructs/| +|:-------------|:-------------| +
+ +| **Language** | **Package** | +|:-------------|-----------------| +|![Python Logo](https://docs.aws.amazon.com/cdk/api/latest/img/python32.png) Python|`aws_solutions_constructs.aws_sqs_pipes_stepfunctions`| +|![Typescript Logo](https://docs.aws.amazon.com/cdk/api/latest/img/typescript32.png) Typescript|`@aws-solutions-constructs/aws-sqs-pipes-stepfunctions`| +|![Java Logo](https://docs.aws.amazon.com/cdk/api/latest/img/java32.png) Java|`software.amazon.awsconstructs.services.sqspipesstepfunctions`| + +## Overview +This AWS Solutions Construct implements an AWS SQS queue whose messages are passed to an AWS Step Functions state machine by an Amazon Eventbridge pipe. + +Here is a minimal deployable pattern definition: + +Typescript +``` typescript +import { Construct } from 'constructs'; +import { Stack, StackProps } from 'aws-cdk-lib'; +import * as stepfunctions from 'aws-cdk-lib/aws-stepfunctions'; +import { SqsToPipesToStepfunctions, SqsToPipesToStepfunctionsProps } from "@aws-solutions-constructs/aws-sqs-pipes-stepfunctions"; + +const startState = new stepfunctions.Pass(this, 'StartState'); + +new SqsToPipesToStepfunctions(this, 'SqsToLambdaToStepfunctionsPattern', { + stateMachineProps: { + definition: startState + } +}); +``` + +## Pattern Construct Props + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|existingQueueObj?|[`sqs.Queue`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_sqs.Queue.html)|An optional, existing SQS queue to be used instead of the default queue. Providing both this and `queueProps` will cause an error.| +|queueProps?|[`sqs.QueueProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_sqs.QueueProps.html)|Optional user provided properties to override the default properties for the SQS queue.| +|encryptQueueWithCmk|`boolean`|Whether to encrypt the Queue with a customer managed KMS key (CMK). This is the default behavior, and this property defaults to true - if it is explicitly set to false then the Queue is encrypted with an Amazon managed KMS key. For a completely unencrypted Queue (not recommended), create the Queue separately from the construct and pass it in using the existingQueueObject. Since SNS subscriptions do not currently support SQS queues with AWS managed encryption keys, setting this to false will always result in an error from the underlying CDK - we have still included this property for consistency with topics and to be ready if the services one day support this functionality.| +|queueEncryptionKeyProps?|[`kms.KeyProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_kms.Key.html#construct-props)|An optional subset of key properties to override the default properties used by constructs (`enableKeyRotation: true`). These properties will be used in constructing the CMK used to encrypt the SQS queue.| +|existingQueueEncryptionKey?|[`kms.Key`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_kms.Key.html)|An optional CMK that will be used by the construct to encrypt the new SQS queue.| +|deployDeadLetterQueue?|`boolean`|Whether to create a secondary queue to be used as a dead letter queue. Defaults to true.| +|deadLetterQueueProps?|[`sqs.QueueProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_sqs.QueueProps.html)|Optional user-provided props to override the default props for the dead letter SQS queue.| +|maxReceiveCount?|`number`|The number of times a message can be unsuccessfully dequeued before being moved to the dead letter queue. Defaults to 15.| +|stateMachineProps|[`sfn.StateMachineProps`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_stepfunctions.StateMachineProps.html)|User provided props for the sfn.StateMachine.| +|createCloudWatchAlarms?|`boolean`|Whether to create recommended CloudWatch alarms| +| logGroupProps? | [logs.logGroupProps ](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.LogGroupProps.html)| Optional user provided props to override the default props for for the CloudWatchLogs LogGroup for the state machine. | +|pipeProps?|[ pipes.CfnPipeProps ](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_pipes.CfnPipeProps.html)|Optional customer provided settings for the EventBridge pipe. source, target, roleArn and enrichment settings are set by the construct and cannot be overriden here. The construct will generate default sourceParameters, targetParameters and logConfiguration (found [here](link)) that can be overriden by populating those values in these props. If the client wants to implement enrichment or a filter, this is where that information can be provided. Any other props can be freely overridden. If a client wants to set values such as batchSize, that can be done here in the sourceParameters property.| +| enrichmentFunction? | [lambda.Function ](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda.Function.html) | Optional - Lambda function that the construct will configure to be called to enrich the message between source and target. The construct will configure the pipe IAM role to allow invoking the function (but will not affect the IArole assigned to the function). Specifying both this and enrichmentStateMachine is an error. Default - undefined | +| enrichmentStateMachine? | [sfn.StateMachine ](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_stepfunctions.StateMachine.html) | Optional - Step Functions state machine that the construct will configure to be called to enrich the message between source and target. The construct will configure the pipe IAM role to allow executing the state machine (but will not affect the IAM role assigned to the state machine). Specifying both this and enrichmentStateMachine is an error. Default - undefined | +|logLevel?|PipesLogLevel|Threshold for what messages the new pipe sends to the log, PipesLogLevel.OFF, PipesLogLevel.ERROR, PipesLogLevel.INFO, PipesLogLevel.TRACE. The default is INFO. Setting the level to OFF will prevent any log group from being created. Providing pipeProps.logConfiguration will controls all aspects of logging and any construct provided log configuration is disabled. If pipeProps.logConfiguration is provided then specifying this or pipeLogProps is an error. | +|pipeLogProps?|[logs.LogGroupProps]()| Default behavior is for the this construct to create a new CloudWatch Logs log group for the pipe. These props are used to override defaults set by AWS or this construct. If there are concerns about the cost of log storage, this is where a client can specify a shorter retention duration (in days) | + +## Pattern Properties + +| **Name** | **Type** | **Description** | +|:-------------|:----------------|-----------------| +|stateMachine|[`sfn.StateMachine`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_stepfunctions.StateMachine.html)|Returns an instance of StateMachine created by the construct.| +|stateMachineLogGroup|[`logs.ILogGroup`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.ILogGroup.html)|Returns an instance of the ILogGroup created by the construct for StateMachine| +|cloudwatchAlarms?|[`cloudwatch.Alarm[]`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudwatch.Alarm.html)|Returns a list of alarms created by the construct.| +|sqsQueue|[`sqs.Queue`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_sqs.Queue.html)|Returns an instance of the SQS queue created by the pattern. | +|deadLetterQueue?|[`sqs.Queue`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_sqs.Queue.html)|Returns an instance of the dead letter queue created by the pattern, if one is deployed.| +|encryptionKey?|[kms.IKey](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_kms.IKey.html)|Returns an instance of kms.Key used for the SQS queue if key is customer managed.| +|pipe|[ pipes.CfnPipe](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_pipes.CfnPipe.html)| The L1 pipe construct created by this Solutions Construct. | +| pipeRole | [iam.Role ](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_iam.Role.html) | The role created that allows the pipe to access both the source and the target. | + +## Default settings + +Out of the box implementation of the Construct without any override will set the following defaults: + +### Amazon SQS Queue +* Deploy SQS dead-letter queue for the source SQS Queue. +* Enable server-side encryption for source SQS Queue using AWS Managed KMS Key. +* Enforce encryption of data in transit + +### AWS Step Functions State Machine +* Deploy Step Functions standard state machine +* Create CloudWatch log group with /vendedlogs/ prefix in name +* Deploy best practices CloudWatch Alarms for the Step Functions + +### AWS EventBridge Pipe +* Pipe configured with an SQS queue source and state machine target +* A least privilege IAM role assigned to the pipe to access the queue and state machine +* CloudWatch logs set up at the 'INFO' level +* Encrypted with an AWS managed KMS key + +## Architecture +![Architecture Diagram](architecture.png) + +*** +© Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file diff --git a/source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/architecture.png b/source/patterns/@aws-solutions-constructs/aws-sqs-pipes-stepfunctions/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..1838dfc7cfdf2278f6615e2d5f7c2b715bfb26fd GIT binary patch literal 58677 zcmeFZgnEW<8W_;_Un(Gp9sFSrGH+JnI=hw{6>IY|AyHdB(U^60UgxM~(``!6 z4<^7~$Vpw2!mPU>%O_0Ivl9;-&$1NB8Ocs8x~}Q3nXQ82Ka9kF`(0?P7s`)#xWf-l zJtK7`D(wg{iRiyZOhxaJ?!NLW@3c{0(u#1g-YvFE{FROh7C>weW_AO#8Lbq(rLTZ} zck{27DZjJAHp@^IYak09s!q{89g_s2M@1R8*gHW1HmSlkDT#M5g}atfh|dPLT?xfM zMZw9%?{>k3kNn!DJqg<@OYLw;TY7lkY=DaEr;B9* z$HF`M@yODq>t4P`S_2DQ9c8o{orX)&N=Y)Y9NKPM&HZ{Nt4J&?GAf$Yb1iI&)lKJ} zjqZT_uEaBCTVMDqM?}tSGIhANvtZT+N*ej2AU20 6z>{3R?=C*DbPrRIWSBDJ6KwMM2Zf)7?Bm{TD%dk zqrP*L&+DZU43X?LsWS0Ho q2oZzdLo&_H=U!=zOt1FT-N5yo|7?3 zy07vZ>Qrfoj_jC)KCJI#yr)m s! QHmA9zbdrESFGO{k>-#HuvjZ%7cbNPk{C!7x`YP%BLrRbj zwt(%cEu&=iOhL;n7E_MIVnK=pnQ@t3sb-szU+oj-8W52Y#?BSHwxHjQeJZpV-%xB% z>`2L1uOqU& 8S^ zbPz;RDR9ZENx_;Ai%gdXWl=InmpvKmxQhQH&K}qD;=11kUSCZbB_e>7+1uY*y4 hqye%^aPKc5f~4J0Kfxdoj}BM9lc|C*Q0Z zCJ#(;sF_z(Cf|RwFt(JS#>mfOaSd}{!q^35fUBzE0CyRD<0xY4WB}C3&W|dkDv;Vv zq*{SrqB+xzXYkdvTCEovadgh{Vg!_liA>|+C4LpSR#RHnH$Fb)#v9@;1G#x=~o5m+)TX|y`cQK z73pES^=0o}Oqoa3*?V$15ua_uV9&vmvVEf-i9x9i|B7Q81c}#Y!q+T#ANfd^tn|jl z!c~!I@MX7hsV7YB;+}8FCw(K3hOpCHHb>5mOo3zk2gt_cV&GopoyWl8UMOv(VUkt& zu;V*vA_Di5a`VksL_dW{J5z+< z>@^x6h zRb07W$LMUi#FjBoStB$gEyXp<5B5)c6Q)y?zF9e|P7k5XW@HY#tT H6xlmYfD$VPy#KA(#?o3Cjw fEe*%?>Oi3Y9H~y zTUKAN UYI_DHp;bw`TtT9=T8ksF&6OyMfJbtezvo}-V0iG~g5MaC1~y3+X8f2fU@ zT`xN-bQms0-w{^$f&%n019aJs5nL MXPEw+`Cc UBnRMe?I&&CQar@`1RuNs+pfa)apI?lr+uD*wAT4ibf#p9t^!+b&z3 zzWnJ}8IItrHIpqr)ySJH=#ccbTkm=gB}sJBOEvtmA`hl}5?QPWW?SwxXpf`!G!6XT zZB~o3aYhm~glJ+-SKGv%z}@-EuJW9*TTLQYt_k}T*xlkmW4rfA4ANYL8tvk><|Au{ z?zT&UL6gn(CD7E;X LGL2RD-WuUtfbTmb2|@iT ~FUEa~1(D$W zu j4-5#Sz}-yu|8mQt Vh&VKWGm;|rOw79X;ZS0L{yL68CA9RPq6DRl!2l2(p zSiGv7;H32IIU%2pH0D7I*|>>&*;;8#u?tDimN+fr5|~cNR$aLu{J8Ab`V`v1^`y3C zZCH J=Hu0BXA}xYMBQpCq#tTZkkm_*$R?WGz@`W7jeWU&8 zHCJ7|ImMoiH~ZbCWH%^`$^v8P{Y>=@6fnh2(P0bD$!mkU`i*y;#XVCcp!IC^r^@Qa zj9cxmnegggFb(%`OG<7Nl_)84ua6e7*imi60^iHw0oRuTV1g9|CaAdp?TvL~ X$1= #K_8Cgln3r?i%V=Q)m0G)Y8t!0lG^Y({(*4@6 zF2ls&+M~_;4wXZq^%VW)-JY*i<6y+e*umK&m&C>0Xuvg!`Flv0qq^RkYM371nP?|S zko3xh;&m;_g7OWLltxTGDLRKPF^1TFy?+^i8kk>`<*)ndBsC;i8HTt4Cvg%YFzkzx zBPE$iV+&M13c>FN%Fb2G(8^gc9;sY+%?z7gUx2UkPtr&NNA!OsC>G^$J)tiqnTtBR z@OlY5kXngL@fHL*r^i)?PpVC{3CmgSs-W3x0bwDvwO8(XlwjZUV5VfXUoVHjxxVmL zFDEC-p6GD$`Kh>eSOXwX1z`)eS^b(H{R}iRY*PZ&HJrp%E9W&-(QBBT@93dewvX=h zU3%`iYf@(2X0t~<@VS8$v-o1@2UpJM{av+!BrnX3&oO%nTUr>5Np8accFpF=cm&t9 z9R5*;+#{*-JIEn4xaU`?Y9#%mI|BpQX~M09v@5ZSx`20lRHu>N6WNMTCu`$8X%PoU zWX-=)E=WA8Z^zd}1@I4^<2(jApp2XxzO%R#X_2_D)RD+r<(lzUpNES(hAW$rWYU4- z?bDYHW=CpeaAO4lccTIIYsU6qW MEJa}(PrUull{!B5@924+*~4LIJsrF^G8Hu z{o3-xSeP7yQn`XekVvpG{^!0gKVea_QyRNQRIQS%NYisTHKMlmP$2i0Yqn$5@Fb<$ z2_gpsh3Urc99c%KE_)0yOdHvKn_ZJOu}wd%?mYHi`z7f~yS>6)Hdep~ 8POfRpflG%i{d7OvZ~C@jcoxO@KY*WHQT9__w# z;&%1UW%svX#EHj^ebgPU()=2UphB?U@q!VoKvOBbX7|PV<9kU8ZKkv7W%V(p2Q3}f z@XK0_LJ&2q6=NfK9O+Wmcg!!JJp2; H+sI97bA3!|k 3D&u$WZ-(;&&aa(n?p?bDZ0#a?8!E zjK}8}ey5Gw(SC1d=_>N7eNEXct)$3ZHI^&_Ro8Bi=}Q#PEhvKN{ts(k9aYs9y(>s} zNJ^(jiAWt 9qq&0EL&yYF}J9pnA?#@K_Q=fK)) zuf1Z s{4@}rAnYrnn5#sZM)_n_>;P*S{49>uc17T&HjTvwL=m@u| zPv2d(?M$w?FD!jJCwJMQT&fXQdClR8$P0U~*?=kI=-H0N&!UI={!Z-)Cc6jwbbav- zW}CgH@y;@-%%DQ`9H#Oc _r%T$`_Uss66##zo(lHnD;^FydQ3!4BsE7>|n%>_1glL$xa{y zlI91ER(pOcEi>$!DL+zb1UYrvJHf8I{gSbe`tdv?c7ag3{nx^)aS!cB8zpd&F5ghm z2EW`!+xuG(FgM*>tJjT&h&nLIvGzplx^b5%?^d&Yqr=&89sLw00*{_utk)DjZNuu~ z2br(f{-K()Kdsn^kCGFnEpaq7*Ex_`7Yj@`y4!u>_k;&-!3!L^Ff7f_$vqav*%3H4 zRGQ183^a4kOML*R2G&zsz2oaSd>!fI-C8)ZHO636s^B7y(c`3SGcJ`mg9%YOfY2Y{ zTC-C68!{k_%Cu%C%=YgD8Iebah@=A!hvzHb@Zz(jFglm!vljaE7SuOxlaJxzGJ_L8 zQ%K5}-$frOQ(|s;hpJ3Hw<>+Bd6_&m%dZ?{DeyBC-v(oVhch^V>6@ZE)uP=9S9e9H zyfPT5heDxW0eQs3Gt+FaZEro8IEB=`H~N<<+ULYU_@Diucg2rxT_}46EoEgE418AQ zID%`24u$93#ydIb>>{_8Kh{AAwX5RP`&PEn+21hU{LJ>$185wgTf@eK?iT$6nxEs7 z4Wwgn+NsYkfD`{4|J!A=6Wu{YFAXG`_Dgi+8VDbr+2lZ4!$JeGpCnlJSDtrX90$|N z1BHyet;+Fu_Eq^fF+6w*ICzdQ@kj2!&PYmFJEWA+F(%;u(y-oo4xd_+b=lv%+m;K_ z&RQ-r7NhvE?xJ2Ku~v2x&Y!cGJU4i0PCuwKa5^`)xAW*2r&oARfKGm-C?X{7X4b;p zk>-oYGD>zBiN40h>DM1c&K5aT1LtzgMTyiTzltoozpPPI8pe65_0LMYaT=21{@DLK z>Ve_jr$8$X(MHRJa`BnIx>UOCytdsDN)0X68S;kp-`Jn~pzZ)meg>z7ChY+)Uw3i+ z1$d#;%nI&uq{8yELv#s`Lu1{CQgnwLey=_rkefJ$Z V=h mM3?Cj|GJefyJISu!%_(z$Yqg*z% x^9f?v1>N`* zl7hSGA#sgd_*c|e@5Ah;p%D-14=&DR#Oz;fg0lhT8`^U)q4iO+F`t$&W{N6P?T}Q( z(=!S5DkZYB8Yww>$i!8sHOyf~mM;B((C0U^%vo^`)ifMCnx}dGj?~IgAWACwt)qI5 zSW5JV7VS9?qh9a17zurRtIaER)%^L#CQXm;=vkbh|NOYi`{-mWrrPTg0_B%Fcvrh0 zhjVjXfoIR2zS8z0#~|l==% PlZf%zpxLa;mq^ zkA3U5XEvgbbOKDAbSG4>UF$3IcFKWFsm?@?goQms151vXeJ7s!Y(u?GQI+Z#mWAny z;V`4&%W21|uH-D9xc8h7v!GY6V1a0OT WOAq1v}r&Qm~4AKKTz R#d^{j3`Z9g#Hm$?L zXnSPs+0oF9Of0#JA`#zUV$MTZx{XEoU3abG*Y7p8) |0?wIJbi|~b0A20R Ytlu9_*(nuztXJ3r*l?dM2W1M!&& z$1KMrwm>eP?)846l+%*+Sf|}fLj|?*ftDJ*g1~Pe)BAITh=xRA0_VV7FjP${yGCLt z$>&S)16Giys^mT=uwli-zh%SJc<@?=wI|YjDAl0pvXOJC?%L3@G&g$IxXU*|;*Env zXwLOKJp{`Kq`)_-rXO9!(S2C{XcJ0Kb(v?*s%{i?HG?e2)1pHt`>IO$Ipf!eSy~>R z;$PkV?~)MHKJX2eRn-_%HhkwrB9dN;agkmbpd27lg)71wbFo4<4t)Smjk0MRnNxq0 z=A4T8Lg%UPYY`&<)zlh*#GP`^WyQVPBljF)nUk<#=1z()UEA<=jeSqumcItJc8)vc zkDvBrzw`2I1m*70n%o{|-^=Fpm3=>#8QrkccV)QC@1M47cr;h%;es z= )^=hACekws)1^L@8h>1 !-8_ku zP$q ;_0|$Vbes zjgHmtBn|F{0{)9}ATE{UdFX2hd@uFSs)9F!2R2cw3BF-=p>rb&j~6cc02bD6w%{;N z7IltU?7 *@~Lw~2eHii;M{K*D_)$a)RVsysL6vn(0I7YQ${f^QQj %+J3#uuhz45=d4R2a zNv?b-(@^=~EBdD{NdBw)$vO|V>X=EHX(jBgn_!w$S)0qK^bu_uoM2;>iYL#jHDzt{ zEa!zx!Y}1=W!+yi2}+q#-l>ra$CDSDW@)nx6@0_@*Mf_3@R29}3 RlX4$cYJBaTT zUE05*bChSFmV7az8_LNqyCBSCCaGc!77=LDQt0iCY%OOa#I(0BxytOFjJF&s <*a1hGl9@<;-1LD^Q%+X%B*_b}OAnb|mn27q5>Ag?|tgzjK`Y;0%vyQ#YC z#QCG*(*ci9M!u-T-t8v1!Em;U?>4DUgM!mu6n@~5Iab|UxX8uqiE8V2ihI}8_%g^U z{%Jx|l9l^lb!SFy^Z-WhjJq4t?E%O0i+owS`;*>zAi9P2zM`}8ja&B)Dz1<3=MY$K z_7aHQ<^Sma#n|&Rp&gK?iM2gN$x Kk@QZ7+*r}MYvuabKaUh7T>0HL>PGcTRGW?HvffXf zZ74r;2q*GPMR<8{u%O(#i~U}t9))%OR udl-EBl=tGETZg50G+2?jWM-J76 zs1XhmuIdJQ;LLB=CrpCmv<~>nA!y|6hXHX4Gt5}mQOLXVn~mH~rYo}r1FP~-t)J>! z`D`v3ZnN)+&NF_uQx2Y-+0ogJsP)bpdlQs1J4U3z5~f6u;W%$-4^}O9bJyALBg;l9 zxQCKM3-9;+Bmot=L2ZPG%#s0TM)Jo2SJV&I`-brhfq5z%SmtAZwG#ipK+4GB1kV}c zhpgeq@`bKQi(KV#OWqk{b>UB&iW}LSDO NumkdhD`X%(K8}savS#K@(-$kYzP1`$}yWXpEvQ{u1 z>@3vX=1%L7n1i0K;{DtR4IS>Aw@B7Hcs(hA!IV?Yjz!*6YSKB)t+6xG(&e%h_bN3y zHj0TYxBT4t5XOYTvBqFJTmN{KaSbNtIlmualE{F|6LuUFkVMF0-nL1!$UV3ia&W8b z(eX89l7t||JBVJsp5knXX93ALk@DCxZ_uKNz><4Go9ER|+pAv=!<99eJ64}FJ+{YS zr2n_+%d?Ex4%)TZgq6tB8JdzB67ntnTV*7PWZvlFBO{+(EprmE#Z$*fvw9%{r@vu+ zsuej7vJCicG=epn_pLwsy0LeI_kdfQ3daw;QLvZuk0BVbdazar+wdblm3-hS#G1~D ztD-GPYcx4^bD37fw~pxt5sO?_!@IN3;vV)fM%yj04CLF3gZad7Eyy|Hf+K>+h_N zQ`dxZw+*7Yg(%msWAhar_$8E!gCRnNK(hbI!U=~DNgKdb%vS@DXX0;SKCRerrw;CA z&2l$6^|`F_x`A$(a#u^5$g$#aM`e27exI;0GCy=oM~K|x$2FIU`~Om|Wz_#r9H~)1 z=BtbwvQK{?r+qbGK%FOZHABlEox|BinA}+ZNV~LtZvLnr@4MSo6qfTJ!OW8 f0@y5a zXlygnMOokA67DV-v<_>Rb+CiYY-$gT{>Yt*qDH6P#@E~xMc<4v$_8-`BIz{2#X|+O z+(v)Baob@Qjgd7%fCrf}=x>8u{|pre?l e98qR8@Je(R5Sh9kLaenOFUuAC#r znr{l#CsyL(laXV<4naxulP#$FW~iVTB3pJoBR$ex{M3ZrdE~tc?O^#`_|--e&SYB= z_V9iQTh)4~* z+yrhBRrf#hIUm4_0>&jGs-)vTk~G1FtO>$A)`)z6N$mZxM_>)U0gFlcS=qlESON|B zGWY!c(~G}@R4KDpz}2)%r*;W5_uJR4@X|`3tz>%KH$0YETr_O0YIv$|H=H9=QW8>*e(m-q zGZ{;NLHOqd(uO5i+*Rac;ORvEb;(eJFPMlEP*KAl{OjsaOn-8hJ)Lp{)Ox~!1eY2u z5*FE$ouYb6N1kt~raJH5(E|ai23Mdlb<~b6toD7b-ihdj%yCL@q(rYho(l+tcmSOT zic;e|g_Ng;hK4{rjEL{3ht?1XXa!x8(J;uxQx!Mg)y2Smv+ruBm>s6?v|GE8S~hKz zG=9NCEAs_G1^3IW_suU|Z%l)hFL3!Zo3wCvRGyz1UPlR#avFUVxVu;xsdF%^U5%8b zF+ICIU+4#{Exn~~N{ L=%AwwgtF#m5%HSDx!K`KrYUqq*|Oj?;!jX-O*^ML>Wpg~#pc M{z@gz;-zh%{hK0E1M;hAC$&pzxngq5JxzIG*xs S@uea1#E7mI@lxuWEnstI}+P;L)Q`7cCl -nqH!0?o$&poqK_^qP_(<>?PW~cbpr}d6h6m5GHN-+{c6j7mUBn|Uy1D@ zm|ibYXMs}M+2A7rg6Y%sBr7cW7JfV`IJ|F+@V|SqfI?Wm!8nlpB2Z$UJZNc&EczN4 z-~q(Y_NMo$7v{rcjnXoUoNHU3J=MQ6%K+dC`=e4Y2E#(oCb#R)Fu|hB47Hz~3?=%V zXvIL>i34#Ne644L6Ciwh5|uU#rj2DETN@A{aTFHVyadX9H%eD_P(DWDwK?l{%vh^p z?Z%gFb0IDs{uH0Tp)bdPw}J$x1vOh31rx<;@^nIjUH_1_D+_}GHmS1l0;DLmBE_QK z)pb0}HrLYLJIA!|&A4>v7n4qmQh@Zf71yql3JbvI&$@)ffCu@BICVIYONZoF;*8Ip zJwn&JwDk1!M -|0xZ4ad+4%L>e|FqObdv%EaH1iW%NaoyW$wuI=l{x}WD9Ss~_fWm_#3FDHz zUv2ZxQZVsdi^H|TeML76{GhtQSS>wKib70NVV<|~Q=@?=FjYTfO4rx+B@*`UJoj+N z)y;-ukXj2?bK GXXZfdf^&0o+ozOc-grC+s&e}-vEq-}u0F)n(M@OQ35i*|`^AY$6%Bit` z&Efk&HeIJzeZL9%82mB+sSI~!8BB)moz8cUIiRoDm=z{8)9hqJoT6T45SSK+hU3AF zYEr^Il_Q}w?|LjR3uv`NS4(U{H$NULvlcpjY{2B1{iztX7Q@CGi>#2$JIsDxIjLP< z6T&k;mF8?~Vgm#xKP8h+l(@b 0@ zreovNS JZr#h& V#Si=I(+F4=s}cd?IN8ZEW?QVZK{Ku_(vdPb-lIX`A(jSwOs zDj84hI-ARNzW-QZ*nE3t*Lh9J+U8*2>jEZMV~%XxXGr2+wU|`(?MbP?)UP!kgroO% zgItY3tZ(8hK!S6`^D{NM`yDs{)TXbN)Z7Cqz&6%IS@r3+zM|C7t?MBLRgFEu97IX9 zbyrv%8=M#@aIOamHeS1)?LzXdR^fU}3whU4VV_(*)R25y$;mxWn JiHe6u=nr+nr>#XV3(DP^sPT8Ryy5+Cq{1jtMx1&El%=RP z2sn6xvl*tq25R0VWa~P$6ze=Id}JLgFhz{k`sK7=)TxgtPmQ+#p{*6A2Um)0DPt7g zkacrbyRxuq<|#4R^1(uL^V(#knJV@xl4Lfqif69zka6-z3d-^%^*5u%gd0NdzZTU9 z%!>W8tS?;iRZtYz2ikZiV`B}?2k02H*hUjnI4SMY1k33Ho#jjfLC;1g9mbT{(CafF z=JgW@YN>3+t@Vx3%cdsLV>k!!09#22&XxAUXsn@}5-IF)4eEl&k^z!Y2vCt*5k781 zXTMmWdc?UgyUA$~X16J#dv;T|Q)DN4H@=7I40NAvOVXv{M6a@1!Q4orjQIu@$MuLT z`|0r) wG^C~Ym%z72NcRI4>Q|0mMK7cm)P7E7$G^}&~bOjrAQk|y0n*xb PA9oNCk0 0PW3^6WBe-UWVImK zluoc<31m^ 4NMQyzYEODkbOD z)iL8VHzVKrTfNrC!n&iL AMObB@bIF2 zH567;@J`6kP1vWnrfo|>dn%isG(Gs% z54_j{purBM*zpTkFmAn03ZGAx( zzJu`0EHn!;TTQ$}s;RJNfD}uElv4hdaJGDHSqZ7BJzW`bNZF7r+dRAdInCI^YkhAl zX1rO`hduc*%&JIPjpBxBQ45xQx&0XB&QElB`s%5)fo;C+DWbKC8_Sy2>?4Fkc4czm zUOF(vR}GImQ8LW)CAk&tn>cJs=UOAMkQ^wS2%=BJo&T=PXcihZWV%b6>1-*Bauzmh z$mp3Uzp e>pzJYOS+^Vvi{?x{%=dkKFgChG=>fNiyz3k z!B3?#WnIPdnrKs1ml)%fI(8pP=IiF*5|VOwVaZz5W%n9Udt?7d$NPHMV$$7{wq>V6 zq8EXpLglc%Ao;U>0q$s$b3i=Cpq$ue*hz4QMGfJWF8mzf Ag0a4}dVssXsEGwV82P2~E zh=uetdSG;W2hbhtZXsNO?1C<5;;((|RE)I5D+mzluqQLU%{4!3mwSsqpXx)7OoO~q zlV~A{IHwjZbb`6%9Q0&GH{xzop(62)et1YZaBZ|T1KsjNAGT8MHht1LeH2h*87S-q zIN17{w?9@~a-3q&(@&|7;2Ho)WGfb-1j@`%5!6_$?Ivu`H@UP6?rv>@t>10qwK|Vz zD$5w^JfK9$kVr$Xx5iBJwPIv5@bu#jgbpX&eV%Q8PPs{z?ZIfZ*s+iCS|8)}D8s9b zc!gq<$#vx0kRKy^03)_9_NocUN>>%ANf~($JDl-f`sH-+8qsW0;V*fWVdo}iB eJ}Sj50DZ9JoD3)Y3VbsO)PZziHjT`cvY{f(vmxN>I!Q);-Qec4?wxa3%QK! z7BiW+_u+P(yxIqdA@{x3t*-VTa? ` zt2vpxOiB(gjNj=Z^cLo0bxn?k2|gxH>GBWb{8AD|yP0mSUxlGCsCTWzJBpDa%=KQM zc6oERRqqUAkC@v$Ubh*kE#r0OG2z=IP3<~_VMugq=wnImaA7E`w^tI&+H|piAYJ$x z!lBmI$VbmzCFLU}4R6jn>sU-Oqt3{+ ^{P<yFT7l-FXDxg z5CfgDpl2{YRS!O&HcZo_tI!;6AKQ+=rjKMq_ka*8gX#3=lxH8qR0v7B9B= nne zNlxAK&L@*? Q_fK2{%R(T&(#gajx^q;WnxAn% zF`MFiSW>y;r)b;pZ_=HXTqpaWzP`R2+us7*jqT>91$K?PT{>~^uaE>aQ!>6T|D42E z;BDo;q7`TuMOk`yeENw|;ZhuuTnneS10_VQN^r;Paf0VP#vvzkW#-k%W$=fiH5Pfe zLEjZgGDQ}4lS#7k 1nzfV zliE>IQ2c%3CN(gdOCU!*?{GG 4t$7RMlamw4`8w@Pu(q+d7Z{gHlF2?Rtx&?& zsSx~NxrO)j^X4>>(HKJR?<_v^5q}#v7Crdx!@-;3R6Sx~zF(p6Ui!F|O;!I$@ymYK zwWgI;#%>a0ED4Ma6UJ~b3uv5|zG0I!XL&2*-in??>9)wJ5>0bQr(3{1Xg{)u|K2^) z1HggKqgEAyO5UWmmx*U_QF#REG_iT?LQ#p0_FE&Lm7j2apDRHBU;J)NoJtcnpNVy) zEKCGSy3^C78qv&!MatEygJ#b6zrPkPB7_SXQnX64pa&4h-&YSQ{8S_y^C ?F#hSDXB{3fpKZ45#gc!ekoMcRbdx$Bjf;sg zmR(;-Mw;AQnqMgj{PuAL>iTz$$I;QzJ${cZp8!SmB>U~LUc|}Ijo172YXEeN1md-- z6YtFmNk@eNB>CKNSB-DwE2lx@=|nyUD-3uPzXM>041g^|p1iNNWR26bl?gG~=05^z z?Gdn22JC0x)-k{mMaOaz2KMdkq2{4abtRhxTHL5ZuQwoBeUXy-^Yr14@b|X|9)bi1 zHn%MAE1v9~IuH;8#I5=m%I^Y$ebCExJnoLuNPmrdQ=&ZpI1##YZf;Vlpv!yr1%Ly{ z_p ?G)VyC-f}!9@Hu3G*+BVMl?5l-_}}m#s3LK+bK3RTN{#;vz>~r( zL;}MziPH7@y<6f(x-(-qJq6U`bj%3@p|C+N3uysR6? t4$z`0lj zD)6!MjV@BGDls&z*kqEkL@a7lz&8Kk3a~p{Aj94R9-S57dJbd8-k$usCEg#UUcaw2 zCBVo{1Ct^U0cOLiyX;kFt;fsQA`>+1mi1=?s5HB1Z>BOz$n8q{4=`7L%kW+U-$dEM z7821A&s~e~Er33b(e4kxJ5Eyqtc-r(4OPE)HW+D#L=g;Au&~bpP;jmBd_F2p`!f@p z#4hOnP_|Ji8(nx5LMYIA)_^1zhP0=LLVb+QPSp U4)u!?HGq%=NI<768B! zUva=er+6kXA@5%gDj+N@y8~P_{7FH%&_=SIdGur26Hu?9ZZk4H{6zx*RHP|A!Bm-U zngiPZ5L0@~+BL#pZ1?Kl?vvrP;e4`jY%xieb-e~ekA5gC@JWE_yAtO#spXVF }TVrv=rpzjOZ!&QJO;Xl#u@iZvc(*_1(>Jpgt~FB1d-MwInruZOqhck z&WM1wRoua>=(Xv7v4jl6M|MF84eok}HdMxovJC+7zffO1s}Plh)sZ;60AQ<;&88jV zTo3^>EJ?b=it;u`42w_PzBY|Tr%51w;Oh179w7lAp(%iMnmbbufif!nXC-&Q8>4pa zsteq>Oq>A%7*-hf53z)Atap&lHiyMRK|z2^@G@`p&7wwspV8l9Y{{^Bjum_|C<<+9 z^F?n2AZ5n{3y DE1iDT_ zewZ&bupw9EM{(9<4k!xWjvdW{zkIo@6bF#Z?&Vnuj=d7!F-)HD;2FIV wDryzfucI5;nW@b8rA?6?%@9LI z*8oV;?Pod~om+o!BC;?n0@H_GNq{Zl?p4E4M-0E|*b~K&t0||zrht46ZzN0sa )*P9;yUiy3e&?8AA~U|Fya>}k6=3TOM&i_MV#<_Ia$-G zP*Ue82Kl3yJb*q9uw$Q!tCxOtNcZ-sTG{}$e9rAB`jpKA&%Q#JT_8K_8*7g^R`XN{ z8i7wgQ4g>SR{0v5T$la=Z=}vc;JsH1q<`55yKXGc5z142u=dk^cOs>SnDW9q6{yjl z>Ie3yHXogYixA>l_hB{h^n@Lzft3!)b_hIC#fxBsWUa--#3W?qlrK&odzLLm6S^)y zYau&dL(zkN1Z>&LUY|Kk%wB`l$I7~z01eW(03iE@Fr}5265|v8*Tlm2H+EKQeKDy; z_DA;X(4$#UKIT>+%Nfs2=)DGYKh?&w9xM`UEC~Mkmb(_ZRP$)9nr&hk6!+CaOpqGd zG`o6PhtOO5M?WJ_A(LA_f@%CBte=QqJQ%h?g?!zmBBLE0%~P!YCjO`tl;8N7I_dVb z)qJpcbdHedAizx=2?HGZ0WvMU2(Hasx9M4{WJYP>gejTncCUh!#qn;7w2Z~$l#t6S zaaE#$aMFhUt+&%FeR&JdOaglaT<-z d?Ofnh`YGr>AhI@R?u!+vuI zT%!?(+Em1P$YgfF)*GXKY&VAU rMBLQ}aXK;Hc()V|_AB6JI0x9#dij0;pT<}W5Ufur=f~xgz4rlHC zS^%?-#X_suY&Q$74w}OMYj3$T*t?bM%7Nk4;X`ES073O{Sy(s; $?37la7|#Cwfmg`Y*RCb(%bx zpIP5m1fa;iprUYIhEjB-o~MttlJGx%MobvCus8W~`iGpKfR`gg9gbwW(Zxx)sj6=K zxv{6fSOV8Hh^``(eF7DqxPn7ewH#v76j_G0g|I2YGpwiXk(>i=Z9X1=1IKJ>>3V_X z* 00VR8LFq!q63b%#BBydavPLL4-{xoE-RLImsX zvVAK)Lu=&{k#mGG#r}=>oA@Fr;~lUSJol!m6aT4*Kd*O03s1qQqgMo(^`*QZdn?{P z>gNO@XU>+ba>Zm%I;RUaL9oBH?kiyEqE*z@#3%xBCmGR|E9ND``hmHOqu1ZO*aX~O zOe5zPL+p%S;UKvm+N~!TqhS~=zILue#2I+r)?7EVBbSUx)B2*3Q`76Ey|9P4`Y`;H zolK?g_azteDz}r&jIZp;5$574hAC21LQ(ACEZ2MIcHWR~ROauL=!&m_ib3ds==0_k z!;-BU<%jrq={%)J6DH=Up$*7sqg=zS8dt#F9FcxNSf;D=?%W2Ag&rb-u>|Eyh}d*0 zlugzmlxfyy=pUT_fIvh}x?xWedfe6U(sN{OGSkJ1cEqP&RZFPph5F0CwEzrE?3onA z-xpAzK)RnQU6KxX19yxFzVcMni)=Z9asN7a!m91TYXN73e*4SL#7=)iGhVGKGFO%& z+X)>HxAN5hPl_jcretDcU;NbLyS|dLkZue83U~G9$v+k_s@KgKE_7`(Cqw#Ok_m59 z@>46x&iu)aDQ(QmComyagSSk1zaB2OThl&8u^T-& zTUd@{2U22+I;@27pheTWu3DEKNoZCy= d!z<4prfihj+!~RI&n?tcALtEKIEsED{&+xILelOXu z$zP10w*TB{TN-K=rC^Ei!|>EFPH||XEc{K;M+6n=*v^H;a*UeUnlAKPF=maDEErzX zWki3siTHL|uzzey)e-#+<9P@ 2BT0*OO*~gglf?&y LG8l38)xjS_xYdD;n=xKHst@#o#dr9D{MxTty25D9__{tuv2o^~!NUgdfyifc}V zNsCJ29lLdiT^GLe9K829 i{9bw#H^AKE=_%x zah$Nvi<;f(vTvJ^(sI%ej(orDsE$qO$V4MUHGJFobf*-zXJNtkAWWmmIn$I+2G~fU zrDN)tc2jsC<0y-GRWUZ~^|&!MV(6C-XF*XC>W9c!7!hu!qzgKv#7zwx@g9G6Yd5hC zDzTLI2ZB%?Z*Nxzwi{<3 {@ zdW1;9{?CNb@ -1cp#@BAmX1uOg}7{zUc5~ zQjL$6 _S$oE1iyIjddX_lw(__*70>p7m^{&F^M-!M7&tu;w zmnWheLyO4k9t~H&SvOGpSr=b^MqtT-N#ds=FCM?|U0&vpkZDCqY3x(mkTNcw?aSi$ z)jBnc;>jK*t=CCUd#(6tzKfqKUXKwx^r=om_bA $BAT(UX%y z!+RDEHCYF&ShC-HKZY?50RN$C@#987hR4bkf4Fzae83%Xs&7Mwx)!ukLzOFhskVu1 zl{9qi=fi{BCY7IM1^xV= bsZijNWun5oi9KE|dzV_T~|E|Zw%pIGYTY%ma zb8=zQ)I?FUBORJIx^mXKn5DEMmKv)0%k6coR~y3U*k%9-XdaS5`&*p}S}VSBL-F4* z3%kG1?iTfZ)=joQ=9PsUb;73O+5(AOX&rdl<`oprmUUvE%bMAa07qCHj>VpK4v+yZ z{1)Li=WY{@<2j7D2`Ql3-`S0l6gdH|{}=J5olpF F%$R zZ3njQfArR)M% +@J#eI}X-=6a^hk0z&I z$&)g>N4gS6mekGL-icB_eQj<^sl0CwhILw=>fHiY>}~6NqE#P9lx+gM8H5Bh^pC@< zj|`r2k9;VO|3$R)G~o8h=T8-z7zw%y5;d~oO@6}tE_gi-@PTKlcuSswnw!5Cue-F1 z7w_2Ou;r@T7elcMe0JlUelMw>w3e{EHZC_;xBuj-wXY<+E898mO59{UVY)KNJin1u zNpRg#vS!fgg~+hr#r-fdMkfkPt6)i%Q@ytxZ-#hHQ7=AzVA5Bj)TR0Z35lej=oTYD zo)9~AFgV-CF8+k`a^NKX{bhQ;)f;(|$wTOGNXda(BtOt0qw9Z|Ze@NCBu8MBm*R$_ zAob`m!--5;x feH7aYGQx=pANT02z|gy zj}m!+2S#^*ap*+qP+sH85;@1p{u}%XuSTeO!C|E%_*mgTU$DWy?HG|rT?4!KKS_A2 ztZ+~Dov*j2t^eHYZ*-2z(;f!eu^q9*{TnNzvO;~0+y6^-$S(@k4O^5Oz%sA}h)!Ps zE^TBKwH<&Zs61cbU4X4oE?4k!HL7yL>!?^oT<-Nla3`)7y+?mNz#amFLh=)DWb9ag zvwT16kQ0S JjV_1HvAx>9{7x zKZStq4A(x3hW+MIVv9xf90Twv!_A5tRbX{oF99(oyP#mes0oX26~| $FT0>>*IhYO888?DcP~mQ zOLF=+h2JR_l;ISG3SFC k6=U=8JzmA(0VzRwB(pU@Xp z^O}AbBs2gJsgg?*nll)PQPpM}+*YkVy|9&`_;(g+zhMvvaFZ5bb&jOo&(!B{U~s za(H7>WHf-neX`Y-a|I2oRZ}zq-(3M9@Z9ynx|MUMpga`DxMJ9#`nA}$M%kuWPsM-G zbZvPF;3q>sJQxAyI|;9Kx;Qhk%ivFyXk~3}OA|%N7Twlx7IIm3|2udJU^xAmN %eo7r61lvaXaKlLaYe=0 zXlRr#wc?)n#ZVh32&E|U^SHYLv~iCwdmJk`FQp*-1cq|;V8Sav&C3V5@6N*xD1-sJ zTI>WsLMb1o-Kk*>Kx@Jli6Mh=_uT7p<&&=5-^Dh-AO=PBFs!prfGih_R3?M`c_6F; z#^}p&cwS($(gpaHrJZ82wRPlxn&F pz6<$fT82c9C4?&)T)v2__GkKkB zrl_P8k%wmbjOmt7@{3EyNpb2QY*U&F0{8W0J4*LzZ h)EqVvh1_^a0+LQooED&P{m z8cG$k$dyO_YVPo$O=*%zIk!b43k>psxg*AD+6$A&K}%Mhrr9B6Y_YCXP#Cn#eENAU z08SB4Cq3Dm0S!t#MkF79l}0NRuZN>EauBuw0B8~(jwjOv)F$>>%}pWnFv8$ew_c)c zYaKRt*Yz^B8B{#B@VNjpu>8>Zc>R(V$_@-&>G$vESy&}AE6eflx?wTuUB<;#jMbMv zEvlxC&h`Mq^mnW7LA0_$=^;Q(JKNql{|@C{Mp48>bpEmHlkF=oTvT5@uA*_d J}da?b(sTTmfEBg^5C)wz~h7zZ1*^uGY+73r=LLZ zG@n3lg8RnFCN}zL&Kz4$bLOpdaZn zyFji q3+@SwEcJ>8vd`J;gQsCzw}nPbzZQ^D(J$OBG@>DxOsmp>Dl368kmy5HnA zPAo1B2AeBi1R|1qt-U7|iQwmoxjN%<_^i1P?T}c3ABK{`d=N=kW!`lBGC;z+UOM@+ zQ=)(r#f@B_N|Vv9Si&mP@jhfu{SK7lcCscrtvY)S{7bdAPm7+&b-ML0P1^BV{3+el zR8-;6duw7J_q13wU3M8Amn8FV>W*y!rYcwYZL9T_o&SSEhIMGYfh^cIq^Y(Lbb$_v zK6DP4id8)D8~Z*7_qHap1S#Zdf_7LFy(PjJSQ`kfqOD$);%c&^nAg;@jwL$FW&LdU zYCPx*d3xB#L}2W+@#6skz2L59;DGgzzoZJqpR+c3l$Lt{T{fu1V3Q1r)~>D-U+N#z zab;h!jIsfxurn0fr*K}4um!bclg8f@LCwHw`Ko5ws95C(J6|JTJy?(9aPfu0W0|_I z#i;J;Zh*xoRAhTXMnwW*ZA=zJLe~W?RQ~UU@GI?&hCZy4q|kgtfE^lMM}XX*N>+vD zZ%xJ^|0mid3eS6@#rn8!ce>nygeJ8Ai|rXW;eQdB*o3Kb?2@#z5XV0M(J3|!c=Yna zo^mfg^TrqJd b59%YlsMxw6=wVm%)s z`z2?1pS*5>xs)W&bicq*<}+_=7S1}5Iv|JIAqWE-+;LFi=!?do5B!65%nB=a8wx>P zaIKQB*uqnBh H J69%YTNJ x59413Fj$(|J`apL-gw0%{Kqy97cmD$^Z(;7 z2uh=M4Ril8N&ojJPSOK{(kXLd;N|~21%?Th0z 9eoSANLF$@zKVn9i$Y(5)A%vNz;Pu z%fx3W6Ci)E-Qn-+C<=7(#`VCQ>)%}wAqAGbEUhb{{J;Aio(5LL+J=j9{(l-8kp|Ip z(xvi`|GF)e=M5crBH;yojsG?z33EYE(j;J>!#}Q7M$pmA%lT>3|1|Uh51dSr7c7bY z+j-biPzl3akfC#^%6}TljsQ>gqRn{tKVyF>13K#7 +8qw3Br_g9Hu@{7E-zW(JTp9MRrqPY SB0#mTkVub0wEU;I=rg@LF zq~86F{U;yJ5ArfT_)nTY56j@rFv2uN%@iNFMcgr^d_zC|zz2?0KE7!q5!x=XeiDvU z1Ev|!@xaM+_a9>_7_K7`$4g2`q)P{md`V>F?jn4+ajYXXP#AprKcD@%G^OI$cjNwm z-JL)%HyzY Y=E6k>E?ZO^5HzIl+~d)k>+auQc<|mz e>au3!%rb)X#4sBS|@i zu4uC~N!pis_);9^K1*PEtyd7jlpYQV75a$XNlhu-PD_+%ZP$y_p;4Fqw^^717}qUJ z(+_b3>v|_jr#%L&2yQRpg0*?>mNYR$*#Fjepx!|4TGA}hKYce=bs5Z*dM%H2?HL-e zgD=W@@z)}1fC);d4RTq~lafdI^4*w8f6W6AmIPK&7mT 0s}$_+|^D9>)ouDQq`@=$kZeKVib;T@&s4n&QV z@L^|mg+;~S8LO8|X0uX|Y)F!6FUww!{g*;k0vaSZ$ln*brN3X{d|>A*cf|7kmu3lv ztAY 1gbLWF?5DIk8aG7!ce_RJ#6DJ0IkgIuL@;^s_qvS!=Nsd+t9RGbOQCjc; z6XDJO hDbH3emm@O&G{UFcAna~lD$c=ZHYu5zFRk9M z!^BMA7aR0y$hdt~DC<0~S@h|nxWs{KpsI|Bk-H0F?)t8pIw00{JUC$lN!i;Z_^Sxc z31?<=4~IvL6Z!&-3=D5i&Dcb0^ajGTc?~0AuIDlrC$dbY{rjzJRFKk$#!c^)X4ExF zeU5^bTDNG@HT~yQRge1|9lBCzLs)Rs!1P=WYf?Q?Z&o5`)QmhrW2tRI`DE<*Zmin+ z`*?axzUuG{M{OB>`xGjSCnW}1FyhO+UW)U8Y#s8qU4phb=FY}?SqMX@yD=JZfXe4< zz)*qf6O)~pd`||?h44Dummr;9TKKNAuA(`5d!(<#U?jP{{C7KB`R@ZZWkDNh*{pn* zl*2)@oRG{{&8dSgd#W yHz-nW2<(%r(~IZLpf4Pf$RnPOF|gN-{ow(7l@j15w+#9T7lwmRuf{0SI&BB zy>-X;Fp4P{F#;QY&Y4C9e(ag}U9CBGN(WC+n`^Ut6vkfQB)%puEC%qee3q^whxjOK ztHbyuHQAN!OE5;Okv5CU-?ckN+q4QBn`1!?TBg1-uSZf>_FRFAj;|sR|z8FG49}HYp@4xuS&(?(wuB;R>>M{d{t^e zO)aVR(Tp{X`PG;9!I!O7>ovu&5HJxM@W71QEML$@O8RgZNWJMQytY1*&^dXt ` zu9&=%)OXbM09+xo!x^>eWAqLeZVASs9W~I5%Pc?FN2oCJJL{Rdt%sWEHurJlboDll zJh<$2WxZVj$tWBgR@l0Bsaz2p2Cj!Ke=IK~-v#-t%v=>2zp~$1Y-_#?uH`P{^j;J8 z11`$TI!7XJaRVeZwUeFu?Adpo&$MP23)h>}HE%xNzjocdAi)beO13Rpe6QKm_^SFk zd>;*O?HfJhExdn`vr}R_kkKKeZb*fhX>cx7ZZ)jHMc8~?JCLP+BtN+LW-F_5Alu~B zt37P3z|!_#u#HWw4^O1oRv|I6y`8gcE(S0D>6~_rx_z7iUjqPJ2A!x^b1SPC|s` zS))$dt7diigQrbfSHDIsbaE7JA6OdUKf&IzJ4ahF-v{>TTCiyakiESmxY8N>4aKUL zu8r}93WQtvu7Jc$`zNe^uu1zNv-UESDxC*EWIsnMb8`gMzJ>DR$2icgFyi$Ue|)#( ztYn#`t1bZMePmi-ufH#@Jp4g!bR>-c*exJt)LjT!r3v!D_|I;p8V!TIh)CJ=bftIr z)#e7ZgxoArcH4Oa#S2FXUR1^!hcSaXJaMlh1HA4FfYqrL1FKUrORrrKgcRafXdp}h z4ZO(d+Y;NCIpH&tl|WL@aIAsM%+M!@mc6%Ad+BcB@p<_*1+47(>F`*&S*QEcUlTc? z1 kyD5U@P``eaL-=pI2Wz(&7W_?9n!r4Rl(W)YgeiRPl z=JSuY7g(5UP7M =7cAdj7CFL%w66NPTyVNnvQ7ZCvcIKL}km?!5 z#MIJ{(c6t>dck{aD#rc0*mRCgg+rC~=jYG~fldogAn2x9CNox- Hm_+tg@u7aN$>Rk;RdiabC;n_J`u=XUU@bZeo2(+4J^0|FksGy4w zOgS21e$pyq{{fu1`xq~F!F(U$gnV8PWvwBZ)=T`op^q3CKa*#q;AhifJHS;R$d) zIHxrCSRB2#q}GRk#YC-0j||>nw0aK1 `~lbSHv {CHyj8f4iAr%Vjim!b`vi|Y^Pb@x}ECh#~yf|m{M6kGj?d% ze$*_}{%I|?uyTj6iq`Zg*e8iVy{q}<^hys=ydVi)djIcGh9sJAB>9F`HK>}0U0sH$ z;wxFE_Q}sa(|T_2lhvyGD5>l8m&8?V0I5SXm?SH0<7iCA!KCyD^2a(=F8->QyfW$@ zyh^dnSsbp-a#7vp6-ND^1C7`0lVl30_$okAsMhm>o04}hjK2HroSd}p!A6L`l*{o+ zw)XMe5ua(K?J=M=e)7)^7NIC4R0W9%9s{-6s%=6Fsm&m`Qk>K1Z1-)BC;CAbc0Os_bLzp_An&l(R^o-KhFX5f?(SwSOa^ilNkk#b|hn_Ah+ KUXCJ0F1UTQG}e zJjz6P^5f@wO~D4^>Ih&hl%N+TYX}(P4 CLBW>Wym`*KCbWI)3)fdHk4d z!d2hxfF+de_px7njmu!tcR_;06>Duiy^TxA(02dPm1q&GAlKx12=*o`CXd$C`FAgU zz4#qf 9kX-6@UABD{qV+^|g@rk8;(`L-R_VYh1?}rD#@ZIICwX!E$jAxRRn{X9 zTYUziB?)`Xdgnbq<{b5b;uB;jUt)v`PB*m@Uv^7^V2@u)_j>* ;3EK9 5N%fFs!P-JYXsdX(#2x=9%!MuJO3Jbmr!5$whW^Ourrbzx=YCuw~ zaGowDYkqV?^SoTclH1<)-ElCG8y294^UG9BvX%xO75OXQlVSEEysq8oqW@9E5Jwy4 zX-*usT~Wq4o$02|bV6IyUG`~%#7X63N>=#fB`v=?vjlTfeC1??k{U*AXV5@_>8Z5S zvBuhv9TUCk4u$=a_Lr+mrus}b+r$-Di|L=iFYuurbYjS_I5h?q?l%f!0X6?1O#xpM zHs~NyOhYBarZ7G$S2QW84~>2uIh<%?TWVr&sk@X}>%|dVS6PHbs?liL7nb`mXK&6U zh}s ES#sr*L@D^e-6Bh8?q|%AOMV-+73lH;7qqa z$G5*unyaP`sxM+J+I}LQ`Ph-VIbG^q%o2;P(oEq3wc2I=(fgw0s>h6SI=t=?ODf+K z2c(A1=byWx$%zoiIh$xM{YGQo$sU8TKn?sxtUzQeh{Y3PYP~<^ ^sV%N5DpSdGr|qmI1t*!V2*xLc4le<0(-9?V|g zb+s_4r?nm+F*L_LdOh%7k`EKiUfvt>m8>3+P?XkIdT&rOu92IM8#oH9^%Yf1j=u;o z89|s&Fkj{CW@|RsY>f2Hran65NsCb#<`j%(bKEYh%y%6z%|ZNVZYC7`6{>p9&2nE9 z${W++LH0}cV)Fq0a0rJ1qfa;N+O1PTcae$f>Ge92**FW08*HqTDobS<-43b9I z^Kn+EAP!eubs*8-lyt0-RSY(AJ|AAf%aYSceEVc6ZT;~ju)8}gz-l8dPBl+Zp>zVy z{ithizxp1$!{qSYv#g1C^->dAXOpgam^E73zbg1_b}uMSf;3N@6+@k(UuV+}0@cZQ zWn2#Pv@GNl^kBxYEFX(4H!?anF>qwW8=$fMv}Ai>G!{TwOzYp5- 4(XLX|vXSNW>P=!9WjOBdo;b zAX^@f?CZ779eOV}u>wvwdlq}4IrL&i5z|7{v8?4vZWBxMtMyXh2?)3dWk#fujs70D zHl7!-O;8H=9}A0CKcb)&OVN@p_#)Ah%s~;Vr7Gv^QVb63SfhDLt`;R-eQBMcpKgfp zy)e2}+#ZYIAg-m>C*UaFqyfIK=gxbs&@>R#UNCKshIVeaJF3GRA%0S>kQ+$NX@wln z=({%&VxM&$<5$}-$(`+3UDB3>#v1RO8(e<9W&5F9(|hi>N6}+7DN(nlw`w*U^^Cd- zMtVpigt~pTpPsAK??KsMa>%9hi%S-g1F;ODokSL>ixY&xJ}b8crCn9CcVE`j;5(Wb zj$*DPpX92(FX7Jqo~eh8_!)wcrBo7PQHZfHcHh;PQs8;}F){nfdx<`D^gWido!QI% zzMI?mXB{MGN7o5*_*0+?7e2Ic<~>wQk1cE2e34cDWcg1L2?yJkk!;`{J>qK? XOlnpfHrHTM&E_s1HSTre #?GQb$1^nHVFWSMg_1XlkGZtgX^JvzHY5gIo^ zk69upS|@zN+{we%+-qJ}dAvcX=xIJ7Yn;E|Tnq>wF_hb7G3plurajE6R~yJ?Y1sE< zTpu5VaZq#I-;4Jg_f%$c6)$ofqeUhtt5f{RaU$ZwEAYi@)7J@Un_qvNU zQi3vxpKCB((X?4UV$hSDn-U6b@Y09qPA))Djf$mM@bf~%z$PAf>=H%TeuY#(V2~y|1AEwW;F})oB!y_0q!hAm zkaQP5zaU?*CL$ YS=rDQGd@)gKhYP^a-Nr~_?zi>4b zqLI-(9TY1$j0qaIpfS413v47H9f4O3Dx;jBL*ZG^dq@eVZ=>2}sO;PB%edRpvDgSa zFJg#2fm_|qAlc77HqTd;PkJ=VE iK11Ga%&Jboi+zLu5JmI2T5Gpd#sn$+HHtPU9WX>;B}jO3%JiM N7dX-RUR%)!*OL{!6>*h&1sr^W*NKljnsCb$lC aFH3o I#qd6cD@LQ4zaCX_p`Shm4xZ2lsqGUeLY;F+X`ip`0MC&jcki z@{>=!0}yJWzar_IB5R3+h$SOF#RX|D^d5^|0wo2G!L;VX#j5Ba%{@)_>t1e265r&l zVNsgQZsPY&F3jB@+r=?keFv<``K9iVT-BrQ2Jjfod)fwOvRLWRZKVq81o7w&wU|=y zi^Miw0YpAg8_+K-5^TO7f@G}TXPeAkU?O-zq8(p?X H>aSPO*u2!PQc z2ducJccxyhs%`97WMu)+Qo~W$&{gGAXhCqAk-#uV`_2R0nZvT7Ea2787`hrrl{oe_ z+jS@3uYDX34Y?i&=63P2KUP^Clhi~e1fg)9g9DKtoZQ5ExXA;%no8GIBlAiXqBF#P zMNgK;?}v;NUOJ4h0`>0Lr(Ar$@Eflsj2xW&YxAk^vOt6ZZ|=^ reXbVZ!G~ zo~C=}->%PSZs;5fsEJPGvokla_{6U9&orTc&-LpqR3;$H`TpN;{PTzJFb+uXlN9#? zY{% y;S}lCk(L#-smy@!vlJzaS_xQ@wv(Iq>&~j8LhE|I38`Dw09EEefS?NeHfEt$&RW z4!;B0yHw9j{;TQ*j`AI(Z~vu+|2;7HBU}VZHlxQO{+DDkCzNdd|27S{QYHMKds@8d z3!rp6LAfjysa2`(-TRLzc}UvTq3AV~FH82UveLwd5||A0cmEVvcG{s-m=45-lJQHs zy4G@9jVopYI9= b^bovQzL_YNbIiTw@M4MQ{z%LEU=YEdYK7Az?7;i;0Ojc>=hM ziUwc|0F#jwI3?XnH9gg0e?2TX7S4S(gEv(RptQIeK)OBi52m#H0Z3>bhJOUx*Y0 OLK#NwxI7O?JrQWEFDHJU=+eN8g4$6-31T_=m838EU52RIRrhR;Q=8OLb^Ax z$zh=VvzgO!R5sOPPwM3IY{wYXi$~FfJslhjBjZ;m2Q_~upldrztw7ywTtBc2^uxo@ z(9oz{8v#(mUO)d%CjjhV(lmLqcQkFU04U1sGcz;W26a6TM%V!;Z}`e Nx9600h=WfUg@V9zt@m2Enl=Jl1~=!PcTI?StyRm;??} z>7(r#V}K1)-cG1}Y9s4bQAiUU%$6f{1t1Bi z#{a@pjY`t|4v%X^EtPoxPxW~XdNP2@9lzShD>uNb2Ni~!m!EH7zc*V2HTWj4NNqlS z7)uwvvHufsj!#774ywZI$FA`;+wxlQBFCr#A1vf3p>+*mz42}aE$+wWpkmcfXgUV3 zAz%NVP!W?o`TEj=+U?&LGj)#ppjTrxX9hfe7A)i1-`hc_xdzKQSx_BL1-emruNznl z8l#{nmhJo=CQLJG_MR@?PSKeTQl$W_P~%Y<1M5h6q#ji$oCjWZZ$AT^X3ush35_CS z02+3+_`wVmj_OerIi?^xFgP4`t1t&=|5)u!m}6TVDTq4)HIeSYM2`-^Iz7dhGm4Ts z_h>p^z`%HBx&VMW_J`qbdHxMTc??QHSp@bpKfToN900hCR??K~ITcP`O?)(_1GYda zW_OyeZ04cX{^~g|cZ@mETyb70e~+1k<-ozI_Y4S h7mvkC+ zPFohz#4Wfn2>C;iOW`t%I?xL VyqkAurpm-fl=+LH%=PhiPL|uF{a=^C(GJ=ZZ~yx zFds-UnHr>`ATZJ`UI$=(>lZHx4q7hXg7WbRueTiiPg=vErpn$Dzz%35JO=P6rk?qh zf`$#WkEm;Y{UbW23$MTY1`5*%-PbA-0e1q$uoGUao2F!-in!8hVo-8a%_NU~Wo2IX z_EIcO#FWN1Qt#GL&nV=(6C wn`v Ji6^ZB! zx9jOh93p2j;Jt90fV_GSnqQqXVe#+WdMUqH7<_8o{0`cDVAxN8!QW5^JWJbHS7a?@ zXj#J*Q}QD=DS8PY&LcrvsFuUdGs)4qm6q Gn5L4u|nkKFFGu6@+|nYJ%o `stkiT%kYZX88>V{h!2*nmYr%ki@`Q}my zOb+)aw=hpoXULqVsQ|`hzkt2_-0R(Ii4yd+ia=-zh8PcP%44G4y`l5{fw4NMQ7m`| ztO|<^DDVQ}Thu7yAJLovj*3KzzQ}{vt@$5H_eVZ&5+6Fyw$6Vu>GcrqY6) =NiD{PeQtD_M+=W61O#~ zdrhSIlh&oi-LItfgjQ8^_#9vnkdWy$7%GmB!Xa@T6izYzuadb%E@Xg5wlOyF^}kH3 zOg~`mIVhSY{B7!izg^{kF|FI|#SIEr{x ZWR>twq=TH?C@GYE`r$Z@mF@aN#C|Nlwi=>m%z*zdIVYCi%h<{UK9@ zqB>}z+~oHwl#C+ ySj2ITT(TJYoDAB5M3UqR`5fKoG#aKd|5bb&{^Zxe+ z{xz2lYS7NBG@^1{*9l)h1)ZV~O;cU73pL9`fL6t>y*Qi_ JmNtvDmwj%X{ z1xWAZhv57)1g&6%!FOA)GYH5in*Gb_ZrJx9!)HB@bRg)jOwcWIZ9YS21U&>1OMtUu z5%lXyQXZWukQAuwgQlKtU{BFCx*bFUZ=xF@RT;^+zUDdyKv-K&py>wd<`8rQ38UJ{ znQQ^n67eITI--Cf5E$;{Cjn6?_}9C~nL!{quNFiZSy)2q9!zEZj6scrc4+xKb|G+J zG(3E4HU5~?e8yo&ZV5EpzU_v`(RKsCSevs_2rw qKvCHA4isxsvlLC%%*#Sbj^%L4pFf<4LlS+uKVQ(YEr z^MaOV x;bszf^6WUmVVj69uoPfw4vLUHggtv gG$I^?&$su|7%|-}g&dKeZ$7<@vq~V?xw<#($Ig6iJv`)D$Q69| zkM4S#=0j3cdVvO7?wmug0owkXHi@aQ%mBJ;>VUl=Z#3Nk^&$_ULzC6cqBM!h6`jnf zql5K=k~+$rFSjUu?<>=gRNH2N0zE$pWBsuE$u@6l&gkS4UB2*9y!?zzSWG(A2=D^C z&-s3?tdx+>uoMF0FqKJ_u(&&U44|lCNDz--`7?W!hRMe9Xz}T=E`DHT#nMo7gJC&- zlaHqeJlCnC34PM?Xm0DC<@BdA2uaQSwJSN)=sT&IY9>4K 1BYQ(!^FHao05&bj1=9p3^y&cC