From 8a6e7fd9e0a3881cb10e9b03cb3931c0bc91cf5f Mon Sep 17 00:00:00 2001 From: Tejas Vora Date: Sat, 14 Sep 2024 14:33:33 -0700 Subject: [PATCH 1/7] Adding Lambda function code with test cases --- .../src/DataIngestFunction.cs | 57 +++++++ .../src/DataIngestFunction.csproj | 32 ++++ .../src/Models/DataModel.cs | 13 ++ .../DataIngestFunction/src/Program.cs | 26 +++ .../LambdaFunctionJsonSerializerContext.cs | 21 +++ .../test/DataIngestFunction.Tests.csproj | 22 +++ .../DataIngestFunction/test/FunctionTest.cs | 124 ++++++++++++++ .../src/DataProcessFunction.cs | 156 ++++++++++++++++++ .../src/DataProcessFunction.csproj | 34 ++++ .../src/Models/DataModel.cs | 20 +++ .../DataProcessFunction/src/Program.cs | 26 +++ .../LambdaFunctionJsonSerializerContext.cs | 24 +++ .../test/DataProcessFunction.Tests.csproj | 19 +++ .../DataProcessFunction/test/FunctionTest.cs | 90 ++++++++++ 14 files changed, 664 insertions(+) create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/DataIngestFunction.cs create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/DataIngestFunction.csproj create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Models/DataModel.cs create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Program.cs create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Serialization/LambdaFunctionJsonSerializerContext.cs create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/test/DataIngestFunction.Tests.csproj create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/test/FunctionTest.cs create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/DataProcessFunction.cs create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/DataProcessFunction.csproj create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/Models/DataModel.cs create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/Program.cs create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/Serialization/LambdaFunctionJsonSerializerContext.cs create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/test/DataProcessFunction.Tests.csproj create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/test/FunctionTest.cs diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/DataIngestFunction.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/DataIngestFunction.cs new file mode 100644 index 000000000..b7d93d833 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/DataIngestFunction.cs @@ -0,0 +1,57 @@ +using Amazon.Lambda.Core; +using Amazon.Kinesis; +using Amazon.Kinesis.Model; +using System.Text.Json; +using DataIngestFunction.Models; +using DataIngestFunction.Serialization; +using Microsoft.Extensions.Configuration; + +namespace DataIngestFunction +{ + public class DataIngestFunction(IConfigurationRoot? configuration = null) + { + private const string KinesisStreamEnvName = "KINESIS_STREAM_NAME"; + private readonly AmazonKinesisClient _kinesisClient = new(); + private readonly string _streamName = + (configuration != null ? configuration[KinesisStreamEnvName] : Environment.GetEnvironmentVariable(KinesisStreamEnvName)) + ?? throw new ArgumentException(KinesisStreamEnvName); + + public async Task FunctionHandler(DataModel data, ILambdaContext context) + { + if (data == null) + { + context.Logger.LogWarning($"No data received"); + return string.Empty; + } + + try + { + var jsonData = JsonSerializer.Serialize(data, LambdaFunctionJsonSerializerContext.Default.DataModel); + context.Logger.LogInformation($"Putting data: {jsonData} on stream:{_streamName}"); + var result = await PutRecordToKinesisStream(jsonData); + + context.Logger.LogInformation($"Data ingested successfully. Sequence number: {result.SequenceNumber}"); + return result.SequenceNumber; + } + catch (Exception ex) + { + context.Logger.LogError($"Error ingesting data: {ex.Message}"); + return string.Empty; + } + } + + private async Task PutRecordToKinesisStream(string data) + { + var recordBytes = System.Text.Encoding.UTF8.GetBytes(data); + + var request = new PutRecordRequest + { + StreamName = _streamName, + PartitionKey = Guid.NewGuid().ToString(), + Data = new MemoryStream(recordBytes) + }; + + return await _kinesisClient.PutRecordAsync(request); + } + } + } \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/DataIngestFunction.csproj b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/DataIngestFunction.csproj new file mode 100644 index 000000000..bbc675590 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/DataIngestFunction.csproj @@ -0,0 +1,32 @@ + + + exe + net8.0 + enable + enable + true + Lambda + + true + + + true + + true + partial + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Models/DataModel.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Models/DataModel.cs new file mode 100644 index 000000000..7e8768b5f --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Models/DataModel.cs @@ -0,0 +1,13 @@ +namespace DataIngestFunction.Models +{ + /// + /// This class represents the data model for the data ingested into the Kinesis stream. + /// + public class DataModel + { + public string? Id { get; set; } + public DateTime Timestamp { get; set; } + public int Value { get; set; } + public string? Category { get; set; } + } +} \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Program.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Program.cs new file mode 100644 index 000000000..174af0964 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Program.cs @@ -0,0 +1,26 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using DataIngestFunction.Models; +using DataIngestFunction.Serialization; + +namespace DataIngestFunction +{ + public class Program() + { + /// + /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It + /// initializes the .NET Lambda runtime client passing in the function handler to invoke for each Lambda event and + /// the JSON serializer to use for converting Lambda JSON format to the .NET types. + /// + private static async Task Main() + { + var dataIngestFunction = new DataIngestFunction(); + + Func> handler = dataIngestFunction.FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + } + } +} \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Serialization/LambdaFunctionJsonSerializerContext.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Serialization/LambdaFunctionJsonSerializerContext.cs new file mode 100644 index 000000000..f6d0cd418 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Serialization/LambdaFunctionJsonSerializerContext.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; +using DataIngestFunction.Models; + +namespace DataIngestFunction.Serialization +{ + /// + /// This class is used to register the input event and return type for the FunctionHandler method with the System.Text.Json source generator. + /// There must be a JsonSerializable attribute for each type used as the input and return type or a runtime error will occur + /// from the JSON serializer unable to find the serialization information for unknown types. + /// + [JsonSourceGenerationOptions] + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(DataModel))] + public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext + { + // By using this partial class derived from JsonSerializerContext, we can generate reflection free JSON Serializer code at compile time + // which can deserialize our class and properties. However, we must attribute this class to tell it what types to generate serialization code for. + // See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-source-generation + } + +} \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/test/DataIngestFunction.Tests.csproj b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/test/DataIngestFunction.Tests.csproj new file mode 100644 index 000000000..265bb49f9 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/test/DataIngestFunction.Tests.csproj @@ -0,0 +1,22 @@ + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/test/FunctionTest.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/test/FunctionTest.cs new file mode 100644 index 000000000..3eb29a867 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/test/FunctionTest.cs @@ -0,0 +1,124 @@ +using Xunit; +using Amazon.Lambda.TestUtilities; +using Microsoft.Extensions.Configuration; +using DataIngestFunction.Models; +using Amazon.SQS; +using Amazon.SQS.Model; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; + +namespace DataIngestFunction.Tests; + +public class FunctionTest +{ + + [Fact] + public async Task TestFunction() + { + // Set Environment avriables using ConfigurationBuilder + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "KINESIS_STREAM_NAME", "AnalyticsDataStream" } + }) + .Build(); + + var context = new TestLambdaContext(); + var function = new DataIngestFunction(config); + var data = GenerateRandomData(); + + var returnValue = await function.FunctionHandler(data, context); + Assert.NotEmpty(returnValue); + + var testLogger = context.Logger as TestLambdaLogger; + Assert.Contains("Data ingested successfully. Sequence number", testLogger!.Buffer.ToString()); + + // Wait for a while and check record in DynamoDB + await Task.Delay(TimeSpan.FromSeconds(5)); + + // Check record in DynamoDB + var dynamoDbClient = new AmazonDynamoDBClient(); + var tableName = "processed-data-table"; + var id = data.Id; + var getItemRequest = new GetItemRequest + { + TableName = tableName, + Key = new Dictionary + { + { "Id", new AttributeValue { S = id } } + } + }; + + var getItemResponse = await dynamoDbClient.GetItemAsync(getItemRequest); + Assert.NotNull(getItemResponse.Item); + } + + [Fact] + public async Task TestMalformedDataIngestion() + { + // Set Environment variables using ConfigurationBuilder + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "KINESIS_STREAM_NAME", "AnalyticsDataStream" } + }) + .Build(); + + var context = new TestLambdaContext(); + var function = new DataIngestFunction(config); + var malformedData = GenerateMalformedData(); + + var returnValue = await function.FunctionHandler(malformedData, context); + Assert.NotEmpty(returnValue); + + // Check record in SQS + var sqsClient = new AmazonSQSClient(); + var queueUrl = "kinesis-lambda-dlq"; + List messages = []; + + while (true) + { + // Get latest one of the messages from the queue + var receiveMessageRequest = new ReceiveMessageRequest + { + QueueUrl = queueUrl, + MaxNumberOfMessages = 10, + WaitTimeSeconds = 10 // Set to 0 to receive immediately + }; + var response = await sqsClient.ReceiveMessageAsync(receiveMessageRequest); + if (response.Messages.Count > 0) + { + messages.AddRange(response.Messages.Select(m => m.Body)); + } + else + { + break; + } + } + + Assert.True(messages.Count > 0); + Assert.Single(messages, m => m.Contains(returnValue)); + } + + private static DataModel GenerateRandomData() + { + return new DataModel + { + Id = Guid.NewGuid().ToString(), + Timestamp = DateTime.UtcNow, + Value = new Random().Next(1, 100), + Category = new[] { "A", "B", "C" }[new Random().Next(3)] + }; + } + + private static DataModel GenerateMalformedData() + { + return new DataModel + { + Id = null, + Timestamp = DateTime.UtcNow, + Value = -1, + Category = "InvalidCategory" + }; + } +} \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/DataProcessFunction.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/DataProcessFunction.cs new file mode 100644 index 000000000..aeea16343 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/DataProcessFunction.cs @@ -0,0 +1,156 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.KinesisEvents; +using System.Text.Json; +using DataProcessFunction.Models; +using DataProcessFunction.Serialization; +using Amazon.DynamoDBv2; +using Microsoft.Extensions.Configuration; +using Amazon.DynamoDBv2.Model; + +namespace DataProcessFunction +{ + public class DataProcessFunction(IConfigurationRoot? configuration = null) + { + private const string ProcessedTableEnvName = "PROCESSED_TABLE_NAME"; + private const string ErrorTableEnvName = "ERROR_TABLE_NAME"; + + private readonly IAmazonDynamoDB _dynamoDbClient = new AmazonDynamoDBClient(); + private readonly string _processedTableName = + (configuration != null ? configuration[ProcessedTableEnvName] : Environment.GetEnvironmentVariable(ProcessedTableEnvName)) + ?? throw new ArgumentException(ProcessedTableEnvName); + private readonly string _errorTableName = + (configuration != null ? configuration[ErrorTableEnvName] : Environment.GetEnvironmentVariable(ErrorTableEnvName)) + ?? throw new ArgumentException(ErrorTableEnvName); + + public async Task FunctionHandler(KinesisEvent kinesisEvent, ILambdaContext context) + { + if (kinesisEvent.Records.Count == 0) + { + context.Logger.LogInformation("Empty Kinesis Event received"); + return new StreamsEventResponse(); + } + + foreach (var record in kinesisEvent.Records) + { + try + { + string recordData = GetRecordContents(record.Kinesis); + context.Logger.LogInformation($"Processing record: {recordData}"); + + var data = JsonSerializer.Deserialize(recordData, LambdaFunctionJsonSerializerContext.Default.DataModel); + if (data == null) + { + context.Logger.LogWarning("Failed to deserialize record data"); + continue; + } + + // Process the data (e.g., aggregate, transform) + var processedData = ProcessData(data); + + // Here you would typically send the processed data to a storage solution + // that PowerBI can connect to, such as SQL Database, Cosmos DB, or blob storage. + await StoreProcessedDataAsync(processedData, context); + } + catch (Exception ex) + { + context.Logger.LogError($"Error processing Kinesis event: {ex.Message}"); + + // Log Error in DynamoDB + await LogError(record.Kinesis.SequenceNumber, ex, context); + + // Let Kinesis know that the record failed to process + return new StreamsEventResponse + { + BatchItemFailures = + [ + new StreamsEventResponse.BatchItemFailure { ItemIdentifier = record.Kinesis.SequenceNumber } + ] + }; + } + } + + context.Logger.LogInformation($"Successfully processed {kinesisEvent.Records.Count} records."); + return new StreamsEventResponse(); + } + + private static string GetRecordContents(KinesisEvent.Record streamRecord) + { + using var reader = new StreamReader(streamRecord.Data); + return reader.ReadToEnd(); + } + + private static ProcessedDataModel ProcessData(DataModel data) + { + // Implement your data processing logic here + return new ProcessedDataModel + { + Id = data.Id, + Timestamp = data.Timestamp, + Value = data.Value, + Category = data.Category, + ProcessedValue = data.Value * 2, + ProcessedTimestamp = DateTime.UtcNow.ToString("o") + }; + } + + private async Task StoreProcessedDataAsync(ProcessedDataModel data, ILambdaContext context) + { + ArgumentNullException.ThrowIfNull(data, nameof(data)); + + try + { + var serializedData = JsonSerializer.Serialize(data, LambdaFunctionJsonSerializerContext.Default.ProcessedDataModel); + + // Implement logic to store processed data + // This could be inserting into a database, writing to blob storage, etc. + context.Logger.LogInformation($"Storing processed data: {serializedData}"); + + var request = new PutItemRequest + { + TableName = _processedTableName, + Item = new Dictionary + { + ["Id"] = new AttributeValue { S = data.Id }, + ["Timestamp"] = new AttributeValue { S = data.Timestamp.ToString("o") }, + ["Value"] = new AttributeValue { N = data.Value.ToString() }, + ["Category"] = new AttributeValue { S = data.Category }, + ["ProcessedValue"] = new AttributeValue { N = data.ProcessedValue.ToString() }, + ["ProcessedTimestamp"] = new AttributeValue { S = data.ProcessedTimestamp } + } + }; + + await _dynamoDbClient.PutItemAsync(request); + } + catch (Exception ex) + { + context.Logger.LogError($"Error storing processed data: {ex.Message} for RecordId: {data.Id}"); + throw; + } + } + + private async Task LogError(string sequenceNumber, Exception ex, ILambdaContext context) + { + try + { + var request = new PutItemRequest + { + TableName = _errorTableName, + Item = new Dictionary + { + ["ErrorId"] = new AttributeValue { S = Guid.NewGuid().ToString() }, + ["Timestamp"] = new AttributeValue { N = DateTime.UtcNow.Ticks.ToString() }, + ["SequenceNumber"] = new AttributeValue { S = sequenceNumber }, + ["ErrorMessage"] = new AttributeValue { S = ex.Message }, + ["StackTrace"] = new AttributeValue { S = ex.StackTrace } + } + }; + + await _dynamoDbClient.PutItemAsync(request); + } + catch (Exception logEx) + { + context.Logger.LogError($"Error logging error: {logEx.Message}"); + } + } + } +} \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/DataProcessFunction.csproj b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/DataProcessFunction.csproj new file mode 100644 index 000000000..938bbce7a --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/DataProcessFunction.csproj @@ -0,0 +1,34 @@ + + + exe + net8.0 + enable + enable + true + Lambda + + true + + + true + + true + partial + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/Models/DataModel.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/Models/DataModel.cs new file mode 100644 index 000000000..6c298e342 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/Models/DataModel.cs @@ -0,0 +1,20 @@ +namespace DataProcessFunction.Models +{ + /// + /// This class represents the data model for the data ingested into the Kinesis stream. + /// + public class DataModel + { + public string? Id { get; set; } + public DateTime Timestamp { get; set; } + public int Value { get; set; } + public string? Category { get; set; } + } + + public class ProcessedDataModel : DataModel + { + public int ProcessedValue { get; set; } + + public required string ProcessedTimestamp { get; set; } + } +} \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/Program.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/Program.cs new file mode 100644 index 000000000..98fe8f8cc --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/Program.cs @@ -0,0 +1,26 @@ +using Amazon.Lambda.Core; +using Amazon.Lambda.KinesisEvents; +using Amazon.Lambda.RuntimeSupport; +using Amazon.Lambda.Serialization.SystemTextJson; +using DataProcessFunction.Serialization; + +namespace DataProcessFunction +{ + public class Program() + { + /// + /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It + /// initializes the .NET Lambda runtime client passing in the function handler to invoke for each Lambda event and + /// the JSON serializer to use for converting Lambda JSON format to the .NET types. + /// + private static async Task Main() + { + var dataProcessFunction = new DataProcessFunction(); + + Func> handler = dataProcessFunction.FunctionHandler; + await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer()) + .Build() + .RunAsync(); + } + } +} \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/Serialization/LambdaFunctionJsonSerializerContext.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/Serialization/LambdaFunctionJsonSerializerContext.cs new file mode 100644 index 000000000..c15bab58e --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/Serialization/LambdaFunctionJsonSerializerContext.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; +using Amazon.Lambda.KinesisEvents; +using DataProcessFunction.Models; + +namespace DataProcessFunction.Serialization +{ + /// + /// This class is used to register the input event and return type for the FunctionHandler method with the System.Text.Json source generator. + /// There must be a JsonSerializable attribute for each type used as the input and return type or a runtime error will occur + /// from the JSON serializer unable to find the serialization information for unknown types. + /// + [JsonSourceGenerationOptions] + [JsonSerializable(typeof(bool))] + [JsonSerializable(typeof(DataModel))] + [JsonSerializable(typeof(ProcessedDataModel))] + [JsonSerializable(typeof(KinesisEvent))] + [JsonSerializable(typeof(StreamsEventResponse))] + public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext + { + // By using this partial class derived from JsonSerializerContext, we can generate reflection free JSON Serializer code at compile time + // which can deserialize our class and properties. However, we must attribute this class to tell it what types to generate serialization code for. + // See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-source-generation + } +} \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/test/DataProcessFunction.Tests.csproj b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/test/DataProcessFunction.Tests.csproj new file mode 100644 index 000000000..60992d8a1 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/test/DataProcessFunction.Tests.csproj @@ -0,0 +1,19 @@ + + + net8.0 + enable + enable + true + + + + + + + + + + + + + \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/test/FunctionTest.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/test/FunctionTest.cs new file mode 100644 index 000000000..0b6a23d73 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/test/FunctionTest.cs @@ -0,0 +1,90 @@ +using System.Text; +using Xunit; +using Amazon.Lambda.KinesisEvents; +using Amazon.Lambda.TestUtilities; +using DataProcessFunction.Models; +using System.Text.Json; +using DataProcessFunction.Serialization; +using Microsoft.Extensions.Configuration; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; + +namespace DataProcessFunction.Tests; + +public class FunctionTest +{ + [Fact] + public async Task TestFunction() + { + // Set Environment avriables using ConfigurationBuilder + var config = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + { "PROCESSED_TABLE_NAME", "processed-data-table" }, + { "ERROR_TABLE_NAME", "error-log-table" } + }) + .Build(); + + var data = GenerateRandomData(); + var deserializedData = JsonSerializer.Serialize(data, LambdaFunctionJsonSerializerContext.Default.DataModel); + + KinesisEvent evnt = new() + { + Records = + [ + new KinesisEvent.KinesisEventRecord + { + AwsRegion = "us-west-2", + Kinesis = new KinesisEvent.Record + { + ApproximateArrivalTimestamp = DateTime.Now, + Data = new MemoryStream(Encoding.UTF8.GetBytes(deserializedData)), + PartitionKey = Guid.NewGuid().ToString(), + SequenceNumber = "1", + KinesisSchemaVersion = "1.0", + EncryptionType = Amazon.Kinesis.EncryptionType.NONE + } + } + ] + }; + + var context = new TestLambdaContext(); + + var dataProcessFunction = new DataProcessFunction(config); + var response = await dataProcessFunction.FunctionHandler(evnt, context); + Assert.NotNull(response); + Assert.Null(response.BatchItemFailures); + + var testLogger = context.Logger as TestLambdaLogger; + + var expectedLogValue = $"Processing record: {deserializedData}"; + Assert.Contains(expectedLogValue, testLogger!.Buffer.ToString()); + + // Check record in DynamoDB + var dynamoDbClient = new AmazonDynamoDBClient(); + var tableName = "processed-data-table"; + var id = data.Id; + var getItemRequest = new GetItemRequest + { + TableName = tableName, + Key = new Dictionary + { + { "Id", new AttributeValue { S = id } } + } + }; + + var getItemResponse = await dynamoDbClient.GetItemAsync(getItemRequest); + Assert.NotNull(getItemResponse.Item); + } + + private static DataModel GenerateRandomData() + { + return new DataModel + { + Id = Guid.NewGuid().ToString(), + Timestamp = DateTime.UtcNow, + Value = new Random().Next(1, 100), + Category = new[] { "A", "B", "C" }[new Random().Next(3)] + }; + } +} \ No newline at end of file From c59b18957efbaad3827b68ea62529b5dacce1573 Mon Sep 17 00:00:00 2001 From: Tejas Vora Date: Sat, 14 Sep 2024 14:34:21 -0700 Subject: [PATCH 2/7] Adding CDK code for Kinesis, SQS DLQ, DynamoDB, Lambda functions with Kinesis Event Source. --- .../cdk.json | 70 +++++++ .../src/KinesisLambdaDynamoDbCdk.sln | 60 ++++++ .../GlobalSuppressions.cs | 1 + .../KinesisLambdaDynamoDbCdk.csproj | 21 ++ .../KinesisLambdaDynamoDbCdkStack.cs | 198 ++++++++++++++++++ .../src/KinesisLambdaDynamoDbCdk/Program.cs | 41 ++++ 6 files changed, 391 insertions(+) create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/cdk.json create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk.sln create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/GlobalSuppressions.cs create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/KinesisLambdaDynamoDbCdk.csproj create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/KinesisLambdaDynamoDbCdkStack.cs create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/Program.cs diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/cdk.json b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/cdk.json new file mode 100644 index 000000000..bd8acd6a0 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/cdk.json @@ -0,0 +1,70 @@ +{ + "app": "dotnet run --project src/KinesisLambdaDynamoDbCdk/KinesisLambdaDynamoDbCdk.csproj", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "src/*/obj", + "src/*/bin", + "src/*.sln", + "src/*/GlobalSuppressions.cs", + "src/*/*.csproj" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false + } +} diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk.sln b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk.sln new file mode 100644 index 000000000..44f606fbf --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk.sln @@ -0,0 +1,60 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KinesisLambdaDynamoDbCdk", "KinesisLambdaDynamoDbCdk\KinesisLambdaDynamoDbCdk.csproj", "{4643415E-700B-481D-B952-7B8B067A02A7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LambdaFunctions", "LambdaFunctions", "{235C9FA2-8184-482E-A72F-425DEAB815F9}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DataIngestFunction", "DataIngestFunction", "{D36282D2-74CD-49BA-AAD0-2A37280B7258}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataIngestFunction", "LambdaFunctions\DataIngestFunction\src\DataIngestFunction.csproj", "{9D79A0C7-BC46-4F58-80F1-1E40C7A637BD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataIngestFunction.Tests", "LambdaFunctions\DataIngestFunction\test\DataIngestFunction.Tests.csproj", "{C8542B58-A29E-46D1-BB0F-97C0825384B8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DataProcessFunction", "DataProcessFunction", "{C085343F-7948-47DA-9F12-C2B126577FA7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataProcessFunction", "LambdaFunctions\DataProcessFunction\src\DataProcessFunction.csproj", "{84D60F1B-1CDE-4134-8946-EFDDA35EFE4A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataProcessFunction.Tests", "LambdaFunctions\DataProcessFunction\test\DataProcessFunction.Tests.csproj", "{1E05AB22-8898-43D9-8FC5-68D123A7A536}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4643415E-700B-481D-B952-7B8B067A02A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4643415E-700B-481D-B952-7B8B067A02A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4643415E-700B-481D-B952-7B8B067A02A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4643415E-700B-481D-B952-7B8B067A02A7}.Release|Any CPU.Build.0 = Release|Any CPU + {9D79A0C7-BC46-4F58-80F1-1E40C7A637BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9D79A0C7-BC46-4F58-80F1-1E40C7A637BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9D79A0C7-BC46-4F58-80F1-1E40C7A637BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9D79A0C7-BC46-4F58-80F1-1E40C7A637BD}.Release|Any CPU.Build.0 = Release|Any CPU + {C8542B58-A29E-46D1-BB0F-97C0825384B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8542B58-A29E-46D1-BB0F-97C0825384B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8542B58-A29E-46D1-BB0F-97C0825384B8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8542B58-A29E-46D1-BB0F-97C0825384B8}.Release|Any CPU.Build.0 = Release|Any CPU + {84D60F1B-1CDE-4134-8946-EFDDA35EFE4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {84D60F1B-1CDE-4134-8946-EFDDA35EFE4A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84D60F1B-1CDE-4134-8946-EFDDA35EFE4A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {84D60F1B-1CDE-4134-8946-EFDDA35EFE4A}.Release|Any CPU.Build.0 = Release|Any CPU + {1E05AB22-8898-43D9-8FC5-68D123A7A536}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1E05AB22-8898-43D9-8FC5-68D123A7A536}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1E05AB22-8898-43D9-8FC5-68D123A7A536}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1E05AB22-8898-43D9-8FC5-68D123A7A536}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D36282D2-74CD-49BA-AAD0-2A37280B7258} = {235C9FA2-8184-482E-A72F-425DEAB815F9} + {9D79A0C7-BC46-4F58-80F1-1E40C7A637BD} = {D36282D2-74CD-49BA-AAD0-2A37280B7258} + {C8542B58-A29E-46D1-BB0F-97C0825384B8} = {D36282D2-74CD-49BA-AAD0-2A37280B7258} + {C085343F-7948-47DA-9F12-C2B126577FA7} = {235C9FA2-8184-482E-A72F-425DEAB815F9} + {84D60F1B-1CDE-4134-8946-EFDDA35EFE4A} = {C085343F-7948-47DA-9F12-C2B126577FA7} + {1E05AB22-8898-43D9-8FC5-68D123A7A536} = {C085343F-7948-47DA-9F12-C2B126577FA7} + EndGlobalSection +EndGlobal diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/GlobalSuppressions.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/GlobalSuppressions.cs new file mode 100644 index 000000000..26233fcb5 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/GlobalSuppressions.cs @@ -0,0 +1 @@ +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Potential Code Quality Issues", "RECS0026:Possible unassigned object created by 'new'", Justification = "Constructs add themselves to the scope in which they are created")] diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/KinesisLambdaDynamoDbCdk.csproj b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/KinesisLambdaDynamoDbCdk.csproj new file mode 100644 index 000000000..4b547a5e7 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/KinesisLambdaDynamoDbCdk.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + + Major + + + + + + + + + + + + + + diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/KinesisLambdaDynamoDbCdkStack.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/KinesisLambdaDynamoDbCdkStack.cs new file mode 100644 index 000000000..3d525a462 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/KinesisLambdaDynamoDbCdkStack.cs @@ -0,0 +1,198 @@ +using Amazon.CDK; +using Amazon.CDK.AWS.Kinesis; +using Amazon.CDK.AWS.Lambda; +using Amazon.CDK.AWS.Lambda.EventSources; +using Constructs; +using System.Collections.Generic; +using Amazon.CDK.AWS.Logs; +using System.Runtime.InteropServices; +using Amazon.CDK.AWS.DynamoDB; +using Attribute = Amazon.CDK.AWS.DynamoDB.Attribute; +using Amazon.CDK.AWS.SQS; + +namespace KinesisLambdaDynamoDbCdk +{ + public class KinesisLambdaDynamoDbCdkStack : Stack + { + public KinesisLambdaDynamoDbCdkStack(Construct scope, string id, IStackProps props = null) : base(scope, id, props) + { + // Create Kinesis Data Stream + var dataStream = new Stream(this, "AnalyticsDataStream", new StreamProps + { + StreamName = "AnalyticsDataStream", + ShardCount = 1, + RemovalPolicy = RemovalPolicy.DESTROY, + RetentionPeriod = Duration.Days(1), + Encryption = StreamEncryption.MANAGED, + StreamMode = StreamMode.PROVISIONED + }); + + // Create DynamoDB table for processed data + var table = new Table(this, "ProcessedDataTable", new TableProps + { + PartitionKey = new Attribute { Name = "Id", Type = AttributeType.STRING }, + BillingMode = BillingMode.PAY_PER_REQUEST, + TableName = "processed-data-table", + RemovalPolicy = RemovalPolicy.DESTROY, + DeletionProtection = false, + PointInTimeRecovery = false, + Encryption = TableEncryption.AWS_MANAGED + }); + + // Create DynamoDB table for error logging + var errorTable = new Table(this, "ErrorLogTable", new TableProps + { + PartitionKey = new Attribute { Name = "ErrorId", Type = AttributeType.STRING }, + SortKey = new Attribute { Name = "Timestamp", Type = AttributeType.NUMBER }, + BillingMode = BillingMode.PAY_PER_REQUEST, + TableName = "error-log-table", + RemovalPolicy = RemovalPolicy.DESTROY, + DeletionProtection = false, + PointInTimeRecovery = false, + Encryption = TableEncryption.AWS_MANAGED + }); + + // Create Dead Letter Queue + var dlq = new Queue(this, "DeadLetterQueue", new QueueProps + { + QueueName = "kinesis-lambda-dlq" + }); + + // Build options for Lambda functions + var buildOption = new BundlingOptions() + { + Image = Runtime.DOTNET_8.BundlingImage, + User = "root", + OutputType = BundlingOutput.ARCHIVED, + Command = [ + "/bin/sh", + "-c", + "dotnet tool install -g Amazon.Lambda.Tools && " + + "dotnet build && " + + "dotnet lambda package " + + "--function-architecture " + (RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.X64 ? "x86_64" : "arm64") + " " + + "--output-package /asset-output/function.zip" + ], + + }; + + // Create Lambda function for data ingestion + var ingestFunction = new Function(this, "DataIngestFunction", new FunctionProps + { + Runtime = Runtime.DOTNET_8, + MemorySize = 512, + LogRetention = RetentionDays.ONE_DAY, + Timeout = Duration.Seconds(30), + Architecture = RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.X64 + ? Amazon.CDK.AWS.Lambda.Architecture.X86_64 + : Amazon.CDK.AWS.Lambda.Architecture.ARM_64, + FunctionName = "DataIngestFunction", + Description = "Function to ingest data into Kinesis Data Stream", + + Handler = "DataIngestFunction", + Code = Code.FromAsset( + "src/LambdaFunctions/DataIngestFunction/src", + new Amazon.CDK.AWS.S3.Assets.AssetOptions + { + Bundling = buildOption + }), + Environment = new Dictionary + { + {"KINESIS_STREAM_NAME", dataStream.StreamName} + } + }); + + // Grant permissions to the ingest function to write to Kinesis + dataStream.GrantWrite(ingestFunction); + + // Create Lambda function for data processing + var processFunction = new Function(this, "DataProcessFunction", new FunctionProps + { + Runtime = Runtime.DOTNET_8, + MemorySize = 512, + LogRetention = RetentionDays.ONE_DAY, + Timeout = Duration.Seconds(300), + Architecture = RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.X64 + ? Amazon.CDK.AWS.Lambda.Architecture.X86_64 + : Amazon.CDK.AWS.Lambda.Architecture.ARM_64, + FunctionName = "DataProcessFunction", + Description = "Function to process data from Kinesis Data Stream", + + Handler = "DataProcessFunction", + Code = Code.FromAsset( + "src/LambdaFunctions/DataProcessFunction/src", + new Amazon.CDK.AWS.S3.Assets.AssetOptions + { + Bundling = buildOption + }), + + Environment = new Dictionary + { + ["PROCESSED_TABLE_NAME"] = table.TableName, + ["ERROR_TABLE_NAME"] = errorTable.TableName + }, + RetryAttempts = 0 + }); + + // Add Kinesis as an event source for the process function + processFunction.AddEventSource(new KinesisEventSource(dataStream, new KinesisEventSourceProps + { + BatchSize = 100, + StartingPosition = StartingPosition.LATEST, + BisectBatchOnError = false, + Enabled = true, + RetryAttempts = 1, + ParallelizationFactor = 1, + MaxBatchingWindow = Duration.Seconds(0), + OnFailure = new SqsDlq(dlq), + ReportBatchItemFailures = true + })); + + // Grant permissions + dataStream.GrantRead(processFunction); + table.GrantWriteData(processFunction); + errorTable.GrantWriteData(processFunction); + + // Output the stream name + _ = new CfnOutput(this, "KinesisStreamName", new CfnOutputProps + { + Value = dataStream.StreamName, + Description = "Kinesis Data Stream Name", + }); + + // Output the ingest function name + _ = new CfnOutput(this, "DataIngestFunctionName", new CfnOutputProps + { + Value = ingestFunction.FunctionName, + Description = "Ingest Function Name", + }); + + // Output the process function name + _ = new CfnOutput(this, "DataProcessFunctionName", new CfnOutputProps + { + Value = processFunction.FunctionName, + Description = "Process Function Name", + }); + + _ = new CfnOutput(this, "DeadLetterQueueUrl", new CfnOutputProps + { + Value = dlq.QueueUrl, + Description = "Dead Letter Queue URL" + }); + + // Output the processed data table name + _ = new CfnOutput(this, "ProcessedDataTableName", new CfnOutputProps + { + Value = table.TableName, + Description = "Processed Data Table Name", + }); + + // Output the error log table name + _ = new CfnOutput(this, "ErrorLogTableName", new CfnOutputProps + { + Value = errorTable.TableName, + Description = "Error Log Table Name", + }); + } + } +} \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/Program.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/Program.cs new file mode 100644 index 000000000..b291aef60 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/Program.cs @@ -0,0 +1,41 @@ +using Amazon.CDK; + +namespace KinesisLambdaDynamoDbCdk +{ + sealed class Program + { + public static void Main(string[] args) + { + var app = new App(); + _ = new KinesisLambdaDynamoDbCdkStack(app, "KinesisLambdaDynamoDbCdkStack", new StackProps + { + // If you don't specify 'env', this stack will be environment-agnostic. + // Account/Region-dependent features and context lookups will not work, + // but a single synthesized template can be deployed anywhere. + + // Uncomment the next block to specialize this stack for the AWS Account + // and Region that are implied by the current CLI configuration. + /* + Env = new Amazon.CDK.Environment + { + Account = System.Environment.GetEnvironmentVariable("CDK_DEFAULT_ACCOUNT"), + Region = System.Environment.GetEnvironmentVariable("CDK_DEFAULT_REGION"), + } + */ + + // Uncomment the next block if you know exactly what Account and Region you + // want to deploy the stack to. + /* + Env = new Amazon.CDK.Environment + { + Account = "123456789012", + Region = "us-east-1", + } + */ + + // For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html + }); + app.Synth(); + } + } +} From 40b3d120a3774ab5b5c28fda6f42d7be61535405 Mon Sep 17 00:00:00 2001 From: Tejas Vora Date: Sat, 14 Sep 2024 14:39:59 -0700 Subject: [PATCH 3/7] Adding readme, example-pattern, architecture diagram and profile picture --- .../.gitignore | 342 ++++++++++++++++++ .../README.md | 106 ++++++ .../TejasVora.jpg | Bin 0 -> 11448 bytes .../example-pattern.json | 64 ++++ .../kinesis-lambda-dynamodb-pipeline.drawio | 92 +++++ .../kinesis-lambda-dynamodb-pipeline.png | Bin 0 -> 67520 bytes 6 files changed, 604 insertions(+) create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/.gitignore create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/README.md create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/TejasVora.jpg create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/example-pattern.json create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/kinesis-lambda-dynamodb-pipeline.drawio create mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/kinesis-lambda-dynamodb-pipeline.png diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/.gitignore b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/.gitignore new file mode 100644 index 000000000..a4609e758 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/.gitignore @@ -0,0 +1,342 @@ +# CDK asset staging directory +.cdk.staging +cdk.out + +# Created by https://www.gitignore.io/api/csharp + +### Csharp ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + + +# End of https://www.gitignore.io/api/csharp \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/README.md b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/README.md new file mode 100644 index 000000000..92966a0dc --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/README.md @@ -0,0 +1,106 @@ +# Real-time Data Pipeline with Kinesis, Lambda, and DynamoDB using AWS CDK .NET + +This pattern demonstrates how to create a serverless real-time data pipeline using Amazon Kinesis for data ingestion, AWS Lambda for processing, Amazon DynamoDB for data storage and error logging, and Amazon SQS as a Dead Letter Queue for handling failed records. The pattern includes robust error handling and retry mechanisms, and is implemented using AWS CDK with .NET. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/kinesis-lambda-dynamodb-pipeline-dotnet-cdk + +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. + +## Architecture + + +## 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 and NPM](https://nodejs.org/en/download/) installed +* [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) (AWS CDK) installed +* [.NET](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) (.NET 8.0) installed + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +2. Change the working directory to this pattern's directory: + ``` + cd kinesis-lambda-dynamodb-pipeline-dotnet-cdk + ``` +3. Build the .NET CDK project: + ``` + dotnet build src + ``` +4. Deploy the stack to your default AWS account and region. The output of this command should give you the Kinesis stream name: + ``` + cdk deploy + ``` +5. Other useful commands: + ``` + cdk diff compare deployed stack with current state + cdk synth emits the synthesized CloudFormation template + ``` + +## How it works + +This pattern creates a serverless real-time data pipeline: + +1. Data is ingested through an Amazon Kinesis Data Stream. +2. An AWS Lambda function is triggered by new records in the Kinesis stream. +3. The Lambda function processes the data and stores it in an Amazon DynamoDB table. +4. If any errors occur during processing, they are logged in a separate DynamoDB table for error tracking. The Lambda function will retry processing failed records up to maximum retry count. If a record consistently fails processing after these retries, it is automatically sent to an SQS Dead Letter Queue (DLQ) for further investigation and handling, ensuring no data is lost due to processing errors. + +The AWS CDK is used to define and deploy all the necessary AWS resources, including the Kinesis stream, Lambda function, DynamoDB tables, SQS Dead Letter Queue (DLQ) and associated IAM roles and permissions. + +## Testing + +1. Out new record into Kinesis stream. + + - Use the AWS CLI to put a record into the Kinesis stream (replace `` with the actual stream name from the CDK output): + + ``` + aws kinesis put-record \ + --stream-name \ + --cli-binary-format raw-in-base64-out \ + --partition-key 1 \ + --data '{ "Timestamp": "2024-09-13T23:06:55.934081Z", "Value": 81, "Category": "C" }' + ``` + - Use Lambda function to put a record into the Kinesis stream (replace `` with the actual data ingestion lambda function name from the CDK output): + ``` + aws lambda invoke \ + --cli-binary-format raw-in-base64-out \ + --function-name \ + --payload '{ "Timestamp": "2024-09-13T23:06:55.934081Z", "Value": 81, "Category": "C" }' \ + response.json + ``` + +2. Check the DynamoDB tables in the AWS Console: + - The "processed-data-table" should contain the processed record. + - If any errors occurred, they would be logged in the "error-log-table". + +3. You can also check the CloudWatch Logs for the Lambda function to see the processing details and any potential errors. + +4. To test error scenarios, you can intentionally send malformed data to the Kinesis stream and verify that it ends up in the Dead Letter Queue after the retry attempts. + +5. Additional unit and integration tests are located in the `test` directory under each Lambda function's directory. These tests can be run locally to verify the behavior of individual components: + ``` + dotnet test src + ``` +These tests cover various scenarios including successful processing, error handling, and DLQ interactions. + +6. For a more comprehensive end-to-end test, you can use the AWS Step Functions service to orchestrate a test workflow that includes putting records into Kinesis, waiting for processing, and then checking the results in DynamoDB and the DLQ. + +Remember to clean up any test data from your DynamoDB tables and SQS queues after testing to avoid unnecessary storage costs. + +## Cleanup + +1. Run the given command to delete the resources that were created. It might take some time for the CloudFormation stack to get deleted. + ``` + cdk destroy + ``` + +---- +Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/TejasVora.jpg b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/TejasVora.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c8d01afb80995d92862bdcee266362eb119b3802 GIT binary patch literal 11448 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

&j6dRz-;RQ@yw$}U--~kNCGzpHqXp?4nc(fR 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 W-lehij*DOOc-pExB)INz;eP<$`qB#k literal 0 HcmV?d00001 diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/example-pattern.json b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/example-pattern.json new file mode 100644 index 000000000..490f76ad9 --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/example-pattern.json @@ -0,0 +1,64 @@ +{ + "title": "Real-time Data Pipeline with Kinesis, Lambda, and DynamoDB", + "description": "Create a serverless real-time data pipeline using Amazon Kinesis, AWS Lambda, and Amazon DynamoDB with error handling", + "language": ".NET", + "level": "300", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates how to create a serverless real-time data pipeline using Amazon Kinesis for data ingestion, AWS Lambda for processing, Amazon DynamoDB for data storage and error logging, and Amazon SQS as a Dead Letter Queue for handling failed records. The pattern includes robust error handling and retry mechanisms, and is implemented using AWS CDK with .NET." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/kinesis-lambda-dynamodb-pipeline-dotnet-cdk", + "templateURL": "serverless-patterns/kinesis-lambda-dynamodb-pipeline-dotnet-cdk", + "projectFolder": "kinesis-lambda-dynamodb-pipeline-dotnet-cdk", + "templateFile": "/src/KinesisLambdaDynamoDbCdk/KinesisLambdaDynamoDbCdkStack.cs" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon Kinesis Data Streams", + "link": "https://docs.aws.amazon.com/streams/latest/dev/introduction.html" + }, + { + "text": "AWS Lambda", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/welcome.html" + }, + { + "text": "Amazon DynamoDB", + "link": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html" + }, + { + "text": "AWS CDK", + "link": "https://docs.aws.amazon.com/cdk/latest/guide/home.html" + } + ] + }, + "deploy": { + "text": [ + "cdk deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions, including how to put records into the Kinesis stream and verify processing in DynamoDB tables." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk destroy." + ] + }, + "authors": [ + { + "name": "Tejas Vora", + "image": "./TejasVora.jpg", + "bio": "Tejas Vora is a Senior Solutions Architect with Amazon Web Services.", + "linkedin": "tejas-vora-b4758a47" + } + ] +} \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/kinesis-lambda-dynamodb-pipeline.drawio b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/kinesis-lambda-dynamodb-pipeline.drawio new file mode 100644 index 000000000..ee74b4f1f --- /dev/null +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/kinesis-lambda-dynamodb-pipeline.drawio @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/kinesis-lambda-dynamodb-pipeline.png b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/kinesis-lambda-dynamodb-pipeline.png new file mode 100644 index 0000000000000000000000000000000000000000..f30023994a6e95b79988e8f7b32b5e4cf54e6e7f GIT binary patch literal 67520 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 UPAHe21qS#d37DP5!ume{3(Sn*K>z>% literal 0 HcmV?d00001 From 792711860586cbb2a4388ff002c1964b789be728 Mon Sep 17 00:00:00 2001 From: Tejas Vora Date: Fri, 11 Oct 2024 16:25:03 -0700 Subject: [PATCH 4/7] Removing DataIngestionFunction, DynamoDB table for error handling and DLQ. --- .../KinesisLambdaDynamoDbCdkStack.cs | 75 +------------------ 1 file changed, 1 insertion(+), 74 deletions(-) diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/KinesisLambdaDynamoDbCdkStack.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/KinesisLambdaDynamoDbCdkStack.cs index 3d525a462..11f8d14ef 100644 --- a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/KinesisLambdaDynamoDbCdkStack.cs +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk/KinesisLambdaDynamoDbCdkStack.cs @@ -8,7 +8,6 @@ using System.Runtime.InteropServices; using Amazon.CDK.AWS.DynamoDB; using Attribute = Amazon.CDK.AWS.DynamoDB.Attribute; -using Amazon.CDK.AWS.SQS; namespace KinesisLambdaDynamoDbCdk { @@ -39,25 +38,6 @@ public KinesisLambdaDynamoDbCdkStack(Construct scope, string id, IStackProps pro Encryption = TableEncryption.AWS_MANAGED }); - // Create DynamoDB table for error logging - var errorTable = new Table(this, "ErrorLogTable", new TableProps - { - PartitionKey = new Attribute { Name = "ErrorId", Type = AttributeType.STRING }, - SortKey = new Attribute { Name = "Timestamp", Type = AttributeType.NUMBER }, - BillingMode = BillingMode.PAY_PER_REQUEST, - TableName = "error-log-table", - RemovalPolicy = RemovalPolicy.DESTROY, - DeletionProtection = false, - PointInTimeRecovery = false, - Encryption = TableEncryption.AWS_MANAGED - }); - - // Create Dead Letter Queue - var dlq = new Queue(this, "DeadLetterQueue", new QueueProps - { - QueueName = "kinesis-lambda-dlq" - }); - // Build options for Lambda functions var buildOption = new BundlingOptions() { @@ -76,41 +56,11 @@ public KinesisLambdaDynamoDbCdkStack(Construct scope, string id, IStackProps pro }; - // Create Lambda function for data ingestion - var ingestFunction = new Function(this, "DataIngestFunction", new FunctionProps - { - Runtime = Runtime.DOTNET_8, - MemorySize = 512, - LogRetention = RetentionDays.ONE_DAY, - Timeout = Duration.Seconds(30), - Architecture = RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.X64 - ? Amazon.CDK.AWS.Lambda.Architecture.X86_64 - : Amazon.CDK.AWS.Lambda.Architecture.ARM_64, - FunctionName = "DataIngestFunction", - Description = "Function to ingest data into Kinesis Data Stream", - - Handler = "DataIngestFunction", - Code = Code.FromAsset( - "src/LambdaFunctions/DataIngestFunction/src", - new Amazon.CDK.AWS.S3.Assets.AssetOptions - { - Bundling = buildOption - }), - Environment = new Dictionary - { - {"KINESIS_STREAM_NAME", dataStream.StreamName} - } - }); - - // Grant permissions to the ingest function to write to Kinesis - dataStream.GrantWrite(ingestFunction); - // Create Lambda function for data processing var processFunction = new Function(this, "DataProcessFunction", new FunctionProps { Runtime = Runtime.DOTNET_8, MemorySize = 512, - LogRetention = RetentionDays.ONE_DAY, Timeout = Duration.Seconds(300), Architecture = RuntimeInformation.ProcessArchitecture == System.Runtime.InteropServices.Architecture.X64 ? Amazon.CDK.AWS.Lambda.Architecture.X86_64 @@ -128,8 +78,7 @@ public KinesisLambdaDynamoDbCdkStack(Construct scope, string id, IStackProps pro Environment = new Dictionary { - ["PROCESSED_TABLE_NAME"] = table.TableName, - ["ERROR_TABLE_NAME"] = errorTable.TableName + ["PROCESSED_TABLE_NAME"] = table.TableName }, RetryAttempts = 0 }); @@ -144,14 +93,12 @@ public KinesisLambdaDynamoDbCdkStack(Construct scope, string id, IStackProps pro RetryAttempts = 1, ParallelizationFactor = 1, MaxBatchingWindow = Duration.Seconds(0), - OnFailure = new SqsDlq(dlq), ReportBatchItemFailures = true })); // Grant permissions dataStream.GrantRead(processFunction); table.GrantWriteData(processFunction); - errorTable.GrantWriteData(processFunction); // Output the stream name _ = new CfnOutput(this, "KinesisStreamName", new CfnOutputProps @@ -160,13 +107,6 @@ public KinesisLambdaDynamoDbCdkStack(Construct scope, string id, IStackProps pro Description = "Kinesis Data Stream Name", }); - // Output the ingest function name - _ = new CfnOutput(this, "DataIngestFunctionName", new CfnOutputProps - { - Value = ingestFunction.FunctionName, - Description = "Ingest Function Name", - }); - // Output the process function name _ = new CfnOutput(this, "DataProcessFunctionName", new CfnOutputProps { @@ -174,25 +114,12 @@ public KinesisLambdaDynamoDbCdkStack(Construct scope, string id, IStackProps pro Description = "Process Function Name", }); - _ = new CfnOutput(this, "DeadLetterQueueUrl", new CfnOutputProps - { - Value = dlq.QueueUrl, - Description = "Dead Letter Queue URL" - }); - // Output the processed data table name _ = new CfnOutput(this, "ProcessedDataTableName", new CfnOutputProps { Value = table.TableName, Description = "Processed Data Table Name", }); - - // Output the error log table name - _ = new CfnOutput(this, "ErrorLogTableName", new CfnOutputProps - { - Value = errorTable.TableName, - Description = "Error Log Table Name", - }); } } } \ No newline at end of file From 562bda19d6e0a2ecd3b58e27f841f9f8a0800191 Mon Sep 17 00:00:00 2001 From: Tejas Vora Date: Fri, 11 Oct 2024 16:25:35 -0700 Subject: [PATCH 5/7] Removing DataIngestionFunction code. --- .../src/KinesisLambdaDynamoDbCdk.sln | 17 --- .../src/DataIngestFunction.cs | 57 -------- .../src/DataIngestFunction.csproj | 32 ----- .../src/Models/DataModel.cs | 13 -- .../DataIngestFunction/src/Program.cs | 26 ---- .../LambdaFunctionJsonSerializerContext.cs | 21 --- .../test/DataIngestFunction.Tests.csproj | 22 ---- .../DataIngestFunction/test/FunctionTest.cs | 124 ------------------ 8 files changed, 312 deletions(-) delete mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/DataIngestFunction.cs delete mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/DataIngestFunction.csproj delete mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Models/DataModel.cs delete mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Program.cs delete mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Serialization/LambdaFunctionJsonSerializerContext.cs delete mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/test/DataIngestFunction.Tests.csproj delete mode 100644 kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/test/FunctionTest.cs diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk.sln b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk.sln index 44f606fbf..ea171086d 100644 --- a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk.sln +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/KinesisLambdaDynamoDbCdk.sln @@ -7,12 +7,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KinesisLambdaDynamoDbCdk", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LambdaFunctions", "LambdaFunctions", "{235C9FA2-8184-482E-A72F-425DEAB815F9}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DataIngestFunction", "DataIngestFunction", "{D36282D2-74CD-49BA-AAD0-2A37280B7258}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataIngestFunction", "LambdaFunctions\DataIngestFunction\src\DataIngestFunction.csproj", "{9D79A0C7-BC46-4F58-80F1-1E40C7A637BD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataIngestFunction.Tests", "LambdaFunctions\DataIngestFunction\test\DataIngestFunction.Tests.csproj", "{C8542B58-A29E-46D1-BB0F-97C0825384B8}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DataProcessFunction", "DataProcessFunction", "{C085343F-7948-47DA-9F12-C2B126577FA7}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataProcessFunction", "LambdaFunctions\DataProcessFunction\src\DataProcessFunction.csproj", "{84D60F1B-1CDE-4134-8946-EFDDA35EFE4A}" @@ -29,14 +23,6 @@ Global {4643415E-700B-481D-B952-7B8B067A02A7}.Debug|Any CPU.Build.0 = Debug|Any CPU {4643415E-700B-481D-B952-7B8B067A02A7}.Release|Any CPU.ActiveCfg = Release|Any CPU {4643415E-700B-481D-B952-7B8B067A02A7}.Release|Any CPU.Build.0 = Release|Any CPU - {9D79A0C7-BC46-4F58-80F1-1E40C7A637BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9D79A0C7-BC46-4F58-80F1-1E40C7A637BD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9D79A0C7-BC46-4F58-80F1-1E40C7A637BD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9D79A0C7-BC46-4F58-80F1-1E40C7A637BD}.Release|Any CPU.Build.0 = Release|Any CPU - {C8542B58-A29E-46D1-BB0F-97C0825384B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C8542B58-A29E-46D1-BB0F-97C0825384B8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C8542B58-A29E-46D1-BB0F-97C0825384B8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C8542B58-A29E-46D1-BB0F-97C0825384B8}.Release|Any CPU.Build.0 = Release|Any CPU {84D60F1B-1CDE-4134-8946-EFDDA35EFE4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {84D60F1B-1CDE-4134-8946-EFDDA35EFE4A}.Debug|Any CPU.Build.0 = Debug|Any CPU {84D60F1B-1CDE-4134-8946-EFDDA35EFE4A}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -50,9 +36,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {D36282D2-74CD-49BA-AAD0-2A37280B7258} = {235C9FA2-8184-482E-A72F-425DEAB815F9} - {9D79A0C7-BC46-4F58-80F1-1E40C7A637BD} = {D36282D2-74CD-49BA-AAD0-2A37280B7258} - {C8542B58-A29E-46D1-BB0F-97C0825384B8} = {D36282D2-74CD-49BA-AAD0-2A37280B7258} {C085343F-7948-47DA-9F12-C2B126577FA7} = {235C9FA2-8184-482E-A72F-425DEAB815F9} {84D60F1B-1CDE-4134-8946-EFDDA35EFE4A} = {C085343F-7948-47DA-9F12-C2B126577FA7} {1E05AB22-8898-43D9-8FC5-68D123A7A536} = {C085343F-7948-47DA-9F12-C2B126577FA7} diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/DataIngestFunction.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/DataIngestFunction.cs deleted file mode 100644 index b7d93d833..000000000 --- a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/DataIngestFunction.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Amazon.Lambda.Core; -using Amazon.Kinesis; -using Amazon.Kinesis.Model; -using System.Text.Json; -using DataIngestFunction.Models; -using DataIngestFunction.Serialization; -using Microsoft.Extensions.Configuration; - -namespace DataIngestFunction -{ - public class DataIngestFunction(IConfigurationRoot? configuration = null) - { - private const string KinesisStreamEnvName = "KINESIS_STREAM_NAME"; - private readonly AmazonKinesisClient _kinesisClient = new(); - private readonly string _streamName = - (configuration != null ? configuration[KinesisStreamEnvName] : Environment.GetEnvironmentVariable(KinesisStreamEnvName)) - ?? throw new ArgumentException(KinesisStreamEnvName); - - public async Task FunctionHandler(DataModel data, ILambdaContext context) - { - if (data == null) - { - context.Logger.LogWarning($"No data received"); - return string.Empty; - } - - try - { - var jsonData = JsonSerializer.Serialize(data, LambdaFunctionJsonSerializerContext.Default.DataModel); - context.Logger.LogInformation($"Putting data: {jsonData} on stream:{_streamName}"); - var result = await PutRecordToKinesisStream(jsonData); - - context.Logger.LogInformation($"Data ingested successfully. Sequence number: {result.SequenceNumber}"); - return result.SequenceNumber; - } - catch (Exception ex) - { - context.Logger.LogError($"Error ingesting data: {ex.Message}"); - return string.Empty; - } - } - - private async Task PutRecordToKinesisStream(string data) - { - var recordBytes = System.Text.Encoding.UTF8.GetBytes(data); - - var request = new PutRecordRequest - { - StreamName = _streamName, - PartitionKey = Guid.NewGuid().ToString(), - Data = new MemoryStream(recordBytes) - }; - - return await _kinesisClient.PutRecordAsync(request); - } - } - } \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/DataIngestFunction.csproj b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/DataIngestFunction.csproj deleted file mode 100644 index bbc675590..000000000 --- a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/DataIngestFunction.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - exe - net8.0 - enable - enable - true - Lambda - - true - - - true - - true - partial - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Models/DataModel.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Models/DataModel.cs deleted file mode 100644 index 7e8768b5f..000000000 --- a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Models/DataModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace DataIngestFunction.Models -{ - /// - /// This class represents the data model for the data ingested into the Kinesis stream. - /// - public class DataModel - { - public string? Id { get; set; } - public DateTime Timestamp { get; set; } - public int Value { get; set; } - public string? Category { get; set; } - } -} \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Program.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Program.cs deleted file mode 100644 index 174af0964..000000000 --- a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Amazon.Lambda.Core; -using Amazon.Lambda.RuntimeSupport; -using Amazon.Lambda.Serialization.SystemTextJson; -using DataIngestFunction.Models; -using DataIngestFunction.Serialization; - -namespace DataIngestFunction -{ - public class Program() - { - /// - /// The main entry point for the Lambda function. The main function is called once during the Lambda init phase. It - /// initializes the .NET Lambda runtime client passing in the function handler to invoke for each Lambda event and - /// the JSON serializer to use for converting Lambda JSON format to the .NET types. - /// - private static async Task Main() - { - var dataIngestFunction = new DataIngestFunction(); - - Func> handler = dataIngestFunction.FunctionHandler; - await LambdaBootstrapBuilder.Create(handler, new SourceGeneratorLambdaJsonSerializer()) - .Build() - .RunAsync(); - } - } -} \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Serialization/LambdaFunctionJsonSerializerContext.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Serialization/LambdaFunctionJsonSerializerContext.cs deleted file mode 100644 index f6d0cd418..000000000 --- a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/src/Serialization/LambdaFunctionJsonSerializerContext.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Text.Json.Serialization; -using DataIngestFunction.Models; - -namespace DataIngestFunction.Serialization -{ - /// - /// This class is used to register the input event and return type for the FunctionHandler method with the System.Text.Json source generator. - /// There must be a JsonSerializable attribute for each type used as the input and return type or a runtime error will occur - /// from the JSON serializer unable to find the serialization information for unknown types. - /// - [JsonSourceGenerationOptions] - [JsonSerializable(typeof(string))] - [JsonSerializable(typeof(DataModel))] - public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext - { - // By using this partial class derived from JsonSerializerContext, we can generate reflection free JSON Serializer code at compile time - // which can deserialize our class and properties. However, we must attribute this class to tell it what types to generate serialization code for. - // See https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-source-generation - } - -} \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/test/DataIngestFunction.Tests.csproj b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/test/DataIngestFunction.Tests.csproj deleted file mode 100644 index 265bb49f9..000000000 --- a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/test/DataIngestFunction.Tests.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - net8.0 - enable - enable - true - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/test/FunctionTest.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/test/FunctionTest.cs deleted file mode 100644 index 3eb29a867..000000000 --- a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataIngestFunction/test/FunctionTest.cs +++ /dev/null @@ -1,124 +0,0 @@ -using Xunit; -using Amazon.Lambda.TestUtilities; -using Microsoft.Extensions.Configuration; -using DataIngestFunction.Models; -using Amazon.SQS; -using Amazon.SQS.Model; -using Amazon.DynamoDBv2; -using Amazon.DynamoDBv2.Model; - -namespace DataIngestFunction.Tests; - -public class FunctionTest -{ - - [Fact] - public async Task TestFunction() - { - // Set Environment avriables using ConfigurationBuilder - var config = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - { "KINESIS_STREAM_NAME", "AnalyticsDataStream" } - }) - .Build(); - - var context = new TestLambdaContext(); - var function = new DataIngestFunction(config); - var data = GenerateRandomData(); - - var returnValue = await function.FunctionHandler(data, context); - Assert.NotEmpty(returnValue); - - var testLogger = context.Logger as TestLambdaLogger; - Assert.Contains("Data ingested successfully. Sequence number", testLogger!.Buffer.ToString()); - - // Wait for a while and check record in DynamoDB - await Task.Delay(TimeSpan.FromSeconds(5)); - - // Check record in DynamoDB - var dynamoDbClient = new AmazonDynamoDBClient(); - var tableName = "processed-data-table"; - var id = data.Id; - var getItemRequest = new GetItemRequest - { - TableName = tableName, - Key = new Dictionary - { - { "Id", new AttributeValue { S = id } } - } - }; - - var getItemResponse = await dynamoDbClient.GetItemAsync(getItemRequest); - Assert.NotNull(getItemResponse.Item); - } - - [Fact] - public async Task TestMalformedDataIngestion() - { - // Set Environment variables using ConfigurationBuilder - var config = new ConfigurationBuilder() - .AddInMemoryCollection(new Dictionary - { - { "KINESIS_STREAM_NAME", "AnalyticsDataStream" } - }) - .Build(); - - var context = new TestLambdaContext(); - var function = new DataIngestFunction(config); - var malformedData = GenerateMalformedData(); - - var returnValue = await function.FunctionHandler(malformedData, context); - Assert.NotEmpty(returnValue); - - // Check record in SQS - var sqsClient = new AmazonSQSClient(); - var queueUrl = "kinesis-lambda-dlq"; - List messages = []; - - while (true) - { - // Get latest one of the messages from the queue - var receiveMessageRequest = new ReceiveMessageRequest - { - QueueUrl = queueUrl, - MaxNumberOfMessages = 10, - WaitTimeSeconds = 10 // Set to 0 to receive immediately - }; - var response = await sqsClient.ReceiveMessageAsync(receiveMessageRequest); - if (response.Messages.Count > 0) - { - messages.AddRange(response.Messages.Select(m => m.Body)); - } - else - { - break; - } - } - - Assert.True(messages.Count > 0); - Assert.Single(messages, m => m.Contains(returnValue)); - } - - private static DataModel GenerateRandomData() - { - return new DataModel - { - Id = Guid.NewGuid().ToString(), - Timestamp = DateTime.UtcNow, - Value = new Random().Next(1, 100), - Category = new[] { "A", "B", "C" }[new Random().Next(3)] - }; - } - - private static DataModel GenerateMalformedData() - { - return new DataModel - { - Id = null, - Timestamp = DateTime.UtcNow, - Value = -1, - Category = "InvalidCategory" - }; - } -} \ No newline at end of file From af9108e149882c96c42588ba2602dd8a28a679e8 Mon Sep 17 00:00:00 2001 From: Tejas Vora Date: Fri, 11 Oct 2024 16:26:01 -0700 Subject: [PATCH 6/7] Updating DataProcessFunction and related tests --- .../src/DataProcessFunction.cs | 34 ++++++------- .../LambdaFunctionJsonSerializerContext.cs | 1 + .../DataProcessFunction/test/FunctionTest.cs | 50 +++++++++++++++++-- 3 files changed, 65 insertions(+), 20 deletions(-) diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/DataProcessFunction.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/DataProcessFunction.cs index aeea16343..51431e5e7 100644 --- a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/DataProcessFunction.cs +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/DataProcessFunction.cs @@ -6,21 +6,18 @@ using Amazon.DynamoDBv2; using Microsoft.Extensions.Configuration; using Amazon.DynamoDBv2.Model; +using System.Text; namespace DataProcessFunction { public class DataProcessFunction(IConfigurationRoot? configuration = null) { private const string ProcessedTableEnvName = "PROCESSED_TABLE_NAME"; - private const string ErrorTableEnvName = "ERROR_TABLE_NAME"; private readonly IAmazonDynamoDB _dynamoDbClient = new AmazonDynamoDBClient(); private readonly string _processedTableName = (configuration != null ? configuration[ProcessedTableEnvName] : Environment.GetEnvironmentVariable(ProcessedTableEnvName)) ?? throw new ArgumentException(ProcessedTableEnvName); - private readonly string _errorTableName = - (configuration != null ? configuration[ErrorTableEnvName] : Environment.GetEnvironmentVariable(ErrorTableEnvName)) - ?? throw new ArgumentException(ErrorTableEnvName); public async Task FunctionHandler(KinesisEvent kinesisEvent, ILambdaContext context) { @@ -128,29 +125,32 @@ private async Task StoreProcessedDataAsync(ProcessedDataModel data, ILambdaConte } } - private async Task LogError(string sequenceNumber, Exception ex, ILambdaContext context) + private static Task LogError(string sequenceNumber, Exception ex, ILambdaContext context) { try { - var request = new PutItemRequest + // Log Error - to Logs to Database + var logItem = new Dictionary { - TableName = _errorTableName, - Item = new Dictionary - { - ["ErrorId"] = new AttributeValue { S = Guid.NewGuid().ToString() }, - ["Timestamp"] = new AttributeValue { N = DateTime.UtcNow.Ticks.ToString() }, - ["SequenceNumber"] = new AttributeValue { S = sequenceNumber }, - ["ErrorMessage"] = new AttributeValue { S = ex.Message }, - ["StackTrace"] = new AttributeValue { S = ex.StackTrace } - } + ["ErrorId"] = Guid.NewGuid().ToString(), + ["Timestamp"] = DateTime.UtcNow.Ticks.ToString(), + ["SequenceNumber"] = sequenceNumber, + ["ErrorMessage"] = ex.Message, + ["StackTrace"] = ex.StackTrace ?? string.Empty }; - - await _dynamoDbClient.PutItemAsync(request); + + var sb = new StringBuilder(); + sb.AppendLine("Error while processing request."); + sb.AppendLine(JsonSerializer.Serialize(logItem, LambdaFunctionJsonSerializerContext.Default.DictionaryStringString)); + + context.Logger.LogError(sb.ToString()); } catch (Exception logEx) { context.Logger.LogError($"Error logging error: {logEx.Message}"); } + + return Task.CompletedTask; } } } \ No newline at end of file diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/Serialization/LambdaFunctionJsonSerializerContext.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/Serialization/LambdaFunctionJsonSerializerContext.cs index c15bab58e..b35b37817 100644 --- a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/Serialization/LambdaFunctionJsonSerializerContext.cs +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/src/Serialization/LambdaFunctionJsonSerializerContext.cs @@ -15,6 +15,7 @@ namespace DataProcessFunction.Serialization [JsonSerializable(typeof(ProcessedDataModel))] [JsonSerializable(typeof(KinesisEvent))] [JsonSerializable(typeof(StreamsEventResponse))] + [JsonSerializable(typeof(Dictionary))] public partial class LambdaFunctionJsonSerializerContext : JsonSerializerContext { // By using this partial class derived from JsonSerializerContext, we can generate reflection free JSON Serializer code at compile time diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/test/FunctionTest.cs b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/test/FunctionTest.cs index 0b6a23d73..da8637e84 100644 --- a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/test/FunctionTest.cs +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/src/LambdaFunctions/DataProcessFunction/test/FunctionTest.cs @@ -8,11 +8,16 @@ using Microsoft.Extensions.Configuration; using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; +using Amazon.Kinesis; +using Amazon.Kinesis.Model; namespace DataProcessFunction.Tests; public class FunctionTest { + private static readonly string TableName = "processed-data-table"; + private static readonly string StreamName = "AnalyticsDataStream"; + [Fact] public async Task TestFunction() { @@ -20,8 +25,7 @@ public async Task TestFunction() var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { - { "PROCESSED_TABLE_NAME", "processed-data-table" }, - { "ERROR_TABLE_NAME", "error-log-table" } + { "PROCESSED_TABLE_NAME", TableName } }) .Build(); @@ -62,7 +66,7 @@ public async Task TestFunction() // Check record in DynamoDB var dynamoDbClient = new AmazonDynamoDBClient(); - var tableName = "processed-data-table"; + var tableName = TableName; var id = data.Id; var getItemRequest = new GetItemRequest { @@ -77,6 +81,46 @@ public async Task TestFunction() Assert.NotNull(getItemResponse.Item); } + [Fact] + public async Task IntegrationTest() + { + var data = GenerateRandomData(); + var serializedData = JsonSerializer.Serialize(data, LambdaFunctionJsonSerializerContext.Default.DataModel); + + // Write record to Kinesis + var kinesisClient = new AmazonKinesisClient(); + var streamName = StreamName; + var putRecordRequest = new PutRecordRequest + { + StreamName = streamName, + Data = new MemoryStream(Encoding.UTF8.GetBytes(serializedData)), + PartitionKey = Guid.NewGuid().ToString() + }; + + var putRecordResponse = await kinesisClient.PutRecordAsync(putRecordRequest); + Assert.NotNull(putRecordResponse); + Assert.NotNull(putRecordResponse.SequenceNumber); + + // Wait for some time + await Task.Delay(5000); + + // Check record in DynamoDB + var dynamoDbClient = new AmazonDynamoDBClient(); + var tableName = TableName; + var id = data.Id; + var getItemRequest = new GetItemRequest + { + TableName = tableName, + Key = new Dictionary + { + { "Id", new AttributeValue { S = id } } + } + }; + + var getItemResponse = await dynamoDbClient.GetItemAsync(getItemRequest); + Assert.NotNull(getItemResponse.Item); + } + private static DataModel GenerateRandomData() { return new DataModel From 3eb638f00cf4f0a74cc421e66fd602e7bc67b305 Mon Sep 17 00:00:00 2001 From: Tejas Vora Date: Fri, 11 Oct 2024 16:26:32 -0700 Subject: [PATCH 7/7] Updating ReadMe and example-pattern with architecture diagram. --- .../README.md | 25 ++---- .../example-pattern.json | 6 +- .../kinesis-lambda-dynamodb-pipeline.drawio | 77 +++--------------- .../kinesis-lambda-dynamodb-pipeline.png | Bin 67520 -> 39931 bytes 4 files changed, 23 insertions(+), 85 deletions(-) diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/README.md b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/README.md index 92966a0dc..e2a38ff12 100644 --- a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/README.md +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/README.md @@ -1,6 +1,6 @@ # Real-time Data Pipeline with Kinesis, Lambda, and DynamoDB using AWS CDK .NET -This pattern demonstrates how to create a serverless real-time data pipeline using Amazon Kinesis for data ingestion, AWS Lambda for processing, Amazon DynamoDB for data storage and error logging, and Amazon SQS as a Dead Letter Queue for handling failed records. The pattern includes robust error handling and retry mechanisms, and is implemented using AWS CDK with .NET. +This pattern demonstrates how to create a serverless real-time data pipeline using Amazon Kinesis for data ingestion, AWS Lambda for processing, Amazon DynamoDB for data storage. The pattern is implemented using AWS CDK with .NET. Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/kinesis-lambda-dynamodb-pipeline-dotnet-cdk @@ -49,13 +49,13 @@ This pattern creates a serverless real-time data pipeline: 1. Data is ingested through an Amazon Kinesis Data Stream. 2. An AWS Lambda function is triggered by new records in the Kinesis stream. 3. The Lambda function processes the data and stores it in an Amazon DynamoDB table. -4. If any errors occur during processing, they are logged in a separate DynamoDB table for error tracking. The Lambda function will retry processing failed records up to maximum retry count. If a record consistently fails processing after these retries, it is automatically sent to an SQS Dead Letter Queue (DLQ) for further investigation and handling, ensuring no data is lost due to processing errors. +4. If any errors occur during processing, they are logged to CloudWatch. It can also be stored in a separate database table for error tracking. The Lambda function will retry processing failed records up to maximum retry count. If a record consistently fails processing after these retries, you can use SQS Dead Letter Queue (DLQ) for further investigation and handling, ensuring no data is lost due to processing errors. -The AWS CDK is used to define and deploy all the necessary AWS resources, including the Kinesis stream, Lambda function, DynamoDB tables, SQS Dead Letter Queue (DLQ) and associated IAM roles and permissions. +The AWS CDK is used to define and deploy all the necessary AWS resources, including the Kinesis stream, Lambda function, DynamoDB tables and associated IAM roles and permissions. ## Testing -1. Out new record into Kinesis stream. +1. Put new record into Kinesis stream. - Use the AWS CLI to put a record into the Kinesis stream (replace `` with the actual stream name from the CDK output): @@ -66,32 +66,21 @@ The AWS CDK is used to define and deploy all the necessary AWS resources, includ --partition-key 1 \ --data '{ "Timestamp": "2024-09-13T23:06:55.934081Z", "Value": 81, "Category": "C" }' ``` - - Use Lambda function to put a record into the Kinesis stream (replace `` with the actual data ingestion lambda function name from the CDK output): - ``` - aws lambda invoke \ - --cli-binary-format raw-in-base64-out \ - --function-name \ - --payload '{ "Timestamp": "2024-09-13T23:06:55.934081Z", "Value": 81, "Category": "C" }' \ - response.json - ``` - 2. Check the DynamoDB tables in the AWS Console: - The "processed-data-table" should contain the processed record. - - If any errors occurred, they would be logged in the "error-log-table". + - If any errors occurred, they would be logged in the CloudWatch. 3. You can also check the CloudWatch Logs for the Lambda function to see the processing details and any potential errors. -4. To test error scenarios, you can intentionally send malformed data to the Kinesis stream and verify that it ends up in the Dead Letter Queue after the retry attempts. +4. If you have implemented DLQ, to test error scenarios, you can intentionally send malformed data to the Kinesis stream and verify that it ends up in the Dead Letter Queue after the retry attempts. 5. Additional unit and integration tests are located in the `test` directory under each Lambda function's directory. These tests can be run locally to verify the behavior of individual components: ``` dotnet test src ``` -These tests cover various scenarios including successful processing, error handling, and DLQ interactions. - 6. For a more comprehensive end-to-end test, you can use the AWS Step Functions service to orchestrate a test workflow that includes putting records into Kinesis, waiting for processing, and then checking the results in DynamoDB and the DLQ. -Remember to clean up any test data from your DynamoDB tables and SQS queues after testing to avoid unnecessary storage costs. +Remember to clean up any test data from your DynamoDB table after testing to avoid unnecessary storage costs. ## Cleanup diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/example-pattern.json b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/example-pattern.json index 490f76ad9..0e75cd21f 100644 --- a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/example-pattern.json +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/example-pattern.json @@ -1,13 +1,13 @@ { "title": "Real-time Data Pipeline with Kinesis, Lambda, and DynamoDB", - "description": "Create a serverless real-time data pipeline using Amazon Kinesis, AWS Lambda, and Amazon DynamoDB with error handling", + "description": "Create a serverless real-time data pipeline using Amazon Kinesis, AWS Lambda, and Amazon DynamoDB", "language": ".NET", - "level": "300", + "level": "200", "framework": "CDK", "introBox": { "headline": "How it works", "text": [ - "This pattern demonstrates how to create a serverless real-time data pipeline using Amazon Kinesis for data ingestion, AWS Lambda for processing, Amazon DynamoDB for data storage and error logging, and Amazon SQS as a Dead Letter Queue for handling failed records. The pattern includes robust error handling and retry mechanisms, and is implemented using AWS CDK with .NET." + "This pattern demonstrates how to create a serverless real-time data pipeline using Amazon Kinesis for data ingestion, AWS Lambda for processing, Amazon DynamoDB for data storage. The pattern is implemented using AWS CDK with .NET." ] }, "gitHub": { diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/kinesis-lambda-dynamodb-pipeline.drawio b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/kinesis-lambda-dynamodb-pipeline.drawio index ee74b4f1f..ec447dea2 100644 --- a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/kinesis-lambda-dynamodb-pipeline.drawio +++ b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/kinesis-lambda-dynamodb-pipeline.drawio @@ -1,90 +1,39 @@ - + - + - + - - - - - - - - - - - - + - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - + + - - - + + + - + - + - - - - - - - + diff --git a/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/kinesis-lambda-dynamodb-pipeline.png b/kinesis-lambda-dynamodb-pipeline-dotnet-cdk/kinesis-lambda-dynamodb-pipeline.png index f30023994a6e95b79988e8f7b32b5e4cf54e6e7f..a6339fbdd28aa2ee2ea65f0134fb7495f0d477ac 100644 GIT binary patch literal 39931 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 literal 67520 zcmeEu1z1+wwzeSkAxH`m(ka~_AsrHeba!{RfHX>Xqev*y9a1775(0vxq#z*;O8@JF z?^ASp;_iFTx%>Rj-H#iYbImn#jCZ_ajJXVzmy*kwTSq5KHc?7eRwD;}S94oNLt6(s zMty624_h1HcWdAQ&`RIb*ap%}*4D$^%1WP-Rh)y78F)k^t8Zv-<7De-M#&}&l%;H( zjIDsbfokCAT}9xB8t^|0Gb0Pfc?$;@N;V-DRvt!XTHt|%v7x0c&>Of3@P`Gc5;u1+ zHnDXBD$iP-Pr=&O$lS!-*ywz+tjw(J49q+XEbL0GEa2{Vz(2Scx!HL%&fB{fJ2;x# z+CZAKFmf@npFc2m(l+U=xhg#+|H-K!p;Vnx{>>N4HpM63-BnW4$yHR!-6Xm z%{?F!Vu3buHa9YMg!Fd)rjxC$m6N&M&y|L@Ha5nFkb{QwsPEul>-uvu6I(0D>L6|G zfQA3mJGes8P~Ym0wW{VuPJpC<1&$lK3UF&lV{=n8=*WO1oX^r)AKDmF;b^9BWb1nV z{#n(=Z|hi<&1rLmJCc)J0S z6}EMDvNE?Z7Wsb2;7UMdjPo*NSa7evF`X%pwHwgCof)IPt0Oz3se`RE_+?WEDL~MG z&i?pR7ig$!c(xRv6DJ2-OJKU^6Qg8h6A}{U66OXE2Oto%9x`D--JQ$$luatH&H z6=A020H8qxTw-Jefbyq`U&~xSR{&>nq4`+}+y}J7^NL@~Kpz(yuzW8uGoE#Su>w5Y z#q!w%FI4>e7(5$rAJFd4mdE&O`Fw&vn}4f*0{Z8q4`2aUH17GYhc`aRPn+>lB@Z zol8tu2pHnKqJ#J0-=XMS5Y2`tIt$cDU(j?8sP|hV zheD3F&JJgm3wZ6f?by!ybJBM(HHPqy-?oFYM_?(yG=Z1SOu*kq%-`a@gRzyq6QD#F zdI1RbUk1H7&-dd3ddoXI0f#KA?*x$k-yFA$z5zg2e>x(l*);^<1dK7lzglN&b0Z@# ztN@S#Ac_7Di22zhfKl;$%!{`7x5on!#XlHiR%QsPg$x38ccy=T;@}{LnwgQAm5cj) zTL1J}{xie>uYXc)d&^WEYgw4PK_{|jj+qw7w-leIrqtoxm#7|WBXEA~> zi;L{wJG=O~@|;loTnAwp7c0-1#>E=2aX+v9ZtlpUOnRz zzjC6p%JW%&ANZ^eGHYn%Szo_0s`JX9-#)MV@%G}*#M&!6XgQC zbHB0jCTITvPrjRAa5E@WoWY8`t)uzbX|stM*g81@;fNm%e$Vv3L(T<@@sGR}eMh@< zuh+!f4Q!cyPiX+CILW`9GY1I#_=#oZuE=7Z(SDE`N0U|G&fu%X4|vH3DdrE)eNBlhyx4)L?;n z7yt3F0R*Ie)%pKCuY~J-4gb(9figqz3NJVmf5LkFOQ@`ngM&Ue#sxJh5MBnH&p$K6 zb1V15*8M_@*?1rp0wS`U0Cxd{{Xa{Ap&bM5&*%Bi%n8DTznk5^|IYtBZ)W+URg`zI zH8gf~1egod`2Aw?z{2s@@Y0~V`3Ji5uW-{?I3P&yFA;5PH>fy5y)qVv{6U5NKhZ4% za&-R-;A!k;?xY657c&qZWfKOM)PWK>AP4?k6zm3rU4yd%c(Xn zXJddY;ewYB7Oa0u9^x0n*tj}*3b!L42KE3>bJKy;zEAzicir9ZmV?mJ6;ihfEJUH0}r8tv}cQ zU)S{fR%!m2RuGj@ILEtR(Ba?W5dG8E`-~M|L z1L|5pE1 z5XuEB0x=MO{eUh+w*R2_|B84nGY>0dxbGC=-|06qK;yg6(V=Cizww_)V4(S<3$iB! zO|px+1D`h7iULv9UoYeQG2`%Si~r*_XLp$Y`qy(rcvv~kBJF>;ZU6se+TG|(ri~2# z%gKU2Al#oYLl94l{g+fbI2s51Vg8RJHc$ZkQ{Zvt{4oEGxbXjZL4Qpz=>O2-$0tDl zCO_y8XaW6d?yn3NzYSnPqD{X;3q=LRKeCM9MecqAhTneB_P4I3{*4daE`DJGzOBl| z!oejBScjj@A><>ozuZMtJMcyE3*YU4Q|7<@9>?C<82r5;Bt!lOWA=0S@o!7^pEr7J z(EI>|J+pE{#Ni)^F+0Qwgp%xk#kJJ4828`!ToiJH=fa1%7x&=oDfCm|^9PU@z#m$l zUve6*ugbl2iR6-$sF0GI&Z;(il9Dm*{ZRwY#I-d!sT?6}F;Uv+5_ttt>{w#+%k)a3 zuLZF|Ffj1fmTaQL>pbiIkNft6)J{^}N+c2K(>hC?6XRZwjuJLglQXrWvbkP=TG{UJ_xBH;s67^rTX>CY4+`b>*=NO! zjbS$=?QcnW924|oIwVLn*I;@mN@PN3^ObT@dNDR1hJ>l?pL8YO41Gl{H`^Ldmx5!C z0D2VuFgRF3fZ1<|EV{qHGm(k-!R5=9?1`KgyPAkWG9&X7qlwlFzjLkB=J4v%oSYon zxHtF4b7k$OKeK2xcyLHus)%V0e5*=A$#G3vqx#b22_ zgSd=O*ZVrg33e@&Bx++Qht&8^v%k7@*+**Nr@eH;NOd`V7|dG{oS2H`WT_rmfdM^M zI6q!A7+H!Ek?jSwa%+MZ#d~EWZS%_+?cwOS#Ki)lU_w`CS>BVlhz~OfzhA zUJ)b5?IzE6mD-x~UpH&FGj5HR{*aHP2*C)7DCswo%GFmn*CsyMm9A1>7cxlUs5;%<7u`Q#gMTD3+nOnEU%=- zRz;0G9;g_4eh-g>PWj$(vgF!G6S4Di73KTkDy1>{HVP`*{l3gBFe(HGJ_>;*4neiuy z!OKh(?{*e-aUkkIf;3TzWF!rnd`YEPO<}+D3y+r?mSoODTyg^?*!rtPqOcfLnr}kk z+Nmk@-lSCK-z!QZlU}v$OyM5>Y)yjc5h~L=$?M#PGw?*a(FN65($e*)Tit1WM96%2 zq2A5IZp^4q)86YXED&r1sdLU3=uOO0AfYeGxKGc+)m7A-Rx4W*CYe7|}LVSnR zFNKKKoZ?)$!HcpHY$gR6&32gRP|BC67ORo88O zk|F+?>u6ATSK4@qi7X+HGjcr|(Y+M$;jPl8gv*K}cQ-S{!%McOVM>UJQ;XCYb!wj< z*zp^2u9sn>pbD+DhqHZ~xk1?^MD?QK!))LcQ)yVuDd$AihnhX_C%*MnuDowRwHUUO zgiTH>+uOn*E1{QB8>OJ9kYB-~{PF|Yn$6${V+4myN=M^l>pj}u4l0_z9=00KAhL?a z0vQRv*5K6C&*Y(!wz4_4ob-qzMJ2?_uU~#og{g*omsd}UsHW`nHL}ctY8%Cr=1Ec3QMid`##cB54hc@AYmhv2cRDaUVy|+;TQilK^dQ! zO5Y;~KAMY4SY@htRdTxO<>7m+;RMTUJBqXv^6Rmb#}|Z1`Amq)N<(9h*VCRM8)&0jZYN3FoP3?y;V_YaO%}9&2X}Zh zikuguk7dtArl;~HjYAowx?KEgS)^Vnm1~ZxvEvHH*4Od{4x=W1Dhn&|Uiuf56L-wb zd$sG*PB4lCIf4k|3{t716njdj`j$;Q4;zBJKc=lmX8RW!mgrIxr+#QlX;n7DkH|tY z#<^Of|G-+Q3s038c8TeSYPu4^j*26es}x+_e?v(@rJyJI+{8&3)*n4F$+z%n?HrOu z!TdZ{zB_CD$sT*IOS zU7yeyUil#nBw06LNd3l2V-kLPixUY6)LV7?RX}nqOC?u538kE@Sq7>R`h7p-*6{;|OkX7)dBqpk7;6TM#`8{}Tyv zfCuJP3tKL8aR!1Pg#{(`KHfr855G8A?E~MaW-UK;svou%Mu^0gRPPqZ%U}>k=JtK9 z#{;=)3KfhRO2-WgTgERMokrh%mH1HTui$J^z|yfg8Qkx=_dN5HcMB;KAKKi?(Cd(~ zN+*oKBep37-K;l~IL zFu(>T3oejE+oo`~jJ)(@*`BAJlpy&{I~K%o7d_h#gv&yJu!kJivA5*sr2A@Dh zkAxB0A;*pJ?A_aRW&8++;^4n{I`I%gMsJ1yPpa$&pkQ=7t_CBc6@Yp|zFCi(gkBFdo#(3Dvo0bLj{R!Re zy(#{MDb0PvMS&l8rE`m?mufvrcHSAM`E)#a_%I&BoW(~ZkwrH5V=_eoG^lSAN+*85(KuvX+Ov$>rJ9NBMRewL`9*S&OmuLy0J zd-%|d;ed-Ve7!nD;Y)^%20}Kka{MXfg>6S9wUbEkcf5mB$ie*dT28E#l(ge5=C(Vq z$!`2g_1DditZozdJ-+faTW(X2-u-~_sOFeH==47N<@KB42sy%^7cbXjB288EuRJ*l zh|kyRB#wMf=XZBFP0-D&Y}s+uLaUC)?fulL{&KFO+g1^)**#8drjshPFQJn6M_+Q@ zY^+DV|B=Y;>&bGG(Dv#r+`w4!G1BIa?W-O;#sxp788wcSM07ny(W#_fv2A1hP;#=E3vR&&H>;g|jnUV>FnZMkR>Roh3C!N{4!t%TrHf%xrF7Va@!38WTcD zNpi5^Ay<2s8Y1DnjN+zC}p z<+cx^3lU9|D&7fkDM9xP6HQ6|#3WAr?b8cpMoo;B>qqPR3_EQPm2Xq|o$`%i?@A*S z^o2b-K-@my5?;9j?>Ls=#rOkKt3j}?PkBa$KI<`<76-za?15Idw;5S`#AngXSDrUE z;fIHy-#<3H!7-VWB$|dC+0pp0ze_dh7~v(~){L7PVT#klnQb9^acqk-2{wz zx^EqR(rTv$SHxgd1*RU!^d-<3k@{)KDUE%1?W%EMF|Qje@;QcG9o|CvOzKx0gEoD{ zj~nNs)1+Qsx-i0VbfScXy|LYzjm@()Oj9kyc5t_0rBsJk-s5Meg9lA76l64XYO^TZ@&n?OYSrVgVX7VMMTml0PcXOom ze0MtYZ&kLmkdi@H1$OY~#lScwn@2|RPPVzL1oEhw9=2c80Hcy}Awj!I{R)~5en?g> zOwe7x5weqHxQq;P!YW~XajpW*3=f-Y`xs{?a1(7tcT+1j?lIwrJWCA26L^{Aj~c8b znUzWsuMaWFHuiwwU*;8bjTZDSurEwp@*^!^(pEXSzZS>&I_$A*>65iI;dUITqiaKZ z)|`>l@4zlr=f}LGB+$_@^Vdlx#9-GY9ZT?1hh0|(q@n0s<9+s&eiC>((i~OM=)IF3 zu!9Nh6b?oku%Yka34ocr>ak&BODgtzoA?;Yi2O7$0dsc{(W6cA4&`k=Fhdh7jj2*; zCMfBc-V-9>lFU-gr#`a$)A(4>it|F&w(rEf)-9U?-|@%T3?Yw zy(>0bOF@y5^I_kJLRYF+F{}O|TuQ0bpi~xKY61_GtN8KWfN_m#KFaZy&k{j`CDYCROB!$5b&rsPx|9dU-GVUNm6_EZVx)9rDlDjQUYL5H#3>GIq8Y zKO4L9Q3YVU&9*adS7w&#mbJ$PvA<%qsH1xKNOp1I6`A9A?(dEPqbfyWd3>i)4O^@Z zx4VR&^OLykL$U5&ZjaYwYfW&8RqL9Jw~s9MNkb|hqQaufkKYaNFqze6K&xY~%IIAq z^D}9a1W>bQq#{EkQ2=_f&0hk1=_pFfiiK%-pTO~X@ncii2V>b8%Uvhq5+$DzeWIyv zfaN%YC>)Qo*;TA>lm7POv@k4cfR<#IpI(RV>xSg2hJH+uqj}fortE&yV?hQ(Z zkllXCD+WkRPsyW{A`rXbplg@^%|mRcSP+tC1;ZuWP{4eRh^-=$LFjxn(Nw#B?ve|z z3zsE-b6WfqC7Vr^Gc${RY_W{9trc%;VM$Zdq|unJo-XhC%T+LvG0>te=4pxVM5q;N zuU27Ue#Z0~P=`t>I$XDSvpVMIyU|URr}@tlUS_ulJZ)c)Q?d!o3(M*zphWSbTV0nV zyy4K5Q+jdQjOmR|KQ!brTpFd{yW_|fi@d2sg2?bDHJW@|iy|*MIFz0zJsd11 zF%YX5R6K~=5PuzNtkg7DoY^uII3pLSKi&ce`J?Ngv(>pQ?c|h0&6*tn$uu&kVP{)8 z4d3(Q7n(S?lnUSCL_y?F*N>`qg|vBKq){1y?lEC2x`SLR`FvMGm7cW0V_<%UbxW>C z2C<{d;N5|)^j!#Ws8M+}RqU{Np-6QF9>IA{h|5JUiYj~;X<+v!5);|i=ncye2qa_G z+6Zuc8o~y#w5W@~LZT)v`vFiEbM3p)PTv*Sh8#|d(hmh*^|VG(nV0d@smG30ZEN%i z&9Q}jPRn?YhTsi>5cn~~?5_7)%kTGG1Ri~xD_UfB0uFMnpEVuaU$P=}ay5e(?|@j& zw^G`q!Pjnq9=WgFob9=b7$OAcH5QNeSTo@*VO9!@9=G4-O0Mu#+%bnKxUWd`*!qF8 z=the`2MUh^sb5etjsJI>_`p{ksYV#qm3^#u`6z)<3;Q(@U7QY2z}FZrO0npoZ*w3? zzUZeFwxOn&%9DD9_qKQjU>k)s1P4yOG#>ggGLWd^JZq|}OWFEnhm1rTV-ZjTz=b{@ zhc=d3dv3TJk`c;xAW{);R>m}&tT**VM0P?JYGHgljbtDZgC83f5F4ugfH$U>x*%VB zd~;b*lsUHBF*6n?O2>nOpCo`iv0kHw2;D&2zg{v6j|DR~R*;Rea8W zUA^z_?r#1iX5~v7l^@u}(QyBS*qI{e*=jQJuBrv8f$ox@^D&^^w>!KkGo)LatkH6+ z={S;}xa%?ot!zGpw{n&jLs*I4@kPJG;CL{3#UrR>TtYHSUmh^ysBnrwXJ?Tw%#%=tq&Dy;@ZC4 z#qs#Mmj2~q0)uAI?CXdA;{=p)vCRbTu+~y~(M0ewGwEomI017W502;iKL$2T1GuPz z{b@z$BO%~F*xt&0xEpx5j#(OJ=!=)jQU;%N87dT`l7Gg zx~O~}L^Mm}=d>$|7l|3voq79qdn9q>{JaspdKp~g?GtWBt(vTmcZDRQ(amAk=w^Gk zrbMl*=uh?+i7Tt8d`@qm@oo6NDnupZM(;`Cc;(o~Z#uG7&xTdX%8L^QQ9Wrq}+cR8+K@Ttl@woEEI3-=GpA@pROEe_jt8}b{o-(y8c?ARN zQYYm2ID3iWU0|iO>ba2kjibX*`L(pPxSbw<9a_7msHl)72-BU+h8TSHMr)3AV#)nu z53#2=Rf}G*2h_W+i}z*lZ!Q-UKM@bd=eC=^l;N>VyE@fGoX+dozL@C~VemR^CYboX z{9NwSr_*n!GnF*BsZyAlj>-ncoIk&Vc@=~zGS?oy2{bim4FH9CHGZ3Y80Ndbzdw58 z)vH&Rn~wJg#3P9chqYdMICB*7%aZtTI?Q+O#I8&=-R@224ZcRFlD`*ACEL0=*U?|3 z&FgjerCP1d>DkzoK7Pjq8LM&=`vQs=yq%Wm4ow~g}AZ4Nb#QClNiwLpbIZ0!X_l$2(p*FJWsW`kSl z^4qx=Ixq3zuiz*>ebm!`_x3Tnj@t~pSk-EH|4UC-I0S^7Sax5=D&t*>H7YGqB&T&}LEFqI;S9JSMAFn*4zDbC@hTk(E+w_x?*lgMkLei5V_vNA-MooRN=h0M5+d)zrfn%r zfRjt*l6ZAfZQ`V1X6i?r+GaH2STGBPs4lu8$(EB$2# zlD=&&7N``K@gi6$>06%3wR?oW-RUDOI22*_j)vQWs>PB zqC#&TqYQCEZi;w_ z%k)Gv6qDO!_4+NB)ezv=>3l>vU$M3rv6a7M86oV}5E~XwxT%c%0&jkAPm(0Hcoa8U z>SnCW@Lp*ox#hQ!Vis1`dl+M!)?@h`8v+?_v!M6Iy1L0Z?o5biL`o@AoS^Od4$@dj zO>t-z-9=Bk8z$BDl%>wmIDr8~T8@5G43azC@u+v$6_D*vNtI6EruWXb zMF4+kw~~NB_Q>tiBgb1MGCN6nTN=sO4q1qn45Jl2RA$4BWeEJdKPy|1EB=`Lr zGJ;3_q7>RP4%dAZJlyQzhF#MX-&I$j)xb(Rs3E&>@J@Gg4jt6A-FUlS{O0{pv}LXd z004QM#`1;$XB0Y&(ucTBYw~ag1_nw_*85wZ;_qfDkEljeT8f2Xzwt$hr5`>~&X*@W z_NH=wp>4=;Lt;iF5JZN>N;Iyr8s3t*eap++TLEY=nP-2nW1gdIgvC`HE5#&1Tpkn^ zN}O7tZurK;o~p>IYN4vi^8;&bIgpO>-KFtJH1_$LjaT6<6i1j6E9!`kcPbOWysg3Q zkyPNTbu8dk5TU>W~VuA1(aKB4iZeuJ6(fEkbpn@PNoT8_fS zuuFhr1sSromsKtk1#8ZBj%T&QY!s|L^i8?HQ9P4}DLUS@1j3Hn%&y8pxr%RI@iJlg z{FFl;&c&u5Y z38R#zMm6r>oQ!yL;3dh;23?JpJ$`i5A8!?yGctsOIZH@1^BLJ?2&T9GF%q8NyLa-R zOgrOMxkFt{>;vJbbfwy7sO3l^ZrapnK9`}*b3oV8#*UaPPJd%x*NTQ=o~9&TSzniQ zfYMicH^P669Ub#CiUv=`wF~+WcwTmwVJtB%ifUpK5`ygK+Koy4i=qVSI@1Q%TUAw? z{Yci(Baimq*?C%TOi3VQ3V++`w92D(^P#!vqoGm>I(&$!p;(8P{TkwRrvQZ1dgn6f z+(n=BYFMqu(5z>N5QPeBC&rt^ydWUzFeY6-n&LkfN!?ofgc51dlJ`dkdWk!qIz0ifEu=xYZNJ>2Ah>(;)qj7HB_3M zU1bmI%}y!}z95ogLx_S0kO0y=5x~lK^{sb74FD^=wND`6<#wRw;SKQVtS?>gbeer{ zoowW3`F%{o$qm76K=x><58-OK;@L8InhP4O%CshG-kQ7f7zh?91u@<+|H>+DHz=rd z`bPLtGB**C9pN+kh{{I?^4(+7l35Cp1KdA(9HKx>tx-g~$_nq6^YVO2c{v61Qx5Zm z+-M%|gBX20KbfNwfGietgbA9;W|>86_q6plh0BRgYRh?dn_4RgE!L35meBZkA$X6<{Ou{AwOj( z87dQ~inLF?$MoVr>uBcr_`op0KYQN zfDKe}4td+4ff7hWpEcI88u9A!{SG_+es9{qi&#odbPrGD-29Wc0%69_L}|NGf;v+B zrP!6@_3{T#xT%Q|ZM60|2<;K)v~{eWJ$@~}9B2^oIF1R?gV(4NVzDK;N5Mu4gmz{E8%k@ zOtsZgVH&QwI1TxcObnztI{6&0Qe;cG?5Z|06HodlRqf9^wM}z+2cCz0OTT{#OQs_B zxSi>#`_nkKHy`fBq3N~0NzGZpw|W7J8em$p;}c%HdQb7j{Vh1M4-&k*g$sKZ<1Rha z45$!>FCzgy+Xz84P=(3{H`hL9uWaf28w*O_sxH`e|0uuyMM_{dDqlocY*)V_%^WP>9))^Z7sbE8^Qy!WFVcSwNu741=wf3ozR2GAq~s=Nxe zlc8gI&(hb4QI-Ub8)3Ynl3Bd+@hs4&EDSFYDt78KoS_F*6!xd`xMse*R))ZoFLde1 z;v3m*c)N`z?=XeeQBOYJY<_jKilSursO$Mah;oh61Ry?x*-_;p54I79(=^pNMhBPd z5XwuVk?Jn6$#AdhM)VLf@RbZWbpCI3!m#R&ix&9TBUX1weOf74QMVsf-rxP8v4vh% zlAPoS=g?M+Q<_^`s|*B?p`n{k`Z3UJZSI%P6dqAklBD?a*Z@)*m0}TxdU-(sz1)yM z*GaL_0dpE6a|*1C1LjpS+S`rT4?4B1<24nq%n*n%F_Q)dDKjDVASnzia?R^ELG#L` zQ#UN(nU0;pVfnS*UQ;{n2t%?V%IEYnYql4)4twJ=mqwQR4Is_){SVZ~Cd1A{=Rj;x zwtlfG_R-CAfSA4tfPLL%<;7UBVELRV3ni8SfZsiM8$kmqewW(lt%@5wH`aPHjIGftSj-d-5-G{Q;7cO|w!PoI;mp}X$G z)fpD@gqz<2b({Nb)k2aJ>EYltq&4ZtlEex|G=AE$rbNVrU%YDMU~-M|6V6L=oXrlb zC^xdu6FJe@WYPEqO6r+u3`<(>t7LC<+k<0-4_m9Bpn1_L?AXT&x_uk%7OTD7cenOM zMRb}>-3$(Ji-%$Z2LcHr{H`QO6gmhEu5EX3V<5}`yn(H*4;#6E+JmAYF@_@1%mK|<6ffGk-Abv+B} zjcIPKQ;Igs$RX?K%|*;d4&i$4tt(_cZAH1-heUY8W9(Vw_5LL3p`kQG^{G9N!wKM@ z8ND?UG8n@GWyLV4;Ed3vm+G{*jdygacYYi>#2Vr}(a81t}Ew1(R z8rqk9Ww}A;Pw-5@Hm{k`7c`9*K@eh@sgpp#|sG#)6n2p8n0|w_c;cC}2NnMB$ z9|xzT)_7F-DWX(csZ=>%mR(n8X&coZE4c39(dw>swE4VTglFiZHibN_NZKs~bKsT; z_-2kBHTaG$f8L8kI1dDmClmQGG)MA&C|4A(iQp){Mn|O8dNdOF{JI0NGRjgysR7al zCV`VNJkg@^Oo8^0+CoDlAdoNnbAD_BytJ>EfF3ne6-yjLo=?7oA+L(jVVS?KjKZ^| z^%{NL-ij`0o@o*F)Z#wUlOzACYa-k*yPwoQ*v+KUr6|6s3CjftXp+A+KOXd^lifI2 zS@gC0u{AREd_t#Fc*bDh0{H z6n7i3l%2C=;=vV=6G&hQvS%bnW?)O_@*>TA=_+QD`~4!MS>188M&s>Qc@{rmS!GxX* zk>KT0Ty{aU3=_s8`bNzc83wIrB*hKolyV!a~Vw(XvI`G|zW<^rm zWC8R~;C5gK0SnU%C8FpN88EY*`L*}KsXLgM`RL3k`(0wW>rDw-Mh9n2Y=nV6`{NbB zO)`K!f$X?C2J;x}b9lo+Z`kmU716lKuAizlUb@EN5OP#YedI?35#(CfWNM;*B4a}^ z2dpIyJ*tyiZ&`coDVs^5yE!(g-$G^(&{ePCnj9b%#SMPGKq|rP`lE(U*n^{xBiCIn zkI;~#ES!5xH<-P&R1xAHc3<1E*>lDP(d)o}=6%E3*D>i=vC{s6tN0Oco!}@os$Ovl zZ$77fyYlqq!>N;R1m%sVgsvPf3T#DcCoD%a=Qa*A-wvAYZf*+2itlc;BylOcFq_fR z39nj8sqjY9pTSrP7ZG`|DY&@j=K1Z)#^zo-*X^SRp|MT9%xe1%IV}7B{rSr2jN5z^ z!m#B$k9<3PgieCIqZ~|lW6z`H}l(E2C-*sSHd7*0$WluSO*@vXXy|A2$SI zdN|K6?VPrirQY?t^z2RTOSZ4Q)D-VtezU-vl*goT&BN(LwT_KTdvvR3P1#Vu$p=w} z@N3oMxq8cz$F`4JLKRJ!zm$KCPK$Yz<5S#<#^xjDB}`WJLM`NI-+O|ia?PHg`ab$( zy-T~|u?Hy`E3)IlX~gK26T$NK-Ob~~*bR>8X7fSQnM|?UMGjs^{ z8G~MKU<)K~-Pp4_x}u|#A)1T%?wRIozJXnVZuJe`qvNBEgSUM%d81cKwx^EG)QWK$ z_gDFMPa-BgH(T;_^{4z!f4Lw_MEvH_3wwN;BM-WDgn;aG10s$`_fQh@>Wbn|~R-v9r_7d^NUt8<%nB1Dm#tVLg3=IK(2_=Rw(jVpaS z9c-693S8E`cX9^8bn>RE3pF@fD9B#(aTON`*RrZr+tU?kp(=XIW%38_KRgkRZiV06 zo&PFdYrqpAA^M;)F12bcATyH*;r1>!0{>U+tw+)hooF&(}c=cPjv$`7FEm>ERpT%^8f_p3Jz1o?lyFgqvtpSe%;ZiPnT9Xx@Bx z)H{7l?^_5ctY-KAypWj9$fF|)^O}Vi6U&|;hLJLd$7I5|OuEXKWcL>&#FkZhEZn>N1k;k^N7V4Gk`)qMl#nuH2c)nE< zu(dE>Z3prN#wG@3q6^Yj5=gjd&<&?DUkq2e^}1TxIV5Cn2#s$r}W2F8Fv#`LYJh?Y3~)yprMqm+I!Rq9xWqiy}%j%%<~caLFS1@rl{A2=R# zyLYG6qi0Wf;6;A*P?OAJfOSuS+e?MdM0hEM^^MXTR`;E&h|%(=!4f7|V(M%5cQDD? z6D2g>!;7>#J4ZD*MAz1#l^+6^bG90FzTs`4!rjkAI;!qpO~>bL0-$;GJzL6S3i&lT zeu=DlG|R#@i$RV_+!D+6JcTD}3Sw}D6n4whSno=|OeV2d3bfzfP`~xeqh`I_OsR?U z{y;{ZSN{u9&Tq2iSCH!#6I@C^;K}CK9%;tYu0e2 zg+*Q2jtp5J+OpRgqQeWNcH<}BL{cB#t8=Y{!0mm$eLY7yz8TSQN@;%ir9v?sX2*%! zi(^As!dSs!gDT4*ScQHTGq=0AM{@Jb;Z`M&q{{2@r|B`uIzr!-QpJF1i&rvmWU)zk zU1qV@ylZ@J>7UHMUEf$N8@fX`nm=@T>2SW1l@@@Y%@4NWp@vhf@s|2Y zq5^&#w%<+z1I5ro8z1OQ63Yh+MPIkZ8`=^~2JY2(47wkaWuIyz;n~Axz`rHc-|Yy| zwA$}8tOD5N0^EcYtYAH^U~9k9+P*A!QsvQla|<{%OC3N z$t>y7cD!P`Ul+0^$?tnhP(;1?=y-RJve4*m4oHTCxTRXh8z#N}Ye!0sBRW35gQ4#2 zfyS-^o}>U4Kf}VseAnA82eRV>J$;e8pMjg(>I7fg=J@0A>V~}eOZ7*+t_5s;s=ghq z{_U;|m9wQavD zy+6vAQ`$pk03;RhL->56`-8Tt-LAq!%86+>@R5nf3u*{I?PuoZ*U_ozOw(~9TAjWL z@-o3km-+fCCel}Ah1F)mCXCYJ&etUl2g-DtT7O#t3{0~Vg|5kG>nK~TlcO__8@u|# zW$Wl^X`h90+uI?fG9|;66>GfSGQJVh+{jO?0{0N46B%4T-IPte(K**gi`m(FKM+2| zjRjP8u)FDrHaUgCgi>bIuZQ`1Vd(b4{bI@KOJu&sp-5%iBCwmA}?Ed46s^H|Z_JFr|Q<(I@B zV&9?mQ&HSL%51uKKb)RV+`o?F&BRHAHfAT>K`iam>_$ZVw}c}YwnMm}K#3Tfa=OM; zRt0m(?Jy)z_W}j%VGos~9TUZi%m+2_O!%%Q86S~uIA1056GLyZqMtow<-~oQ5x5Ze zP^PbLTZ32?0Q^2bVdKvP9Q%^%>IwF)uW!OcV(}R5`!!&-_I|3IT+xk;5B!OL#IMTr{$NU+BiJs&7Bhz_lWPYbN_~nGi z*Yimt$mjdM4kv}?>*gVt+~-wK&W%;PCtq(){q)|(n-!5vWf*+mt_jP!cUYP69okdA zt1j-V&mMo4YIB)D)fJ=TaTbMDJvw>VTCCS1e@z}!7Qg?h_ThksfY=;@^EHrBA(bU| zEG>!g?m04 z8EFqidnlE+S^iye__yXmFS^#b(vKd|16#tlJ-Uv{^O? zH~c~ck6h~Vw+%!8lJ#jHi!{Xbm8=RILjDH#@gn+ZJ7&<~WtVkcNwLwgI2s-7Z4(*g zTxB4ha2R5yQ5iH0!?p-AVsjezOrdLS7jRR-J7_nvgR2-41;CUQuhDHOjI5NNaCAJ( z>yo<2n_Ppd0dHhrxfo7m#SqG=99!0X^!J}6>urA-WNLgILVX3X!b|ML_XT`U8g+@& z8%(#)ei30uWg~B~)FLF8JhP|ddC+f!_OoDWWS9YI(*D#&Wk%L-a8aBa^n6Fc7+()p zB-NbBu}TsY9YqMrnOM23-o%k=Q~KSY#Zu6ADCVhySKHAUW>B()cXEHdb@C{Y_RhDb zwBzhv_kg=*BFj6z4z7Tk7HH&z58u>_(`wI)WLc<&)d}~ATxS$FvZpyBKnl~6T)keo z%1J7x)-kw&=isu~n%YXf7$#P%-CC=dG1Vf6cCHdzpk#R>kC?2B;pRzXI=8HVSKy%c z<;w2D>hOLq$+x!x;7t7Q>7DXyJe!K`Zqbe6aSOrWOShQ&;7+eiLT=B$fNXwQ>ZPFp z!u0qTGRI{S6sy&npcj)cHHmy^F<8ER^h-r$NT~s%?1bK^-#$4zz~(jCI)fnygntM2 z(;EA!pl$3mJ0LP%*8Mn+!!hr!kVu$OqxNgFxA{0}K&l83FO|B^t^FVpr*8Q9?LCpz zh8D>QGaXrd;9B@e{rp6`GH`!Vw#c7D*cXOYZL6O z+WYr}taAQINvl)5lqBMpzbH| zB@*W%x*+K8wqluCVN8YPRfGEjA2T|3kr@5n^)C)#mp1K4%TH+!)+?^r8Ex*am_Jwg zSa?%hQ@nB-_?!l?)Z#iu0vz=u)0@e9<%KGtX=((|aKBnw7mdO193kwd^83G+MnvBi zV#qtd9|9N^izL1ftUD#zOEI2ent1F(gtDnAWkloLf+-O{GS2;Wbg=MG5*7i{5zkX7br(_GO2H$#fOW znzeQJjjVk2qK9vX#5rw7rFdXY1F91f1xRGp;;}&U&q?Uh)=UnVwo4M9`_QKxGgwU0 zd|5Et*MgrnzH{74IW@rmXh`aj-&vjt8jupxf@}2b-6TVfhd*ez+lQ0WWe`3+aF@QV zKYBt$>M&I;W;^d~t$%Y1NAt`s4r{V1AaDEB?zXtIxVyW%F8WV$?|tw6)KpOvJ3Bqy zJx9ND#uiDDh$0Jus!YCCX?r4*SkoF;#+T9B+W&rbXROqOX1dt@>QN$=7wjDEm+;U| z?gN5jb9;u(@pZmBvYu_e!S9#9LMoLXMCmYl!eKdMczD`*Q+O?#U_yc+d*zK|3=57) zEvW{#vJ@)Ngd3+fhZiXvIBwpCmm72{F-cjf9RM!ghUew7Sz+Fw>!VQzJYz;m;Pw2T zt4-xKJrMiQ*ZhGv{VOVtx$q6dxe!+FQ($r)fx|IAb@TXw6MC6%#LP{Br--LJZ4{E> z7mT$@qhXeoDH{46#{>?p>ImM2emtINikoq|UQ|B+|YY{#0d~U5o94162I5H5r@B%m$IXkfpt4?;*y9SkSRn ztAc8JsaCTe5VTObvH&M_-^aM!`KNmdT@}Wj_6JLEkSl_3cY`fs!Szx@CZEpdjqHxb zWg{zk(_9b?hc#ugtuRUAwD=Jc9c9M%Q8u_0KvrF+7|3|(KP&Luav@6Q`y)v;2q#Us z`}ZvjYebj3(?V09YHyy(G1BB}+`VC6w)0Tho^+7R*1A++c`_=#c0TUw4X`8Coe!@n zC|0K)BDNa5PyYekf3G6aSIvfU)$;QB`{hFC2N>M@Cq2dV1WMma4;%Of9xK{3&j!36 z>AOLTMmdt;oqbF!wnV_$g$$jl$SiudQ4OWRF?y2Apk0qT9C@9KGVI`moG& zEYM%U6p%FDQ_xb%><|Y^<8VaG5s$_h7rwhX@Tb%5$>8^WgBV|BHb?(~R_n9i1pI5&j9*tCTN=z{?vBsb{i)WrAX=SQSmcAF(@wZ`l28yhetM;}skn5j#73vvM-C z`RQI>dZrdvuj0%4GshKoe~YDt-!i}XrH1oc!9WD9-1ho-k|o%>`49@SsR?26f8Qe@ zcueYc`^sQg)5l$bA%V&FMy3X(^j$}GxNqO|A5X8L)l(=-328NXWLdt&N2Bc4bAzXA zH4jgu5e~OApGBM0=B9j~;S{=-ee{-C4jNKOOn(U_I~82Ptrt!bloCEVNM1RX{-=2gZ?loxcI-`f(NE%HD9Ae4 z=JqJf*rM!>Bast?{mNpqH#EX!S}KZ_XvUNwt_mYD7L>+j>t`a0&a0yG$>sVmaC<0u zV)4vp;?4-q zb5{zR?V?YJZKnx|R}RHf-VA79R!ywUc?T}(s^92X-+Z|=r|oymT-g{)L;!wH%o;{~ z_JCAEj1B63y{PzcJ7}HuOqRui)VA`Dr8#OnwUZ=o8b}#EKreJ3ZqN2rRV(!Z?>em3 zI$mA9BBDiz=JjA!uQ3tAiZ5E-NtqdX_7scD_6bMKR;l;_9Is4fhMT+U4Wf7#`5g1+ zM)G1oPi&s-isPsF(Pd=)k%?1ZBw&#q9pCY|3p|k#Phr(`0#3SM9gW@({EBOBR#U8o zD`(*;EE?5J6&FuauI0h|O&jA@w;N3EJGZ35q5-)^VMQR&h+e2vYINhy4AUxU@5AtR zX7}-O;N;s&XyG_;E{AMvtL}uQh8y4^C1`RG z4J!fx@C>7UA>o6YgGeeVEG7Rdjl#!uHe>2%s&a9&NyD-4VwF&GXD z`XegfZup(jig;K~6)NtXxC69#q5Eex8axh%ca|+s(&pd}wG0zw1-(fTBRD zmDC4!X`T$Ri)7Q#;0(gmqQH0KH~xnb1Jk%>VKlN+a`xHq6y{J%u8s?Of=!I%Jo&xY zsFX89kCQ`~)!XYs!#{W**yY*XCV_aZQt?5^rw^USG@eGY0}w@17jj@5{TzbDX4ksA zsPq5D;D5T*szfh>Z4Ivu>Cf%}0DiKbLZSTW5C1;$(Nuu@ZkNdIVu+>iJ&$fU_Z@i{ zE?3ZuwkHmNb;^t|n-Yn~HvBGauF%xl0if8h=@RuKN0sPy!PteC$Lj@^avc~_$t0hR z-ta5{Di0zK!{JckS}A_NQmOxzMyn}dYi^|Y+l;M8;{BgggplEhlzY{unSnIvjHBJe zZ#sDvkEf%LI1{E%Htw3GogDY~BeKuO_jHDL7+(Sz?sp{R8i!N5hyskXqbHB*M6APM@|O?DDmegCx4jN9|JEUfEJ|03pF!qyAp z{dz=8Jo*V6_luC_uff0-)(1z?R&P-){GYYbV_R zW-nhmdwF?zZ=gm578-9B`$ch**5N6qZ~nv6{l7%~DL@&o7nQYi061;d_T73gZMiex zBm!`_F=}FfR-*%TC#TEV*!&14Dnz%|6FuKEUEhwm_lZxRj)-=P6Fb>2k=2^|yE;N% zOti%7k;9z$?rw$X4tretb{L=UgQgTauBnO1d#m)#2nM2$i zh^JK^;WINUwh_^Ry_%5bDY!UUX>BF^^{^~^Ay#Vt28ZoadFp!ZAm`!8n;ZT zL`|&9jKnKTBwX10_4~>kOY--u#BcbX z5G)JQ(gza%gOc+*=(FHTi@P;YC$Jt!mL@~<5h%V$9xq-26qah5J88IZFrccC?f+F( zKoME7b#m*ibPgS*d?D!gkJ6W+7-&^@p#K&pv|XuOz#2t`5c9(U%+kn^?qB-Bpye9ZMT(y~eK=FvFaGh?t~ zgli6imWxHkn83eXp&3hOo}Nf>a^P}!>!32}SpiakS#x;4EilG9=2-<|fy(jGeu z4yjvR*>bWrq;-P`z&nc>Ckk(0O4d#niAP{+FZ`UbM;)Bl<1u|P|P&CFZ+17dL zZZ;Ugx(ZJFMu-M;-Mda_BMwYv6V3GNN~H*2CX)q^swP%KRx$)CQNakCS4zJxwMdHe z&4=;2G`(F%3s3Ox>u^|>_q`9zVvK;dYhtM|Mp;221`^MEEfxf8Hs;y`6gtXJ& zL%BSBF`UbU7t-HgTG9_O*`Khy3Y>m5C>5%pR-^@@dj;`la2?cq3i z!0PZ2)9E0b5&@JZ^U|@<(vJ`-M!NWN9xw!o=%DyC*c*nNp zHBSnUrw2MJ#DI}BYSCZP_~Grn624bc8NC8()l2X%_hFjsn1QH@LyYc=0T^FEn% z1)%J|eHqPijlD@rddS>H0|Km5Ijq{QF2SqdfNw--LWr}o_0~kVu7?I`(jH;68iOPE z9lmcr%5LsNNe+LmZufBgeAuPUI_vhZ?8*{7e%^2x*#^`ee4j1<{v{zWkka%;qRu&M zvFe}%EDqPbaQd(R6&m2?giRbCXSGMq@A5RJ%_SW$1UobugXbRf?i4$a^|rPbIegWz z@_8ds*E9h6V&cq0%8W>6IZ!>BwP7TbTkBjMU9G^=w)~A_)3Hy+FWS^Xgzmd7DlB zyX|QRCc`8x|26e@Jit+Np{SxWRZ~gs-QL`vDk^Gu2wnt?W$odIb+);tURvgOYni?h ze%KO~Nej!dcl5uj-rPfR7R1KdI3Rj%3il{gex;zu_Vr8sIu-rk6YcG4xBBZcbrkS7#M249$8?1B#MSO*|hj|b+KGOWpY>O2{LWawuAo{gU z@Mo_3`E-yuwQVqHdCxJgb|OOQ7w<8^qR1c(Us98A4t1VV$-Oczposb0Kv*ggVbU#p z{_3O1E{9>n$i&+UK~%IrDt01hGPmzft(C$vqMlfL_N5R8TWj^KZ}=RuRR-BWWsJ^H z?^>t-M_@KP9tL8+5!|z@`pW;{e7uyb95Cil5$FhmK9ocU5 zoTa%Q%eF#P4=x@qMkvkSJfG|3J;UL-A>(;CO~kBH(G1tl=pxp#h`bLXGCB|F78z0c z0Rf_s+Vp2pt$6nj#%FldMuIc`{Q7?jc*aME(zw(k>6sZAP%f^ngcfSeD-1_)ugog0EzZ`v zP2PF*K*73_>tyL}nGeVe(Y&TXpP0EWss=oQ4|O3)qKG!@EoOxV3=BLQhXVJ*94Gld zQJ;7^iKmyVMvgIr#ehl)Fh?jSJ>~(kz#nSmvyp{{9`}`NB-p{>&IE_VA944Q>zu_I zw%~gnGL)v^dug7@3PEhcM=r3GRs6H~?Xz&Z^w@CaIFfHRZl- z9(rJ0M5$cli@ zRld1LOXG4?GR`lV!bs6v*_+6VJt5#3;iQdLd0Ov0ScR$pdvka&cA|>XF^|D$+3X+; zpi0EDXG+3;g8V*N{Q_G@`pT^Ok-f6eK;n574jY#_j-opd!5qub8xmu>dG;l;>ha;?rDA zO0;$s!?up&(V7DuOP--J5ayNs35d+Ud=3C-w8^g>2M^|Lzl3+2N8SGf<&zZKImbY0 zuM|~zezlJNuCLO&&7*2|hB3lP&(`4)nM4l_;9K7ap+EE^I>lN9WOH4hFg(g` zS~A1KaJAOP1YyfZRog3AvW3s$`7HomwW!Z-4?Gg*RaUdN$o)DsjRBrMZ zd%T98T4bi1iyrr8iZzv4Qf(9;ssF;Zvu^aaZt?m;QR}-j*iZlo&^=)8okW|_MJwL7(HB94|s*)>zz-}?)Y|Mk^F5?;} z%t!m%e*Lug)5-*4x2dpNrJNp76FO>^R6NW!mU2z27~Br(di2dc(1zT({M( zBnZRec;O0iyB}`!)&#ws2!1J`;tI*8V-XCNUgayJyR~@QT~ryy;qlKhbhNpNvLN!R zyI7*o&N#V!maAiY0W?8nZCPzET2%zUk3xF%-LRn zJ}=Bcw7Q&0jdV*e@nIu)LXz^aT$mwW8vKZr-XYW{GHMhVEWwdkJO4rI$Iw5uLW|rh zy$Yx*5jN2n84&HTr}tZs73{IrQX>#ps$B7Hf)J4?sa|D}A!R&yNob`0Ms<2j^fBzOczLcAnrP%gw9L=wWDHhP9WYq3%DV&b&9|0U463!@Aknzb zKVxpB#iycDwkOZQ3En_>RV_IsP4iFw1`q~+$UUV$D-Lws(Zo<4We{K0-0;Z$tnHsq zOaIX<$Ns(+6Y;q;I_3m7K1f^~Kin0&g2`WxAQI~u=X6(L)AH4)$OaOC1`%O;uWQH?*j2*AI>nRMV-vaM6R~AmYV?D8L%+;_EQ$AesmKmP z<~NG*gNa^hV?_5|mDt9;+N6Gp#&2G5wFsF=sWfAo*{GLTW=y z+Gp+3d9zrK!h%BvI;{(`VyZY$uIL__9q>JHo<$;?Ha-iVyUe&C^GQ=HH4~9d3-1ew zPnIho9NsgyVO`Z^Cp)LV>Dt~5$+6clA;U;S)6t)pBu7>8ju{GVRF!L+eUoR$Cizid zcr?p4PU}0Sy!>`O0wN-Xf?svOGnh?dT`eQPkBFR%1f^kMAGqW*H8y$rBlmY2) zS#xfmCDpovV^WgD=Ta9_H>%l~kx>(f1T8H88ByZ8Wr=?XSM$~)) z_gn}gT&cY-8i3*un%Q?p#K5>%|7TnPYq0kDUqW&wPv**T4CJH1m} z2?{tGS(*zN{PPWOvcZ%qZR-pWMb1M6kvlbS{!E$)&gU1(Qfg|oxJF~VYFk*wSaXwu zPrirBqBLdXGUJ(;RDxZVZjZ_4w0Lmn<2Sg|sO*%0^bs1oy#~}9U~KRZVV+$JxBW@A zR{u@4&O?GFpli+U@1Xg(N`r$>qVs(FVV?!JuCQ%kpiPg+f3wz16uL+>8B#6of5;x| zft!HT3k4|wgWJVuR7HQ84iofiO1)=(B4DCW$YEVXbp~2^t8n9>6g#Z7QI*cv&9^%# z*&yTlLPnz2cG7D!0b9GWn%YwmLSu9Y)>N2=wpMlN$=+Fo}@I|)zok29RfFMt>%+-h<7qi~h}L2QfRKMna; z>rfGo!iYhC1r>?mrzF!lEMJH&-o~PHDki(Z_$n;(#?Dx-4T!36u<2?eN&B?e_Q>h$ z9T`p<+Zv!Mj)_?xS4^nTn}bO&2^}V+{yx)BBjbFl6+q&7DbA2y>ZoWGnM3 z@~p;sp)eAY6EINuSl5oq7!}PHH&m}d?NjrMirVGn&|Y_Cz?<=}fg3u=t{u6$QcM%) zbmK3s?V6Sc$bPD3t4?MPZOZz9w%x9eExvz1h-sWyA|u&YDTV*Te*KF6XXfoAp*)v# zZt6&#SG&RJE8Jw=t-Uj+=G*~d131~JwlPC2(YMQwu!@+_b>Uc)@z*`eXj z3uARc2-}o|6^hOsGCee{A|~SCCNL+DiM_s$sEzCKFq}F1{D(F%ixQ=meut6iU5&|0 z@J*cahp*?h!}sgxz>=(O4l4mmBJjsl`@ht4Jw(A5aT@mSmdN6)NK{3VnVpQpw&HSd zY0mJgk0~M?I^P4&2Fmk}Xr94o!Nhwn>4)4qLH9{yiX8{GDIQy0Fht`QZF7|pD(fkO zxXvq)&0#R#Yn+mw-a@!esr!#|u5k%p-?q=EdUKGA+*I~NJU;BoN?|Z_jCc&3kEDJ0 zIM@{$h=az7E+s9W7yURzMdr(zm$SCJ)`Jvz9r9};mZEr+34s^T z&-(uZ#{roMk5Fxk=y4O82fu=XNaSYA&VKR*2l@TxKf%M0I?c%Jt#70Cx-^c2cXCoX_Cs+K-2D~E9aH*y#>QPOz@_Y1*s8Ivw7c0 zZ{>H=a`#AG2FmDWVa>oU`)o!AJ@(qz3x9}46C7*xaOSJc&57o|hXMMFn&j>8)S@a( zUSXp6aM@ON^kw#;eoeq)*JKFVYkJcV~C z(o;MsA5BwNH;E{bxvuaVtBid5vY#iSLLWqFs zKOyvwIWiSISk?)DyTr5n{rzf6oKbQI$^O8#D4SU8`zn;yVci0?UQvy3Fn?s1-pN3W z^4Epo8Zc(;e=NXf9r|-dPJS^$>xJ-uRHmf!z=QOMfB;?aE{pl#D>-5;38bQ<{TwBX{aoRm4=HrZ5`|wH}&_vo^{ew94IFr6zACtyQ);)`!ree+6O|P z2#AlWCJ~0eJ>aH*T5CJ4xz0HWdt&+voc%@=fA&2~wGrhfbb9>d2xwxnAi_X>%&W%E z9$>Q~?mF3p=m&Y`!n!`y@@TrAlNEcWZ!s`8(&W_^!%Pxtz}&UPP0!7Oh9U>JAkn`lf1BODnoSY#75LL*bEw%|gTTwi zG0fp)4YK>|XEHL-$^omHR+*-9RCXuEnCbh{PQZPrnTnK{OJeq0R}{8Dk9*?}!x*2~ zljt9!x`sVq1J(0P4~>ntWy^mR@VBN^-N1f!;_f5=&Q4o((Kya9ue}NqG zg0NG@eP6N}$f6p$j%Rb^SnpXveX`s!LWpe8^W2eANVgM>c&XK0e_dOdI78b=QgTtxU>hE1-}7 zM6rgpW)7j3vANaJ&CAW-UhIU9r^U=QpiQ5?Z~mRS0k2i$5YEFRoqhg+B-Oz9_Jt7X z0w`&g@QikgTrU(aKCCKUF86Lpp+9VVq1@RenIt(<00lMw5z|&#ycqzxhhIOjEc z2{)Gc;1?seS0kT~+v{ECYYf=9kQ!*{-;}RnwHKz*8n3Bf?-buUK6|>8ALW!bi+U32X1TrB;hQZdr4a;&Fs#&*3X_b$eWVm~|HQ!ApI9I=!DBvw0 zQZsQ7`1HY3&Va}St5mS|iB03p3F$ScP)l6a9X2ghJ8)wTbE*jGMi+KEEb^;w;S#mi zG;TE;68%gjH6*J~?Cu;|Ro&t4p{s|{f|5xOuC^i>K()nQkRQctwi1&!SZmegOe!Xv z$sD5Cgfg9{rbu@u`XP0D5B~5eq}3^aFD5v@+CtvSC<~~jf!lF+z0a%>D6+T0ZHXnk zCCvQn9ciyMSCx18EFZSR`Kt*@mM465!&JMh=UdTIw-zr69KQPf7dv=Y7G+1mQp-Ve z)jXMYELm@oua$F^kM~XvNo25Ofj)a^BeNbJKjv}zPh6h({wfY7wzWA%v(W#?1Jfm&9>KUW0Abwiv`h^O`9oQSn z;d9Z;ZZpyGY4SWg@>s_dN$tpckjc~i-00)J%T;26$mHGo&|@a>9+}%Rf!H99vi1+D zXD!^4+6dF9h7WGfjLqrP68z|l0laTl6P*il2xY9VEYP+Vk5iEr5FV?k-U?+J(%>0> z3r<%!jY_N@20J-;Z0+ujNKwO+ zQ-`#(r%Nnobsxv^nj%Rbc~kMVnr^1hBfVN#6hFZT&Nls$tPXzqP}c*h5+C$!)ja$F z7D~dDl~H0`Vap-kOgkM)ocsL9XRuv<rQHi}ud<~KfXN>x|nAulVy0+PW;gg3D4&cs>WQxFjh2MO)NJT} zq$>n@SuxVh5N$gg5RV;GJje=H{aDo9QDvN!n- z-*#9n4%O~WD5pznIsK~@)U^bYq*N!thKk*KJ1H z8vA~*p)!jHGKf{3DqR^1FG2WANG&h}#Uj|_qv`ZO!%J#4aY3Btc+9=KvC(}ZjmoeC zl-@wYTdg~k&O`twAy}NEQqq3FRy#--VX5H*C5iPr^sdbzpcpkymugPC=TvetAfA@Fqt|fQdt?Z*r7K71O1V+k|yN9WCB+ev7amS^Z|d#M#10 zJc<_gMpcoNdgqKEeD!`zf*qZDAg}KIg~WbLAe=rLN~@ya@6WY|*7FHGEXYgfuZ@=% z42!K*>rvNP#2TLIu{60v?RYhzrq0Fzyl7;7e*S+)*145)H%=!DQd?j!d1KcY+*UDD zI?h7CnWKT^Pt+w@*@F(J%dQ%0q9$bE=7R<`K>727LYh&kck97(9p4wW?9>C>20K(V4ULSlkBWP0@v!1^Dan6rF(91n zVa?n=Iw7s`WMV-`2ISN_P>t>>LN5`4F$6vm_b+%xjLe`5GZNTn{dbaw5`@nO z!UV6gDsu*~)%e5iS4sEbTU~fB?{HC05E$$9G;g0eQ9s2pnA~dR2JmunwCZEvc=DR^ z@fv@BXTmc@H;!X=#Fu=IFV8_ow|2XGi*GAID@i$50lqb#dz; z!@uA|LL%&!{o$(-Mf_XE?A#*Q2o->j833!oX)(?D7WkDkcJ|+Ms3|*r9g<|@*%%B+ z)4vzMiQ~N>7xF+C)Y%MqhXVtAD~oBGuVJ1uBLVEt2Kt;TumgpAy3kJ+G(l>vGENbR zB8=?ZkU{J~_Yq*e!g&NUoEM9o(bH15$3nvXli5ulKh_xXfrP0~j9lx0I}ffcC*1u6 ze6;HIKUes3J43s-hm*g4jsuFPl-$%+SQ6a2B$QDD7Ydh1G4XUnH+IHnuq=A$iVmFj z@MkmzLsh4TjNt}$ZVo2@%(8M!%}f3N&Krtl`Zbha`uzg%x|NE7O?yM@fTQw*@mOynE>IU%>u3G8AF| z?jrB`EB!yB^`;Kk%)J?sFy6YnFF=2eHWUP-zrzSCcQ~Yh#eqS+ga3Q#3)nDz-oM{% z<#Pp#vI}43!~N?8{7>Nc83?NWyVhuV;*JSioLLSSUe}+h=L6f%{=M!h`M;m2Ie6to zU=q0Ig!}^L@8R|JzyA!N63S51eMb8CYWOcu^1YaJkVeQC+rMB8?~J7HwP%sV(7CJG z*x1N9*V-dKfBGy@N<%}cNySR+qmhy-$&L=^^=CLG@T+sil$9%5%5RkzsY3|%4Hh3b z<}^V`EA|s3ZUBq>C{uvDH4q(LTcSxWoxp+n@6Mas<3Y5vv{d2F{|BS8R2EVlCU%;UX!!1B(0jR~SwXoQr?=H)}^0fp9<@I9Pe2_|*WA!IQSTTU8QEqe4h! zw@p)X*p-BHEdDKlIX=cycOn<+*9u{4w6iJu;Lt0T@|#)neqv3tbu4gV$Eg3XyV|<@ zv+*lVk8`Rk9Okq_d8)#-4tJsypVimTBa51#<3< z2rOi}dyes$qqJ3_5sqq6vXch2ecD6Bb%DMKrHNR{Jt)@8xtq-XPMuU`^wIugHVG)B z<>%wWqmVuel51lsRQKMdR+V%``kX^HqBY8Trp82YJgZi3F+fA{M&169XxUZD-=TQw zDmMJ)+(TptdxlOG)N*#7`&&16{7im_Tro&*vfe||WTBGo73j5G&iqh;)=**f_XC=s zgJY>j7rTt9gSzqVftdm;FV1SCX(wl%S`f3j3P5YDP=?uN@ilYEFC{7J;Q@aOFzit0 zsYq4q`lAXNSF!q0d-WWf%AeNP3xS-5#bU|?qJ3(>E2Gxcx{s*+(G1B=1`8H1H^VWW ztk3U4>&idWW{60qjmP2KQ>c!2Ra9*cjkHec zwvoHs-WEKxGg>(D+?{!@jQ!F#-7C0tJ3ZKW2Kv>t>Rk&uze{39Se@HT_*cyE9m&J| zc|;PeI6Z$)nJo&dy~rC@mDr)xij3p3FoO=HF`-Ffv&8H(9q^JoD=Qq{xPG}=ki=yW zZIF}ClaR8_3(IA~t{n?FV<1bwy?_qX{{TlZy73~-#h&vpMgB13Et9g_nwNyWB-Qn+o~+TN z{9d6Y=xn=_0iEdil z)Xm2KsCc|T(x7%LLp6U3tsVD)MV*`5^G@Ok24-?CK6iZHVY{r=m;8`GQ-=#KERK(m zyXB-nWlnAUZbr3!U=}utUGa^}{jkU}+NP*_%W_4up%Db|ox(a#rh-I4xQ`psSd1o7 zc54*_@%LG@us7za1jV6=Mp@-85{WgUL9!N2EL6&2RF~ox!g3%j_L^``;I)G%ibX3) zV)0Z`GrqI>-}^V#azny}GAi!*L4}VA!b4|uCyUV+{V;$Wr}==4Og^)e)XmU^z zzhW7;(}DMdIbWtgw;LY*ZLx6mBYb76h-EAOvpS#6TS~S0$pUiCc?I_1o^VAh8MD0E z&6~BCsXQ&>X}xhBFkBom!RZHkR%PgMiIHdqZfNeN5}+=s?N3@rAEm zozU%+cTR5eA|f-|Tup0ee22%88uXDGzmB&2tLa>Ii`~!{LKy@XIYIm~65WH)9ruIb zN5XY?PU>_Xp=0QVgT_O>@r--K17DXq4$z&OaAx=1IXcJmb}l%++G!dTz>7WXZdcEk zmil|a^Yhiex=zr4#7573v>ILy1`^jCO1Ztys!?wji&e8vxYU_KpL;!Gp3~@Ll}Y3vtYGSyTE(?aYp(2(-#(5!PO9iPM>;-lE%6` zL$^YWMqddw`Bo)GHRu957KeVA-I2$L>pNeLSp^go=@cFl8CtN2>Ea}qUjN+*)24RW zMPB%CH?VF0u3;_tnd>W0cu9M-r1TH;hE;Ld~ds*v-+9c7tDODi^P! z&piu0O1v67dIqcP>l8ddRPpx_6a7SveZw|B}W6PXprnVzDQ zY%8{d*>lKVW2ObO#Vk$^?AQNUa`!MbIfj!e+JH4?&a{j41S6*qiZ>>`eJhWp&iuwr zTTytTlF8z3k)+EQ`O@}|qV*S9C{})OgZtAt)8lvn(xdB*S9L_WBCYH6AfsVF#L@=l z3*lGJOJ-^;gn>6><5Sq1Oso(eG^qSvD zxGKxf@v58P5R93nmX~cyOnfg!hVzq!{|0IJjzR=Lg(Q9(eE*onFF+m0qyL!pG~(*c z!bJQ2221TGYa9O=v!k#u|3r6CrlSC(pNN+q@846~Wa0m_s12Y`sZ@_XDl>W~;pMr5LSNkCxrhJ}GVM!TTpCXr2p5K6?x^Zz43{fi$H8I(U zq2CY$(d;M(7hcP_5u`U;C1On%NQnuiN13q~-ettzTlvPRzE#>i=RdE19g;w9N^6_e z$1|dtEkcNFmb2x!qGyKx`<-yVw~;k zpGamhn4qg_wM1`;MY+eSHu0|) zh#kON(?y8lm4YYTYE3EX{H8$u$@d#<3hD{m8|9W4=L#&U5I1*j z_;9OS*6a$G%WLuvzxv7r`%gPXidbw+bA2E@$5bki3F>)wMAX~$O^3}BpQkH5fLn>2 z$YdxC?~83m_hv3FP*FOM*zRAX=ce5h&{e57_1XoxUnB7a7e#={7800rRW`s>Ko*SE zjv<)V`;IV?A^v$FkxM+mSXNL+q4Y=z=RTn3Ttk^5fus7SP4@iUgUr+F-DgZS;bV+Q zGcMI}VmTE~PpDX&%4Uim<+Mev@4$?hwQP}^5Y~KIa`CF}Ap_)BrQFfBLYaZ$e7SwR zM%}-@0IDY)26}p*P*u;sOd_1;@@>_1>knUj8^;mQneMq9v-2SpEOTG*lhYc15P48btM-&!%s2%$e+zYQhyLi5N~BE7gWEEU zOYg4eRzg6pxiMCIMPM|QS_If~(xq7YB%d@8`GJX}oJ zcl^#oqt4H*ymD%D5ZYQ`S$9qU`=pQOIrFLGV@K;R@RO+<5|V1k8su!E+V~##?qF6* z?F$Hg=F9fxv_^v|G)Xe^a7jvy95<%OH`d@P)0m%1FSalD#zIT#k2hLi8yDdJLtK`P z7|#BPgTFyTMd8bF!rDuhQEW@g#7_xA)^l>tRYnACcYQ(d#pEp+wMS2ymFsw~#WvWs zXfr=%b|umIX&}lBeb(Y}l~Hn)Egj&I6Z=3ZSW~2&$rh|6=GE%H^dnj@gL7o(aV~Bbl~CPqtcmbA2w0Tu5dY( zluFh{epQU<@XJa?&e;5jd>L)(Mym?eA{E||b5x{^{ms3O^^oQFJf!4WD|}wj*&`Jj zxCU)8ibVTL3=5^wTIudWQjn_FuD+DM)LeV0G=TP$GvNElhQ zHVii-dm@~%bUTv!LCJ@A_Mi*X9eA{G^NF}{x;pX(qFSAzp)83#nR%Vr)8m4@hO#KV2@hqu}JN+?*1g{_zwB z`T$R1Z!FsIyj`YTk4cTuwrSBdYm&wQMTOkc!ep_55@)f(k6vGtELwDjLhDfBMTP+oV^YQNlMjsu^E6E~+@Eer@E1tmjU0wIOxLxh+iNn^&<2lSo z>nKz_0ej}#whQE;h?$hd!znz8Ronxn8{>X)acOB6a%?}I)J@$4&)7AZ!j?i@Oceyb zStBbrNI8JeX8s=w@b+^Hk7vOLTD4JP6r+KfA)MKU14jzjS(6U!Rv16rmW(zSNX95@aE2gV|Vd2+C9iRWy=<@l>1A8Xe83OGLCfe&CW9hq6`bhxLiiZB}5TdA(P&%dGC0@5TLP z-$5xSS^o%q>wKT~^1Qc_&?CkWX=QZQdJS#ge+3P)qeP=E7E^slv=b+U@5-(g*f4vK zh>OOfnestzBPqBiNc3l+^St6c--Fh)qDg6gf)}zwtX%nMmFCHhwWf zHhT98+j39o90~p_IV=%74Iv6-3O#Q4y?wM~3b%HHFImW;bga;ja_u?^I+Ge$u>@#{ zom=!pQ^otqwrEeg&T*zzAxq5Jgk=9kmC9=9ZEMiD_2J#bz`yiRL?*s|&_+N7FxpnSlh%VT8*t_8cmmvNwXQN4hQ^hJ!x>($jOXadv0 zR2xgnS4-S{arU0!HTry$WVU1%vpn@yrUqx~=UfhZb%bw~yCUqO-$w@6`qTt0Fps z+uYTa`sLCWE77T_mv$u=E;GKZs}779Sv(MldOnJ9zpZ8C|0mwd(O7#LB0LQT1i{tc z&5vS`1UF^on94OO@rbh+|BjjpTyi}8AxXR_%@!Po5f?hfj)4XTye#ZX=EmikUmb|s z^-Rw2QATj}51u*GA*+H_)sMI7AUMYigBE{-!rQ`sW1ma`3zkiniYH?^3%@ zy$H;l*JonDZ)joP(+$RvMDMdL;uo7z9b*iCBBLIE<(qu9AF94CZV|dz=CU`bWn*tW zM$P0}e6-lj>Qd!h{fRPkhNE%y)?Y{v$jR26ar0VOt~6(1{5YX}3!|G(Ed=^<)f5`Y zyBvRYom%#ASiY62c~BrnT-W%Kw9?KwoOc-{TmHg8Wv{@=s+1@zrNpUmVomR*aS2<0t;V>fSP{%C>D61)Q{W zisVE(MUavP=|;Md5~M?#Ntbj=vk3>|ac z*L7cc#&H~{EyRnf)Y&h6tU)ay-Hn4jA;EF1_Gkl~s*c!z?;v*(_{8m6~;UrC@70d=4WC_&GP6=1~6+uK@jN>e!-UWwOv_+@t zBeTYTF6^!b`IgQ}s4Gyf8QL)sFG|TZ>)%tS;%wz5Rc|pv7pWvuBv6Q*mh|`=^>%r0 zm7%*!9v+?HsCv%R3i?4WvASoBkK^(gp%OpLM-tJAFMPR*2C6C$&BASdT1^4C$jU_mIiBATeM5TBKxaLpUaSB8>7r2|xpkB` z@9*OXRMCvJr?Ry^KvK3^p(()bTiKx@Gzt47x9;lc%8jHHy3l-cJDG`-@)0S2^{~y0 zX&r-)ubfi&_2=IRxGI^25ZGuM>=It62}V{&y& zv6a=X=4``rC~I90~mR(l$$KkxZH9wKXmt0@C%J<#ehY{K6K^hKhQ& ze-mg7qB$UurUUP3uoi1_$vS;00?T^tQ4hWQ1+Jf+YWoOS^ld=!r2DG*lK^3Tz|ftC z7~9GatT^Hwx#$bcRZttCGv;hPMRrHyMQq>Bq&2_IZcdZ_Zr1a=rVxVZp=Uob<>gKq zA8~`pZq1=#H!fDUz1i$3`$MwPJX>k zUA1G-Yg{3Z%bh5VQmV*CP>&eQ9GHG|gx4yuAhInuyjMZ?o2eq&!*6>KWZ*)22OFK8+#g zr;6M#oM^6$^SRiWB$%u8f2>xYxz1i$vtX$uV`)f^n9S2k;X<(QJ2DNOWY#LEFde`( zVbWZw=m-w=^;^StgB{Re!b-=-@u!@o@PdFtNcNuLxQN{aS+Jb0N)vO+=ADzaJ8OuTm zZ>ZA##SPL8g-;FFk(cZe+fV{c9^QfeeC}gbvZ}%qV<;c6X!#I|oD6N=Jmf54khoAD zqcuW%u4Q6jqVGNK6Z>5ml)gUD|3q#6=0ZPAM(~|tie^k}wEdIO1d^%x#&G9{VbeOG zX`^A)G)`^whXg7Nh}cIkBk0ica1l0FK0R%@^Cro>je;jaPBYc}-mDh)L;WXj`WE>G zW}NnXvscM;oiA+0W}RWrRDk-C7ew`nEZQ}K{-^}#BHkT(UEw6=fJ#qRldZeUXMi@_ z=S3r%G`?O5l-3aiBoOY_`SavbvjFjn`E_U)+EW~Fr1svw=~Y-niWaa9P&ZQLy9%A_ z$vPU7M!6r*jEHkcpVhZ1Y0#o2-1LEmsujT>q zA@K0Pz~@Wo0ol54Fb)kqw|#SbXQhjH))Ct7(zB|EQOiCe5}f=eg-i>J52<(B%Nos< z?la=zcIu6#`vwsI+0BN@eF4Y}E}njw^fP_*kSPW%L%0#it@CTqmJN{MnIh`dSQbOCoqQ$Mj zzCT+VbG|<(4G6l#W{pM>iDpm^;9sKr?T(^MOC%C8k3)XtI?!-m2GYGsN@ZeMsUft) zvoD$oRW^kyYJ0N8dMv6G$^CRwG4ZNBlpunL-Hcoe&;SNrt`tEBZnG0%}Ku~5r-4}2@)xI!epwSZXy&w)gw)kdF&jicL} ztK$PeM@kZE1FZENDg`VEG4aaww$#n#SvK%K5<3|Q2?=iy2Do5@1Pjssq7!T|;V$V6 z)m3aUL5Ny5g>kPNNM@#^XJ1Ve8Mcg6J`FwBw=<31Bcv8M-1 zMg?hkd3m!lU7wkf8}j6*1iFPN#=gqNfTR8Qf%?0mMeBY_i$_fWJ+Z7RMx!);bux+5 zT8{Ehl|2Le+|3rWQ1CCaB{~-eoctokdz@5LyfM-Yova?QTEhL5UgyH`@ zAJdx()1rkD4(aJj)aP<=kF7#BB?TXm9*RN{-&kf)WGuuj} z{sigiNXrXDq`&X34+P;$v+mJOlfXb}8gh~TU6z6{@%J6A@#SC*CWL#qL!IwL@I5KdKpJQ4N#g1hinfDRl8Y-YAUJFq!%<+sxcx`#+A$ zpo!{_+jQ#uy~boYmc*OMRGr?g(eu1Cy6~6)msEA%$5&|-8(l5K0hdMg^--Z)QK0!ne zBTcO|U?GS5h(wTp@G#!P_`qy^9H5EloSRDpWTYLRgaU_nqL+nryPi16xisFC!05@t zH%sY(Zx$?v;S(ft*@DqXcvLi>Ordan7^el!bivBaWPDyo3ntSbeJAMt7<}msjQDFB zjWUGJ7YrGVCfEShWYt#haiNAH34%2JSfjWPL8 zh@(4~PDCyHJgyy0DgZq*)h`T6>Kal74>!!KJK@8LFs=kvJWl_vC=7U*?GO{Pegt5a zBc2cbUB9peSl&pLPpR%9VE|5V3H`M-zYYH53VZtE{+T%i(95!y=&g;KO%ZT{Ec^_6rSnr3+Y>9GxE5nr%$ELn;UV_dKz{1V$z-t4cIOTFF%E)*f3* zdn#tOqW;+~ZfLbUf7>)PyvSqO`e-BOfCk0$xv(5_S&rB+w`aaFYqfZO2W$18mFVA7 zi)ZSXO}wkpENYDu_`!-(4}%=utI^*0xcP{afQ4r~W(s(11$x`eJ#-p#?FKw;#j{W^ z5Ax>F+vOp|<7N^wMe#Yd_RJcWSvr0#0@RM|PcCz7XTN2yp%|P@b=BN7Gg6>w%C~L% z`}%HAr&A=mxF46rNa+33PYi;QWaep+bIDRdQf5=$(_CA_kM5w&ui9_(!Ovrq7aB#S z3^1?fa9~R@Ebqw9ziv#(3E8gsr8T+M&&WZ=ydF!fhV!kCSQ7>;Bxf5L4Ix6BM_^vZ{R$B=3rCU>;Nr-&{Gu}dr}Z%J^h0WbajH$r z{mktFe#?D-iJ*@g!^Zt#xo2{NOE=dVR#*2$BWRj4HbV6@e!=1lnIzax^V88;$^8_f zq1Fv(a=#WHT5rmN67VOwhP18#jkN{F{9`@a-9PE23Ip6qNcTcReMMigF0I37evhD} z{8^P|>THJ_WsTRr?*`9r=K}W~Jj_O_9_FRmOw)$#aq-K5?-##i0UiWdx+8B_nyn{U zm(kN#SX4itC|Nm0U1jkT^B4N{j*!XI{X@5j9}co>!e3}|V>ae?po#OAUImv#mo&fs zdsechx_bW3iqFHG>jxvb*RG@S5uf#!qbJ%peSh7C@|y+EKZw{jL3)9mfkr=RpN>{` z1DB0uX+dB%V@L=nr2I%>Sds^D5?~o^l>0;25ED4+`lx5HvAUhlr?0|Ektxgywn@rX zcoc6bxpVOjzXb_;@*L!9JYjZ_Qpb*|9S)7N-W*Zy^DlVVnf3LHk;D>GPC@n_5 zkXqo?fccJBx9GgQj!|6{kf4#iZ1)R-wUg zWFQZm*`p(k(y-}?W?dmHtbXvxmZbqau2;W#oj0aI?e&#g-xJ|j{{OL7Qab$XAG@$C zPcE0jwYs$>c@2%Bk*EDQU5QIbDACl^gp2$B!jt7Hf_2+9LlYD)Cpbl58hZsZg>9B! z_MFpT3bcBp^X9nwK+tN8D2X41-b$%3LmIvwaIf4T*O27%EMyjmW=st;a*Y5B7xgk+`SA=f zB!~5|ATRV@XMw2#4^w(+_cn4DG*BFr86729>3=XRNcKc^1<%&6HPt!G?ok}TGJaAC z>itu;HFceE`o7LZRj^4f?c)@!E~#^j)c|r1cVfplfYqj_BS!q|cD*LgP=8_o&rVjX z32a>YYcpMIC|-3aiKuv!gE7AwR`HhW?T zz7KMqEwaz~4ev$^lir&3zxuMN`LP1n@Zw>9b91ftfNSKgzu^hs%0UdMw491oE!L)c zA~HOV!zvCs)?xcBDcwUv{cG;<4By(d>99)X&d_R%z|^^pFDLRx0Hu$}ynp0fkzj>Y z_A2RW44J&A&=5g0+YS#V#Khp0JNCaf5fJ;hl^z;4WpPv`VQ_BEWZsA!_g%hKnx(k3 z3qQ+wXH*&3NgT_HQ7vTHz0S{Hca9~b==u<*jj5FaB$ zT=cLWSeuv4VXK!S)Z!!|16Mak;~tlPHlDbU6@>`m8!&d(r1p7s&?<*FeAZ(pt=(|Q z{!}|hXT|LNgiqoq}`hX>| zNiEj?uJ)^4CYLaIxmE%c!?T7|f7F4hC^d*pUW&k=w(gKXN|p1aaZ;peQ$K_Oipc+R zOZO}Bdj>4C2twrgdf&a5X3Eshy6y*8P=;nPsI61iem$s=5ew2<;=84~bG6DsZ}q#* zI5}Bpo!q@*g=uc*ifeR+s)$N-W!2%hhLg90+XE~>Ga9;00uTiPR9|!n1GWQi4|JmUhC6TJ1imEriIC+;%5r;F+r|m@-Hml#;OTj!yx$K=Z6*ri)F@rj zl}5c6mH1mGg>#R#%el;w>!W>Pf)n(?0H<^d`@&(K@oHzjl-y%?`f!`NL z@G&Y-N(9Nre_aaF{J1Ss7paMrAH?K=mG4Da{5+-95YCXGSnI2TTG_Nyb}NaY1(qvEWqj3&mYSpCvF(MW#Ec8>o*-3Z0c0tsBhH*0pUOX`?k%cqNFCG9ANVgIqV7xvT}?Sre){r=j~ zJj%N@dOF>g8(SN&{aid~RMxSgv6&}vzZwsjglsW-Yk5>)%554Zj!8X^IjxK<5neSR zh-Lr)=N-W2{~s$GAzqL{E)6+Bi2;sJ8ntiMa0&hVN`?R>5Mj>dwn?42Y;%H$<(=(aOnc&0*^ovRQKQjc_a74bDsn{YvY7!`!v+i*#!MIo` zXQX}M8TBdFLI>YvFSMgrxx2oE&a9@>X7JsYo*r2w!Kl2$^smlz?Hf4UZXq|pOwqme zx{O{%6_p%7(_c$o?~s@S2ku#b55ql$2RLwJ1XWOyG7|q7a_e@v&vMiUG+b3 z@Q|2qzLKEm#%c>LpdOz)T&aj~_|`8F!$7*XF zC2!+o7B0x2HdOHrxU>Cuf2+O<*m*qd^Qra2OOLE-eE3Q0MXb(H71~_TROViVQI#rE z9H;0EEnWZ>6-FASfg8N{R3Smiy^Ps5MlZ1{wNTlT5YW|qxlTp!haa}sIztat-W%pm0Y zyUZ`@0Qfx&%m3x26-o#em0m1Amtd&~sOs++j10oIzV-ie(4Eu>1$owOi0Piik8I=_ z?2LW=#F}E*fmY@I%;JNFSSOf%o>Lt3)hycY-ejxaXgnb4xEmDt>aIwn1@aj3ByT<; z>xRxEKC3OLUYkkQL1`ApNgJ!rZi?OEG;qPCGxcc{UL|d5LDZ8T!{bR!mLR+V{QpSP zo@kXoG7B4KCPQdm+ zK3MMAIq@hTl#mZDpo+c1EtzaIs)z^#tn~pN;#U8?ee~+C3|sj}>$V#?A0Of=bjo&O zmQ5g6Q%X@g(PMq;kzDIbIfX-5x@3Kf7aziP5f$6W|uJ~nBHJCnfKyY`c#}=(=TMzDSku@3jA3ahg~POmO~qd&cn!ScD9o#8M4SR4$xavU>BL zj>xW0ze|wvNj)dd<70$);$XxqjAz%~a#4L25B9VW(&Zq)fe%37ZNVlC5pRBL{b$xA zfI61x(-CS1B~;rAR~!UXrq@vp3ZdxHdG8%KdMc)$#-w4Ki9VN8rd{1kTS z8Uk#-xIpc14h&&4zmv!q0SYw`+2@w7d5rFPi-8#NAawiaxkE$l-C96=^ZLZGqRH*2 z&nmaH=q*nN%2+ck9)u!&OD9hjw}y}1Co$Q6UWF~PgHFudHOdnts$Vk!V5g?+OwI2L z+B@Q1Aq${PvfddK=(P$6pJ3VhA(y>=1f*Yh6~{#aKQs> z5hQ5xr$Zpoz0UG$%iN@+#ERoZ)G|har8Y?Ol~@>LQ-7h=ts5w$R(DmA%(K*ACu)@` z1gHfp6ng^jBa39!4ge`a(j+tOjz`U{PlosI4uxj4bQ~TeER%;0J13W2SDz}@|-(| zzqVWEbFervjpkU&r@K_o(B78Fp_1#(k7E-C$FMvb^0L8VhGu!O$eZw@i=5J<$|@*5 z6!Zb5hem2Pm#yD=xR)jyJ*=IYs7m$@`sIeAKYG6Xvc4vl5O}&@Q_>m%nnYdSCYRiy z9z}l3&w4l_?#ijo$G4oiz&YY(|nOpmhuD}WJ+a>P9FeYg&P2YP?N zG6PbWZcoa{BAL9I^*Vgn>S*V1{w&CP@K{qfm5jdE@19;?eP@Au4hqn63ZVk9li~|@ z7uDmb(Ks9E?`^-bGq0WBwjrX_5J(6?1wxvAX*X>nWln$@TExo5B}R;Dm)xL_7Os!d zGEuYZ17}5qx0N(Ca1ANbvsSFG~tCyaQuk`RZyUBK=JE0~w9aQ7s&1{cu8 zG8-A~#VpV~D|^0sBdLkQeI{Kh0l%`k{Q{JW!`&6Z!gu%$Q-hubk9!XykJl~BPacp{1Z%F#8WH+{yd;1B1eAo?5R zrb8}vjY?gf=WBd9LCAopuLE3f6IV8_%_UgDOU|gvG0V>wClYZ3rT;!!<0%U@+~4py zfC|NCpAbl9ddOWx4Uq*DK>V;EasIr2nv80Fu?P+KQCeLdEhbeOeKagu=u~inn_Q0w z=e4*RR`<>2^{g67Yvhg|l;wB_6OvhB(hZz@?^r!t847)`@3P+71lgPc=zYxe?z!$+0r4LoVbI%izm}`V zwiVwB79p*MH%jn}P6|NNQDbQGbY7NC^^;hVi^+)UL|;7N)cZveef;jXYj?mPM_^pA zx}{U%^mJ2H)jVY8J7LGpACa6D*1r}A5F8TsRE#pR zHxmmF?XV7_Cufc|(x5hnj9JfL&5xw+M_A5YS+fk~ZrfQS?JZouORp7`WI#=$JutATqZ8Scj6Bz_+o3y4?0U8SEu2s3# z0gGow^5+B1?9lJUtyg$cnp-scUOxk%fq-Yv#6|ev00BIA(W+7iQrPr6!*Hb4S#LTm z>Nt|K`2)&hc>W2hV5dIJK#w$*ktoXjfsY$W%bqA_|xJ7h!54q0&3R zmLjP(M4Du2nuNZ*NMTLk?<9h%76Up5VoCp<0ZO{hM_rD;eeh2_q%-U~+TXn~-qS@V z9I>top>P)Kh(z3`57^WbZ#I-jdCH8-8OYl~aDjLR*!XbUYC8>^7g>|kdC_J6FD_SY ze9Iw$b02PC^tg;fu-F79)H1V`A|R=XKEP&P5dHR$dr7?H$EO)al;6L(06$qaiJgJW zuiRD@A*Antu&)ZFHIn#a6bE`w)1C>8YJwH+(6x6sZN=n6)VPW(G3|uSq-PKn>DmuQ zfkX_JglEDH-Qt3vG>iGj1ttW4>F3{=ez+j{cYicSh+;jxB?a8i2yX_T_JJKatF4tt z3r&J$q~+g7scppPREMF3JZ<<*m}H7nt1d|J{3uxw^1y*8@RkV|Xa6CIQi^N>BiWbO~Z=18OIDogFCl+g9oeNTBY5)Q)w4I$tG^g7uh)IuhyDg0zDfYAh64cy4-f-7eO0Fe z2nL0`f&=CVtM0w7Dt~ZfO9EB&E@ty7$iRDA6%;f~~H^OfT=r;I&Mq^EM zkQ9Fp$WV-qd)h|=Mw&}w*)AjC_%rSB5B^*XP`ypM5V**r9YM=xAKgxu!p zo$I8PZ<4Z2A$g?D*3jy?a+=u?93aL4R8spdcVS(Y_aTJ|NfdAK>PbJHnh{t>o9)Nq z%?*;b)xv*uvYXGhcQb{t>h_D}gY|bvJr(V~&{3$C!1OGsD-u}Y2`aY8Klmn08_Rp} z!ufS3^s%S+pNp)ZT`ObLd}AJEgC?LBLe$3w}@#YbN|!zhRn%-f2xlK z3kp#8!=QP*ckcsK-bUEoNDlojA|-z_TlX>@Mr+=^0BW;%es<{_!v|Fd|*4ceRIBj#!Nx5jK#@y9F~amB--$bF(XwaS&-+ z`4M^mlzO+kEVl8yau8nA5fSEBY&KXiyUP^V1WFjK0*I9dcAhu}7$ai1z4=~Kf#1;b zx@q*g3au=FOwXy=-#^X;xG&?tEz;YkwO z%>Pw;BG&^kLJ?q{e^Zlv(>SA=bxwab|4g{k21>9vqO=U9{BlV}Rt`s$!oNxfKh>@wE zGugShRc2f1`Bnq;rxqt~PY1gaURzFy?flsPiYkG4ed4CNju!veW zG~{c{q{XsJ8cx@{)Q&j9IhX1P2-k2wo(S(j=Rqu6O(I>2Lxatur?t{sy<0b<#zdHe z5^*0m^ISO!MgxlKbJhy=BG4;cZ&d3(UGU&BM+V{#5V7br+icOxrj&W#t1nDdyw6F~ znMNwmw%z71W501cl1gMQJE$y$X9>oFa{;<@6N*o=f*V4_zGmsHFf< z8P{xA@&;pmeQ>%{GKIEoJzS@cWd~C-iaas!L_K)Ti08Xy2ugQIzgnJnKGjj% zw%41r|2yq$RENuP5PN&NiE6DcRYZOFt-`=Io_T-dyi&u_ZNpzqo9#S3-D+?ArCwrY zIh9TOpJ%bML4)7%`jmRG#l8tPX)7^$Vz;r0N7>cgZl!lGgQ3oC%iJHx*9KBk0a*s7 zIWLX^)eN#`mbVK8EZ9ti@4|Ul0z}DAHe8rgbI9W^%KUKE2;_G&K}URkYmQ&FS$H`a z;{Yzc4^u5b_{z=px|UQMx|Va^=g)XsR6km?z!?m?lGyw4QtJ#b!T%~pmVuf|Ht#?1D|M^tLv>3qSFUZ666_g^_! zr+>EWleoRO&$g!Hn}Y60`8?lRa9ZVIJ#ka0c#fFol%bRaWR)zD6X_Rk^4?P zTUut=k+tWB-VGETJ9cumk5R{)EIiJ=GKxb`se6xnAvXB23^TEUpfH2MYd=A6+fq!0 z(bnDaUKoK<`0}LEaq>XP;nriV4%d^Bd_9=u8dz~kW#t@e1YA%C_dd za(W##H$A&$(tpIBlW+Sc}99@&`;J->HAvTa%6HNE4h_ zY8lW{Pu6R2W%@x>s?YIj5xshn1_}CzlL?A#DW7<54#*bN38DxzV<>@cJnSN7uiEu` zuc`|%e^tv=D%MEk^=szX2Xg>Y5!&44-7L$1qEM(n!Fw14MB_t8T&zC|qWh}Ydi}D` zQJnSftjgsrbB$gq)Voq$SObNHcexYywRxDE$CFWQJF~pC0G%NuXWK2-Vjj;_G1!H^ zjwub479*OWVSrw>%AJQ{m22tWtKd65VSXLtX~5NhrMGE$^uA27&;~YyNqZqg-B^e# zeC{U!^Vqj1TD$n&i`^%NGNAI`JBQNB22578Oc01+#A3^Q>Nlx5d$Q5gD!|jzyv#P~;21pPH-*F5Q+w(M4dv~GRY7cre;%8*w16u-Rs%1IBG{JM}5D4(;)k*}O; zwNNX6mgE=-j(mQ=AoHSW@uE-l0&ZjS!@B~Bds0-_VFE|}k(0GHwV1v%1#w-VESO29(m9BKH`0n+Q&M^<;p%W@6yB335h}mLu_+D(=*u;c? ze1BSgGTnTMYMJUQ&1U!4cueopCCtUs2t>e<(3_*A-{d}@LU8-V0gEF`t}S<_cw(sC z?cmdAx_DFL#;s7n@cCP;PHFO)jC70{F-BCgTl-mJyEK8LsXhCsc(>gtqEgeGO`%$Lm!Xdp)-m;qjbKa#?h|XaVVh%1Z;3 zc>0VjKAc#9iSvdA@4#021sp3rlb$%hJj%v9$y4B20vG2hhm}LG! zC-%uk0%?s&S5njF%F&wT2|E}YLfJ7tbhFc4YQ?!==v$W|mS!W7FKsSfNKX#C zMhOQ$KhQ{RwDBjNc~|BS8CxX$0Ayv(@(`hqcuLk_n9x+&8R#~naj#_We6xUPNUV~f zfUanK%PcryUcmsia4aXaveNcS5~N=07?>S6bbSWp^jOlzp2txCVa{v*Z6%)G*lrr2 z`ZzQbm4j6Agq6#P=m}Oq!_5xcl+8hL`jNXDuR`<3qDJj{J_3?1O5UZv6^QC1S3(7z ztM$kI0^>7XVUSEVmVkN=xpqg0J#_CWIrEIA8l^|1Z*m<0T`4X&P1_M79LP20nt!6j zt!?XPN;m0XK5t8I6SbF!wEhGTFVbmhN+94@<2dR0OlWX_=C#h{&M!DbA|}i$SeHG& zA)Lh5j`ZK(WoKQNwG1evwLBG$4JRAXTNh;iwR`V&NdsJtj}aL3`^XPCj>9+@E%D#j z-Fm0Q8a-}JztJoU%UF#H2(%PO^JJNrMAs|xz{`JfC3xGeaN-(0E+-8vaO*#D50guO zeY(1iANV!2d-dGViwH5=W<_)agEZtjCHQ+RG~N8$l!Y$Xh)cg+ly3hwk);$W%B6&S zdUNCq!Kb1IZ2L4(_4Bqo^pH)>2Ihl#uNUWsR1>}&Juq(&HgAS|)4Sa)WaeMhY%No(a^-f@?WadRZ(7!bHP;(McT3)maF-~1d{1uxs zeYr9SuxJ>YyWYLdQdZ9LhhfBsn-WcRJ*@bhGBH0tQ6oBEYI;43B57C)kC0I!Sd zjwk`)>fJc5=f;Zk^XlEKGcb9E=!j3H<@|d(I2su}LC!&vYZDo%Z`{lS9X*A$? zGe1VcWB@BG#hvFXhZrdBkH;(SU?h!VNFSb_TzVtzG24F3gmRYjO8zcjLnPpUdnGD&K0v3FW#o$#7GB}Q|Cm7(MTQkwtKkYHSEE|G_SHt{2e<(4 zkT0)t^ExW1{kC?56jy#2l~6qByq)MVP0~J-VXp}W!&&RSk&=^>D;JmIKp)I=UOFgX zBN;qND9IsDd1Z|InV_eX(`rtH0+8W|L&*&~VK2vv8(X&7Z)|4P#d5(L>+ze`rV1!3 z{^_^4JR3}EyV83dCNNn-chKQNK)|TQ5JPoWqHd7HZOi3C9b|pz_=C$f-}UFOO80r( zD2ziSTY577gN98?8t6JbQ%YVGgY_v9dlIuwqfkdEF7Db2cF-H|4zc6IQjT;DkS>0g zrQVO{WZ3e%s7_K)J#Co}#A~fj9WYa7s>+GG5%b`9w|5kK|p| zCPxl+PAk=|w%>ICFSg)63ODQ`0O?~@`{RX_O8f*_O>ZCtR<~Ziwq7E9sM4q$}2P90h7QFTu9QiiG=y3ltR?CH>-^S=9Kvd%LUpu6!VOxrfV;s9`?M6RcprD4~5 zzq}jYOBMR;@9+HE8z9-%Tn(k%C0{yOu;B2XU_4=7`&Dhc!m8W{K5U5+Xjj+tcWD%C znmz7-U~EPXLWKU2t5^$Ti-%LgHVLCN^er$Z_RcXIKW!R;I$AcE0GW4 zYQ`XY8X}m>%z)bQA+Al+FZM)3koL^q2u6xqRKyLgmaWq}Ej2$L)7dDmKgwq(DSxIv zzKu+*gP?e@Eh}PXIc~@N+y~EL87ixi1272LEgEK}yE(E0!@dZoPP1p*aU=~N!t(jm z6i^PCPn1QJp1U9^PN}NeZ_SrgRVKM0OM_`Z8>=$`y7COYu-l<6%BDkoy(F^;5lJ!; zX>YP8Lcc1vMo*ZH^dFTU2Y?R&4mEzX_J~+>)m3h=HXEhEcS7!@j?@e+{~lZ?)WK+_-CRSA~HBU4~O(LjlKkMdT><} zm-z?v^*$>Hd0rg*m*de>GRdxppQ1hqDGH?Oog6_X+*7{h5eBe?rDBEZa&2qRli70Q z3wXW7$S0LeY&$*|eZvt)%b1>E=<#CMsF1D#G{KA;H^#pyZ4Rn^I0!K9h5Ky5E}z<{ z{%d$Go>R@~S;kLhHIb4$tgF*gu|*sMM->oJLZ{J>L%*n-Z+5Bp4*Q#O20BlRn13!8 zSq*$gV1fX2ri%t14ti;V5<8N$1?|zuu7cS773fs?r?pFgO7pchba2igd+-ehvB_n? zCbIb7Hc?9bCl1;4dOs^2F0hVY=Q-#&fd}<1)p1O7xAOt-3+aG+!O>fu>M?p)#Bk8e zaGD@cScw2~A3e~Vpx@qRwrA1_uhDI`tgdYNsQyx%+H##d9I)3FC#2T$rBc_~Kh7%9 z>#ZZYY<0C667W{XFv9);*zSx+U8;QO5|VLXZdo#c@x>kBbVQ|uOK<%2ZD!$HP|Frh zfYO%_pM#;V@Vk?x=OZSc)<>fO*;yRO<+7rl41HAZCDa_y=)mhuPX@#tX~Y(b(WJ$; zS_|z$eKKw7+cXDUh4h1nG`a8}E*3#$&>z%PZGy=Vsf!M^Ew<<6&H_v$f**~^>{+k)p*%1mAvkjOCW0E%M` zS?T`jhQGv6*Kn?o>ZMVcha#}^Usif?`j(X7#ft!OZwjm$&kIX>mGZi$YzCnRgN=b^ z{i{=Np`c^WR&!y8ZO-uMb$0G0rl6&}0SlTk!h_Wj-_6Da#P13fbVWLBz9{Q`IB!9^ zH4YPtpTgjCL@N;};Ebkea919^uVE~tbec$Im+|k4iXoU*jBwnC9-%L-uue3d>=Iaq47fe&8^&-`*clg@B)BUUB#7mC?fa2PQ zCx@Z0yRe&^_IrBpeB^`aIbEzmI?nU%T0h@!6U#c{y;Cv?-)93 z>n|#M192ZtmF~(#DpV^VsxlvuNF%sipz-sz9LsICVv1ur6VPP!U<^)0R08eK*T^7@ z+q`-$R~3r(KbTaTSo&&Id3;A62Q*9y#6he&>P5eeyMuj3z|<|^13*Dksiuq}VQx!m z#XB-@NYDMm)kY#HjM1cZ0AKXx{BQ&Z1>5}nCzcGl67kmpjHenB$J%u@T*?MmEv?Ii z?auyS-gjqL#9ut2x0DpGW0Xc;1XI3Q_|5fhfjvZCP+tP~N!Q+gY^Z+97H%@@J2&~d z8y8fVJX>yNIMu=^{pki$?sE-HNI0L@Cf|eM1RT(WaN3e48}q*HZbyLpt%Np3z}$qU znGK?a0sc#b{}^tV3X(7o3VO(r&A*{^sfZ`4btpE8mCLk3t z-o|M7^gll&&xi&{Z2z-sFt8^`0NLq{6&LDXvl3|af#M<}PsZ~fedP>z;PU_f`b9-i zvAU^;vDAvCA}NnIvX?H7d+)k-6o(9CAlK|%)@igG)+JXT%p1`f-TgbY>%)hz(r6TK zFD6DekQR6q?;3s2PKs`W>5ZQ;0g7R0_|X5v?ty_q0@7PSasM^`FC$?@^`E9VJbm`> zdy~VJA^c})2$CZJq5LpNOgP2A;e8lU@?Q@Lg8NuMuqFfnR>Mo1(U;Mk z9+Xt1H?HTq39G?4U4o$h9-Q_!a3u|m!Bu*NJ9oENRO@JDl>d$kBvG0?Dv#!d41Q6v UPAHe21qS#d37DP5!ume{3(Sn*K>z>%