From 042e9e9a46231c7761369108df00955b076b2ae2 Mon Sep 17 00:00:00 2001 From: Marco Jahn Date: Fri, 23 Jan 2026 10:01:59 +0100 Subject: [PATCH] added lambda-s3-athena-cdk-ts pattern --- lambda-s3-athena-cdk-ts/.gitignore | 13 + lambda-s3-athena-cdk-ts/README.md | 267 ++++++++++++++++++ .../bin/lambda-s3-athena-cdk-ts.ts | 12 + lambda-s3-athena-cdk-ts/cdk.json | 22 ++ lambda-s3-athena-cdk-ts/example-pattern.json | 74 +++++ .../lambda-s3-athena-cdk-ts.png | Bin 0 -> 34251 bytes lambda-s3-athena-cdk-ts/lambda/processor.js | 58 ++++ lambda-s3-athena-cdk-ts/lib/pattern-stack.ts | 133 +++++++++ lambda-s3-athena-cdk-ts/package.json | 22 ++ lambda-s3-athena-cdk-ts/tsconfig.json | 23 ++ 10 files changed, 624 insertions(+) create mode 100644 lambda-s3-athena-cdk-ts/.gitignore create mode 100644 lambda-s3-athena-cdk-ts/README.md create mode 100644 lambda-s3-athena-cdk-ts/bin/lambda-s3-athena-cdk-ts.ts create mode 100644 lambda-s3-athena-cdk-ts/cdk.json create mode 100644 lambda-s3-athena-cdk-ts/example-pattern.json create mode 100644 lambda-s3-athena-cdk-ts/lambda-s3-athena-cdk-ts.png create mode 100644 lambda-s3-athena-cdk-ts/lambda/processor.js create mode 100644 lambda-s3-athena-cdk-ts/lib/pattern-stack.ts create mode 100644 lambda-s3-athena-cdk-ts/package.json create mode 100644 lambda-s3-athena-cdk-ts/tsconfig.json diff --git a/lambda-s3-athena-cdk-ts/.gitignore b/lambda-s3-athena-cdk-ts/.gitignore new file mode 100644 index 000000000..cec1a33a0 --- /dev/null +++ b/lambda-s3-athena-cdk-ts/.gitignore @@ -0,0 +1,13 @@ +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out + +# Parcel default cache directory +.parcel-cache + +# Mac files +.DS_Store diff --git a/lambda-s3-athena-cdk-ts/README.md b/lambda-s3-athena-cdk-ts/README.md new file mode 100644 index 000000000..32b0e9a04 --- /dev/null +++ b/lambda-s3-athena-cdk-ts/README.md @@ -0,0 +1,267 @@ +# AWS Lambda with Amazon S3 Failed-Event Destination and Amazon Athena Analytics + +This pattern demonstrates how to use Amazon S3 as a failed-event destination for AWS Lambda asynchronous invocations, with Amazon Athena for analytics on failed events. The pattern includes an AWS Lambda function with business logic that can succeed or fail, automatically capturing failed events to Amazon S3 for analysis. + +Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/lambda-s3-athena-cdk-ts](https://serverlessland.com/patterns/lambda-s3-athena-cdk-ts) + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [Node.js and npm](https://nodejs.org/) installed +* [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) installed + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + + ```bash + git clone https://github.com/aws-samples/serverless-patterns + ``` + +2. Change directory to the pattern directory: + + ```bash + cd lambda-s3-athena-cdk-ts + ``` + +3. Install dependencies: + + ```bash + npm install + ``` + +4. Deploy the CDK stack to your default AWS account and region: + + ```bash + cdk deploy + ``` + +5. Note the outputs from the CDK deployment process. These contain the resource names and URLs which are used for testing. + +## Deployment Outputs + +After deployment, CDK will display the following outputs. Save these values for configuration and testing: + +| Output Key | Description | Usage | +|------------|-------------|-------| +| `LambdaFunctionName` | AWS Lambda function name | Use this name to invoke the AWS Lambda function via AWS CLI | +| `FailedEventsBucketName` | Amazon S3 bucket storing failed AWS Lambda events | Check this bucket to verify failed events are being captured | +| `AthenaResultsBucketName` | Amazon S3 bucket for Amazon Athena query results | Amazon Athena stores query results here automatically | +| `GlueDatabaseName` | AWS Glue database name (typically `failed_events_db`) | Use in Amazon Athena queries: `FROM .` | +| `GlueTableName` | AWS Glue table name (`failed_events`) | Use in Amazon Athena queries: `FROM .
` | +| `AthenaWorkgroupName` | Amazon Athena workgroup name | Specify this workgroup when running Amazon Athena queries | + +**Example output:** +``` +Outputs: +LambdaS3AthenaCdkStack.LambdaFunctionName = processor-function-abc123 +LambdaS3AthenaCdkStack.FailedEventsBucketName = lambdas3athenac-failedeventsbucket12345-abcdef +LambdaS3AthenaCdkStack.AthenaResultsBucketName = lambdas3athenac-athenaresultsbucket67890-ghijkl +LambdaS3AthenaCdkStack.GlueDatabaseName = failed_events_db +LambdaS3AthenaCdkStack.GlueTableName = failed_events +LambdaS3AthenaCdkStack.AthenaWorkgroupName = failed-events-workgroup +``` + +## How it works + +![Architecture Diagram](./lambda-s3-athena-cdk-ts.png) + +This pattern creates an AWS Lambda function that implements simple business logic with three actions: `process`, `validate`, and `calculate`. When the AWS Lambda function fails (throws an error) during asynchronous invocation, the failed event is automatically sent to an Amazon S3 bucket configured as the failed-event destination. + +The pattern also sets up AWS Glue and Amazon Athena to enable SQL-based analytics on the failed events stored in Amazon S3. This allows you to query and analyze error patterns, identify common failure scenarios, and gain insights into your application's error behavior. + +Architecture flow: +1. Client invokes AWS Lambda function asynchronously via AWS CLI +2. AWS Lambda processes the request based on the action type +3. On success: AWS Lambda returns successful response +4. On failure: AWS Lambda throws error, and AWS automatically sends the failed event to Amazon S3 +5. Failed events are stored in Amazon S3 as JSON files +6. Amazon Athena queries the failed events using AWS Glue Data Catalog for analytics + +### AWS Lambda Destinations and Invocation Types + +**Important**: AWS Lambda destinations only work with **asynchronous invocations** and **stream-based invocations** (on-failure only). They do not trigger for synchronous invocations. + +| Invocation Type | Examples | Destinations Supported? | +|----------------|----------|------------------------| +| **Synchronous** | Amazon API Gateway, Application Load Balancer, SDK RequestResponse | ❌ No | +| **Asynchronous** | Amazon S3, Amazon SNS, Amazon EventBridge, SDK Event | ✅ Yes | +| **Stream/Poll-based** | Amazon Kinesis, Amazon DynamoDB Streams, Amazon SQS | ✅ On-failure only | + +This pattern uses **asynchronous invocation via AWS CLI** (`--invocation-type Event`) to demonstrate the Amazon S3 failed-event destination feature. + +The AWS Glue Data Catalog is required for Amazon Athena to query Amazon S3 data, providing the schema definition, data location, and SerDe configuration needed to parse the JSON files. This pattern uses a simplified configuration without a predefined schema, allowing Amazon Athena to perform full scans of all failed event files for learning and low-volume workloads. + +## Testing + +### Test Successful Requests + +1. Get the AWS Lambda function name from the stack outputs: + + ```bash + FUNCTION_NAME=$(aws cloudformation describe-stacks --stack-name LambdaS3AthenaCdkStack --query 'Stacks[0].Outputs[?OutputKey==`LambdaFunctionName`].OutputValue' --output text) + ``` + +2. Test the `process` action (success) with asynchronous invocation: + + ```bash + aws lambda invoke \ + --function-name $FUNCTION_NAME \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload '{"action": "process", "value": 10}' \ + response.json + ``` + + Expected response: `{"StatusCode": 202}` (request accepted) + +3. Test the `validate` action (success): + + ```bash + aws lambda invoke \ + --function-name $FUNCTION_NAME \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload '{"action": "validate", "value": "hello"}' \ + response.json + ``` + +4. Test the `calculate` action (success): + + ```bash + aws lambda invoke \ + --function-name $FUNCTION_NAME \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload '{"action": "calculate", "value": [1, 2, 3, 4, 5]}' \ + response.json + ``` + +### Test Failed Requests + +1. Test with invalid value for `process` (negative number): + + ```bash + aws lambda invoke \ + --function-name $FUNCTION_NAME \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload '{"action": "process", "value": -5}' \ + response.json + ``` + +2. Test with invalid value for `validate` (empty string): + + ```bash + aws lambda invoke \ + --function-name $FUNCTION_NAME \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload '{"action": "validate", "value": ""}' \ + response.json + ``` + +3. Test with invalid value for `calculate` (empty array): + + ```bash + aws lambda invoke \ + --function-name $FUNCTION_NAME \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload '{"action": "calculate", "value": []}' \ + response.json + ``` + +4. Test with unknown action: + + ```bash + aws lambda invoke \ + --function-name $FUNCTION_NAME \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload '{"action": "unknown", "value": "test"}' \ + response.json + ``` + +5. Wait a few minutes for the failed events to be written to Amazon S3, then verify they exist: + + ```bash + BUCKET_NAME=$(aws cloudformation describe-stacks --stack-name LambdaS3AthenaCdkStack --query 'Stacks[0].Outputs[?OutputKey==`FailedEventsBucketName`].OutputValue' --output text) + aws s3 ls s3://$BUCKET_NAME/ --recursive + ``` + + Note: Failed events typically appear in Amazon S3 within 1-2 minutes after the AWS Lambda invocation fails. + +### Query Failed Events with Amazon Athena + +1. Open the Amazon Athena console to run queries. + +2. Query to count failed events by error type: + + ```sql + SELECT + responsepayload.errortype as error_type, + COUNT(*) as error_count + FROM failed_events + GROUP BY responsepayload.errortype + ORDER BY error_count DESC; + ``` + +3. Query to see detailed error messages: + + ```sql + SELECT + timestamp, + responsepayload.errortype as error_type, + responsepayload.errormessage as error_message, + requestpayload.body as request_body + FROM failed_events + ORDER BY timestamp DESC + LIMIT 10; + ``` + +4. Query to analyze errors by action type: + + ```sql + SELECT + json_extract_scalar(requestpayload.body, '$.action') as action, + responsepayload.errortype as error_type, + COUNT(*) as count + FROM failed_events + GROUP BY + json_extract_scalar(requestpayload.body, '$.action'), + responsepayload.errortype + ORDER BY count DESC; + ``` + +5. Query to find errors within a specific time range: + + ```sql + SELECT + timestamp, + responsepayload.errormessage as error_message, + requestpayload.body as request_body + FROM failed_events + WHERE timestamp >= '2025-01-01T00:00:00Z' + ORDER BY timestamp DESC; + ``` + +## Cleanup + +1. Delete the stack: + + ```bash + cdk destroy + ``` + +2. Confirm the deletion when prompted. + +--- + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/lambda-s3-athena-cdk-ts/bin/lambda-s3-athena-cdk-ts.ts b/lambda-s3-athena-cdk-ts/bin/lambda-s3-athena-cdk-ts.ts new file mode 100644 index 000000000..2f59a8491 --- /dev/null +++ b/lambda-s3-athena-cdk-ts/bin/lambda-s3-athena-cdk-ts.ts @@ -0,0 +1,12 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { PatternStack } from '../lib/pattern-stack'; + +const app = new cdk.App(); +new PatternStack(app, 'LambdaS3AthenaCdkStack', { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + } +}); diff --git a/lambda-s3-athena-cdk-ts/cdk.json b/lambda-s3-athena-cdk-ts/cdk.json new file mode 100644 index 000000000..8eb3ac511 --- /dev/null +++ b/lambda-s3-athena-cdk-ts/cdk.json @@ -0,0 +1,22 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/lambda-s3-athena-cdk-ts.ts", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "cdk.out" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": ["aws", "aws-cn"] + } +} diff --git a/lambda-s3-athena-cdk-ts/example-pattern.json b/lambda-s3-athena-cdk-ts/example-pattern.json new file mode 100644 index 000000000..e2cd539a9 --- /dev/null +++ b/lambda-s3-athena-cdk-ts/example-pattern.json @@ -0,0 +1,74 @@ +{ + "title": "Lambda with S3 Failed-Event Destination and Athena Analytics", + "description": "Capture Lambda failed events to S3 and analyze them with Athena for error insights and patterns using asynchronous invocations.", + "language": "TypeScript", + "level": "200", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates how to use Amazon S3 as a failed-event destination for AWS Lambda asynchronous invocations.", + "A Lambda function implements business logic with success and failure scenarios.", + "When Lambda fails during asynchronous invocation, AWS automatically captures the failed event to S3 for analysis.", + "Amazon Athena with AWS Glue enables SQL-based analytics on failed events to identify error patterns and gain insights.", + "The pattern uses AWS CLI with --invocation-type Event to demonstrate asynchronous invocation.", + "Important: Lambda destinations only work with asynchronous invocations (S3, SNS, EventBridge) and stream-based sources (Kinesis, DynamoDB Streams, SQS). They do not trigger for synchronous invocations." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-s3-athena-cdk-ts", + "templateURL": "serverless-patterns/lambda-s3-athena-cdk-ts", + "projectFolder": "lambda-s3-athena-cdk-ts", + "templateFile": "lib/pattern-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "AWS Lambda S3 Failed-Event Destination Announcement", + "link": "https://aws.amazon.com/about-aws/whats-new/2024/11/aws-lambda-s3-failed-event-destination-stream-event-sources/" + }, + { + "text": "AWS Lambda Destinations Documentation", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/invocation-async.html#invocation-async-destinations" + }, + { + "text": "Amazon Athena Documentation", + "link": "https://docs.aws.amazon.com/athena/latest/ug/what-is.html" + }, + { + "text": "AWS Glue Data Catalog", + "link": "https://docs.aws.amazon.com/glue/latest/dg/catalog-and-crawler.html" + } + ] + }, + "deploy": { + "text": [ + "Clone the repository: git clone https://github.com/aws-samples/serverless-patterns", + "Change directory: cd lambda-s3-athena-cdk-ts", + "Install dependencies: npm install", + "Deploy the CDK stack: cdk deploy" + ] + }, + "testing": { + "text": [ + "Get Lambda function name: FUNCTION_NAME=$(aws cloudformation describe-stacks --stack-name LambdaS3AthenaCdkStack --query 'Stacks[0].Outputs[?OutputKey==`LambdaFunctionName`].OutputValue' --output text)", + "Test successful request: aws lambda invoke --function-name $FUNCTION_NAME --invocation-type Event --payload '{\"action\": \"process\", \"value\": 10}' response.json", + "Test failed request: aws lambda invoke --function-name $FUNCTION_NAME --invocation-type Event --payload '{\"action\": \"process\", \"value\": -5}' response.json", + "Verify failed events in S3: aws s3 ls s3://$(aws cloudformation describe-stacks --stack-name LambdaS3AthenaCdkStack --query 'Stacks[0].Outputs[?OutputKey==`FailedEventsBucketName`].OutputValue' --output text)/ --recursive", + "Query errors with Athena: SELECT responsepayload.errortype, COUNT(*) FROM failed_events GROUP BY responsepayload.errortype;" + ] + }, + "cleanup": { + "text": ["Delete the stack: cdk destroy"] + }, + "authors": [ + { + "name": "Marco Jahn", + "image": "https://sessionize.com/image/e99b-400o400o2-pqR4BacUSzHrq4fgZ4wwEQ.png", + "bio": "Senior Solutions Architect, Amazon Web Services", + "linkedin": "marcojahn" + } + ] +} diff --git a/lambda-s3-athena-cdk-ts/lambda-s3-athena-cdk-ts.png b/lambda-s3-athena-cdk-ts/lambda-s3-athena-cdk-ts.png new file mode 100644 index 0000000000000000000000000000000000000000..77e6d0dc895196237ab244f8832e574be8bbb070 GIT binary patch literal 34251 zcma&Oby$?&);A7_Frc)E^w2GcgmevE4k<0&pmZZ8-Q7wz(hZ8#fPi#^;0)c}yf=K$ zdCvJ=zvp_d_fLj>?|ZMcSFg{yL*FV%V_}kDA|WAR$;v=fkdRR1kdTnoKxn`(b3_Dt zNJzLyvJf#fH{|V1j8BRal)b*u+w$Td*z;<8Vr?#bmEw+su;+<#>2jXTwM4eF`RZya z;-uV@qh@T+FibJ1uHJRt`3(I^typVIy)s+$JW4XSSg5%f+H)N`TXbDIyBTV7#U%TE z_Y7H56fcAdRik3g<^9T;FPYnNZZ`J(*=7T}orlY;A8A&akxKvgSd{)#ES=yS5F-jk zm?-kdzn()zHDlLjmsrfC`AN^3pib&4KWIgW*mjA`8+NzNvyYBfluPxP9g)cPdBx#H zF8&Mr7_*PujJa9(tN|r5(k(T8v?${ov$(B;NS+! zQTg`1$2Kg1^Q1O6Somf=Iulo~w3+$~a1cJ~n2+fFP({KA8bBf_OKiF<$`NU7>dZ-r z6+5J)4$s1fWC(y~$hU(=4V($@N$tZxrjz5N%G(M1M2(AwTJTGYF5CAk*lBMXLNZmH z!9aQ3i+%bnoxylk3-2xJGh`wZxdh$!7L!jBCBKNzqW1qLAWt zfOa?uG6iC(&fGWaj26L#bjrGn?#s7fW1=lS<9AV*uKNh_ z;cbYf>DjJc>ZF5`n={hwetJ#472`dfxGw-U`h6VH2KEEJMhVxjiQ7yZ@ThJ+tyFi| za4&>aKJkrvx!}<^lqmpjpQ}vtb6xyX+@an{>0_&<`ric6wYLww#R~zU$ZOii_7h^C znRrgR8Vu~}wd74rcq4ZG0uZr9||^KvsP5OjzPWs9CH9=>zuV_ zZZG9%cO}z0l^yPHKW3DwK;Vwv@ssjDmjwIafr4sn=D+FRzH3)=>)+WGlYj^D3MYsM zVGd{-L?pB6+#W>W(_(aF2^8f#NWKq<5qkIt5`KRX%qWY;@kP0*v@2J#_8w@p?J`izQ=%EJ0PdIKmkI2;}xk@1lmlu z{#5PNV8|nBlSZGXj~4k*IPWwORBgNUyx37!qFB59v*nS)cS@hO^^&NEWBB2|&3Oh& zM6Dd#U_!a@6Ww7B9!NXU5p1&a@gi+Hy6rW+yS+J_4Of<%?ZPn29vx$k#?KyJgQ3Zt z|73S3e2@Z%08zhrr!Wo5>|t}eFyZV1;cTNnM@(qK2HQq4W8PnVPO7Vo1`9IAt7j`) zcQsNQY-%Eu(`n*;&lTwvQqB8RlKg|8-tKRSsjJ_c6Cb>g^*-Y?##cv=(6K|pjJ`~X z+88|Y(r}C`A(WHbSP6oksgpXY3|g5#a&1G49CXm}L;1Q;u^%9EZALsE@jw<8HSTOC z1LSW>#e-as`ne32_A7^Lwv!&T# zz0u`kY7bI1PyGeL)X--s0zBA9h_X$*Ir6o_>te@frrKMx!UlCKP-xqTQsl>Rt^WOb z*my_i_d@!MbAVjjY+0B>N;B9dmX2C>`sd42V=@%-EP%EyjQaz_AsZZo zeh>-3G-T>f(&RD>4j*OFozfbjdrm8Il{pEAiB{9ZWFKkfBxJ9b+Dn3Ac;G`v>FDRx z9t-ru>8GkPmDou9`pBXpm&WLx zMA-}$!8h>K(r6gZcl!CyyaX!gf7x^LV>KgWWby#&m^T0=av6@ruc4?b_Xe(GCuh~c zIT=l*P8Uz>bd8@_1Yz#({;Pr0Y5ZxtN_u&G8_nwg0Y7Z0e@ z7f>KFU{wr}UR~0U=l(16Ef9ZQ@6+x+jPfac02h)XJ*0Kx+i?Jau{HtHCbdk2Eavy=DF z-}kSAeFA8k798Mbeo&bQ1r9+5)^&fE{Y~{hCkl-ey&h5uRk_X*AaDBjTfTrG_of;T z?cE^YVgP#)4O&kDJR$g=AmTUc&<7#;eEKEE0DO3>JS?`7=IHiPr7Xy9Ralmq9 zb5soeOQMxBQ> zIn6rVFy>!^N_kTipbv(fC!ku5@c)akjgo&_f~*mt(3*U??&{GxZ)1rl@zkdVjK{em zF5igcd?(aVsUwl5Xs76>7^j%0Y#4<{3hy^+6TL}fd@N2!=4=HxAwZD5#NYR4s2^Uq z`pTR|Ddv65>~|-53@jAdUWKK&XrOimB27_0>|>$-Ym`!8`e#K4+y6MvXMS&_J{ffX zS72k9isAAtSZ_PHyND!I0M*F!yKaDPxNf{|`tl2+hyS$LGD975zq&t=vho7?R4d}r zp~}fZb!uA-b4_h}$xmrt5q?7ydB?r`njIu$1 zE?d^W=33aZJ^h^(Tgg7_bDvLGxxm@yYs^pCtGA5g%4PQOhVF2|XusZ)gJ6Td?oJU$+sSpS3Wd#;{i0)`EWT`{7JNQ1ps zM1y0xvTA3!x#@c!>yL=YkDN7Hbki5>H`&NUo$bsqoGmO$-|DujXf#;25 zACAlKnZPp7M0V?1Mt)NIJV}ZBC!%|}npJVfNWd5fzO1i?hUKO*~S0}vgh-_0-zxbEU6&0XqVin#CX7*_k<`LHk4IPC`; z(8+1VSV%occ%wMj`kRjHZfsmPrg@@czu@$pS+S?C1mEB7BS#gi*(i}~H1*$TlHdr? zhw_hu?yqFIFxo1GDH&ATuhr<4FCn`ffBD^=zT2vKJ_lH47BV_stZMrEi`a}3xbW0Q z!1^C!j}i90-TSKM_Pec;DqKE*J;sy5jhSJ>X6}L}_k2)8mXy$sgX()-5BFNP?lN@{ zVuht5d2ZJmvikByTHouh#F%ZD_3%K{&YZ&H0J86SBymncS{ic`kKG*F~W#uKNJDd6Uhte8lCMxVeWX)R; zqGDxLlE;s4ff7?FdV*J`Iu<6{N&U9Sr6i%?2Lo7$16iXl$KJcaWd8C-|KoJ$Ao@QPCi! zim@KJpN~5GAMdWmP)RGe@PGa)m9Ntf?*AbxmN5~<$;m&Fk||7oA~)Y7~r5bYJ`3M3TFj7#(+ zl8z7e%W`}(=IGr{nE%u4^@JN*w(r?$2UEH1{^hWWhA^G%yref`X)j2-LNBlJ?=QHb z?QPEon}ti2Cb_z|%`fx-+uZR=HX($ODncVy_Q6K;pm-)L5k@j2_V}!(T1&2L-JI3X z{3x#_yS4oJt72*7BUt&WyFmEHeG2q=a*m+ihjmJ*u-Fe`{zBW@KPWl*;sv!tBCu9a zE&G)_ySRJn%zFu8&j1Rf74YfM%yMOT@|7gw+P#h6L(o0*Sr2E&!K zzq5ZyYX62p!fvR8y!=JBJ?Z715Z%3W72Hp#6{&3C64|*9k?#oBk1^XB4sWDf9nFz& z<=a}>LL98=$JcGWrl3M4yU(BYqkyxt8WS=NI?heTS=J=Onj;qtN{IU30X2#1z)@xA z*SO#?h}vfm7Pl5Ej7l1pCh+ndeow+GE`!XNiAX2vva!+#8T%B)6nM&r_S%T(YC)+u z(^ObGr`EeyscG+E4N>4p8BfQeA)i(Fw|0=-Fk7` ztA%8FZ>d{p{zNgm@8zd0i+{R#Cd5CU$ljsHs#Cv*nXCn53->NuA)i8!=6f6=l$QUT zfOd05Z1fsL5E#uT8P9s}vmMwR6h9O2ZYw$n3jl#aT_ziJw$(XZ@Z@lg)BFd1F|;O$ z%pD%9k|%>V$7`sPJja7j?)rh zk2@2-6^tz;ak67?-S^jZTMiPNldzqxHVq#yQD+&qE1@g|O&7(rGlTDx!0B%V8ib{! zl+&`;gygrU?rTZuzBOS6!*{`mRi0%M3m$qqU~S%(0}E<6OHIC&92iDKa-4T!G&w((@L-*Cu_<;rQjMIC~C zOETzoJ+`1KowALqvJ?rr-bA8^E_etjLD;-Z3UeTZ9EGGC`3pXx3fT(93TTCT#mw8D zfcwH5`S#;Y$^^!fmOBh4trDj%!m?1!#t(+YZ@>DtitL8xrPzOy#n&R?cPiqt9EC&) zoxZ!+t{FDzi69;=*J&twX=!Qca5ABpen_A!NHtNanH;agFZ&sb)NFG;IFUtD?&@^M zbh$IA+I~s6X{(|o&t+%UYhE%y{->>d&s;iTXy*^ zXKRa(w`WvW2h$lGSAWI6)N4+0!6sl(O0ceI>WMh}Olrpgt+tvdfjuVD1-H2FNl8-p zoEyH@-;Q|ks&jmUY7dmB{L8H}PAc&*%2c2A+i=dFyJDVqICJ%T7&Gw!j5YO@c`B9S zcJH5PAEWF9P6M+4B(4s@->aRMQlcRG^CNVs#J$0Q*)@w4?vNq)SH#Xb^>T`1igSu< ziYI_J|2~!Zs(ha{w~68>Fb>{Y#wIYS(!b5lD`wHp2~@aqaT)!^`x^pOeCTV;hm*8w z%wRhWOE05^FN^Z>^7?>?C7cTjb_= z3^63I$JK>CTKE13%Edu1C8DaABA%t9QGBvoC)g@UA&m>R9iG01`!$wEx_^*&$!3;b zAqC930bOdaH}Lx+w)$dgG+#av`;twkKEZab9+qz3Zh1Z0?si~wak*7F=q7Uh=aIy$ z@XfKR`~DAfIw7HjV=RZ?R?z?_uh~oT*uNZuv{5JAeqGdurwvT#k;qIqNSe%KZ<$;t zWFru-mim7~Un~5(FpNj0p^|1+8JGUPiw4BaocaDXi>!-Xqi49TpL?Kgfcsb7FK+TE zYe6tnyTdR(ffcHY`3y8o#?lD!B9jb%p2)p*8`YTo&h^91$&~z8@gO)LbV-PTHb zy9+G?(v;q`KwJ~c-f^umBy#6PutTLS5eOpRT=d)%dMzZQ2m~w{9-GCZRmRe)=(5Go z+|LoDyh<9C`fSqV&Iwc;1+R{N7ppQfolNRrjkq7e7;5^oZZ1!PLh$momVECBWuQO& zA1l^|eB4=RnX1hOuB1n0uHH7NkklQFtkNsk6HRH~qt{8*rCy>IyD?VCu)sZ>&FR+% zr$y_+#koZ3$3+hJ=kG`ps4ts&V6r}_k< zP8gv?U{zY5{v2Jv zzu(@t*W%wTxKqbyi9+6C()hyYlKX-weJuK9T3mMW)QVLO*Rp-n5OTA4pkKEN*Dl-B zQ~_vMac4`uOArSOhE7!E02?ar%i~z7NRp8Ry2_5*W>FW5j`NjpEW_L?rA&Ua;z$Iv zZ}jf=Or&Ixlk6{bNw>-Zo-~=^<_;O-v^y@FqqL%Y@=G?GRcQvTz+{P}X1g#rZPSc( zLRQoDXPl6C)$dD=)KF2V-ov`!7ZRvq!Rz=&vILL^Zv{b(c-4%7eYWh^Zi zDwl_`sY@MzL~F7$TX#6`I1DpQ)-%iaCK5u&8gg2p*YeA!;Pud(pS1^@k3)tQT$k9k z&HX=QdhRxu3Z$SKUMgDQzmaeAG&?>SHrm1rD3*tr_cQ%rndC$Rz5A(kcUCpJZmQ ze|%;B%PiFuG5wm?vOM9xUJoZE>wCzi@z^rQfRpyna41-}-No&i zHwwxu2wY~Sy(hKIWnJ#7iZc`!SpU{pj!9jfY%$QuCncDi-1SFBsBdEapx#gfS}lyviDEEDl7SC>6BrC@Ql3P zEEV2V}a(KbX)D0ZoZs!(T52o8m=M=t0b|HKiI`S}?uQu6fdTXNsC zjGy2A9t~zuwIoNH)-tIUq#)i<3sU-CSAF~Ce7+n!u-9?7dyHFC%k6vX)pXd;lJ9kK z$mz2E&ae`{Yt;{(f`fne(N)Iur<+L~dyK=Iv!(QQz{T*&+gG+8h8;+c!%j=zRpo>_ z%p~XHPmsJei?DsZNZ>r7u1RdvZFXQeRsfrCatvaCS0`}A2Z`$UVF$9ZLs+bFKcaa5DpCFnwk5ViK=HtWgu&Z+U7T)N+oc;=xTG~!1 z-5}{gEt`!&TN|9k06z*%5Lo#@6}je4J3Bq|Ww6Jl^2kUF8~?dNHX}}6m`mRa&(J=a zUNXlBcjhR;&N2I8!J`OV!<=nNX7v&iP$toW$vBK<2KHn`PO7!-S1Kjz_Q6pOsZ6lx zriqprHrL&CQH@DwcEWiy9oy?slTZRUMknWtvi!y_&pTx%lAkOSVTQZ0&KQS*42 z)yBh0!<$`>B+fZbx;vYPn}1zlOy%jO%P!8*j_?&nE^aq(|)8i~HO53#`q7cvDp*7XcZK$k&`&oU=9iIW(5Brq*+8lFF zz8?!^mVpDsuA1N-jhn50sK$o@*i>iTAP!y@1)WMi4!&#c_!DXfbc;{^Z)+z_ichIV zxk5ssYhG0{drH=%G&*vKOj@{TlkBrDG(Wfr!+PUm(vZ4v99sU@VeITbLgNU^L%Y

ztItp4dkws#OEgNrnM$%%n`!h+D6L9gY-Xxu`LFsEuijU`<}i`A%OTt>*pJDivp}cF zTAy#KX$34^XUlh=+Y9p}xu^m54&7COff@b6!JjySZ-2CRy!Uov{Q0s8eQf0NH|~e{ z7?RYh1o4`fHlEX(_-)VNMeUlE&;kM|Q@rdS2|tUW^0!}q=2-?9BTmjlV$-l+@vrQw z^xUj0-6ixBMstl&GlOLe6mAyRafNcQy+MQ*ho%D{UnA4KMPH`=rF5PF0ZQe#rMq{w z{T}y4zNLsDPV9oAW=lCMuq?ojzWy`Q&XS(r%= z*!B+hIU!pXjk0eg^oUIJR>z1o?`fSnt3>y={^%sm&FQ)f;ACW-Xr^EXQ@{ggi3aDW7P`;WwGeY&8@!H#}>dsKi z@*B@w(I{;m;h~7Y!+LUoe2+TwmJ7`%RC>)W zJIu8QL!3$dB^u>cS@ejFu_=Ae9qQg%IuKtySuZnyORrf)b7XWQzX3Lc>l?L+Fvthf7~i5mUei0 zJv_1pHIPP$nFfzAe4mD1Qw5Y{x%?j|6L0R{5&}Gw5L5eg7@#y>{FSiCqLj{gDE(Df5x7D z6>|#Vcsw>(ktJf^bt{_lc5A9KncI3Yg?IO;IePn4+|>Ma>>RZ$=eLCx)t*vR>QF0r{?Hgs3zRcTtE2+9b0C&BGW?UzSAN|jcY>*ZI(*B zj*Zk#1>wot++u4fSzaI@TsP^EKXk z4vTjl+*%mHuvE4hEplsdJ4wwhU+1i^nzZQ_O+4V(s+Z09*2Q}uk%fUry!-RMXfs2m zQ`~-GI9Si-g~(wa&ES}$pHqC*%h|jfscwQmkZNO<%UlH4#gnsXM`Ly?NNC}L+0d<4 z62A$o|ICrdjIE*A~Q zB2pmFYR%8Wn!HZU_Bh!L613KTHsC{@v_6g6Zuk>TtJ$fT%GfVL&*uU$+UDuXcL9%& z)CZI6a|l@c2ImGWdY>&Yw0WLk=cSE1ek|ePxQP<7^)T~S3#@bj{<1EXoyFt zz*>f5_37PxT<>w^*z7#_x5hx<6Navn(bS@AMCt}m4 zy(X7l>>FMZh7VsWq;MdsAE6gk-OZS1!$|F0V^8G#iC>$^0QHavZ1gPuzlvAaLOmBp zYw`=t&c#vk7-wq^7qh8aZ&0bZwIZ8-?JMPp>S=xcJGg*LPaC7e=1@Aa<+>H(v(1(S3mD z!uUknT+z!QPq#=<5^M$9m*E~l!zQyxir4D=LdNz{I=;UBw-T#XH5?`s1srPKey8bX zmS2(3@0}QUK6?PMbsNqsX%%I8gl=w2#$b;Dl+8q!hO6ZD>*S;2cjwWyb76v)|!HYI>*TPZ3L66QCsF z&TlWA?Q`ng4(|cC4Cmj?4Lde}-f>X(ku=`VUX7tAHLd;|#6i`hm6V&U8E1Oaty%(M z3a(Mlar?OZIE5MgS@5ISUhr?kn-70P18!iErRY}J)_Rv+VkpSfQcUp-C6G<_-BVHJ zzKgA^WZOGY$S(ul2861TEcf|_`T3`nn1SIBU#y3HUY2TBQWWu7EL6Yn*o#T$P#lWj z#G~^+pO%RgL3SVzvik0EUew*%*{RZ3A@pZPK!2a{5Zv&Pp zUp@`L7c4Tcw;U?4w#7}64UsbAK! zEfh0QMkvg<!3Aj^fN5})kT3Qq@iYnza%4~TrY5D79#5^jfvr8Zqx2I|It?Y!fqF(>1KJ&^t~ zcnwOR<#|^HiX|qOiwr?0hox)0*yf7fVI-q^v6_C;P17pwbyDTE{P`GtFqcIT&jmBSgwgm9U;of-iF{hF-=w8 z!xNHA7kP(!$afOnd+J3a=W9R9S{GI905UTjNJvVGd}RMe7^1~;-Lz3Vi_P*VCl z%3YE`0GX`-xru}IF@n;kySK;$Gr=PH!1LCVb{vr6--f~DTzRJq^$DBOl+JH6pBs_- zsc$t5ZlHp zpCWsvP0MsiN28!(VBm_*j-ICt&aY)Vmyl_E`;uytnKl83Ybcsrx*m1iYSzjaE(^(P zggtv{lg>40{{IYtybwIjbEUN6f}teshA}^$t<9W@oY_c)u<{PLx)aQ*K5m{|B(bBP=5`z}s`$z~AuId#Jnb`uT9e7fIot2by6mf7=sINbm5j z3@VR?W6aU!Gw9mbH;sUEF`$M##rnU37#{!XlX+mu`dIYs^s6wB5N4F7NctJE)#F7P z-kB=z)&h4rZT*{~Eb5qSP-AlW{D9U4AN>hAnO}r`?VlYyTdj*J58~rrTcf`FES&H} zsKe10bNC$JlWMO+yZq8!FNOKTP94g$Q$OVn`lVqVlS7XXivJCz>H1$N&7&w$yIk4H zqs9C&g~@_~Km0($iMtd|Z>p8ZXEG0Ac{|P=ygOS4f{^){IKP+8Okbya^!!3Df%SVj zk9}gDuYKIj6%AQWH8%GkA&Tm1S=?~Pl|-|v4Rz#}+E60aTCxhUg11+XOj9e-`ox3`(-Il!eicW|L#E}qHV3)^6rWxV zI#Wj0^v}S5At@$*fEqF6J$Pu>{^;K&uG@!B`quq$+(FdY)^I_5)|=0oUH zg#H6VMSbF#v>(us;Z!H&gc^c2qrX8R6E)#nm}Jqk<ky<({pT=-ZjjU%0WTavBZp0U_cf0;Z-wVN>G;ifr%D&jZKsh!Bw64mI=+omXj%Y?&@%`5+AF`BC*{a6Cs-a%CK!cKl2#nXr@u>m}!#AZ?d40Vi7 z++UtFog73IrWUDPi~iyC8#PXS=%4(KO$ZTkQt^sD8W!h(?JwimTFYRh7fPK@AqOwY zznKgv?i)E38B%J9<^(1`kK7snIvFtf^6mL5iP?I~*xDn5TBCTiO5Mq^YpMyFB`;aD zMdyI0KKphvjfQR8Gc|Ln1`YT?ue@T+YZ3PwYPXcDcwLfaj_J)RPsy`QH+2zTyiXAL z9;kCK6Vb4*Ls&KN&P9s_UwGVX5f3DnP_g~7!e*(SdyiseyfaJ4(*&q(JN)1)?A>^J zD7>9t+8~3#4EE%#wO{HuJgkY91(2MbaruQx$O%AG9Z69;@d@gooWqAy%^XRtzL$ww zI@4IWMa3^9J|RjV_;-a%;DGtH6ypdn%Btn*HnoAc?Fl3#k_5&&t3?@-DG`myGOYmp zQP`?4kIY_pi!2PkJKk>BrPG>sH|Ee(ZrZ5c5-91L_RNb!WVrQP&TN`Z0Voo)!d%-$ zJVv#vXSp`H`~|q3RAi7Ru9F==TULTD3-<+__2PGDl+t^c0Cub^`9G9Hc zsGP=IMl+Cn^AdPP!}cQiWqZ8LQ^5?L3#9G&rZL>kr6ktaQ-->@EBhS+C8Cd(;Uc=a z97j0wQr0Ly+r0LgE+Bh8jw~dpn?WcUGH}YEI83V((=acH9E=5km}WuyH?{G`rvI)5 z;BL_Iw3HF4C65>DOKMbVGXaeyCB{?% z*9w~p^W0k72}n=tX~OZ&LVPhp8ywxzk9L`N1BvIT^>WyT@Q@{COawQdovD^#);a z0na3Y50O?T$PxC>F)?^+H@g|mWVJJtyaC$yQ0KJoLts7tVD(v0A|z=iA^o<#(l@OF zO6Mz6o52q1iW-9*1*iNFl>*C$6o>ddGTvDDXfr7@x{5Q@&!F1#8Y)Ns)~SLRq{gf% z&SSF}3k?~?15h_{ErCvtU`&%69m#NnSzLVw9ZypgUeWMn`_|@Rky?u)jB6_oFYTBKqh&)$a3ZQ+XO<}XX z`Fg>xYgC_1e+IBkHsf;2plJ(Sz*&U6#7g)*8ex$@$CZ`w`3sy<9n;o^*wN6As!>f{ z5_p}ABOZOu8<*d>L?Hwyv3`P7pXc9%5088`b7`Uo zc%H4)GmgFgf=CNlw5U5I18C>{bb%+cPeu))e>ON}REvzR3fHJfOs`fG3Y5uA)Eoq) zE{Puj)kl0>Qx!@mCT;yfX7i3{816N=XRQk!1#UW^s)S%X zQ}d_(UE!{NVR^9mWoOw2!0mNfzMiqk7l6}yuYhZCLfS-|2ybz1X{%^To<)7+?2w64 zwPiuMh(watw^Yi*@aqEOl9)Kq`&BaqGQ>=~s9b8js;74$lp*$sxz4g@z2t>3Jx4Dv z4#Ps#9xn$=J|T+9PJ*0b?MwqC85yr02!uHWv)X!p&4f#WIb)^3`aQ&!WaI7kaC%%4 zTDEx6nP;5@GByHCH)VmFyjmv@EW$BdT&pjq8#7~%cP4EJ`aRIX>{1m?x!0zq`hZdU z04L$MDCH2zvC+%JEaMxnV!!h+sc zJJ~OMiOb_Dr6wqN3DPkMmB9z5N^VSksH_nqA(yK>pj-iXMb5Ka2Kwj0-BMda<Dti5#Xt28lM8A0Z91o$*vu+Zb?|@EMEV_ldyfE^ zeqxDK@-NIWfDy(6(7;es0jO?Oa?$_Z|K?l4nd8({>5rj#nCAh;WIUMv{W{TGYTWr2 z{QN)D7V_Ajea1UmBJSkU7Ot2QL|9+7$Q10HNn~KaxIOV}u35^zM|m7L$_K@8{{+P` z6WgzyhDqQu)@kE`NKyRskBgieWL;haVqG)7P|11rIEG;1aqQQq@)DS{9x3cmo6f*% z@40@*hPV~sS=bw5F?a=>;Z3C7RQ zz#aW(?B;>_GTOjR(d`sZg|eWGKt#PC26%9@?@`RdM+LbW|6I|1Wu5UJ0gVan&3!^% z^ACXT9>pEP2puiGhdDp&2Iv5}pYE`7s7l4xv8aD@2ABw1zlXISzb-pcMHQs}@aH?S zR-IXSjK?;J22!cI4xu1f;W2{<>#(nLks(L?}?rs;i{mj&P^^Uyl|W_a5!Ji&$o!B zmbzOnNksU0+QqIalB88nJ)*ksq$H!ZRo;8Kz}}+Yqm+^|Q8!07)qg(R!Pz0l-g=Fe zdNxMgRCo8&!JnY5d1o=W21G`_=w@D~3*J@+4k2V;b zCbt%oX;7#I00v+AeXm$(L9nd7zU&G(OWDdcWfk~_94*MU7e&mTtT#5?w9B5TOh$cj zXua3Rl-cqmJK1@Q_r=8fsmp$KYj{R@1-}dh#i9R(#9X)L0;{6ZHtWL6T^bQ~ZJW*B zh^zVQ7u1+tYx^andvUgNt|>fchKS9MymwX9Lqa(T`v$~o`X9&YH2a$LlnNQ2_}(a1 zYGo{tu&J6^1|%oNz|UI&fNHWfqH-8NyI&X*0NSDyt;;p-pi zNhba@@;msC1-n#=_hr&Goh^DP?QXi33kt&BXR4jkH`@D}QpGbUn)i6trYzi>zD%8{ z{7oqtsvPfy&rz=S)!Mi7dbDIBgnRlbr(O%xC6n4yzRxj)^xCy=A@h}v6+4`jPr;=B zc5qyhRUgT>gi+%@0eNvFtS#Z!Z;BDuF@A49EX;nqsdA`$>boV>w0THD&+ND*gaboi z=)XFA9i2k?x>7)d;&;CCm*MGHr9kuVdb}7811Fkk!yitp)bEoFhr}9qmWPUU>OnWU zgJHu2J_1_XB6kg_6|P<6{XFDw3NKhDJV_DEr0fDo4?wj$?jz%X0zo z+9KT<&e1w~hFsqL-Sm9|9sY}zf#Nux9a6r!9gG4tD7IVzK0;wrFb>=AAlsX6 zM^!n28ffBfkgJ&X{GzN0!QfqK8d4$sf(!LO&WIVEr;@8FisQo$mHU{q&RF{qG~Rgj zS>8Fgu~m4*d*j1UH+F23SW@|UyxFkI*|aw2{w@F?&ks|n>Woz>`fXV4)!koq+pyr4 zsO9ozj7lJ1E-}G6)8OmL7Wo!#y;=22w7QlDubyiVnPx-%@y?ih6sv*8^wdG7L;(qJ z5knh$%kPWsS+$DBM$aT4C7+sS+`z~s4GL>Z=EMcpYyY94HD`YUL++qo62rT9uVa@0 zg--Cs{y`=8bM4Dmv29Sk)40FhmSm>I2S)~!h}OGhhR*21tc77b>R#NG0rriT`I*D> z7g0MZ`Eq@(B17F78S@2+p--#G+=htBQG8Yh5{1O+BYYIk=kZ~5+Vi*DS#dlkq{crf z7RmUe?3{Ccsz;U3=F260n}90PAQVDX@?z9m!bAh{{`3rBMaKxFH_iBR9h6%3ScggY zudKEJD12z$=P3OvU+NW#lu>eE(nVV(I+1jZ;3?BQ$$Yin3y+!py$@b1fdBjG4oq+ zW&1^i7R6t54Egt7&v(9WujR@tcNT0;YfmU@?|<_@v|nALDYU*h!OJz>s3tt@yJ<*o zhlG+)X!35=NSW4^s4FBSd39qVWFwxj>jRlPo}TKyfEuHL(xvW*YEKji{cHNjkD$o| zhZ=%qIuslAy`~e7RDr**4d#)mh)H5dlFUZB8UXDBl$@rWo;5PxX7C==%VgI$x| zORmlwbMcc(K&`rEQp<|+@-Y2y{*Rbh6Sjb})kI9)*4@?o+lF7E&v>mzKb5&1z;2m% z5B#_m+#M-r+RUSkuk1Hm&jMD_U!Xk^5b+;cpB`fU3srgs%}HQ1S6Pcy14Udp#bqRU z#5G;if1qaT>n6HZZqMdcp0uB~`1U><|H@zK!w$DmYGm`IQWuPo5B(G@S_EcNf=*8% zee~ymV$(w0dF*GbvdU->$p}vK)W|lBc~s}>u-LkD?W@qu^KI?5 z*VAWFexrBwTS4pJ$VKE`0c;)j+2hR+o}Jk?9jmEdAsW!+_-}maH_Ek85X!F`iNgj zh5f|jv$Ez4hh7?IEb#G{L>Tg3!&nrIkWlt*mwJWNgYw#)c|Dck#_ty&okW{67iLN? z8h@+iJR>TUMK1ycsosiGKb#poA;e>p zq;^fMYZngPbPI^^=*1+>Y)=J!pH;&e)cqAzoV3M`>+{4v!6>APB5qe5LQj1|K%U35Fh2QJulje)g-{>;xNsZi;JXOakSbT&A>rJqIYTT=1*Z+KJy906py=d?#Bg|X_m9}VW zpc-NM&FgMmgMd(Xu*@|dVWxDB2!3Add-oGt;8&=b=J4#<`Vz&SDV^Mb51RA!zY_yd zPH>E}qgqaa^7m4m|BOb4cmp{}_buNj9y7|TpzkFobEcy>zWw3GzDZZXz6`uLnJ;;T zg(e<`58F~m8nNkTK}8~xU@Bm8GXb{{%J3Bgq41jtpBjj9UF-XX@x*kZXF;V#TavFu zk7mPBpLApHugU)m&%&h({~6v2c?#sA)B&a1>ZmI?eqb7UPA;ckUb5+J+9Z?3%*h!O zDoS?xs#fZ348h5)%#WX#%}f1P>i}a#y!4-eJS|kt-4Cw>1j$o?{0PqQztsBs-N1_f zHQsn1&36EKZNic{`0Tc>kQx{I|Nka~!Tws~IWo&77#+2)7Xcnn0+>tAZP#TbpB?A+jOpaVJ>@VY zx;n=ww5`j3nCYdxA8E%>n<${h?FD&p-MI4QOAdaMjJy5{t`79RK5ww(YsdXP!u-!n zuF0DLerd_?G$?}o9oECQf_t+4F!Fzw!Sx<$0@wE=#y&Zf?Ld#3I;Q5Ka;-yW-pyeF zO#c~j6F#JrNUTaP zSCdqpFE?Rz*uyd`lP!O@cK9`QPOyQ45&#Ji`0oMWV^7d zYd(=apg`Lnl&s4(j77;-*>;thAX1%ky}#phVLd3P%<~rbtOM+R4qUX81~-{NE$3Ns zp(1)YD3J1hCIX2ow9A`Y&wGL$W+uENZ|ecP;oKyfO3_U7;L8lK&HJf$ zU~U*6C`Y_<*LHdvCVGrF-Ru~hz^XAF|F>kqs?RcWIChgen)`BCkVh0qYv~wjZQ1GM z6M3BVv4N>YBE1P1I_h}-eSQuUK|la|zk|QoFWr{?pUTcUD$2EO`-p>t0yBhw)XXrH zA}C5qhjc4SO9@CxcMLg0NJ>a4f`Fi;2vUk5LrE$iAc8}Ov^3u}xcBqy_kEwQ{&4NJ z_8RWE^1jaVIDV(}L;H&oES?S%Ttj9q*)jeboM?&h->cWyd=Cqn0i5AikkFG8t^~ZA zFbIrh<(0taCka|&i}G>RLBonb?OdiFzOzRvWv1!~PkqIwxd5jFe4Osi_v1!SVH3c<|d4C~UUu@3FQ!14bbX}q=(&pWi&Wr{&GuM}@ zCx47TO$Y{^+T=@pq64uYL#s}!;v%|lNW{*~`cztdroIN$Jne~Q=W+S79`V`7_BJP; z(-9f+pdHRq0+}UBhL12a&i6z;Qi0(}ArJ0|8GkLAm}=k1WN*CcOG?EOZ!i&bSvpXJ zLru9lNkQ=Hh8&s zTJUNFl`}mWCHK^1YNOuX2K7z zg!mNlz5-AIYB005=CtoL9(a~Jj5J;H7*jbJ(5{G z^BDEUfIj_-hwLy?O(BtBh;F-aQ#?=+&I?m+R=K33+MM)SPajebfBzo>a=vS&;wM*a zAqFpRG08W_NQY)r{NKdnhCi6bXbR~>=5K&tACSv|wumEo;H3!fgWVI*nb>%uynp_- z)rZ8}f>TKbf|j2J<(~|@wXj!{oc<3XxfVimPt7~b-=$?oGiD>=)+ao5+-l#coh~wb zjp5sO%ZkZaudJ7tF=7&b5b-B7J~to`QIIeHX9gI7UC8=uoG{2q2TKzxM2P?I%n8{+ zWv{c7r+Gw zSTm9DfdK;Sg69c{7A9^=X5nOD(NXS5=C$mgmOd!ERcdNN(B5<0jJe_gM9qoUdL;A~ zhDrsp#61#$SuJCaJI7y_%?A$?n@-ltGko(v&TIfAy2onu`0JJgi%jaOja}P0iGcP~;_Q%eF?DxC{L4^Wc6oAHu<+nA#Qunauo z01r#rRrLp81QlH@sRThAi-=)kpx0Xm0(q6rl*{03e(}D_QYf|5mA-r}>BC!W{;RSo zT3V^8W`PF$^{a9QRkjI;ODRy`-O{`n$9CEBQw%Kk7Qu?N^u2*u#GqO5f^g&Uk)IfF z9BH7UKgBYObPShUT7y=Q8^tAXSc8+-`b>tiD!5aH@&$z$apXOMUk4auB3B5JV<-{~ zQaOnF7Xpn!47R%KFBq`rjwTbb?Ugb0nWir?sC-d|7YaP^7CS!J$XCG9M2J4S^Hk;( z?XuYRu;p~^Z1%Ml3|W$598XVz5*sggfRvZFi`gnG!XI_;l zSK&q&}XlA4CQ2^Zl;F=8-C;rVU}Xu<5h>HNh|V zcuniQ^|Lt}0D{Kqar?p6keTi?Ig*N-pvkRuw-v6Q$*#8_coB;*eDT3!2k5Z7UD4st_e*4|Gxa^i^GdXwK(CISi zBK34Z*H-#F%D}AG&Bh_@NIN}0RGI@-e7EqjIP6fdAj|WK!*JP$B~E0=Pm+d9rS-bm zK}SAfM>{{erMPpIH*fqNbED)9&Gh*7-9%SUj}q|`*fPLDx#;|5Lo!$)n6OVMe|jto zCU{K*fZVKE967<}N=9I^AcFH>Mg@~XZgI-O?iGQ%m;-m*d_fMuCYK&N;E+6{QUkiG zy~=JN%=WX;kg=sUqo5ULWT1tOYu#ZX!nWLbj^G9qXSu6sDM0IUDcPW(S?EFX!OqV( zLFexh&tB`lGHj;x7HHhb{Xt9IQt=d+<}=zys9TSCQqDwDvpWd~NN;pqyFUh%v^OI# ztX3m}X*iuli&8*a@erRmwM`2=Cq4BS-`}dClJK}$SFDQJl&L7ZWKfZ3K*RA%k%EEW z0hiFE(E&_p?kVKL-g;orhfuGRm9ojC9ssu#LMkV&gkaGRBM@Jp?{#mV>|!UMh{^=O z4(HJB*Ac>^Oz0g^V)2Gm!bVZ={Zi;3KZFy0*xNpMqL%-|pP@tFC43?B<*>45Afeo3 zJN{CYI>WOAgR^Q*=ce$9U(7EOXA_oXri-_4K?I?_miHjul~o<5bk0!)e)RrPtY}Y< z-jM~DILN%j`iI(a@(>D4XcSyk=jN^PlkCYKFitZeU1F}8uX(bugW1gs?Nzx+5Bg9i z+$ifGKa?OWtJ$AZ9^D4Z>J;qMNp_{>Dq-{5!O!>pdxL;ifGH8h*Z4q^4c2@0WWyoc zDxI+BK;&ru{qow71DG--7cb5IXInaXAh6b)2W=8ga#QH!Gl|f8VP{Xhp;aSXHNWy1 z0S5GkSOdOlviMkEp8VyXHf8`V*Vk&) z0Pg)4PlyhkN5H}LXZvQ={QHM^02Pyf@lO363n1jXw*SEv0&h(ME@h6^i9Ok8|MV6T z31wE`g(Z9u`^USD5%!Q3t!J8l<+tEpp}lJ;D$p}Dv&IYRf43Is+b+Ulb?xZ`&3|+u z4>20F4ILxHE3kz9)v(|{$vpFl&{VRyr+;@W+7=FVQ<&d3*l6kKO`U+Yq&z=`*#qDI z^yu_o*p`>ZKHuM`u`;K%E{J_`X_Y1hn&4>Fl`ELkN_Ng%Iu9yhr&t62=JX0e;7sbwdS(C^$Kp8s$tZdGNlx+yeoTe{Q!|ma{=T9 zSL$OpCNMM*p3t3552UhNtT)r@@5-4Axe8{G8>V|je?Vu1-AWiTsND~TdgNMhC{H3q zhszVM+h}l~oSgpjGLR0wR46Jiu;|3VcYFBrt*^=6q+-9QpYk~)`|+>QSy$PEeW1#~ zCRhA{a0HUsp1_yT#t0~VuGMp8zKq>Cn)M{vm~0Q_zDg>C2TwTXUQ~7QJdeD)u8{5d zRwfzhl{NEs+Yy=-cbOQ*t;&R&@8!{rqLmHVl=0SO5v1#Y^cLhS|5`b#gLIt+x*!(h zQ~7^oGcAbF;9#Vdcra_np?tJe%PXG-^)f@=QB4`c*#1F^olGYHW~;&I%1kJDjZyNZ z>*OUvIh384(d$r7TxuO<1^(B>`0GFPAJD^aHdvv~JVf@JNUh01zE?rHMQc-c%6xRo z&#?{5E*OVuJRL47>sFL};!UccujfBzzYxWD=I~uTgIfAL_!Jo*M#eI(jSo8;dUjcv zM((+urZQwbNH?CTg(fFbz=T`**CQ$>SNtH?TWr%{;aq&7`kHg4^(Z?KRE(o89^0@P zabU=FLNehiMK=>s2GYE;I2yHJeWgE(G*Mo`zC055irps9ktLT-y;r_BT)S@~gZm31 zuMQ2SLqHki@39C%ipue%tZ(_uMDz|gd!q#oJ;sD_mJ=GetBzRO<#vAX8cz2PIV4xA zAI>7w_*!e1_mYIYG6+5L;~oSq3S|qEun^Mchst{Yd_&)X`GN>n@0&>ET0mZYU3T&LCZ4J9NDQ?bx}T?h4}yNTE(XpXl}HWDo@+IY_N6 z?m6Vtg@97Bb^NiQ?)t&u^-IaHZ!oo|b8#|HlCPk{qL8cX!~g+-y&xL{HN1Q^q=iv1 zNhPeGg}BQ96|Jl*q~Rgic9}g%5Du@tZWVAv7(<)K%S5hs3zJB4;|Z@-`*Kgt``Xd# zIqYPGJ$Ge^bRlALxNH2FIC>n7TmwTnq$81Q^bUCv!{#k+CMaC)=OY%`NAR0B1+g#` zS5uj?n=%uzorQW5>y%YnVoVrsi9);dgP&UZe7$|jSd08B3DK8yluZlrFbp?jSPjpm ztPlzpjb}!cx}mSj)|A;h%BHQ_cY@G#dkB#ZDpyd!HqGt4tSb)ILiS85CG2XxSPOao z^^v$MvO8&de6b=lmTT(C^`bV|&R3~93Rst&{omZKbA=$|5JhA~(%*c5P=sE{a>LGH@k%%je0lWiCQ74-E0b}8 zNApQpQb0H%69&b+G)O6VX!{2`ArZx?-W9$kPpm?FyZL7F!E>P_6lW8Wdf)WhgAmih zPdvo&Ov)4=L#u!{b^k2n4AL@OaH`a%v&x(y07Dz-ka44xy(LC`G!hVg;tR=Yun?`M zoE;Jfa8aBtQt2_cCQ;{Qr;w@Fah_7G0x#(tj~9S#h7$r8h-dml7lV>Dh2J3aamNbF zIzdx<55j$y5OW+mjA^K_Gc@4k3x6{#V8Q%?2Q`P897`pat3@^46R!eanrks9=!8jH z9~Y%mb!!&(;V#F>99$l+gF<_V^}t;$KCdR_J7dN%4Z`ChOjaiL6C<&LHzN(|jGX-U z%TW5_(`!Hkgdg_>LQ5}Nt|htm2m2NoYua&CVc0)yP)s*YI253+lc}A_Y6%h^o<6R~G$ooees#wt*SxI0kW`yvaDi3S9 zb)W4(g{FnGl9!KMhXY5$duI7A&gcX8ay-u`%SEBv5E_zGYFu1B>`1Z=ea z(zk8%cI4qVF-&SRId{vC5K8s0ZX{%XuXH<`5)sRjzq3q2F{8Z*mN>XCnT+a7;jYzL zt;E7Nd5}b!!mJ}GGNnrywx{hJUSZU$2-n&vBQ`)*C@=-=CB2oqAve+_-~rkX2`7S? zv;AcrN5BU;__}McNeI4lJUOv7i4j?VE6wJZg`*4x`tu_~F<$6^<13eCD9crSvcy<; ztw)Iy@`X8v>Kf6b#9D7rHStdmkwk03neZdeaVr#5C4>&)Bo`_Pv6vPZTH?7e|KL|kv&+C6sZL4#-65{_iM-ibLuM+V>C*xTd*%tHS&c~XN$|Jw9sfUZF1 z1nw2JFCs*Ve~)|RNC3ZGgl*$yzuE`-pCzq{)t%1NA>@!VvcRt#uEwutV5 zVVM#WfB2_~me+|hF&sBsmE1cXN#2kwrtPsC#?ZjQ1e+LUPM+9L9z$W*TAvf*5AVEw z@oPM}o&b(QD!|-^-{_I;rtY z?f1cPZ#^o^SFhvi)8fAGJI1$3t;fyaUSlqi5+F!G`o@nJkN+4=St_2H1nmD~#lGqb z1|h`h=$aks{+sSVxOiSSC~K)>&WIVkxvJJ6jm>@3n76|j%S{OE>)Y3nNV04oO^pHT>?leaD2} zlYR2~T>zTc1!w<{c7i3Qh-6{a$NwO21*xv&$=j;`*u8GQvfbR+##x zY)+jK*yX*}Tt34p4z{&ybsBXwQ^iW~~1dU|@XIhCKqI|oTBqQ83#nQ&`l2n$Ki01>;5JJT!( z!`)wxd2966iOZR~%Nnpcn#T(LCc_!J7&UJ!U*82!P!x5^1sLTYANf^D87I9D^rYdJ z{;Js>#r=ej;FFJ$e{T@>m?yu=!8bnXAZG3dbNT5grNoxu+O_k%x(fbMIdnYQGW_r6 zkn$ll<|FY(1Ypi~69~1mzZo*McfwWk7bIuQf@zuWI^989^xE)^>Su`!^^M{h;FO3c zdG-)IO-JuDd5z@7VavG3JyQEI56E9QU&uP0@QRep`+d9hf#2M-NOjEkAqgS$!XV0g^E zYu)x5N_e}qS>Vw6)$(ll)>=Fj(CU5JB@JJLlZE`(j>=E_(kX zfe7OuF9;W{J}q3sU-VH*`cXW!)Q-%jN30od21%YxWjFUDmNz5Q0Le;?g8rhyRkizF zY35SD74kwyvaXnilnD`qKwyUNUDc8%oWYS#+@skt8qSC%E#)z*@a+SdW0&b6QMoO@&dYDpnB{+diVbJx zGdu8acl$VXw$8dUB{J<`;`oSr?EOx@kFsSj#S%1q-FYg45)oXoHP-aqpyBT8gpK9UdBLZB%85kjy$HSBU z&{mEoZGs=@?--kZVqfY+OC_Ag_00JxhG037!mddJ-env3;}(ai7t|q?LC@GsS{5uv z6ZzMjWG75KXUmuQ+tjk>_=i@(iXi}{-{yghtg%bCoMugV2x6|bMS2B~9>(U9jyy91 z(P@%?y+(f@9}xb|w`xx$P9Lsu;do%s+{m3<#;KYN4eW&mbUZj0Z)7ea{&y_kg9vJF zv9w7zhddB7j5+_C7I_Z@W(E7$4=h8H{fKvl*c;`~Q%PL-G@0N8P;n1b%_Cu2U zbsP39><8;JXA^O!%w)QRO3kRgDmm$5ZeAh3X*?D`I91e5o+&Nki|2q5??BkCirEpKuELikG%;_bF zhp?`Q43boK+OXW{VyK{-GX*=%@jYbW!_vt%Cd+A3n%|tI(vZ*#-E9#UU?`^H8J8fo z<2XE>+qYykm5IJ;R?plP$N?6cQPs<)f==Gu-(c559Y&6B6BtN*v2UCwzK5+~-JurT zRy~O{eQ+nwse`uec(Zb_uqOc;8wKQJ(dTXU0PO2~oo3~CN=;{mEWj)d*O*-Kw9hb0 z-pRjwBj6iMI8**6AiG&^%V{$xV+x5Fx$~Lhs{z8CRV^Z}#$WHpQe0K;I2# zxNDlJuL!{Q5dLCU&U^}mez34?NEIO6n~l8Xm^S%^=5dU!tBNX zIMhBYgkI=qJCiF9O*f#DefzOY+KS*x*K6%(V9I^G#EI&$N$I%`yVIdx^But*A5vV} z`0YUX2kF@aj$-+a@6CWC6fd;TxVxnNLkW8by9W4hCHWU~^TzaiZ>S9qhm);6kCt*m z9~4%uH>Ums$i@swXC*`S|k~`a=0<1>XlVN}@mzD&P&&dcU<4KzoGbm-PEBiBpnD zLUA>_(yA%pVNUf>S|`J?did3np@B1#ge*`P=cU{mgVU(7dm~9dO4~|}wlCLjf3v^8 zu>;aS2e+v%`h96yZ9dx_NQS(Ee+BcCud;50r`-^7U)T}+!N7@%F4)7^>QIXzC{e>d z?o*t~9S^uh<({;?*BW;DlD`=A)l_F#hNxL!X2v};s%o>q7tg@Hs$z191IN100}7n@2381issl9WycI*NA1U+aC`HbSFpVPP2Ipvg$e&hZseg#BKq_F-JG6AB3_sZC zWd)4oP(=moOqS=AYy9z%tWGZn3bax|=B!U*tGPRMS|r7@ro2M`7*O-lGT@89C3Mf) zO=hWj)-SnDMOe0eud$=9UbI-fr}rfE1dMp7%30PGQ3? zL1^upjF(zM%^uaCM@ubH=0574a1fmJI5Q@*ORvQqWUdm-egA!o*<#GZ!ov+p&^7C= zR*vx%ENyK3pQ$aUJ0lC?LIT8kxpjfAy9|%|8gwO5&d<}adc1)=!(THD8~99>N2f^J zQAsYR`x}5M&e&Z;vh!WdarwmbiC(Qu!5f2rc<3g%Zh*SjZYrHj$mb!}<=7^_abJL} z&8T$Bbm|*>9gw|0uNH7X_Eg|E8B!6LBA-X%V15C%mSXYy6n^vGG}IMZgHHsP^><=i zHRMYivNW(o0B)Zh%!DeR)#~ew4qpXxa7W&~G7`a7) zG)r8pVB>Ji!JX1tyN8(;Z4BGd#dp4!`oeIs&C}qb7{JL{UpNP#4ylo?5soc4Q_9dm zWO~0Nd92Ai;@^VBt<5`z@2Y$@e7aKh`(-{G#30P41hCy!p9i~of&DP*8$|l< z*w)UE3m!uw5`p%Eh33(bd30YPtj6bFyU(SCsZq3`oi+1bRP^!|KUulWmSE0 zA~SUp75Osh0A)U#?&+&fsV!HV`gDG!ENzaQZ7H=*A;Xf@?ypUwYZh^|7Ht9`$+p~` z1C774$f@#Kq;3u?^WtR*BMZpa+8gk6(H@~k>0h-Tu2qy z9)au)sA%)P!_wyQG;^#EjIvUVrEirXD(>M}Jc94}X^Gewv)RW);evtgYtH0)g;EDw zKONg+ciW=MW+T~8`LI%VIy#_winHpsdXB#@!7S-O+V}eY$Mp;5Q8($hy0>PPa7NA7 zM-bXw;r|n20>byf#5#uJ&hKO_c7{+l%)ijg<>4Vv8u2YIqwB)P1*SB`* zPUi@?pSv5l(e!<3*6pao`bc9W<-amgLc{+HBjv*FT*#Gprd+kWGUeC@g_JFDf=59r)pr3n81mfmS0cHK7G`w~7WS;Lx6x)zJg>Gze~ zOPo^{#-tnrJOjq}0K^xG4L;6B?fX=zP1kNwBHA4%9{yHyL#iu31)UrWS4S&D54mg- z`96iS*&$VKVE}!)K>m>+{zu!+$5UK+#Rc+d4-aU*_b0pNHf)+8mdRnelXtmR% zE-lrJ!XK=-KCS;kUbS?;%_{YXcxy;p5pGB(6SJz=5!X>qgK}tnxg*e7OAZagdI=(8 zf@&AP5ZO`ng;?oOpE{>dXd4}+@Loj$+aCns4P_zuW@a^;++UGCvZT>bVSMG;_%?a6 z97sWs$lYADU_Is35|eOUK*S1pT_PYQX&9s7HFP6x)w#7=k4v1SRu-;Fww0%E`!R)6 z9K2URZ>4z5<99NNCPwgpS9*}~$?QE51JRydGNnYBrGfih$y1i|D_Vg-f9>G4_@FC6xqizLLqskv@2C$~g(e%#*_; z<(egxgZg1k`;*nA<`<;Oi)Ee=$FlZ^{Ydo`VantG&bg$(dn1In2+e4Xt0bORX`kkD z*pp2zJLQ68pR=+O$(GqOEFi;^5|70s*zQGi-IXIADMz(Qo^i*hKa14%e4AA8nL<4N zQkZZMn#@2NjiIyZY>G<(f;Zs*E+r!)eUt*#$d+z_a;J=e6a(Vcs2^&7b>P?^62%d;g)W;GAt0R&;^>z{I^fa*a0Jq`Wu8aP)nLI-+Lw_}n{Iesfv zI!JKZiMqfHXl`4O#A~WNV=erqv+)^=h#m#<{=Q1DPxl>v6T$iTqVq*zD6fF3^&M(D z^N=p<7%$VRh{T-0!MDhJ(BRB?G(=VvXN0bTB9Qs475Gw14|$DOk|HV;beAiZV-VVY zg@Lz0=FJ-7aQzd%W$XG~4+o?VQk4nzq%rabPLNq)IzGa`@E!}M$(~aUR1yICq zi_@DG*l9hpZ!C_MmXfT;h)%jbeR!;fwW2Ls^!D7{(b4x}%-kUgTZBHqM zFnF_HJhdc1d5G{N#0pUfq2%O3q-XIY0TA8O3FRS_>w_Hnk$a)9tdilk;!(N}=6;q& zOFQ&u4-EI}oLrwd9~&t)+&$YVXhsR8wxnuflt-!_C7^I;@;F#dW2-5VYB3Xg({`fA zyCcHD-=UyDlt^&Jn}I0=jC5o>w+_kW~G!EkuHz=^6+43gn14u>X)>A^LD>gz4Sl{f z#2YzgUwsEjnR`ixYCFNnlX1T8DwVW*z3i-?LfHw^UxMR@X`||lt%gMAM;0ti-l_5= zGzw9jNmg`BgWRzG>5J^rm<95Z5TZN?dho8RT#zy~s+9uLM#HmC<~WDgM4yw5Dht2S(B?m$%AQ7n{UvOycv@)nj*Z{z47JWn-6MwXKaR(1k3VY-Tm6thuF#+1}>%Mu1Frz)XU zRo09`1`2h(dshC>R*F?$eS3b0+-EsY&>|p}&jTWKvh3~cF!8zPsYVqF(t9WBI4f8g zXlc%o;!`q0M!8`jMl_F6BVv0!Jvlsvk{a^z_qM0D1o7iyf)34c25(&64wmTae0CaA z-pd@ixwkDk@^{fFa8~;ANCc2mQtJ6eTba4UzvE@!^IMKszw%D2Wp$H?5i+(sr<#m; zQG*SU#*z&YS+r}a1{dl!#tA$km9es&OWREp_Iq=^MDI4gSaE*lSpAE^mxh{D0tUAz z&Ue)u{KK4qwPbXk6NASSUP9ge7GN(-!O-5=&n>hG(JdbFYZ~(s8nUc@fhHC(E#p^n6>l-ibv}X!MS}wV&E44K)4~Oif zME^USiR$&-v+xr*lcc-OV^GFT#0pzv*sNd*zBakRGI)16V~j0H9(W)hB#m|rkNp$^ zB@f7ylMmWuqu70v-5qp|TY9A{Qs&%x77s87>REM$@A8C`qo!4-zhusX@&&-@@j{oH z94q70t}7P;yOz|CR-i$H69eN$JPb!;ana((`@6ngEEz(HRlA{;>+Q+p;OeJV%^s~g zYgJVo`+Mwmk22jC=qnUpP>U4y=w?GXH`LpeN0Ji?TD>)WNgqe%jZmvsYUi(AyKXA+ zSq~I-3ns!_Rz&m3rm;1;XUPB{$Wz+OAK3fr2JV`liy@idtEJ2teC(jBDwB)nI~CN!Q3}y$QGbzw$@ipt;vmg*lG$KHTE^fM|VlkOCR#-k|eI4*ILG)_}}# zWf=RxbYAiDS}c(j)=S7h5F$23E)tfmQ`(1$*H)o}^c#m^c^+jkw2(}N$ymX)FbEON z%}6>LdS%-UhCL*xj}vz{Xoo8V;$x{^EoCquk4X&LL>Z9FM_qbJHxi(Vb(t^kd*?EK zQ70XsjR>9q)C4T}+?oPQ3mGC#JLgyEz$rqJO(Iu0I?`NX^YkT!C(xif@ywUv3uP=u z$BRa(^2k0=TljPNK-y!ecjYlOk1x9R8om4Qv4KU!U|JgTuNtSrwU%|~YBqfBMoZph z`lwp^Y-7MlP{LQ1)&L* zv5jv}hjhGvW`jGc%-ggMyDG!-&Pgj*hpv4~yXPwghP)N$hnd#{F6HO=W|G7wZ+2EbW4o~y{C zRc2#(BcRm+cBPjufGTkW{*X+^b@9bq#0;Nkvdn#17v=lQpICJp)v@vmr*EsP?Kj_I z4^Y?eSx@kr{@i#6XJ)V8(Y`Dm1!S>~rc~3cK|5Qy#eXag1n-FxOf-zAmV=u&Q0MWO zICdzISCvjO>m8_*rrd53E;;XXWRuj#eAJ(`zObQ%8}4)NG!Zf3rJfk-8T69`b`jGp zT2(D>LitF7+)fBWg-~_skvMe+sQXm(U6O8!@IR4eL<7`?%^`X;)F>pCP^GBrz~_yD z=LXP@12EV0uPysfuT}d=>iMamQbR&{)ISo(yD(e|Pxu$T^$Th{>5+1V3nC5=Rz>TK zsCkW6lSa0{R^UiV1cjdJDIK!GPe(SZa#!LNTL{2R8WkNkGr@A9u zgUUUvBBQ2+!NS`^NtA%QWwzdqs?Rc1MA4l~oZsuMouU3WGzH(2-jOu~#{d0f)S*)1 zd%5Q-#`GWZJs*Fx?%uonf&RcSqT!n6BOW8u@7}w1OvjTvMfwdX=X{Qq9=5sWA&8t_$Zev-9(MJxLujBltS1@A+DB z<^ii;rh(I?yJc|2o)C8626HQ@(H;Eom)aRHNq~uFTD@gg^qWxWMd{0?td9+Vg-*xi zh2~!p)5Hq|ajWRz1G&6^mSSy22ORtm$bB^UFQx1cpid^0+zKZ}|GR9`XtQN+4+Vke z)5LZ8ubYNZpL&A_irmDr=elPo|C@Wr`O|@~-t1{6Tg7z6%##P$)mGYv> zgUXA7<_jTZiYXWrq3)rUsk_HalnfM7WeCF%6Wb3RUiSOF5*t!5>5;4E}XPF?CW>6-GIR`UWcyd>0`tG4a(fB4E)7FQYnh;C; zTwH!9$(;`CfIXGPZIkJRMz7ACOUI*e;!_QA_^3ta)Eg^)x1=(zbH1LLu`bi$R5d-5 zCyqrECFI=jt&0D$DBM0=l_MAEK~zfg^rSRUi_E7kIRcd&=rm*YH`VO=S4GoT6%EI_ z0w)F3tX3;OTJ5;Sb+(jqpd6cdiXK{t%^&}`cP*0oQ{V!$X|2Cu(V%}h^YeP|k6G)Z zo@jNWTo#K*Jco@3T8L(M|Ai6Pwc8?{FNN#@Z@O==z|31ix2InF-FA&3mqxbvf|#*; zYpkHpH+8X%gHG$ih~Dqh@!^BDYsJ^RstSksw5lg+?$qsnQoZp7MayPh|P5CyG`2uQQj2y;AdS`J2oqS zv^4-fs~D|unC6nxS?t_kpDV-dKF4)u+&k4_G;l5_*RXa^fLFf+!xHGtJJ}={nVORE zx?lgH_`C*w@z?$IY6USm%p0u4?~V-O-kew;;3m4E zpY89I#x!-QG%m5U=};#-z(08G%P;Xs3xjx{W`@hA7klbA-x+)Ai16yYtLmd(l#QJx z|9`Ejo6TZxbD{#afS^lX_+KWt$qj{GGRaMv=I&WeKVUlg>gW1pT2lSi%vV3QL=sKZ z?6eJA0uA8JE&hrTu6)`fo$(gu=PWhrPi=aMn%c*-+F_haNgS4a%4-~jyF6sZOCSM# zDZWileD01|r&SU6n-nH9e>j8ZB63J{n)QZf#~JRz)voALwMG4Ie0~8t4O!n@oYd98 zvB;kpBA!bu@f!jSnx+?y9%0WvzGQUso#(vO;F<3MmG1Lnre8};xXt{?@8`Zpi1aSc@QF{45-PP&%0J)R zQYO?3JaG>IOU1nb=?v?=E_VDEk%`A{nVc0vO7#MKXqZaH=~uVbl#?#&)|9Ely}CV~ zM?!sx=lE#f`DWw2Q%svXl{3!@?VaZZK;f>dTidI9n7#`45egst(WnN!PHsfom&YL_ zNBb{J_1{EYVWYdK&!c6LS3{dQJZ;l+T{IFfJymip`F0HJe6;$Mch%K3QirQZ{JwM~oZOCU|a8Gn)KxKXCT~mHGt!|$Gcg_F^ z^7fk!P|Bv(V^o+Ht!g)-{wYwfFP`_Sl|aLboIH&T6$XO&>d*JfeL%<>5SSjT)Ji?C z!E8;gWQ=I86w;SgOigNk?9_?wS;P7QB_vP07Ze(8xzZ8>c4AUvkC zti^_xry0Cb2YMRReT{vRjE8a3^!Lc@@i2JaFnZCoqyNowg}=zg_y_du>Hg)V&!*jN;ie#SuvnH8 zxEkRvw!+GEZHjZ(Wx?Cd&!U*G%y&yd>qU^1;Y!`c&0U8sX4mgxOu<7XrW#yg0j8{b zh3YK()dU)b{L?wn&=D1 z*N6crKeO)v0fqiTfqC_-_)u4?-3kJ*N{3tZyeL1-Hs46a4H7GUHl)hRUI+2 ilZc4u3}LN2O@wUP*Rp)rhX((LNJUXo;kDeokpBg=)24v{ literal 0 HcmV?d00001 diff --git a/lambda-s3-athena-cdk-ts/lambda/processor.js b/lambda-s3-athena-cdk-ts/lambda/processor.js new file mode 100644 index 000000000..806a7bf47 --- /dev/null +++ b/lambda-s3-athena-cdk-ts/lambda/processor.js @@ -0,0 +1,58 @@ +exports.handler = async (event) => { + console.log('Event received:', JSON.stringify(event, null, 2)); + + try { + const action = event.action; + const value = event.value; + + // Simple business logic simulation + switch (action) { + case 'process': + if (!value || value < 0) { + throw new Error('Invalid value: must be a positive number'); + } + return { + statusCode: 200, + body: JSON.stringify({ + message: 'Processing successful', + result: value * 2, + action: action + }) + }; + + case 'validate': + if (typeof value !== 'string' || value.length === 0) { + throw new Error('Invalid value: must be a non-empty string'); + } + return { + statusCode: 200, + body: JSON.stringify({ + message: 'Validation successful', + result: value.toUpperCase(), + action: action + }) + }; + + case 'calculate': + if (!Array.isArray(value) || value.length === 0) { + throw new Error('Invalid value: must be a non-empty array'); + } + const sum = value.reduce((acc, curr) => acc + curr, 0); + return { + statusCode: 200, + body: JSON.stringify({ + message: 'Calculation successful', + result: sum, + action: action + }) + }; + + default: + throw new Error(`Unknown action: ${action}`); + } + } catch (error) { + console.error('Error processing request:', error); + // Lambda will automatically send this to the failed-event destination + throw error; + } +}; diff --git a/lambda-s3-athena-cdk-ts/lib/pattern-stack.ts b/lambda-s3-athena-cdk-ts/lib/pattern-stack.ts new file mode 100644 index 000000000..1391d2eeb --- /dev/null +++ b/lambda-s3-athena-cdk-ts/lib/pattern-stack.ts @@ -0,0 +1,133 @@ +import * as cdk from 'aws-cdk-lib'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as glue from 'aws-cdk-lib/aws-glue'; +import * as athena from 'aws-cdk-lib/aws-athena'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { Construct } from 'constructs'; +import * as path from 'path'; + +export class PatternStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // S3 bucket for failed Lambda events + const failedEventsBucket = new s3.Bucket(this, 'FailedEventsBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, + encryption: s3.BucketEncryption.S3_MANAGED, + }); + + // S3 bucket for Athena query results + const athenaResultsBucket = new s3.Bucket(this, 'AthenaResultsBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, + encryption: s3.BucketEncryption.S3_MANAGED, + }); + + // Lambda function with S3 failed-event destination + // Note: S3Destination automatically grants only necessary write permissions (PutObject) + // to the Lambda function for failed event delivery + const processorFunction = new lambda.Function(this, 'ProcessorFunction', { + runtime: lambda.Runtime.NODEJS_20_X, + handler: 'processor.handler', + code: lambda.Code.fromAsset(path.join(__dirname, '../lambda')), + timeout: cdk.Duration.seconds(30), + onFailure: new cdk.aws_lambda_destinations.S3Destination(failedEventsBucket), + }); + + // Glue Database for Athena + const glueDatabase = new glue.CfnDatabase(this, 'FailedEventsDatabase', { + catalogId: cdk.Aws.ACCOUNT_ID, + databaseInput: { + name: 'failed_events_db', + description: 'Database for failed Lambda events', + }, + }); + + // Glue Table for failed events (simplified without partitioning) + const glueTable = new glue.CfnTable(this, 'FailedEventsTable', { + catalogId: cdk.Aws.ACCOUNT_ID, + databaseName: glueDatabase.ref, + tableInput: { + name: 'failed_events', + description: 'Table for failed Lambda events stored in S3', + tableType: 'EXTERNAL_TABLE', + storageDescriptor: { + columns: [ + { name: 'version', type: 'string' }, + { name: 'timestamp', type: 'string' }, + { name: 'requestcontext', type: 'struct' }, + { name: 'requestpayload', type: 'struct,queryparam:map>' }, + { name: 'responsepayload', type: 'struct>' }, + ], + location: `s3://${failedEventsBucket.bucketName}/`, + inputFormat: 'org.apache.hadoop.mapred.TextInputFormat', + outputFormat: 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat', + serdeInfo: { + serializationLibrary: 'org.openx.data.jsonserde.JsonSerDe', + parameters: { + 'case.insensitive': 'true', + }, + }, + }, + }, + }); + + glueTable.addDependency(glueDatabase); + + // Athena Workgroup + const athenaWorkgroup = new athena.CfnWorkGroup(this, 'FailedEventsWorkgroup', { + name: 'failed-events-workgroup', + workGroupConfiguration: { + resultConfiguration: { + outputLocation: `s3://${athenaResultsBucket.bucketName}/`, + }, + }, + }); + + // IAM role for Athena queries + const athenaRole = new iam.Role(this, 'AthenaQueryRole', { + assumedBy: new iam.ServicePrincipal('athena.amazonaws.com'), + }); + + failedEventsBucket.grantRead(athenaRole); + athenaResultsBucket.grantReadWrite(athenaRole); + + // Outputs + new cdk.CfnOutput(this, 'LambdaFunctionName', { + value: processorFunction.functionName, + description: 'Lambda function name for CLI invocation', + }); + + new cdk.CfnOutput(this, 'LambdaFunctionArn', { + value: processorFunction.functionArn, + description: 'Lambda function ARN', + }); + + new cdk.CfnOutput(this, 'FailedEventsBucketName', { + value: failedEventsBucket.bucketName, + description: 'S3 bucket for failed Lambda events', + }); + + new cdk.CfnOutput(this, 'AthenaResultsBucketName', { + value: athenaResultsBucket.bucketName, + description: 'S3 bucket for Athena query results', + }); + + new cdk.CfnOutput(this, 'GlueDatabaseName', { + value: glueDatabase.ref, + description: 'Glue database name', + }); + + new cdk.CfnOutput(this, 'GlueTableName', { + value: 'failed_events', + description: 'Glue table name', + }); + + new cdk.CfnOutput(this, 'AthenaWorkgroupName', { + value: athenaWorkgroup.name!, + description: 'Athena workgroup name', + }); + } +} diff --git a/lambda-s3-athena-cdk-ts/package.json b/lambda-s3-athena-cdk-ts/package.json new file mode 100644 index 000000000..40c2c71de --- /dev/null +++ b/lambda-s3-athena-cdk-ts/package.json @@ -0,0 +1,22 @@ +{ + "name": "lambda-s3-athena-cdk-ts", + "version": "0.1.0", + "bin": { + "lambda-s3-athena-cdk-ts": "bin/lambda-s3-athena-cdk-ts.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "cdk": "cdk" + }, + "devDependencies": { + "@types/node": "22.7.9", + "aws-cdk": "2.1003.0", + "ts-node": "^10.9.2", + "typescript": "~5.6.3" + }, + "dependencies": { + "aws-cdk-lib": "2.189.1", + "constructs": "^10.0.0" + } +} diff --git a/lambda-s3-athena-cdk-ts/tsconfig.json b/lambda-s3-athena-cdk-ts/tsconfig.json new file mode 100644 index 000000000..507608a99 --- /dev/null +++ b/lambda-s3-athena-cdk-ts/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": ["./node_modules/@types"] + }, + "exclude": ["node_modules", "cdk.out"] +}